qase-python-commons 3.5.2__py3-none-any.whl → 3.5.4__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.
- qase/commons/client/api_v1_client.py +83 -2
- qase/commons/config.py +39 -0
- qase/commons/models/config/testops.py +41 -0
- qase/commons/reporters/testops.py +26 -7
- {qase_python_commons-3.5.2.dist-info → qase_python_commons-3.5.4.dist-info}/METADATA +3 -3
- {qase_python_commons-3.5.2.dist-info → qase_python_commons-3.5.4.dist-info}/RECORD +8 -8
- {qase_python_commons-3.5.2.dist-info → qase_python_commons-3.5.4.dist-info}/WHEEL +0 -0
- {qase_python_commons-3.5.2.dist-info → qase_python_commons-3.5.4.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
6
|
+
AttachmentGet, RunCreate, ConfigurationsApi, ConfigurationCreate, ConfigurationGroupCreate
|
|
7
7
|
from qase.api_client_v1.configuration import Configuration
|
|
8
8
|
from .. import Logger
|
|
9
9
|
from .base_api_client import BaseApiClient
|
|
@@ -11,6 +11,7 @@ from ..exceptions.reporter import ReporterException
|
|
|
11
11
|
from ..models import Attachment
|
|
12
12
|
from ..models.config.framework import Video, Trace
|
|
13
13
|
from ..models.config.qaseconfig import QaseConfig
|
|
14
|
+
from ..models.config.testops import ConfigurationValue
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class ApiV1Client(BaseApiClient):
|
|
@@ -66,6 +67,73 @@ class ApiV1Client(BaseApiClient):
|
|
|
66
67
|
self.logger.log("Exception when calling EnvironmentsApi->get_environments: %s\n" % e, "error")
|
|
67
68
|
raise ReporterException(e)
|
|
68
69
|
|
|
70
|
+
def get_configurations(self, project_code: str):
|
|
71
|
+
"""Get all configurations for the project"""
|
|
72
|
+
try:
|
|
73
|
+
self.logger.log_debug(f"Getting configurations for project {project_code}")
|
|
74
|
+
api_instance = ConfigurationsApi(self.client)
|
|
75
|
+
response = api_instance.get_configurations(code=project_code)
|
|
76
|
+
if hasattr(response, 'result') and hasattr(response.result, 'entities'):
|
|
77
|
+
return response.result.entities
|
|
78
|
+
return []
|
|
79
|
+
except Exception as e:
|
|
80
|
+
self.logger.log(f"Exception when calling ConfigurationsApi->get_configurations: {e}", "error")
|
|
81
|
+
return []
|
|
82
|
+
|
|
83
|
+
def find_or_create_configuration(self, project_code: str, config_value: ConfigurationValue) -> Union[int, None]:
|
|
84
|
+
"""Find existing configuration or create new one if createIfNotExists is True"""
|
|
85
|
+
try:
|
|
86
|
+
configurations = self.get_configurations(project_code)
|
|
87
|
+
|
|
88
|
+
# Search for existing configuration
|
|
89
|
+
for group in configurations:
|
|
90
|
+
if hasattr(group, 'configurations'):
|
|
91
|
+
for config in group.configurations:
|
|
92
|
+
# API returns configurations with 'title' field, not 'name' and 'value'
|
|
93
|
+
# We need to match group.title with config_value.name and config.title with config_value.value
|
|
94
|
+
config_title = config.title if hasattr(config, 'title') else 'No title'
|
|
95
|
+
group_title = group.title if hasattr(group, 'title') else 'No title'
|
|
96
|
+
|
|
97
|
+
if (group_title == config_value.name and config_title == config_value.value):
|
|
98
|
+
return config.id
|
|
99
|
+
|
|
100
|
+
# Configuration not found
|
|
101
|
+
if not self.config.testops.configurations.create_if_not_exists:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
# Create new configuration
|
|
105
|
+
# First, try to find existing group or create new one
|
|
106
|
+
group_id = None
|
|
107
|
+
for group in configurations:
|
|
108
|
+
if hasattr(group, 'title') and group.title == config_value.name:
|
|
109
|
+
group_id = group.id
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
if group_id is None:
|
|
113
|
+
# Create new group
|
|
114
|
+
group_create = ConfigurationGroupCreate(title=config_value.name)
|
|
115
|
+
group_response = ConfigurationsApi(self.client).create_configuration_group(
|
|
116
|
+
code=project_code,
|
|
117
|
+
configuration_group_create=group_create
|
|
118
|
+
)
|
|
119
|
+
group_id = group_response.result.id
|
|
120
|
+
|
|
121
|
+
# Create configuration in the group
|
|
122
|
+
config_create = ConfigurationCreate(
|
|
123
|
+
title=config_value.value,
|
|
124
|
+
group_id=group_id
|
|
125
|
+
)
|
|
126
|
+
config_response = ConfigurationsApi(self.client).create_configuration(
|
|
127
|
+
code=project_code,
|
|
128
|
+
configuration_create=config_create
|
|
129
|
+
)
|
|
130
|
+
config_id = config_response.result.id
|
|
131
|
+
return config_id
|
|
132
|
+
|
|
133
|
+
except Exception as e:
|
|
134
|
+
self.logger.log(f"Error at finding/creating configuration {config_value.name}={config_value.value}: {e}", "error")
|
|
135
|
+
return None
|
|
136
|
+
|
|
69
137
|
def complete_run(self, project_code: str, run_id: int) -> None:
|
|
70
138
|
api_runs = RunsApi(self.client)
|
|
71
139
|
self.logger.log_debug(f"Completing run {run_id}")
|
|
@@ -94,6 +162,15 @@ class ApiV1Client(BaseApiClient):
|
|
|
94
162
|
|
|
95
163
|
def create_test_run(self, project_code: str, title: str, description: str, plan_id=None,
|
|
96
164
|
environment_id=None) -> str:
|
|
165
|
+
# Process configurations
|
|
166
|
+
configuration_ids = []
|
|
167
|
+
|
|
168
|
+
if self.config.testops.configurations and self.config.testops.configurations.values:
|
|
169
|
+
for config_value in self.config.testops.configurations.values:
|
|
170
|
+
config_id = self.find_or_create_configuration(project_code, config_value)
|
|
171
|
+
if config_id:
|
|
172
|
+
configuration_ids.append(config_id)
|
|
173
|
+
|
|
97
174
|
kwargs = dict(
|
|
98
175
|
title=title,
|
|
99
176
|
description=description,
|
|
@@ -103,7 +180,11 @@ class ApiV1Client(BaseApiClient):
|
|
|
103
180
|
start_time=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S"),
|
|
104
181
|
tags=self.config.testops.run.tags
|
|
105
182
|
)
|
|
106
|
-
|
|
183
|
+
|
|
184
|
+
# Add configurations if any found
|
|
185
|
+
if configuration_ids:
|
|
186
|
+
kwargs['configurations'] = configuration_ids
|
|
187
|
+
|
|
107
188
|
try:
|
|
108
189
|
result = RunsApi(self.client).create_run(
|
|
109
190
|
code=project_code,
|
qase/commons/config.py
CHANGED
|
@@ -128,6 +128,28 @@ class ConfigManager:
|
|
|
128
128
|
self.config.testops.batch.set_size(
|
|
129
129
|
batch.get("size"))
|
|
130
130
|
|
|
131
|
+
if testops.get("configurations"):
|
|
132
|
+
configurations = testops.get("configurations")
|
|
133
|
+
|
|
134
|
+
if configurations.get("values"):
|
|
135
|
+
values = configurations.get("values")
|
|
136
|
+
for value in values:
|
|
137
|
+
if value.get("name") and value.get("value"):
|
|
138
|
+
self.config.testops.configurations.add_value(
|
|
139
|
+
value.get("name"), value.get("value"))
|
|
140
|
+
|
|
141
|
+
if configurations.get("createIfNotExists") is not None:
|
|
142
|
+
self.config.testops.configurations.set_create_if_not_exists(
|
|
143
|
+
configurations.get("createIfNotExists"))
|
|
144
|
+
|
|
145
|
+
if testops.get("statusFilter"):
|
|
146
|
+
status_filter = testops.get("statusFilter")
|
|
147
|
+
if isinstance(status_filter, list):
|
|
148
|
+
self.config.testops.set_status_filter(status_filter)
|
|
149
|
+
elif isinstance(status_filter, str):
|
|
150
|
+
# Parse comma-separated string
|
|
151
|
+
self.config.testops.set_status_filter([s.strip() for s in status_filter.split(',')])
|
|
152
|
+
|
|
131
153
|
if config.get("report"):
|
|
132
154
|
report = config.get("report")
|
|
133
155
|
|
|
@@ -235,6 +257,23 @@ class ConfigManager:
|
|
|
235
257
|
if key == 'QASE_TESTOPS_BATCH_SIZE':
|
|
236
258
|
self.config.testops.batch.set_size(value)
|
|
237
259
|
|
|
260
|
+
if key == 'QASE_TESTOPS_CONFIGURATIONS_VALUES':
|
|
261
|
+
# Parse configurations from environment variable
|
|
262
|
+
# Format: "group1=value1,group2=value2"
|
|
263
|
+
if value:
|
|
264
|
+
config_pairs = value.split(',')
|
|
265
|
+
for pair in config_pairs:
|
|
266
|
+
if '=' in pair:
|
|
267
|
+
name, config_value = pair.split('=', 1)
|
|
268
|
+
self.config.testops.configurations.add_value(name.strip(), config_value.strip())
|
|
269
|
+
|
|
270
|
+
if key == 'QASE_TESTOPS_CONFIGURATIONS_CREATE_IF_NOT_EXISTS':
|
|
271
|
+
self.config.testops.configurations.set_create_if_not_exists(value)
|
|
272
|
+
|
|
273
|
+
if key == 'QASE_TESTOPS_STATUS_FILTER':
|
|
274
|
+
# Parse comma-separated string
|
|
275
|
+
self.config.testops.set_status_filter([s.strip() for s in value.split(',')])
|
|
276
|
+
|
|
238
277
|
if key == 'QASE_REPORT_DRIVER':
|
|
239
278
|
self.config.report.set_driver(value)
|
|
240
279
|
|
|
@@ -4,6 +4,40 @@ from .plan import PlanConfig
|
|
|
4
4
|
from .run import RunConfig
|
|
5
5
|
from ..basemodel import BaseModel
|
|
6
6
|
from ... import QaseUtils
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConfigurationValue(BaseModel):
|
|
11
|
+
name: str = None
|
|
12
|
+
value: str = None
|
|
13
|
+
|
|
14
|
+
def __init__(self, name: str = None, value: str = None):
|
|
15
|
+
self.name = name
|
|
16
|
+
self.value = value
|
|
17
|
+
|
|
18
|
+
def set_name(self, name: str):
|
|
19
|
+
self.name = name
|
|
20
|
+
|
|
21
|
+
def set_value(self, value: str):
|
|
22
|
+
self.value = value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ConfigurationsConfig(BaseModel):
|
|
26
|
+
values: List[ConfigurationValue] = None
|
|
27
|
+
create_if_not_exists: bool = None
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
self.values = []
|
|
31
|
+
self.create_if_not_exists = False
|
|
32
|
+
|
|
33
|
+
def set_values(self, values: List[ConfigurationValue]):
|
|
34
|
+
self.values = values
|
|
35
|
+
|
|
36
|
+
def set_create_if_not_exists(self, create_if_not_exists):
|
|
37
|
+
self.create_if_not_exists = QaseUtils.parse_bool(create_if_not_exists)
|
|
38
|
+
|
|
39
|
+
def add_value(self, name: str, value: str):
|
|
40
|
+
self.values.append(ConfigurationValue(name=name, value=value))
|
|
7
41
|
|
|
8
42
|
|
|
9
43
|
class TestopsConfig(BaseModel):
|
|
@@ -13,16 +47,23 @@ class TestopsConfig(BaseModel):
|
|
|
13
47
|
run: RunConfig = None
|
|
14
48
|
plan: PlanConfig = None
|
|
15
49
|
batch: BatchConfig = None
|
|
50
|
+
configurations: ConfigurationsConfig = None
|
|
51
|
+
status_filter: List[str] = None
|
|
16
52
|
|
|
17
53
|
def __init__(self):
|
|
18
54
|
self.api = ApiConfig()
|
|
19
55
|
self.run = RunConfig()
|
|
20
56
|
self.batch = BatchConfig()
|
|
21
57
|
self.plan = PlanConfig()
|
|
58
|
+
self.configurations = ConfigurationsConfig()
|
|
22
59
|
self.defect = False
|
|
60
|
+
self.status_filter = []
|
|
23
61
|
|
|
24
62
|
def set_project(self, project: str):
|
|
25
63
|
self.project = project
|
|
26
64
|
|
|
27
65
|
def set_defect(self, defect):
|
|
28
66
|
self.defect = QaseUtils.parse_bool(defect)
|
|
67
|
+
|
|
68
|
+
def set_status_filter(self, status_filter: List[str]):
|
|
69
|
+
self.status_filter = status_filter
|
|
@@ -86,15 +86,34 @@ class QaseTestOps:
|
|
|
86
86
|
|
|
87
87
|
def _send_results(self) -> None:
|
|
88
88
|
if self.results:
|
|
89
|
-
#
|
|
90
|
-
self.send_semaphore.acquire()
|
|
91
|
-
self.count_running_threads += 1
|
|
89
|
+
# Filter results by status if status_filter is configured
|
|
92
90
|
results_to_send = self.results.copy()
|
|
91
|
+
|
|
92
|
+
if self.config.testops.status_filter and len(self.config.testops.status_filter) > 0:
|
|
93
|
+
filtered_results = []
|
|
94
|
+
for result in results_to_send:
|
|
95
|
+
result_status = result.get_status()
|
|
96
|
+
if result_status and result_status not in self.config.testops.status_filter:
|
|
97
|
+
filtered_results.append(result)
|
|
98
|
+
else:
|
|
99
|
+
self.logger.log_debug(f"Filtering out result '{result.title}' with status '{result_status}'")
|
|
100
|
+
|
|
101
|
+
results_to_send = filtered_results
|
|
102
|
+
self.logger.log_debug(f"Filtered {len(self.results) - len(results_to_send)} results by status filter")
|
|
103
|
+
|
|
104
|
+
if results_to_send:
|
|
105
|
+
# Acquire semaphore before starting the send operation
|
|
106
|
+
self.send_semaphore.acquire()
|
|
107
|
+
self.count_running_threads += 1
|
|
108
|
+
|
|
109
|
+
# Start a new thread for sending results
|
|
110
|
+
send_thread = threading.Thread(target=self._send_results_threaded, args=(results_to_send,))
|
|
111
|
+
send_thread.start()
|
|
112
|
+
else:
|
|
113
|
+
self.logger.log("No results to send after filtering", "info")
|
|
114
|
+
|
|
115
|
+
# Clear results regardless of filtering
|
|
93
116
|
self.results = []
|
|
94
|
-
|
|
95
|
-
# Start a new thread for sending results
|
|
96
|
-
send_thread = threading.Thread(target=self._send_results_threaded, args=(results_to_send,))
|
|
97
|
-
send_thread.start()
|
|
98
117
|
else:
|
|
99
118
|
self.logger.log("No results to send", "info")
|
|
100
119
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qase-python-commons
|
|
3
|
-
Version: 3.5.
|
|
3
|
+
Version: 3.5.4
|
|
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
|
|
@@ -22,8 +22,8 @@ Requires-Python: >=3.7
|
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
Requires-Dist: certifi>=2024.2.2
|
|
24
24
|
Requires-Dist: attrs>=23.2.0
|
|
25
|
-
Requires-Dist: qase-api-client~=1.2.
|
|
26
|
-
Requires-Dist: qase-api-v2-client~=1.2.
|
|
25
|
+
Requires-Dist: qase-api-client~=1.2.3
|
|
26
|
+
Requires-Dist: qase-api-v2-client~=1.2.2
|
|
27
27
|
Requires-Dist: more_itertools
|
|
28
28
|
Provides-Extra: testing
|
|
29
29
|
Requires-Dist: pytest; extra == "testing"
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
qase/commons/__init__.py,sha256=3HI65PJES4Q6YvtkSuRPh6tZboTETJo8wbdHlNYaePU,323
|
|
2
|
-
qase/commons/config.py,sha256=
|
|
2
|
+
qase/commons/config.py,sha256=X7PsggEZRPbrE1eb1JBM0ZmhtR4k06dqaFzcNmTG-Rw,12394
|
|
3
3
|
qase/commons/loader.py,sha256=-MMY4HgSI6q1xq3NaJoq_w4liM73qdFKjYLVCT1E7Pc,1064
|
|
4
4
|
qase/commons/logger.py,sha256=KEQr8G0eFZxlI3LJIaaNWOKD8o3NhKsZD06swXFn3FI,1313
|
|
5
5
|
qase/commons/utils.py,sha256=utPRoYyThLs2tgD1lmjkwJ9KZuE7ZjliUiZkJJWFca0,3330
|
|
6
|
-
qase/commons/client/api_v1_client.py,sha256=
|
|
6
|
+
qase/commons/client/api_v1_client.py,sha256=cxECfjM68KrjRVNr6nsTY4uOc_bBlo5qQJ10C5roEyk,10130
|
|
7
7
|
qase/commons/client/api_v2_client.py,sha256=GsIrXJcBw6GtzvJjbjMYa0tvUIxEEe4pALRN2LBMKPM,9043
|
|
8
8
|
qase/commons/client/base_api_client.py,sha256=qiK93rXXeLOmp6e3cgsLA4lmrV_rCL1BIi_xUeqFrDE,2613
|
|
9
9
|
qase/commons/exceptions/reporter.py,sha256=dP-Mwcq8HKBOjgu3YqhyULDmDGU09BmT6Fh9HjICaUc,45
|
|
@@ -23,7 +23,7 @@ qase/commons/models/config/plan.py,sha256=JbAY7qfGXYreXOLa32OLxw6z0paeCCf87-2b1m
|
|
|
23
23
|
qase/commons/models/config/qaseconfig.py,sha256=ho_22bcouoJb1f68EGffeBs_ovK3DVyfbFrYXwQFrWs,1918
|
|
24
24
|
qase/commons/models/config/report.py,sha256=g3Z2B3Tewaasjc1TMj2mkXxz0Zc1C39sHeTXH0MRM2Y,497
|
|
25
25
|
qase/commons/models/config/run.py,sha256=UeZ_1khhKSLbER3pzAl__5iKfDMErvUsXikelc31iKo,682
|
|
26
|
-
qase/commons/models/config/testops.py,sha256=
|
|
26
|
+
qase/commons/models/config/testops.py,sha256=rJ9wW-VMt-5XaoPdRUKeM9rlV0eTiRINEhEulFVczv0,1893
|
|
27
27
|
qase/commons/profilers/__init__.py,sha256=GhKT5hRbHbhAC4GhdyChA8XoAsGQOnIb8S2Z4-fdS7Q,222
|
|
28
28
|
qase/commons/profilers/db.py,sha256=Am1tvvLgJq4_A8JsuSeBGf47BD2lnSX-5KiMjSgr-Ko,391
|
|
29
29
|
qase/commons/profilers/network.py,sha256=zKNBnTQG4BMg8dn8O--tQzQLpu-qs5ADhHEnqIas0gM,4950
|
|
@@ -31,11 +31,11 @@ qase/commons/profilers/sleep.py,sha256=HT6h0R-2XHZAoBYRxS2T_KC8RrnEoVjP7MXusaE4N
|
|
|
31
31
|
qase/commons/reporters/__init__.py,sha256=J0aNLzb_MPPT_zF8BtX_w9nj_U7Ad06RGpyWK5Pxq1o,169
|
|
32
32
|
qase/commons/reporters/core.py,sha256=JyFMGhcDOr3nBlhh0Ca8Dx6BmvNBoG8m1Y4hpC0qIog,8185
|
|
33
33
|
qase/commons/reporters/report.py,sha256=ZLwtVn5gjwgJFtfbpLUO-vW3M3skEq3AhKJwtmM0nUw,4810
|
|
34
|
-
qase/commons/reporters/testops.py,sha256=
|
|
34
|
+
qase/commons/reporters/testops.py,sha256=uph_8upIt5bu4ncqyK7ehXR9NqtpZ34wGUBFLVJKOpE,7481
|
|
35
35
|
qase/commons/util/__init__.py,sha256=0sRRfrMOIPCHpk9tXM94Pj10qrk18B61qEcbLpRjw_I,74
|
|
36
36
|
qase/commons/util/host_data.py,sha256=n8o5PDs8kELCZZ5GR7Jug6LsgZHWJudU7iRmZHRdrlw,5264
|
|
37
37
|
qase/commons/validators/base.py,sha256=wwSn-4YiuXtfGMGnSKgo9Vm5hAKevVmmfd2Ro6Q7MYQ,173
|
|
38
|
-
qase_python_commons-3.5.
|
|
39
|
-
qase_python_commons-3.5.
|
|
40
|
-
qase_python_commons-3.5.
|
|
41
|
-
qase_python_commons-3.5.
|
|
38
|
+
qase_python_commons-3.5.4.dist-info/METADATA,sha256=VRTDQNbqx1qizieGvflYnqMV_EFvabeMe0ltKn2OpJ4,1857
|
|
39
|
+
qase_python_commons-3.5.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
40
|
+
qase_python_commons-3.5.4.dist-info/top_level.txt,sha256=Mn5aFk7H7Uia4s1NRDsvebu8vCrFy9nOuRIBfkIY5kQ,5
|
|
41
|
+
qase_python_commons-3.5.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|