qase-python-commons 3.1.9__py3-none-any.whl → 4.1.9__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.
Files changed (35) hide show
  1. qase/__init__.py +3 -0
  2. qase/commons/client/api_v1_client.py +269 -175
  3. qase/commons/client/api_v2_client.py +163 -26
  4. qase/commons/client/base_api_client.py +23 -6
  5. qase/commons/config.py +162 -23
  6. qase/commons/logger.py +82 -13
  7. qase/commons/models/__init__.py +0 -2
  8. qase/commons/models/attachment.py +11 -8
  9. qase/commons/models/basemodel.py +12 -3
  10. qase/commons/models/config/framework.py +17 -0
  11. qase/commons/models/config/qaseconfig.py +34 -0
  12. qase/commons/models/config/run.py +19 -0
  13. qase/commons/models/config/testops.py +45 -3
  14. qase/commons/models/external_link.py +41 -0
  15. qase/commons/models/relation.py +16 -6
  16. qase/commons/models/result.py +16 -31
  17. qase/commons/models/run.py +17 -2
  18. qase/commons/models/runtime.py +9 -0
  19. qase/commons/models/step.py +45 -12
  20. qase/commons/profilers/__init__.py +4 -3
  21. qase/commons/profilers/db.py +965 -5
  22. qase/commons/reporters/core.py +60 -10
  23. qase/commons/reporters/report.py +11 -6
  24. qase/commons/reporters/testops.py +56 -27
  25. qase/commons/status_mapping/__init__.py +12 -0
  26. qase/commons/status_mapping/status_mapping.py +237 -0
  27. qase/commons/util/__init__.py +9 -0
  28. qase/commons/util/host_data.py +147 -0
  29. qase/commons/utils.py +95 -0
  30. {qase_python_commons-3.1.9.dist-info → qase_python_commons-4.1.9.dist-info}/METADATA +16 -11
  31. qase_python_commons-4.1.9.dist-info/RECORD +45 -0
  32. {qase_python_commons-3.1.9.dist-info → qase_python_commons-4.1.9.dist-info}/WHEEL +1 -1
  33. qase/commons/models/suite.py +0 -13
  34. qase_python_commons-3.1.9.dist-info/RECORD +0 -40
  35. {qase_python_commons-3.1.9.dist-info → qase_python_commons-4.1.9.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
- from typing import Dict
1
+ from typing import Dict, Union, Optional
2
2
 
3
3
  import certifi
4
- from qase.api_client_v2 import ResultsApi
4
+ from qase.api_client_v2 import ResultsApi, ResultCreateFields
5
5
  from qase.api_client_v2.api_client import ApiClient
6
6
  from qase.api_client_v2.configuration import Configuration
7
7
  from qase.api_client_v2.models.create_results_request_v2 import CreateResultsRequestV2
@@ -16,14 +16,20 @@ from qase.api_client_v2.models.result_steps_type import ResultStepsType
16
16
  from .api_v1_client import ApiV1Client
17
17
  from .. import Logger
18
18
  from ..exceptions.reporter import ReporterException
19
+ from ..models.config.framework import Video, Trace
19
20
  from ..models import Attachment, Result
20
21
  from ..models.config.qaseconfig import QaseConfig
21
22
  from ..models.step import StepType, Step
23
+ from ..util.host_data import HostData
22
24
 
23
25
 
24
26
  class ApiV2Client(ApiV1Client):
25
- def __init__(self, config: QaseConfig, logger: Logger):
27
+ def __init__(self, config: QaseConfig, logger: Logger, host_data: Optional[HostData] = None,
28
+ framework: Union[str, None] = None, reporter_name: Union[str, None] = None):
26
29
  ApiV1Client.__init__(self, config, logger)
30
+ self.host_data = host_data or {}
31
+ self.framework = framework
32
+ self.reporter_name = reporter_name
27
33
 
28
34
  try:
29
35
  self.logger.log_debug("Preparing API V2 client")
@@ -39,10 +45,84 @@ class ApiV2Client(ApiV1Client):
39
45
  self.web = f'https://{host}'
40
46
 
41
47
  self.client_v2 = ApiClient(configuration)
48
+
49
+ # Add X-Client and X-Platform headers
50
+ self._add_client_headers()
51
+
42
52
  self.logger.log_debug("API V2 client prepared")
43
53
  except Exception as e:
44
54
  self.logger.log(f"Error at preparing API V2 client: {e}", "error")
45
55
  raise ReporterException(e)
56
+
57
+ def _add_client_headers(self):
58
+ """Add X-Client and X-Platform headers to API client"""
59
+ try:
60
+ # Use host_data passed from Core reporter
61
+ host_data = self.host_data
62
+
63
+ # Use framework and reporter_name for names in X-Client header
64
+ framework = self.framework
65
+ reporter_name = self.reporter_name
66
+
67
+ # Build X-Client header
68
+ # Format: reporter=qase-pytest;reporter_version=v1.0.0;framework=pytest;framework_version=7.0.0;client_version_v1=v1.0.0;client_version_v2=v2.0.0;core_version=v1.5.0
69
+ x_client_parts = []
70
+
71
+ if reporter_name:
72
+ x_client_parts.append(f"reporter={reporter_name}")
73
+ reporter_version = host_data.get('reporter', '')
74
+ if reporter_version:
75
+ x_client_parts.append(f"reporter_version={reporter_version}")
76
+
77
+ if framework:
78
+ x_client_parts.append(f"framework={framework}")
79
+ framework_version = host_data.get('framework', '')
80
+ if framework_version:
81
+ x_client_parts.append(f"framework_version={framework_version}")
82
+
83
+ client_v1_version = host_data.get('apiClientV1', '')
84
+ if client_v1_version:
85
+ x_client_parts.append(f"client_version_v1={client_v1_version}")
86
+
87
+ client_v2_version = host_data.get('apiClientV2', '')
88
+ if client_v2_version:
89
+ x_client_parts.append(f"client_version_v2={client_v2_version}")
90
+
91
+ core_version = host_data.get('commons', '')
92
+ if core_version:
93
+ x_client_parts.append(f"core_version={core_version}")
94
+
95
+ x_client = ";".join(x_client_parts)
96
+
97
+ # Build X-Platform header
98
+ # Format: os=Linux;arch=aarch64;python=3.9.0;pip=22.0.0
99
+ x_platform_parts = []
100
+
101
+ os_name = host_data.get('system', '')
102
+ if os_name:
103
+ x_platform_parts.append(f"os={os_name}")
104
+
105
+ arch = host_data.get('arch', '')
106
+ if arch:
107
+ x_platform_parts.append(f"arch={arch}")
108
+
109
+ python_version = host_data.get('python', '')
110
+ if python_version:
111
+ x_platform_parts.append(f"python={python_version}")
112
+
113
+ pip_version = host_data.get('pip', '')
114
+ if pip_version:
115
+ x_platform_parts.append(f"pip={pip_version}")
116
+
117
+ x_platform = ";".join(x_platform_parts)
118
+
119
+ # Add headers to client
120
+ if x_client:
121
+ self.client_v2.default_headers['X-Client'] = x_client
122
+ if x_platform:
123
+ self.client_v2.default_headers['X-Platform'] = x_platform
124
+ except Exception as e:
125
+ self.logger.log(f"Error adding client headers: {e}", "error")
46
126
 
47
127
  def send_results(self, project_code: str, run_id: str, results: []) -> None:
48
128
  api_results = ResultsApi(self.client_v2)
@@ -55,8 +135,15 @@ class ApiV2Client(ApiV1Client):
55
135
  def _prepare_result(self, project_code: str, result: Result) -> ResultCreate:
56
136
  attached = []
57
137
  if result.attachments:
58
- for attachment in result.attachments:
59
- attached.extend(self._upload_attachment(project_code, attachment))
138
+ # Collect all attachments that should be uploaded
139
+ attachments_to_upload = [
140
+ attachment for attachment in result.attachments
141
+ if not self.__should_skip_attachment(attachment, result)
142
+ ]
143
+ if attachments_to_upload:
144
+ attach_id = self._upload_attachment(project_code, attachments_to_upload)
145
+ if attach_id:
146
+ attached.extend(attach_id)
60
147
 
61
148
  steps = []
62
149
  for step in result.steps:
@@ -71,27 +158,29 @@ class ApiV2Client(ApiV1Client):
71
158
  result_model_v2 = ResultCreate(
72
159
  title=result.get_title(),
73
160
  signature=result.signature,
74
- testops_id=result.get_testops_id(),
75
- execution=ResultExecution(start_time=result.execution.start_time, end_time=result.execution.end_time,
76
- status=result.execution.status, duration=result.execution.duration,
161
+ testops_ids=result.get_testops_ids(),
162
+ execution=ResultExecution(status=result.execution.status, duration=result.execution.duration,
163
+ start_time=result.execution.start_time, end_time=result.execution.end_time,
77
164
  stacktrace=result.execution.stacktrace, thread=result.execution.thread),
78
- fields=result.fields,
165
+ fields=ResultCreateFields.from_dict(result.fields),
79
166
  attachments=[attach.hash for attach in attached],
80
167
  steps=steps,
81
- step_type=ResultStepsType.CLASSIC,
82
- params=result.params,
83
- muted=False,
168
+ steps_type=ResultStepsType.CLASSIC,
169
+ params=result.params if not self.config.exclude_params else {key: value for key, value in result.params.items() if key not in self.config.exclude_params},
170
+ param_groups=result.param_groups,
84
171
  message=result.message,
172
+ defect=self.config.testops.defect,
85
173
  )
86
174
 
87
- if result.get_suite_title():
175
+ if result.relations is not None and result.relations.suite is not None and len(
176
+ result.relations.suite.data) != 0:
88
177
  data = []
89
178
  root_suite = self.config.root_suite
90
179
  if root_suite:
91
180
  data.append(RelationSuiteItem(title=root_suite))
92
181
 
93
- for suite in result.get_suite_title().split("."):
94
- data.append(RelationSuiteItem(title=suite))
182
+ for raw in result.relations.suite.data:
183
+ data.append(RelationSuiteItem(title=raw.title))
95
184
 
96
185
  result_model_v2.relations = ResultRelations(suite=RelationSuite(data=data))
97
186
 
@@ -104,8 +193,16 @@ class ApiV2Client(ApiV1Client):
104
193
 
105
194
  try:
106
195
  prepared_step = {'execution': {}, 'data': {}, 'steps': []}
107
- prepared_step['execution']['status'] = ResultStepStatus(step.execution.status)
108
- prepared_step['execution']['duration'] = step.execution.duration
196
+ if step.execution.status == 'untested':
197
+ prepared_step['execution']['status'] = ResultStepStatus('skipped')
198
+ prepared_step['execution']['duration'] = 0
199
+ prepared_step['execution']['start_time'] = None
200
+ prepared_step['execution']['end_time'] = None
201
+ else:
202
+ prepared_step['execution']['status'] = ResultStepStatus(step.execution.status)
203
+ prepared_step['execution']['duration'] = step.execution.duration
204
+ prepared_step['execution']['start_time'] = step.execution.start_time
205
+ prepared_step['execution']['end_time'] = step.execution.end_time
109
206
 
110
207
  if step.step_type == StepType.TEXT:
111
208
  prepared_step['data']['action'] = step.data.action
@@ -116,31 +213,60 @@ class ApiV2Client(ApiV1Client):
116
213
  prepared_step['data']['action'] = step.data.request_method + " " + step.data.request_url
117
214
 
118
215
  if step.data.request_body:
119
- step.attachments.append(
216
+ step.add_attachment(
120
217
  Attachment(file_name='request_body.txt', content=step.data.request_body, mime_type='text/plain',
121
218
  temporary=True))
122
219
  if step.data.request_headers:
123
- step.attachments.append(
220
+ step.add_attachment(
124
221
  Attachment(file_name='request_headers.txt', content=step.data.request_headers,
125
222
  mime_type='text/plain', temporary=True))
126
223
  if step.data.response_body:
127
- step.attachments.append(Attachment(file_name='response_body.txt', content=step.data.response_body,
128
- mime_type='text/plain', temporary=True))
224
+ step.add_attachment(Attachment(file_name='response_body.txt', content=step.data.response_body,
225
+ mime_type='text/plain', temporary=True))
129
226
  if step.data.response_headers:
130
- step.attachments.append(
227
+ step.add_attachment(
131
228
  Attachment(file_name='response_headers.txt', content=step.data.response_headers,
132
229
  mime_type='text/plain', temporary=True))
133
230
 
134
231
  if step.step_type == StepType.GHERKIN:
135
- prepared_step['data']['action'] = step.data.keyword
232
+ action = step.data.keyword
233
+ if step.data.keyword != step.data.name:
234
+ action += " " + step.data.name
235
+ prepared_step['data']['action'] = action
236
+ if step.data.data:
237
+ prepared_step['data']['input_data'] = step.data.data
136
238
 
137
239
  if step.step_type == StepType.SLEEP:
138
240
  prepared_step['data']['action'] = f"Sleep for {step.data.duration} seconds"
139
241
 
140
- if step.attachments:
242
+ if step.step_type == StepType.DB_QUERY:
243
+ # Format database query as action
244
+ action_parts = []
245
+ if step.data.database_type:
246
+ action_parts.append(f"[{step.data.database_type}]")
247
+ action_parts.append(step.data.query)
248
+ prepared_step['data']['action'] = " ".join(action_parts)
249
+
250
+ # Add expected_result if available
251
+ if step.data.expected_result:
252
+ prepared_step['data']['expected_result'] = step.data.expected_result
253
+
254
+ # Add connection info and execution time as input_data
255
+ info_parts = []
256
+ if step.data.connection_info:
257
+ info_parts.append(f"Connection: {step.data.connection_info}")
258
+ if step.data.execution_time is not None:
259
+ info_parts.append(f"Execution time: {step.data.execution_time:.3f}s")
260
+ if step.data.rows_affected is not None:
261
+ info_parts.append(f"Rows affected: {step.data.rows_affected}")
262
+ if info_parts:
263
+ prepared_step['data']['input_data'] = " | ".join(info_parts)
264
+
265
+ if step.execution.attachments:
141
266
  uploaded_attachments = []
142
- for file in step.attachments:
143
- uploaded_attachments.extend(self._upload_attachment(project_code, file))
267
+ attach_id = self._upload_attachment(project_code, step.execution.attachments)
268
+ if attach_id:
269
+ uploaded_attachments.extend(attach_id)
144
270
 
145
271
  prepared_step['execution']['attachments'] = [attach.hash for attach in uploaded_attachments]
146
272
 
@@ -152,3 +278,14 @@ class ApiV2Client(ApiV1Client):
152
278
  except Exception as e:
153
279
  self.logger.log(f"Error at preparing step: {e}", "error")
154
280
  raise ReporterException(e)
281
+
282
+ def __should_skip_attachment(self, attachment, result):
283
+ if (self.config.framework.playwright.video == Video.failed and
284
+ result.execution.status != 'failed' and
285
+ attachment.file_name == 'video.webm'):
286
+ return True
287
+ if (self.config.framework.playwright.trace == Trace.failed and
288
+ result.execution.status != 'failed' and
289
+ attachment.file_name == 'trace.zip'):
290
+ return True
291
+ return False
@@ -1,7 +1,8 @@
1
1
  import abc
2
- from typing import Union
2
+ from typing import Union, List
3
3
 
4
4
  from qase.api_client_v1 import Project, AttachmentGet
5
+ from qase.api_client_v1.models.attachmentupload import Attachmentupload
5
6
 
6
7
  from ..models import Attachment
7
8
 
@@ -30,7 +31,7 @@ class BaseApiClient(abc.ABC):
30
31
  pass
31
32
 
32
33
  @abc.abstractmethod
33
- def complete_run(self, project_code: str, run_id: str) -> None:
34
+ def complete_run(self, project_code: str, run_id: int) -> None:
34
35
  """
35
36
  Complete a test run in Qase TestOps
36
37
 
@@ -41,13 +42,18 @@ class BaseApiClient(abc.ABC):
41
42
  pass
42
43
 
43
44
  @abc.abstractmethod
44
- def _upload_attachment(self, project_code: str, attachment: Attachment) -> Union[AttachmentGet, None]:
45
+ def _upload_attachment(self, project_code: str, attachment: Union[Attachment, List[Attachment]]) -> List[Attachmentupload]:
45
46
  """
46
- Upload an attachment to Qase TestOps
47
+ Upload one or multiple attachments to Qase TestOps with batching support.
48
+
49
+ The method automatically groups attachments into batches respecting the following limits:
50
+ - Up to 32 MB per file
51
+ - Up to 128 MB per single request
52
+ - Up to 20 files per single request
47
53
 
48
54
  :param project_code: project code
49
- :param attachment: attachment model
50
- :return: attachment data or None if attachment not uploaded
55
+ :param attachment: single attachment or list of attachments
56
+ :return: list of uploaded attachment data
51
57
  """
52
58
  pass
53
59
 
@@ -86,3 +92,14 @@ class BaseApiClient(abc.ABC):
86
92
  :return: None
87
93
  """
88
94
  pass
95
+
96
+ @abc.abstractmethod
97
+ def enable_public_report(self, project_code: str, run_id: int) -> str:
98
+ """
99
+ Enable public report for a test run and return the public link
100
+
101
+ :param project_code: project code
102
+ :param run_id: test run id
103
+ :return: public report link or None if failed
104
+ """
105
+ pass
qase/commons/config.py CHANGED
@@ -1,18 +1,28 @@
1
1
  import os
2
2
  import json
3
- from .logger import Logger
3
+ from .logger import Logger, LoggingOptions
4
4
  from .models.config.qaseconfig import QaseConfig, Mode
5
+ from .utils import QaseUtils
5
6
 
6
7
 
7
8
  class ConfigManager:
8
9
 
9
10
  def __init__(self, config_file='./qase.config.json'):
10
- self.logger = Logger()
11
11
  self.__config_file = config_file
12
12
  self.config = QaseConfig()
13
13
 
14
+ # Initialize temporary logger for error handling during config loading
15
+ self.logger = Logger(debug=False)
16
+
14
17
  self.__load_file_config()
15
18
  self.__load_env_config()
19
+
20
+ # Re-initialize logger with proper logging options after config is loaded
21
+ logging_options = LoggingOptions(
22
+ console=self.config.logging.console if self.config.logging.console is not None else True,
23
+ file=self.config.logging.file if self.config.logging.file is not None else self.config.debug
24
+ )
25
+ self.logger = Logger(debug=self.config.debug, logging_options=logging_options)
16
26
 
17
27
  def validate_config(self):
18
28
  errors: list[str] = []
@@ -53,15 +63,26 @@ class ConfigManager:
53
63
  if config.get("profilers"):
54
64
  self.config.set_profilers(config.get("profilers"))
55
65
 
56
- if config.get("debug"):
66
+ if config.get("debug") is not None:
57
67
  self.config.set_debug(
58
68
  config.get("debug")
59
69
  )
60
70
 
71
+ if config.get("excludeParams"):
72
+ self.config.set_exclude_params(
73
+ config.get("excludeParams")
74
+ )
75
+
76
+ if config.get("statusMapping"):
77
+ self.config.set_status_mapping(
78
+ config.get("statusMapping")
79
+ )
80
+
61
81
  if config.get("executionPlan"):
62
82
  execution_plan = config.get("executionPlan")
63
83
  if execution_plan.get("path"):
64
- self.config.execution_plan.set_path(execution_plan.get("path"))
84
+ self.config.execution_plan.set_path(
85
+ execution_plan.get("path"))
65
86
 
66
87
  if config.get("testops"):
67
88
  testops = config.get("testops")
@@ -70,24 +91,22 @@ class ConfigManager:
70
91
  api = testops.get("api")
71
92
 
72
93
  if api.get("host"):
73
- self.config.testops.api.set_host(api.get("host"))
94
+ self.config.testops.api.set_host(
95
+ api.get("host"))
74
96
 
75
97
  if api.get("token"):
76
- self.config.testops.api.set_token(api.get("token"))
98
+ self.config.testops.api.set_token(
99
+ api.get("token"))
77
100
 
78
101
  if testops.get("project"):
79
- self.config.testops.set_project(testops.get("project"))
102
+ self.config.testops.set_project(
103
+ testops.get("project"))
80
104
 
81
- if testops.get("defect"):
105
+ if testops.get("defect") is not None:
82
106
  self.config.testops.set_defect(
83
107
  testops.get("defect")
84
108
  )
85
109
 
86
- if testops.get("useV2"):
87
- self.config.testops.set_use_v2(
88
- testops.get("useV2")
89
- )
90
-
91
110
  if testops.get("plan"):
92
111
  plan = testops.get("plan")
93
112
 
@@ -101,21 +120,59 @@ class ConfigManager:
101
120
  self.config.testops.run.set_id(run.get("id"))
102
121
 
103
122
  if run.get("title"):
104
- self.config.testops.run.set_title(run.get("title"))
123
+ self.config.testops.run.set_title(
124
+ run.get("title"))
105
125
 
106
126
  if run.get("description"):
107
- self.config.testops.run.set_description(run.get("description"))
127
+ self.config.testops.run.set_description(
128
+ run.get("description"))
108
129
 
109
- if run.get("complete"):
130
+ if run.get("complete") is not None:
110
131
  self.config.testops.run.set_complete(
111
132
  run.get("complete")
112
133
  )
113
134
 
135
+ if run.get("tags"):
136
+ self.config.testops.run.set_tags(
137
+ [tag.strip() for tag in run.get("tags")])
138
+
139
+ if run.get("externalLink"):
140
+ self.config.testops.run.set_external_link(
141
+ run.get("externalLink"))
142
+
114
143
  if testops.get("batch"):
115
144
  batch = testops.get("batch")
116
145
 
117
146
  if batch.get("size"):
118
- self.config.testops.batch.set_size(batch.get("size"))
147
+ self.config.testops.batch.set_size(
148
+ batch.get("size"))
149
+
150
+ if testops.get("configurations"):
151
+ configurations = testops.get("configurations")
152
+
153
+ if configurations.get("values"):
154
+ values = configurations.get("values")
155
+ for value in values:
156
+ if value.get("name") and value.get("value"):
157
+ self.config.testops.configurations.add_value(
158
+ value.get("name"), value.get("value"))
159
+
160
+ if configurations.get("createIfNotExists") is not None:
161
+ self.config.testops.configurations.set_create_if_not_exists(
162
+ configurations.get("createIfNotExists"))
163
+
164
+ if testops.get("statusFilter"):
165
+ status_filter = testops.get("statusFilter")
166
+ if isinstance(status_filter, list):
167
+ self.config.testops.set_status_filter(status_filter)
168
+ elif isinstance(status_filter, str):
169
+ # Parse comma-separated string
170
+ self.config.testops.set_status_filter([s.strip() for s in status_filter.split(',')])
171
+
172
+ if testops.get("showPublicReportLink") is not None:
173
+ self.config.testops.set_show_public_report_link(
174
+ testops.get("showPublicReportLink")
175
+ )
119
176
 
120
177
  if config.get("report"):
121
178
  report = config.get("report")
@@ -127,7 +184,8 @@ class ConfigManager:
127
184
  connection = report.get("connection")
128
185
 
129
186
  if connection.get("path"):
130
- self.config.report.connection.set_path(connection.get("path"))
187
+ self.config.report.connection.set_path(
188
+ connection.get("path"))
131
189
 
132
190
  if connection.get("format"):
133
191
  self.config.report.connection.set_format(
@@ -140,11 +198,27 @@ class ConfigManager:
140
198
  if framework.get("pytest"):
141
199
  pytest = framework.get("pytest")
142
200
 
143
- if pytest.get("captureLogs"):
201
+ if pytest.get("captureLogs") is not None:
144
202
  self.config.framework.pytest.set_capture_logs(
145
203
  pytest.get("captureLogs")
146
204
  )
147
205
 
206
+ if pytest.get("xfailStatus"):
207
+ xfail_status = pytest.get("xfailStatus")
208
+
209
+ if xfail_status.get("xfail"):
210
+ self.config.framework.pytest.xfail_status.set_xfail(
211
+ xfail_status.get("xfail")
212
+ )
213
+
214
+ if xfail_status.get("xpass"):
215
+ self.config.framework.pytest.xfail_status.set_xpass(
216
+ xfail_status.get("xpass")
217
+ )
218
+
219
+ if config.get("logging"):
220
+ self.config.set_logging(config.get("logging"))
221
+
148
222
  except Exception as e:
149
223
  self.logger.log("Failed to load config from file", "error")
150
224
 
@@ -169,6 +243,23 @@ class ConfigManager:
169
243
  if key == 'QASE_DEBUG':
170
244
  self.config.set_debug(value)
171
245
 
246
+ if key == 'QASE_EXCLUDE_PARAMS':
247
+ self.config.set_exclude_params(
248
+ [param.strip() for param in value.split(',')])
249
+
250
+ if key == 'QASE_STATUS_MAPPING':
251
+ # Parse status mapping from environment variable
252
+ # Format: "source1=target1,source2=target2"
253
+ if value:
254
+ mapping_dict = {}
255
+ pairs = value.split(',')
256
+ for pair in pairs:
257
+ pair = pair.strip()
258
+ if pair and '=' in pair:
259
+ source_status, target_status = pair.split('=', 1)
260
+ mapping_dict[source_status.strip()] = target_status.strip()
261
+ self.config.set_status_mapping(mapping_dict)
262
+
172
263
  if key == 'QASE_EXECUTION_PLAN_PATH':
173
264
  self.config.execution_plan.set_path(value)
174
265
 
@@ -185,10 +276,10 @@ class ConfigManager:
185
276
  self.config.testops.set_defect(value)
186
277
 
187
278
  if key == 'QASE_TESTOPS_PLAN_ID':
188
- self.config.testops.plan.set_id(value)
279
+ self.config.testops.plan.set_id(int(value.strip()))
189
280
 
190
281
  if key == 'QASE_TESTOPS_RUN_ID':
191
- self.config.testops.run.set_id(value)
282
+ self.config.testops.run.set_id(int(value.strip()))
192
283
 
193
284
  if key == 'QASE_TESTOPS_RUN_TITLE':
194
285
  self.config.testops.run.set_title(value)
@@ -199,8 +290,44 @@ class ConfigManager:
199
290
  if key == 'QASE_TESTOPS_RUN_COMPLETE':
200
291
  self.config.testops.run.set_complete(value)
201
292
 
293
+ if key == 'QASE_TESTOPS_RUN_TAGS':
294
+ self.config.testops.run.set_tags(
295
+ [tag.strip() for tag in value.split(',')])
296
+
297
+ if key == 'QASE_TESTOPS_RUN_EXTERNAL_LINK_TYPE':
298
+ if not self.config.testops.run.external_link:
299
+ from .models.external_link import ExternalLinkConfig
300
+ self.config.testops.run.external_link = ExternalLinkConfig()
301
+ self.config.testops.run.external_link.set_type(value)
302
+
303
+ if key == 'QASE_TESTOPS_RUN_EXTERNAL_LINK_URL':
304
+ if not self.config.testops.run.external_link:
305
+ from .models.external_link import ExternalLinkConfig
306
+ self.config.testops.run.external_link = ExternalLinkConfig()
307
+ self.config.testops.run.external_link.set_link(value)
308
+
202
309
  if key == 'QASE_TESTOPS_BATCH_SIZE':
203
- self.config.testops.batch.set_size(value)
310
+ self.config.testops.batch.set_size(int(value.strip()))
311
+
312
+ if key == 'QASE_TESTOPS_CONFIGURATIONS_VALUES':
313
+ # Parse configurations from environment variable
314
+ # Format: "group1=value1,group2=value2"
315
+ if value:
316
+ config_pairs = value.split(',')
317
+ for pair in config_pairs:
318
+ if '=' in pair:
319
+ name, config_value = pair.split('=', 1)
320
+ self.config.testops.configurations.add_value(name.strip(), config_value.strip())
321
+
322
+ if key == 'QASE_TESTOPS_CONFIGURATIONS_CREATE_IF_NOT_EXISTS':
323
+ self.config.testops.configurations.set_create_if_not_exists(value)
324
+
325
+ if key == 'QASE_TESTOPS_STATUS_FILTER':
326
+ # Parse comma-separated string
327
+ self.config.testops.set_status_filter([s.strip() for s in value.split(',')])
328
+
329
+ if key == 'QASE_TESTOPS_SHOW_PUBLIC_REPORT_LINK':
330
+ self.config.testops.set_show_public_report_link(value)
204
331
 
205
332
  if key == 'QASE_REPORT_DRIVER':
206
333
  self.config.report.set_driver(value)
@@ -214,5 +341,17 @@ class ConfigManager:
214
341
  if key == 'QASE_PYTEST_CAPTURE_LOGS':
215
342
  self.config.framework.pytest.set_capture_logs(value)
216
343
 
344
+ if key == 'QASE_PYTEST_XFAIL_STATUS_XFAIL':
345
+ self.config.framework.pytest.xfail_status.set_xfail(value)
346
+
347
+ if key == 'QASE_PYTEST_XFAIL_STATUS_XPASS':
348
+ self.config.framework.pytest.xfail_status.set_xpass(value)
349
+
350
+ if key == 'QASE_LOGGING_CONSOLE':
351
+ self.config.logging.set_console(QaseUtils.parse_bool(value))
352
+
353
+ if key == 'QASE_LOGGING_FILE':
354
+ self.config.logging.set_file(QaseUtils.parse_bool(value))
355
+
217
356
  except Exception as e:
218
- self.logger.log("Failed to load config from env vars {e}", "error")
357
+ self.logger.log(f"Failed to load config from env vars {e}", "error")