qase-python-commons 4.1.0__py3-none-any.whl → 4.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of qase-python-commons might be problematic. Click here for more details.

@@ -3,7 +3,7 @@ from typing import Union
3
3
 
4
4
  import certifi
5
5
  from qase.api_client_v1 import ApiClient, ProjectsApi, Project, EnvironmentsApi, RunsApi, AttachmentsApi, \
6
- AttachmentGet, RunCreate, ConfigurationsApi, ConfigurationCreate, ConfigurationGroupCreate
6
+ AttachmentGet, RunCreate, ConfigurationsApi, ConfigurationCreate, ConfigurationGroupCreate, RunPublic
7
7
  from qase.api_client_v1.configuration import Configuration
8
8
  from .. import Logger
9
9
  from .base_api_client import BaseApiClient
@@ -210,6 +210,37 @@ class ApiV1Client(BaseApiClient):
210
210
  return True
211
211
  return False
212
212
 
213
+ def enable_public_report(self, project_code: str, run_id: int) -> str:
214
+ """
215
+ Enable public report for a test run and return the public link
216
+
217
+ :param project_code: project code
218
+ :param run_id: test run id
219
+ :return: public report link or None if failed
220
+ """
221
+ try:
222
+ self.logger.log_debug(f"Enabling public report for run {run_id}")
223
+ api_runs = RunsApi(self.client)
224
+
225
+ # Create RunPublic object with status=True
226
+ run_public = RunPublic(status=True)
227
+
228
+ # Call the API to enable public report
229
+ response = api_runs.update_run_publicity(project_code, run_id, run_public)
230
+
231
+ # Extract the public URL from response
232
+ if response.result and response.result.url:
233
+ public_url = response.result.url
234
+ self.logger.log_debug(f"Public report enabled for run {run_id}: {public_url}")
235
+ return public_url
236
+ else:
237
+ self.logger.log_debug(f"Public report enabled for run {run_id} but no URL returned")
238
+ return None
239
+
240
+ except Exception as e:
241
+ self.logger.log(f"Error at enabling public report for run {run_id}: {e}", "error")
242
+ return None
243
+
213
244
  def update_external_link(self, project_code: str, run_id: int):
214
245
  """Update external link for a test run"""
215
246
  try:
@@ -86,3 +86,14 @@ class BaseApiClient(abc.ABC):
86
86
  :return: None
