qase-python-commons 3.1.3__py3-none-any.whl → 4.1.3__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.

Files changed (36) hide show
  1. qase/__init__.py +3 -0
  2. qase/commons/client/api_v1_client.py +169 -143
  3. qase/commons/client/api_v2_client.py +77 -23
  4. qase/commons/client/base_api_client.py +12 -1
  5. qase/commons/config.py +159 -20
  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 +61 -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 +15 -1
  19. qase/commons/models/step.py +43 -11
  20. qase/commons/profilers/__init__.py +4 -3
  21. qase/commons/profilers/db.py +965 -5
  22. qase/commons/profilers/network.py +5 -1
  23. qase/commons/reporters/core.py +50 -9
  24. qase/commons/reporters/report.py +11 -6
  25. qase/commons/reporters/testops.py +56 -22
  26. qase/commons/status_mapping/__init__.py +12 -0
  27. qase/commons/status_mapping/status_mapping.py +237 -0
  28. qase/commons/util/__init__.py +9 -0
  29. qase/commons/util/host_data.py +140 -0
  30. qase/commons/utils.py +95 -0
  31. {qase_python_commons-3.1.3.dist-info → qase_python_commons-4.1.3.dist-info}/METADATA +16 -11
  32. qase_python_commons-4.1.3.dist-info/RECORD +45 -0
  33. {qase_python_commons-3.1.3.dist-info → qase_python_commons-4.1.3.dist-info}/WHEEL +1 -1
  34. qase/commons/models/suite.py +0 -13
  35. qase_python_commons-3.1.3.dist-info/RECORD +0 -40
  36. {qase_python_commons-3.1.3.dist-info → qase_python_commons-4.1.3.dist-info}/top_level.txt +0 -0
qase/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """
2
+ Qase Python Commons package.
3
+ """
@@ -1,21 +1,24 @@
1
- from typing import Dict, Union
1
+ from datetime import datetime, timezone
2
+ from typing import Union
2
3
 
3
4
  import certifi
4
5
  from qase.api_client_v1 import ApiClient, ProjectsApi, Project, EnvironmentsApi, RunsApi, AttachmentsApi, \
5
- AttachmentGet, RunCreate, ResultsApi, ResultcreateBulk
6
+ AttachmentGet, RunCreate, ConfigurationsApi, ConfigurationCreate, ConfigurationGroupCreate, RunPublic
6
7
  from qase.api_client_v1.configuration import Configuration
7
8
  from .. import Logger
8
9
  from .base_api_client import BaseApiClient
9
10
  from ..exceptions.reporter import ReporterException
10
- from ..models import Attachment, Result, Step
11
+ from ..models import Attachment
12
+ from ..models.config.framework import Video, Trace
11
13
  from ..models.config.qaseconfig import QaseConfig
12
- from ..models.step import StepType
14
+ from ..models.config.testops import ConfigurationValue
13
15
 
14
16
 
15
17
  class ApiV1Client(BaseApiClient):
16
18
  def __init__(self, config: QaseConfig, logger: Logger):
17
19
  self.logger = logger
18
20
  self.config = config
21
+ self.__authors = {}
19
22
 
20
23
  try:
21
24
  self.logger.log_debug("Preparing API client")
@@ -64,7 +67,74 @@ class ApiV1Client(BaseApiClient):
64
67
  self.logger.log("Exception when calling EnvironmentsApi->get_environments: %s\n" % e, "error")
65
68
  raise ReporterException(e)
66
69
 
67
- def complete_run(self, project_code: str, run_id: str) -> None:
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
+
137
+ def complete_run(self, project_code: str, run_id: int) -> None:
68
138
  api_runs = RunsApi(self.client)
69
139
  self.logger.log_debug(f"Completing run {run_id}")
70
140
  res = api_runs.get_run(project_code, run_id).result
@@ -73,7 +143,7 @@ class ApiV1Client(BaseApiClient):
73
143
  return
74
144
  try:
75
145
  api_runs.complete_run(project_code, run_id)
76
- self.logger.log(f"Run {run_id} was completed successfully", "info")
146
+ self.logger.log(f"Test run link: {self.web}/run/{project_code}/dashboard/{run_id}", "info")
77
147
  except Exception as e:
78
148
  self.logger.log(f"Error at completing run {run_id}: {e}", "error")
79
149
  raise ReporterException(e)
@@ -88,27 +158,46 @@ class ApiV1Client(BaseApiClient):
88
158
 
89
159
  except Exception as e:
90
160
  self.logger.log(f"Error at uploading attachment: {e}", "error")
91
- raise ReporterException(e)
161
+ return None
92
162
 
93
163
  def create_test_run(self, project_code: str, title: str, description: str, plan_id=None,
94
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
+
95
174
  kwargs = dict(
96
175
  title=title,
97
176
  description=description,
98
177
  environment_id=(int(environment_id) if environment_id else None),
99
178
  plan_id=(int(plan_id) if plan_id else plan_id),
100
- is_autotest=True
179
+ is_autotest=True,
180
+ start_time=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S"),
181
+ tags=self.config.testops.run.tags
101
182
  )
102
- self.logger.log_debug(f"Creating test run with parameters: {kwargs}")
183
+
184
+ # Add configurations if any found
185
+ if configuration_ids:
186
+ kwargs['configurations'] = configuration_ids
187
+
103
188
  try:
104
189
  result = RunsApi(self.client).create_run(
105
190
  code=project_code,
106
191
  run_create=RunCreate(**{k: v for k, v in kwargs.items() if v is not None})
107
192
  )
108
193
 
109
- self.logger.log(f"Test run was created: {self.web}/run/{project_code}/dashboard/{result.result.id}", "info")
194
+ run_id = result.result.id
195
+
196
+ # Update external link if configured
197
+ if self.config.testops.run.external_link and run_id:
198
+ self.update_external_link(project_code, run_id)
110
199
 
111
- return result.result.id
200
+ return run_id
112
201
 
113
202
  except Exception as e:
114
203
  self.logger.log(f"Error at creating test run: {e}", "error")
@@ -121,139 +210,76 @@ class ApiV1Client(BaseApiClient):
121
210
  return True
122
211
  return False
123
212
 
124
- def send_results(self, project_code: str, run_id: str, results: []) -> None:
125
- api_results = ResultsApi(self.client)
126
- results_to_send = [self._prepare_result(project_code, result) for result in results]
127
- self.logger.log_debug(f"Sending results for run {run_id}: {results_to_send}")
128
- api_results.create_result_bulk(
129
- code=project_code,
130
- id=run_id,
131
- resultcreate_bulk=ResultcreateBulk(
132
- results=results_to_send
133
- )
134
- )
135
- self.logger.log_debug(f"Results for run {run_id} sent successfully")
136
-
137
- def _prepare_result(self, project_code: str, result: Result) -> Dict:
138
- attached = []
139
- if result.attachments:
140
- for attachment in result.attachments:
141
- attached.extend(self._upload_attachment(project_code, attachment))
142
-
143
- steps = []
144
- for step in result.steps:
145
- prepared = self._prepare_step(project_code, step)
146
- steps.append(prepared)
147
-
148
- case_data = {
149
- "title": result.get_title(),
150
- "description": result.get_field('description'),
151
- "preconditions": result.get_field('preconditions'),
152
- "postconditions": result.get_field('postconditions'),
153
- }
154
-
155
- for key, param in result.params.items():
156
- # Hack to match old TestOps API
157
- if param == "":
158
- result.params[key] = "empty"
159
-
160
- if result.get_field('severity'):
161
- case_data["severity"] = result.get_field('severity')
162
-
163
- if result.get_field('priority'):
164
- case_data["priority"] = result.get_field('priority')
165
-
166
- if result.get_field('layer'):
167
- case_data["layer"] = result.get_field('layer')
168
-
169
- suite = None
170
- if result.get_suite_title():
171
- suite = "\t".join(result.get_suite_title().split("."))
172
-
173
- if result.get_field('suite'):
174
- suite = result.get_field('suite')
175
-
176
- root_suite = self.config.root_suite
177
- if root_suite:
178
- suite = f"{root_suite}\t{suite}"
179
-
180
- if suite:
181
- case_data["suite_title"] = suite
182
-
183
- result_model = {
184
- "status": result.execution.status,
185
- "stacktrace": result.execution.stacktrace,
186
- "time_ms": result.execution.duration,
187
- "comment": result.message,
188
- "attachments": [attach.hash for attach in attached],
189
- "steps": steps,
190
- "param": result.params,
191
- "param_groups": result.param_groups,
192
- "defect": self.config.testops.defect,
193
- "case": case_data
194
- }
195
-
196
- test_ops_id = result.get_testops_id()
197
-
198
- if test_ops_id:
199
- result_model["case_id"] = test_ops_id
200
-
201
- self.logger.log_debug(f"Prepared result: {result_model}")
202
-
203
- return result_model
204
-
205
- def _prepare_step(self, project_code: str, step: Step) -> Dict:
206
- prepared_children = []
207
-
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
+ """
208
221
  try:
209
- prepared_step = {"time": step.execution.duration, "status": step.execution.status}
210
-
211
- if step.execution.status == 'untested':
212
- prepared_step["status"] = 'passed'
213
-
214
- if step.execution.status == 'skipped':
215
- prepared_step["status"] = 'blocked'
216
-
217
- if step.step_type == StepType.TEXT:
218
- prepared_step['action'] = step.data.action
219
- if step.data.expected_result:
220
- prepared_step['expected_result'] = step.data.expected_result
221
-
222
- if step.step_type == StepType.REQUEST:
223
- prepared_step['action'] = step.data.request_method + " " + step.data.request_url
224
- if step.data.request_body:
225
- step.attachments.append(
226
- Attachment(file_name='request_body.txt', content=step.data.request_body, mime_type='text/plain',
227
- temporary=True))
228
- if step.data.request_headers:
229
- step.attachments.append(
230
- Attachment(file_name='request_headers.txt', content=step.data.request_headers,
231
- mime_type='text/plain', temporary=True))
232
- if step.data.response_body:
233
- step.attachments.append(Attachment(file_name='response_body.txt', content=step.data.response_body,
234
- mime_type='text/plain', temporary=True))
235
- if step.data.response_headers:
236
- step.attachments.append(
237
- Attachment(file_name='response_headers.txt', content=step.data.response_headers,
238
- mime_type='text/plain', temporary=True))
239
-
240
- if step.step_type == StepType.GHERKIN:
241
- prepared_step['action'] = step.data.keyword
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
242
243
 
243
- if step.step_type == StepType.SLEEP:
244
- prepared_step['action'] = f"Sleep for {step.data.duration} seconds"
244
+ def update_external_link(self, project_code: str, run_id: int):
245
+ """Update external link for a test run"""
246
+ try:
247
+ from qase.api_client_v1.models.runexternal_issues import RunexternalIssues
248
+ from qase.api_client_v1.models.runexternal_issues_links_inner import RunexternalIssuesLinksInner
249
+
250
+ external_link = self.config.testops.run.external_link
251
+ api_type = external_link.to_api_type()
252
+
253
+ run_external_issues = RunexternalIssues(
254
+ type=api_type,
255
+ links=[
256
+ RunexternalIssuesLinksInner(
257
+ run_id=run_id,
258
+ external_issue=external_link.link
259
+ )
260
+ ]
261
+ )
262
+
263
+ RunsApi(self.client).run_update_external_issue(
264
+ code=project_code,
265
+ runexternal_issues=run_external_issues
266
+ )
267
+
268
+ self.logger.log(f"External link updated for run {run_id}: {external_link.link}", "debug")
269
+
270
+ except Exception as e:
271
+ self.logger.log(f"Error at updating external link: {e}", "error")
245
272
 
246
- if step.attachments:
247
- uploaded_attachments = []
248
- for file in step.attachments:
249
- uploaded_attachments.extend(self._upload_attachment(project_code, file))
250
- prepared_step['attachments'] = [attach.hash for attach in uploaded_attachments]
273
+ def __should_skip_attachment(self, attachment, result):
274
+ if (self.config.framework.playwright.video == Video.failed and
275
+ result.execution.status != 'failed' and
276
+ attachment.file_name == 'video.webm'):
277
+ return True
278
+ if (self.config.framework.playwright.trace == Trace.failed and
279
+ result.execution.status != 'failed' and
280
+ attachment.file_name == 'trace.zip'):
281
+ return True
282
+ return False
251
283
 
252
- if step.steps:
253
- for substep in step.steps:
254
- prepared_children.append(self._prepare_step(project_code, substep))
255
- prepared_step["steps"] = prepared_children
256
- return prepared_step
257
- except Exception as e:
258
- self.logger.log(f"Error at preparing step: {e}", "error")
259
- raise ReporterException(e)
284
+ def send_results(self, project_code: str, run_id: str, results: []) -> None:
285
+ raise NotImplementedError("use ApiV2Client instead")
@@ -1,7 +1,7 @@
1
1
  from typing import Dict
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,6 +16,7 @@ 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
@@ -56,7 +57,11 @@ class ApiV2Client(ApiV1Client):
56
57
  attached = []
57
58
  if result.attachments:
58
59
  for attachment in result.attachments:
59
- attached.extend(self._upload_attachment(project_code, attachment))
60
+ if self.__should_skip_attachment(attachment, result):
61
+ continue
62
+ attach_id = self._upload_attachment(project_code, attachment)
63
+ if attach_id:
64
+ attached.extend(attach_id)
60
65
 
61
66
  steps = []
62
67
  for step in result.steps:
@@ -71,27 +76,29 @@ class ApiV2Client(ApiV1Client):
71
76
  result_model_v2 = ResultCreate(
72
77
  title=result.get_title(),
73
78
  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,
79
+ testops_ids=result.get_testops_ids(),
80
+ execution=ResultExecution(status=result.execution.status, duration=result.execution.duration,
81
+ start_time=result.execution.start_time, end_time=result.execution.end_time,
77
82
  stacktrace=result.execution.stacktrace, thread=result.execution.thread),
78
- fields=result.fields,
83
+ fields=ResultCreateFields.from_dict(result.fields),
79
84
  attachments=[attach.hash for attach in attached],
80
85
  steps=steps,
81
- step_type=ResultStepsType.CLASSIC,
82
- params=result.params,
83
- muted=False,
86
+ steps_type=ResultStepsType.CLASSIC,
87
+ 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},
88
+ param_groups=result.param_groups,
84
89
  message=result.message,
90
+ defect=self.config.testops.defect,
85
91
  )
86
92
 
87
- if result.get_suite_title():
93
+ if result.relations is not None and result.relations.suite is not None and len(
94
+ result.relations.suite.data) != 0:
88
95
  data = []
89
96
  root_suite = self.config.root_suite
90
97
  if root_suite:
91
98
  data.append(RelationSuiteItem(title=root_suite))
92
99
 
93
- for suite in result.get_suite_title().split("."):
94
- data.append(RelationSuiteItem(title=suite))
100
+ for raw in result.relations.suite.data:
101
+ data.append(RelationSuiteItem(title=raw.title))
95
102
 
96
103
  result_model_v2.relations = ResultRelations(suite=RelationSuite(data=data))
97
104
 
@@ -104,8 +111,16 @@ class ApiV2Client(ApiV1Client):
104
111
 
105
112
  try:
106
113
  prepared_step = {'execution': {}, 'data': {}, 'steps': []}
107
- prepared_step['execution']['status'] = ResultStepStatus(step.execution.status)
108
- prepared_step['execution']['duration'] = step.execution.duration
114
+ if step.execution.status == 'untested':
115
+ prepared_step['execution']['status'] = ResultStepStatus('skipped')
116
+ prepared_step['execution']['duration'] = 0
117
+ prepared_step['execution']['start_time'] = None
118
+ prepared_step['execution']['end_time'] = None
119
+ else:
120
+ prepared_step['execution']['status'] = ResultStepStatus(step.execution.status)
121
+ prepared_step['execution']['duration'] = step.execution.duration
122
+ prepared_step['execution']['start_time'] = step.execution.start_time
123
+ prepared_step['execution']['end_time'] = step.execution.end_time
109
124
 
110
125
  if step.step_type == StepType.TEXT:
111
126
  prepared_step['data']['action'] = step.data.action
@@ -116,31 +131,59 @@ class ApiV2Client(ApiV1Client):
116
131
  prepared_step['data']['action'] = step.data.request_method + " " + step.data.request_url
117
132
 
118
133
  if step.data.request_body:
119
- step.attachments.append(
134
+ step.add_attachment(
120
135
  Attachment(file_name='request_body.txt', content=step.data.request_body, mime_type='text/plain',
121
136
  temporary=True))
122
137
  if step.data.request_headers:
123
- step.attachments.append(
138
+ step.add_attachment(
124
139
  Attachment(file_name='request_headers.txt', content=step.data.request_headers,
125
140
  mime_type='text/plain', temporary=True))
126
141
  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))
142
+ step.add_attachment(Attachment(file_name='response_body.txt', content=step.data.response_body,
143
+ mime_type='text/plain', temporary=True))
129
144
  if step.data.response_headers:
130
- step.attachments.append(
145
+ step.add_attachment(
131
146
  Attachment(file_name='response_headers.txt', content=step.data.response_headers,
132
147
  mime_type='text/plain', temporary=True))
133
148
 
134
149
  if step.step_type == StepType.GHERKIN:
135
- prepared_step['data']['action'] = step.data.keyword
150
+ action = step.data.keyword
151
+ if step.data.keyword != step.data.name:
152
+ action += " " + step.data.name
153
+ prepared_step['data']['action'] = action
136
154
 
137
155
  if step.step_type == StepType.SLEEP:
138
156
  prepared_step['data']['action'] = f"Sleep for {step.data.duration} seconds"
139
157
 
140
- if step.attachments:
158
+ if step.step_type == StepType.DB_QUERY:
159
+ # Format database query as action
160
+ action_parts = []
161
+ if step.data.database_type:
162
+ action_parts.append(f"[{step.data.database_type}]")
163
+ action_parts.append(step.data.query)
164
+ prepared_step['data']['action'] = " ".join(action_parts)
165
+
166
+ # Add expected_result if available
167
+ if step.data.expected_result:
168
+ prepared_step['data']['expected_result'] = step.data.expected_result
169
+
170
+ # Add connection info and execution time as input_data
171
+ info_parts = []
172
+ if step.data.connection_info:
173
+ info_parts.append(f"Connection: {step.data.connection_info}")
174
+ if step.data.execution_time is not None:
175
+ info_parts.append(f"Execution time: {step.data.execution_time:.3f}s")
176
+ if step.data.rows_affected is not None:
177
+ info_parts.append(f"Rows affected: {step.data.rows_affected}")
178
+ if info_parts:
179
+ prepared_step['data']['input_data'] = " | ".join(info_parts)
180
+
181
+ if step.execution.attachments:
141
182
  uploaded_attachments = []
142
- for file in step.attachments:
143
- uploaded_attachments.extend(self._upload_attachment(project_code, file))
183
+ for file in step.execution.attachments:
184
+ attach_id = self._upload_attachment(project_code, file)
185
+ if attach_id:
186
+ uploaded_attachments.extend(attach_id)
144
187
 
145
188
  prepared_step['execution']['attachments'] = [attach.hash for attach in uploaded_attachments]
146
189
 
@@ -152,3 +195,14 @@ class ApiV2Client(ApiV1Client):
152
195
  except Exception as e:
153
196
  self.logger.log(f"Error at preparing step: {e}", "error")
154
197
  raise ReporterException(e)
198
+
199
+ def __should_skip_attachment(self, attachment, result):
200
+ if (self.config.framework.playwright.video == Video.failed and
201
+ result.execution.status != 'failed' and
202
+ attachment.file_name == 'video.webm'):
203
+ return True
204
+ if (self.config.framework.playwright.trace == Trace.failed and
205
+ result.execution.status != 'failed' and
206
+ attachment.file_name == 'trace.zip'):
207
+ return True
208
+ return False
@@ -30,7 +30,7 @@ class BaseApiClient(abc.ABC):
30
30
  pass
31
31
 
32
32
  @abc.abstractmethod
33
- def complete_run(self, project_code: str, run_id: str) -> None:
33
+ def complete_run(self, project_code: str, run_id: int) -> None:
34
34
  """
35
35
  Complete a test run in Qase TestOps
36
36
 
@@ -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