87
87
  """
88
88
  pass
89
+
90
+ @abc.abstractmethod
91
+ def enable_public_report(self, project_code: str, run_id: int) -> str:
92
+ """
93
+ Enable public report for a test run and return the public link
94
+
95
+ :param project_code: project code
96
+ :param run_id: test run id
97
+ :return: public report link or None if failed
98
+ """
99
+ pass
qase/commons/config.py CHANGED
@@ -169,6 +169,11 @@ class ConfigManager:
169
169
  # Parse comma-separated string
170
170
  self.config.testops.set_status_filter([s.strip() for s in status_filter.split(',')])
171
171
 
172
+ if testops.get("showPublicReportLink") is not None:
173
+ self.config.testops.set_show_public_report_link(
174
+ testops.get("showPublicReportLink")
175
+ )
176
+
172
177
  if config.get("report"):
173
178
  report = config.get("report")
174
179
 
@@ -321,6 +326,9 @@ class ConfigManager:
321
326
  # Parse comma-separated string
322
327
  self.config.testops.set_status_filter([s.strip() for s in value.split(',')])
323
328
 
329
+ if key == 'QASE_TESTOPS_SHOW_PUBLIC_REPORT_LINK':
330
+ self.config.testops.set_show_public_report_link(value)
331
+
324
332
  if key == 'QASE_REPORT_DRIVER':
325
333
  self.config.report.set_driver(value)
326
334
 
@@ -49,6 +49,7 @@ class TestopsConfig(BaseModel):
49
49
  batch: BatchConfig = None
50
50
  configurations: ConfigurationsConfig = None
51
51
  status_filter: List[str] = None
52
+ show_public_report_link: bool = None
52
53
 
53
54
  def __init__(self):
54
55
  self.api = ApiConfig()
@@ -58,6 +59,7 @@ class TestopsConfig(BaseModel):
58
59
  self.configurations = ConfigurationsConfig()
59
60
  self.defect = False
60
61
  self.status_filter = []
62
+ self.show_public_report_link = False
61
63
 
62
64
  def set_project(self, project: str):
63
65
  self.project = project
@@ -67,3 +69,6 @@ class TestopsConfig(BaseModel):
67
69
 
68
70
  def set_status_filter(self, status_filter: List[str]):
69
71
  self.status_filter = status_filter
72
+
73
+ def set_show_public_report_link(self, show_public_report_link):
74
+ self.show_public_report_link = QaseUtils.parse_bool(show_public_report_link)
@@ -25,7 +25,7 @@ class Execution(BaseModel):
25
25
  stacktrace: Optional[str] = None,
26
26
  thread: Optional[str] = QaseUtils.get_thread_name()
27
27
  ):
28
- self.start_time = time.time()
28
+ self.start_time = QaseUtils.get_real_time()
29
29
  self.status = status
30
30
  self.end_time = end_time
31
31
  self.duration = duration
@@ -48,7 +48,7 @@ class Execution(BaseModel):
48
48
  return self.status
49
49
 
50
50
  def complete(self):
51
- self.end_time = time.time()
51
+ self.end_time = QaseUtils.get_real_time()
52
52
  self.duration = (int)((self.end_time - self.start_time) * 1000)
53
53
 
54
54
 
@@ -5,6 +5,7 @@ from enum import Enum
5
5
  from typing import Optional, Union, Dict, List, Type
6
6
  from .attachment import Attachment
7
7
  from .basemodel import BaseModel
8
+ from .. import QaseUtils
8
9
 
9
10
 
10
11
  class StepType(Enum):
@@ -95,7 +96,7 @@ class StepSleepData(BaseModel):
95
96
 
96
97
  class StepExecution(BaseModel):
97
98
  def __init__(self, status: Optional[str] = 'untested', end_time: int = 0, duration: int = 0):
98
- self.start_time = time.time()
99
+ self.start_time = QaseUtils.get_real_time()
99
100
  self.status = status
100
101
  self.end_time = end_time
101
102
  self.duration = duration
@@ -108,7 +109,7 @@ class StepExecution(BaseModel):
108
109
  raise ValueError('Step status must be one of: passed, failed, skipped, blocked, untested, invalid')
109
110
 
110
111
  def complete(self):
111
- self.end_time = time.time()
112
+ self.end_time = QaseUtils.get_real_time()
112
113
  self.duration = int((self.end_time - self.start_time) * 1000)
113
114
 
114
115
  def add_attachment(self, attachment: Attachment):
@@ -31,10 +31,10 @@ class QaseReport:
31
31
 
32
32
  def start_run(self):
33
33
  self._check_report_path()
34
- self.start_time = str(time.time())
34
+ self.start_time = str(QaseUtils.get_real_time())
35
35
 
36
36
  def complete_run(self):
37
- self.end_time = str(time.time())
37
+ self.end_time = str(QaseUtils.get_real_time())
38
38
  self._compile_report()
39
39
 
40
40
  def complete_worker(self):
@@ -144,6 +144,18 @@ class QaseTestOps:
144
144
  self.logger.log_debug("Completing run")
145
145
  self.client.complete_run(self.project_code, self.run_id)
146
146
  self.logger.log_debug("Run completed")
147
+
148
+ # Enable public report if configured
149
+ if self.config.testops.show_public_report_link:
150
+ try:
151
+ self.logger.log_debug("Enabling public report")
152
+ public_url = self.client.enable_public_report(self.project_code, self.run_id)
153
+ if public_url:
154
+ self.logger.log(f"Public report link: {public_url}", "info")
155
+ else:
156
+ self.logger.log("Failed to generate public report link", "warning")
157
+ except Exception as e:
158
+ self.logger.log(f"Failed to generate public report link: {e}", "warning")
147
159
 
148
160
  def complete_worker(self) -> None:
149
161
  if len(self.results) > 0:
qase/commons/utils.py CHANGED
@@ -6,10 +6,88 @@ from typing import Union, List
6
6
  import pip
7
7
  import string
8
8
  import uuid
9
+ import time
9
10
 
10
11
 
11
12
  class QaseUtils:
12
13
 
14
+ @staticmethod
15
+ def get_real_time() -> float:
16
+ """
17
+ Get real system time, bypassing time mocking libraries like freezegun.
18
+
19
+ This is necessary when reporting test results to external systems that validate
20
+ timestamps against current time, even when tests are using time mocking.
21
+
22
+ Returns:
23
+ float: Current Unix timestamp in seconds with microsecond precision
24
+ """
25
+ # Try to get the original time function if it was wrapped by freezegun
26
+ # freezegun stores the original function in __wrapped__ attribute
27
+ if hasattr(time.time, '__wrapped__'):
28
+ return time.time.__wrapped__()
29
+
30
+ # Fallback: use direct system call via ctypes
31
+ # This works on Unix-like systems and Windows
32
+ try:
33
+ import ctypes
34
+ import ctypes.util
35
+
36
+ if sys.platform == 'win32':
37
+ # Windows: use GetSystemTimeAsFileTime
38
+ class FILETIME(ctypes.Structure):
39
+ _fields_ = [("dwLowDateTime", ctypes.c_uint32),
40
+ ("dwHighDateTime", ctypes.c_uint32)]
41
+
42
+ kernel32 = ctypes.windll.kernel32
43
+ ft = FILETIME()
44
+ kernel32.GetSystemTimeAsFileTime(ctypes.byref(ft))
45
+
46
+ # Convert FILETIME to Unix timestamp
47
+ # FILETIME is 100-nanosecond intervals since January 1, 1601
48
+ timestamp = (ft.dwHighDateTime << 32) + ft.dwLowDateTime
49
+ # Convert to seconds and adjust epoch (1601 -> 1970)
50
+ return (timestamp / 10000000.0) - 11644473600.0
51
+ else:
52
+ # Unix-like systems: use gettimeofday for microsecond precision
53
+ # Try multiple approaches to find libc
54
+ libc = None
55
+
56
+ # Method 1: Use find_library (works on most systems)
57
+ libc_path = ctypes.util.find_library('c')
58
+ if libc_path:
59
+ try:
60
+ libc = ctypes.CDLL(libc_path)
61
+ except OSError:
62
+ pass
63
+
64
+ # Method 2: Try common library names directly (for Alpine Linux, musl libc, etc.)
65
+ if libc is None:
66
+ for lib_name in ['libc.so.6', 'libc.so', 'libc.dylib']:
67
+ try:
68
+ libc = ctypes.CDLL(lib_name)
69
+ break
70
+ except OSError:
71
+ continue
72
+
73
+ if libc is None:
74
+ raise OSError("Could not load C library")
75
+
76
+ class timeval(ctypes.Structure):
77
+ _fields_ = [("tv_sec", ctypes.c_long),
78
+ ("tv_usec", ctypes.c_long)]
79
+
80
+ tv = timeval()
81
+ libc.gettimeofday(ctypes.byref(tv), None)
82
+
83
+ return float(tv.tv_sec) + (float(tv.tv_usec) / 1000000.0)
84
+ except Exception:
85
+ # Last resort: return the potentially mocked time
86
+ # This will still work in normal cases without freezegun
87
+ # If freezegun is active, the user might see timestamp validation errors
88
+ # but the core functionality will continue to work
89
+ return time.time()
90
+
13
91
  @staticmethod
14
92
  def build_tree(items):
15
93
  nodes = {item.id: item for item in items}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qase-python-commons
3
- Version: 4.1.0
3
+ Version: 4.1.2
4
4
  Summary: A library for Qase TestOps and Qase Report
5
5
  Author-email: Qase Team <support@qase.io>
6
6
  Project-URL: Homepage, https://github.com/qase-tms/qase-python/tree/main/qase-python-commons
@@ -31,6 +31,7 @@ Requires-Dist: mock; extra == "testing"
31
31
  Requires-Dist: more_itertools; extra == "testing"
32
32
  Requires-Dist: requests; extra == "testing"
33
33
  Requires-Dist: urllib3; extra == "testing"
34
+ Requires-Dist: freezegun; extra == "testing"
34
35
 
35
36
  # Qase Python Commons
36
37
 
@@ -1,22 +1,22 @@
1
1
  qase/__init__.py,sha256=FLefSJnT6MTsTfZSDjMbMImI6674fAoJ5ZT3PNWGcRo,37
2
2
  qase/commons/__init__.py,sha256=3HI65PJES4Q6YvtkSuRPh6tZboTETJo8wbdHlNYaePU,323
3
- qase/commons/config.py,sha256=ivunk-6IBfsXyRf-_Y_Je_K8boD4gDupv8v2Yf3nm2M,15162
3
+ qase/commons/config.py,sha256=SyG1-2RfKQAhQLUbq7szbUUp9ABYvbSiaT8WdQEjMcA,15556
4
4
  qase/commons/loader.py,sha256=-MMY4HgSI6q1xq3NaJoq_w4liM73qdFKjYLVCT1E7Pc,1064
5
5
  qase/commons/logger.py,sha256=V_QTSDWNnUgd0j58rydYYN1AYOu3wa9T2fWjpOyxyuA,3430
6
- qase/commons/utils.py,sha256=utPRoYyThLs2tgD1lmjkwJ9KZuE7ZjliUiZkJJWFca0,3330
7
- qase/commons/client/api_v1_client.py,sha256=_dpvldRonALKubInPU03hYhyGJqxeq3DiIuyjDeOngg,11543
6
+ qase/commons/utils.py,sha256=0TtpLvyAdh7fLQW4HA_6aQM8kWy4a8VtSD_KdkRVZlw,6775
7
+ qase/commons/client/api_v1_client.py,sha256=BaE9Ix-k5Xc7Wiz8ILKKsWb5G5J3hqgjwaIcrocvCdY,12879
8
8
  qase/commons/client/api_v2_client.py,sha256=GsIrXJcBw6GtzvJjbjMYa0tvUIxEEe4pALRN2LBMKPM,9043
9
- qase/commons/client/base_api_client.py,sha256=qiK93rXXeLOmp6e3cgsLA4lmrV_rCL1BIi_xUeqFrDE,2613
9
+ qase/commons/client/base_api_client.py,sha256=4NtXhUxrxEPOBIzEAE-b50KHMwlhxwnSLGcYx8NMjbY,2961
10
10
  qase/commons/exceptions/reporter.py,sha256=dP-Mwcq8HKBOjgu3YqhyULDmDGU09BmT6Fh9HjICaUc,45
11
11
  qase/commons/models/__init__.py,sha256=FTt5dYASBX4r6-tQi-_JAUVx4uvJs9GTxROdAZEV6Jo,272
12
12
  qase/commons/models/attachment.py,sha256=cGfB0BaTDVfA7euJubKvi2cXXNvlSm_dy5x-ak3H3ec,1625
13
13
  qase/commons/models/basemodel.py,sha256=0j8E-LE6hxAKQPYLNM9qThor9s2ZndZys_kibeoLImo,426
14
14
  qase/commons/models/external_link.py,sha256=bdXj7pNHHkfb3dcYtXN8sNp-HFxHvZeYz8QIPGcvX0w,1317
15
15
  qase/commons/models/relation.py,sha256=HymHeh1uBcoQnTs4Vra7WJ_KFkhryj5o7cShjoGQImI,511
16
- qase/commons/models/result.py,sha256=8Tp-R34vxZhxmgqQErr2vH8ie-ZidDNqxZy6Xty4uqg,4019
16
+ qase/commons/models/result.py,sha256=53udABcu_Gzm-vvnvvb9mFui8-el-N41vCU2Dsual84,4047
17
17
  qase/commons/models/run.py,sha256=ANbljW1mgua4JF1wGZG9S-jnUpI7FC4F15sAQOoNnko,3096
18
18
  qase/commons/models/runtime.py,sha256=h0kJcPYRo8dglIQVFyzh-cFpWb3bAaQaIq5wV8VfClM,1508
19
- qase/commons/models/step.py,sha256=svH2jvxiJRBugCjKdifXzOKli9OzQQvn8lsPhzd20yY,5433
19
+ qase/commons/models/step.py,sha256=AU7AtNlZBAokWWIlazZ6Gef_ua0g6qDUFD98I6GmDiY,5486
20
20
  qase/commons/models/config/api.py,sha256=IyYY2f3ncESUAEGvkE3-meatebBFJdvgo7KNOQnxLE0,288
21
21
  qase/commons/models/config/batch.py,sha256=X0H8SVOCCD2pV6LSMqjI-tIjRcLifnrM5MareK2FhQw,321
22
22
  qase/commons/models/config/connection.py,sha256=wK2fGjc0G0rMVVhPnjw_t_M1YWZwANlhwl-awmI7XSo,516
@@ -25,21 +25,21 @@ qase/commons/models/config/plan.py,sha256=JbAY7qfGXYreXOLa32OLxw6z0paeCCf87-2b1m
25
25
  qase/commons/models/config/qaseconfig.py,sha256=oWc03PKV8-jb3JNgs_tqQP-22wsKZW25eSaAacYl4no,2838
26
26
  qase/commons/models/config/report.py,sha256=g3Z2B3Tewaasjc1TMj2mkXxz0Zc1C39sHeTXH0MRM2Y,497
27
27
  qase/commons/models/config/run.py,sha256=UGE5PA_EyiFpfIev-TOob1BocxT1vdUdWW3ELMD58zQ,1239
28
- qase/commons/models/config/testops.py,sha256=rJ9wW-VMt-5XaoPdRUKeM9rlV0eTiRINEhEulFVczv0,1893
28
+ qase/commons/models/config/testops.py,sha256=LdylcNG1ctxjNHXwrwcerAKY3h0WByKWYIrKOzEV2K8,2133
29
29
  qase/commons/profilers/__init__.py,sha256=GhKT5hRbHbhAC4GhdyChA8XoAsGQOnIb8S2Z4-fdS7Q,222
30
30
  qase/commons/profilers/db.py,sha256=Am1tvvLgJq4_A8JsuSeBGf47BD2lnSX-5KiMjSgr-Ko,391
31
31
  qase/commons/profilers/network.py,sha256=zKNBnTQG4BMg8dn8O--tQzQLpu-qs5ADhHEnqIas0gM,4950
32
32
  qase/commons/profilers/sleep.py,sha256=HT6h0R-2XHZAoBYRxS2T_KC8RrnEoVjP7MXusaE4Nec,1624
33
33
  qase/commons/reporters/__init__.py,sha256=J0aNLzb_MPPT_zF8BtX_w9nj_U7Ad06RGpyWK5Pxq1o,169
34
34
  qase/commons/reporters/core.py,sha256=FJx1kxXkUz9vwkSAXapkNS3vF97Igvlq7Yyjp-4BpeI,9539
35
- qase/commons/reporters/report.py,sha256=ZLwtVn5gjwgJFtfbpLUO-vW3M3skEq3AhKJwtmM0nUw,4810
36
- qase/commons/reporters/testops.py,sha256=uph_8upIt5bu4ncqyK7ehXR9NqtpZ34wGUBFLVJKOpE,7481
35
+ qase/commons/reporters/report.py,sha256=2hrpQGhtgEaDphRIiAiVtC1C1YWz3--jy4mw-kvO8lY,4838
36
+ qase/commons/reporters/testops.py,sha256=k3c91l_xuLq6NRLkzfwRqRTLfnpSxEQe38HNH8XMOTk,8161
37
37
  qase/commons/status_mapping/__init__.py,sha256=NPsWKDC7rGSCYfVunnGnHxL0_HmKWH7RBaMjWTH_Mtk,322
38
38
  qase/commons/status_mapping/status_mapping.py,sha256=kohSLNK6_qbMSP-M8gxHTTmOECgzDE3XvLqOzidPlYI,7213
39
39
  qase/commons/util/__init__.py,sha256=0sRRfrMOIPCHpk9tXM94Pj10qrk18B61qEcbLpRjw_I,74
40
40
  qase/commons/util/host_data.py,sha256=n8o5PDs8kELCZZ5GR7Jug6LsgZHWJudU7iRmZHRdrlw,5264
41
41
  qase/commons/validators/base.py,sha256=wwSn-4YiuXtfGMGnSKgo9Vm5hAKevVmmfd2Ro6Q7MYQ,173
42
- qase_python_commons-4.1.0.dist-info/METADATA,sha256=JcshiFrQAEhDrqLCi3QflCIJcPMfDF4qLsEqLt-ansE,1982
43
- qase_python_commons-4.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
44
- qase_python_commons-4.1.0.dist-info/top_level.txt,sha256=Mn5aFk7H7Uia4s1NRDsvebu8vCrFy9nOuRIBfkIY5kQ,5
45
- qase_python_commons-4.1.0.dist-info/RECORD,,
42
+ qase_python_commons-4.1.2.dist-info/METADATA,sha256=NISziDSmN_RTWKgMJ2SrdiLkV42IbzDY1VVPdzcPoU8,2027
43
+ qase_python_commons-4.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
44
+ qase_python_commons-4.1.2.dist-info/top_level.txt,sha256=Mn5aFk7H7Uia4s1NRDsvebu8vCrFy9nOuRIBfkIY5kQ,5
45
+ qase_python_commons-4.1.2.dist-info/RECORD,,