qase-python-commons 3.3.2__tar.gz → 3.4.1__tar.gz

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 (48) hide show
  1. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/PKG-INFO +3 -3
  2. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/pyproject.toml +3 -3
  3. qase_python_commons-3.4.1/src/qase/commons/client/api_v1_client.py +137 -0
  4. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/client/api_v2_client.py +10 -10
  5. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/config.py +0 -5
  6. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/logger.py +6 -1
  7. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/models/__init__.py +2 -3
  8. qase_python_commons-3.4.1/src/qase/commons/models/basemodel.py +16 -0
  9. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/models/config/testops.py +0 -4
  10. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/models/result.py +3 -95
  11. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/models/run.py +14 -2
  12. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/models/step.py +6 -2
  13. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/reporters/core.py +3 -15
  14. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/reporters/report.py +5 -6
  15. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/reporters/testops.py +11 -15
  16. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase_python_commons.egg-info/PKG-INFO +3 -3
  17. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase_python_commons.egg-info/requires.txt +2 -2
  18. qase_python_commons-3.3.2/src/qase/commons/client/api_v1_client.py +0 -302
  19. qase_python_commons-3.3.2/src/qase/commons/models/basemodel.py +0 -7
  20. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/README.md +0 -0
  21. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/setup.cfg +0 -0
  22. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/__init__.py +0 -0
  23. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/client/base_api_client.py +0 -0
  24. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/exceptions/reporter.py +0 -0
  25. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/loader.py +0 -0
  26. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/models/attachment.py +0 -0
  27. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/models/config/api.py +0 -0
  28. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/models/config/batch.py +0 -0
  29. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/models/config/connection.py +0 -0
  30. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/models/config/framework.py +0 -0
  31. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/models/config/plan.py +0 -0
  32. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/models/config/qaseconfig.py +0 -0
  33. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/models/config/report.py +0 -0
  34. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/models/config/run.py +0 -0
  35. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/models/relation.py +0 -0
  36. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/models/runtime.py +0 -0
  37. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/profilers/__init__.py +0 -0
  38. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/profilers/db.py +0 -0
  39. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/profilers/network.py +0 -0
  40. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/profilers/sleep.py +0 -0
  41. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/reporters/__init__.py +0 -0
  42. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/util/__init__.py +0 -0
  43. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/util/host_data.py +0 -0
  44. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/utils.py +0 -0
  45. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase/commons/validators/base.py +0 -0
  46. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase_python_commons.egg-info/SOURCES.txt +0 -0
  47. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase_python_commons.egg-info/dependency_links.txt +0 -0
  48. {qase_python_commons-3.3.2 → qase_python_commons-3.4.1}/src/qase_python_commons.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qase-python-commons
3
- Version: 3.3.2
3
+ Version: 3.4.1
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.1.1
26
- Requires-Dist: qase-api-v2-client~=1.1.0
25
+ Requires-Dist: qase-api-client~=1.2.0
26
+ Requires-Dist: qase-api-v2-client~=1.2.0
27
27
  Requires-Dist: more_itertools
28
28
  Provides-Extra: testing
29
29
  Requires-Dist: pytest; extra == "testing"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qase-python-commons"
7
- version = "3.3.2"
7
+ version = "3.4.1"
8
8
  description = "A library for Qase TestOps and Qase Report"
9
9
  readme = "README.md"
10
10
  authors = [{name = "Qase Team", email = "support@qase.io"}]
@@ -30,8 +30,8 @@ requires-python = ">=3.7"
30
30
  dependencies = [
31
31
  "certifi>=2024.2.2",
32
32
  "attrs>=23.2.0",
33
- "qase-api-client~=1.1.1",
34
- "qase-api-v2-client~=1.1.0",
33
+ "qase-api-client~=1.2.0",
34
+ "qase-api-v2-client~=1.2.0",
35
35
  "more_itertools"
36
36
  ]
37
37
 
@@ -0,0 +1,137 @@
1
+ from datetime import datetime, timezone
2
+ from typing import Union
3
+
4
+ import certifi
5
+ from qase.api_client_v1 import ApiClient, ProjectsApi, Project, EnvironmentsApi, RunsApi, AttachmentsApi, \
6
+ AttachmentGet, RunCreate
7
+ from qase.api_client_v1.configuration import Configuration
8
+ from .. import Logger
9
+ from .base_api_client import BaseApiClient
10
+ from ..exceptions.reporter import ReporterException
11
+ from ..models import Attachment
12
+ from ..models.config.framework import Video, Trace
13
+ from ..models.config.qaseconfig import QaseConfig
14
+
15
+
16
+ class ApiV1Client(BaseApiClient):
17
+ def __init__(self, config: QaseConfig, logger: Logger):
18
+ self.logger = logger
19
+ self.config = config
20
+ self.__authors = {}
21
+
22
+ try:
23
+ self.logger.log_debug("Preparing API client")
24
+ configuration = Configuration()
25
+ configuration.api_key['TokenAuth'] = self.config.testops.api.token
26
+ configuration.ssl_ca_cert = certifi.where()
27
+ host = self.config.testops.api.host
28
+ if host == 'qase.io':
29
+ configuration.host = f'https://api.{host}/v1'
30
+ self.web = f'https://app.{host}'
31
+ else:
32
+ configuration.host = f'https://api-{host}/v1'
33
+ self.web = f'https://{host}'
34
+
35
+ self.client = ApiClient(configuration)
36
+ self.logger.log_debug("API client prepared")
37
+ except Exception as e:
38
+ self.logger.log(f"Error at preparing API client: {e}", "error")
39
+ raise ReporterException(e)
40
+
41
+ def get_project(self, project_code: str) -> Union[Project, None]:
42
+ try:
43
+ self.logger.log_debug(f"Getting project {project_code}")
44
+ response = ProjectsApi(self.client).get_project(code=project_code)
45
+ if hasattr(response, 'result'):
46
+ self.logger.log_debug(f"Project {project_code} found: {response.result.to_json()}")
47
+ return response.result
48
+ raise ReporterException("Unable to find given project code")
49
+ except Exception as e:
50
+ self.logger.log("Exception when calling ProjectApi->get_project: %s\n" % e, "error")
51
+ raise ReporterException("Exception when calling ProjectApi")
52
+
53
+ def get_environment(self, environment: str, project_code: str) -> Union[str, None]:
54
+ try:
55
+ self.logger.log_debug(f"Getting environment {environment}")
56
+ api_instance = EnvironmentsApi(self.client)
57
+ response = api_instance.get_environments(code=project_code)
58
+ if hasattr(response, 'result') and hasattr(response.result, 'entities'):
59
+ for env in response.result.entities:
60
+ if env.slug == environment:
61
+ self.logger.log_debug(f"Environment {environment} found: {env.to_json()}")
62
+ return env.id
63
+ self.logger.log_debug(f"Environment {environment} not found")
64
+ return None
65
+ except Exception as e:
66
+ self.logger.log("Exception when calling EnvironmentsApi->get_environments: %s\n" % e, "error")
67
+ raise ReporterException(e)
68
+
69
+ def complete_run(self, project_code: str, run_id: str) -> None:
70
+ api_runs = RunsApi(self.client)
71
+ self.logger.log_debug(f"Completing run {run_id}")
72
+ res = api_runs.get_run(project_code, run_id).result
73
+ if res.status == 1:
74
+ self.logger.log_debug(f"Run {run_id} already completed")
75
+ return
76
+ try:
77
+ api_runs.complete_run(project_code, run_id)
78
+ self.logger.log(f"Test run link: {self.web}/run/{project_code}/dashboard/{run_id}", "info")
79
+ except Exception as e:
80
+ self.logger.log(f"Error at completing run {run_id}: {e}", "error")
81
+ raise ReporterException(e)
82
+
83
+ def _upload_attachment(self, project_code: str, attachment: Attachment) -> Union[AttachmentGet, None]:
84
+ try:
85
+ self.logger.log_debug(f"Uploading attachment {attachment.id} for project {project_code}")
86
+ attach_api = AttachmentsApi(self.client)
87
+ response = attach_api.upload_attachment(project_code, file=[attachment.get_for_upload()])
88
+
89
+ return response.result
90
+
91
+ except Exception as e:
92
+ self.logger.log(f"Error at uploading attachment: {e}", "error")
93
+ return None
94
+
95
+ def create_test_run(self, project_code: str, title: str, description: str, plan_id=None,
96
+ environment_id=None) -> str:
97
+ kwargs = dict(
98
+ title=title,
99
+ description=description,
100
+ environment_id=(int(environment_id) if environment_id else None),
101
+ plan_id=(int(plan_id) if plan_id else plan_id),
102
+ is_autotest=True,
103
+ start_time=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
104
+ )
105
+ self.logger.log_debug(f"Creating test run with parameters: {kwargs}")
106
+ try:
107
+ result = RunsApi(self.client).create_run(
108
+ code=project_code,
109
+ run_create=RunCreate(**{k: v for k, v in kwargs.items() if v is not None})
110
+ )
111
+
112
+ return result.result.id
113
+
114
+ except Exception as e:
115
+ self.logger.log(f"Error at creating test run: {e}", "error")
116
+ raise ReporterException(e)
117
+
118
+ def check_test_run(self, project_code: str, run_id: int) -> bool:
119
+ api_runs = RunsApi(self.client)
120
+ run = api_runs.get_run(code=project_code, id=run_id)
121
+ if run.result.id:
122
+ return True
123
+ return False
124
+
125
+ def __should_skip_attachment(self, attachment, result):
126
+ if (self.config.framework.playwright.video == Video.failed and
127
+ result.execution.status != 'failed' and
128
+ attachment.file_name == 'video.webm'):
129
+ return True
130
+ if (self.config.framework.playwright.trace == Trace.failed and
131
+ result.execution.status != 'failed' and
132
+ attachment.file_name == 'trace.zip'):
133
+ return True
134
+ return False
135
+
136
+ def send_results(self, project_code: str, run_id: str, results: []) -> None:
137
+ raise NotImplementedError("use ApiV2Client instead")
@@ -17,7 +17,7 @@ from .api_v1_client import ApiV1Client
17
17
  from .. import Logger
18
18
  from ..exceptions.reporter import ReporterException
19
19
  from ..models.config.framework import Video, Trace
20
- from ..models import Attachment, InternalResult
20
+ from ..models import Attachment, Result
21
21
  from ..models.config.qaseconfig import QaseConfig
22
22
  from ..models.step import StepType, Step
23
23
 
@@ -53,7 +53,7 @@ class ApiV2Client(ApiV1Client):
53
53
  create_results_request_v2=CreateResultsRequestV2(results=results_to_send))
54
54
  self.logger.log_debug(f"Results for run {run_id} sent successfully")
55
55
 
56
- def _prepare_result(self, project_code: str, result: InternalResult) -> ResultCreate:
56
+ def _prepare_result(self, project_code: str, result: Result) -> ResultCreate:
57
57
  attached = []
58
58
  if result.attachments:
59
59
  for attachment in result.attachments:
@@ -76,7 +76,7 @@ class ApiV2Client(ApiV1Client):
76
76
  result_model_v2 = ResultCreate(
77
77
  title=result.get_title(),
78
78
  signature=result.signature,
79
- testops_id=result.get_testops_id(),
79
+ testops_ids=result.get_testops_ids(),
80
80
  execution=ResultExecution(status=result.execution.status, duration=result.execution.duration,
81
81
  start_time=result.execution.start_time, end_time=result.execution.end_time,
82
82
  stacktrace=result.execution.stacktrace, thread=result.execution.thread),
@@ -125,18 +125,18 @@ class ApiV2Client(ApiV1Client):
125
125
  prepared_step['data']['action'] = step.data.request_method + " " + step.data.request_url
126
126
 
127
127
  if step.data.request_body:
128
- step.attachments.append(
128
+ step.add_attachment(
129
129
  Attachment(file_name='request_body.txt', content=step.data.request_body, mime_type='text/plain',
130
130
  temporary=True))
131
131
  if step.data.request_headers:
132
- step.attachments.append(
132
+ step.add_attachment(
133
133
  Attachment(file_name='request_headers.txt', content=step.data.request_headers,
134
134
  mime_type='text/plain', temporary=True))
135
135
  if step.data.response_body:
136
- step.attachments.append(Attachment(file_name='response_body.txt', content=step.data.response_body,
137
- mime_type='text/plain', temporary=True))
136
+ step.add_attachment(Attachment(file_name='response_body.txt', content=step.data.response_body,
137
+ mime_type='text/plain', temporary=True))
138
138
  if step.data.response_headers:
139
- step.attachments.append(
139
+ step.add_attachment(
140
140
  Attachment(file_name='response_headers.txt', content=step.data.response_headers,
141
141
  mime_type='text/plain', temporary=True))
142
142
 
@@ -149,9 +149,9 @@ class ApiV2Client(ApiV1Client):
149
149
  if step.step_type == StepType.SLEEP:
150
150
  prepared_step['data']['action'] = f"Sleep for {step.data.duration} seconds"
151
151
 
152
- if step.attachments:
152
+ if step.execution.attachments:
153
153
  uploaded_attachments = []
154
- for file in step.attachments:
154
+ for file in step.execution.attachments:
155
155
  attach_id = self._upload_attachment(project_code, file)
156
156
  if attach_id:
157
157
  uploaded_attachments.extend(attach_id)
@@ -83,11 +83,6 @@ class ConfigManager:
83
83
  testops.get("defect")
84
84
  )
85
85
 
86
- if testops.get("useV2"):
87
- self.config.testops.set_use_v2(
88
- testops.get("useV2")
89
- )
90
-
91
86
  if testops.get("plan"):
92
87
  plan = testops.get("plan")
93
88
 
@@ -24,7 +24,12 @@ class Logger:
24
24
  def log(self, message: str, level: str = 'info'):
25
25
  time_str = self._get_timestamp("%H:%M:%S")
26
26
  log = f"[Qase][{time_str}][{level}] {message}\n"
27
- print(log)
27
+
28
+ try:
29
+ print(log, end='')
30
+ except (OSError, IOError):
31
+ pass
32
+
28
33
  if self.debug:
29
34
  with self.lock:
30
35
  with open(Logger._log_file, 'a', encoding='utf-8') as f:
@@ -1,4 +1,4 @@
1
- from .result import Result, InternalResult, Field
1
+ from .result import Result, Field
2
2
  from .run import Run
3
3
  from .attachment import Attachment
4
4
  from .relation import Relation
@@ -12,6 +12,5 @@ __all__ = [
12
12
  Relation,
13
13
  Step,
14
14
  Runtime,
15
- Field,
16
- InternalResult
15
+ Field
17
16
  ]
@@ -0,0 +1,16 @@
1
+ import json
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class BaseModel:
7
+ def __str__(self, enum_as_name=False) -> str:
8
+ def serialize(o):
9
+ if isinstance(o, Enum):
10
+ return o.name if enum_as_name else o.value
11
+ elif hasattr(o, '__dict__'):
12
+ return o.__dict__
13
+ else:
14
+ return str(o)
15
+
16
+ return json.dumps(self, default=serialize, indent=4, sort_keys=True)
@@ -20,13 +20,9 @@ class TestopsConfig(BaseModel):
20
20
  self.batch = BatchConfig()
21
21
  self.plan = PlanConfig()
22
22
  self.defect = False
23
- self.use_v2 = True
24
23
 
25
24
  def set_project(self, project: str):
26
25
  self.project = project
27
26
 
28
27
  def set_defect(self, defect):
29
28
  self.defect = QaseUtils.parse_bool(defect)
30
-
31
- def set_use_v2(self, use_v2):
32
- self.use_v2 = QaseUtils.parse_bool(use_v2)
@@ -1,4 +1,3 @@
1
- import copy
2
1
  import time
3
2
  import uuid
4
3
 
@@ -70,19 +69,16 @@ class Result(BaseModel):
70
69
  self.id: str = str(uuid.uuid4())
71
70
  self.title: str = title
72
71
  self.signature: str = signature
73
- self.run_id: Optional[str] = None
74
- self.testops_id: Optional[List[int]] = None
72
+ self.testops_ids: Optional[List[int]] = None
75
73
  self.execution: Type[Execution] = Execution()
76
74
  self.fields: Dict[Type[Field]] = {}
77
75
  self.attachments: List[Attachment] = []
78
76
  self.steps: List[Type[Step]] = []
79
77
  self.params: Optional[dict] = {}
80
78
  self.param_groups: Optional[List[List[str]]] = []
81
- self.author: Optional[str] = None
82
79
  self.relations: Type[Relation] = None
83
80
  self.muted: bool = False
84
81
  self.message: Optional[str] = None
85
- QaseUtils.get_host_data()
86
82
 
87
83
  def add_message(self, message: str) -> None:
88
84
  self.message = message
@@ -119,96 +115,8 @@ class Result(BaseModel):
119
115
  return self.fields[name]
120
116
  return None
121
117
 
122
- def get_testops_id(self) -> Optional[List[int]]:
123
- return self.testops_id
118
+ def get_testops_ids(self) -> Optional[List[int]]:
119
+ return self.testops_ids
124
120
 
125
121
  def get_duration(self) -> int:
126
122
  return self.execution.duration
127
-
128
- def set_run_id(self, run_id: str) -> None:
129
- self.run_id = run_id
130
-
131
-
132
- class InternalResult(BaseModel):
133
- def __init__(self, title: str, signature: str) -> None:
134
- self.id: str = str(uuid.uuid4())
135
- self.title: str = title
136
- self.signature: str = signature
137
- self.run_id: Optional[str] = None
138
- self.testops_id: Optional[int] = None
139
- self.execution: Type[Execution] = Execution()
140
- self.fields: Dict[Type[Field]] = {}
141
- self.attachments: List[Attachment] = []
142
- self.steps: List[Type[Step]] = []
143
- self.params: Optional[dict] = {}
144
- self.param_groups: Optional[List[List[str]]] = []
145
- self.author: Optional[str] = None
146
- self.relations: Type[Relation] = None
147
- self.muted: bool = False
148
- self.message: Optional[str] = None
149
- QaseUtils.get_host_data()
150
-
151
- def add_message(self, message: str) -> None:
152
- self.message = message
153
-
154
- def add_field(self, field: Type[Field]) -> None:
155
- self.fields[field.name] = field.value
156
-
157
- def add_steps(self, steps: List[Type[Step]]) -> None:
158
- self.steps = QaseUtils().build_tree(steps)
159
-
160
- def add_attachment(self, attachment: Attachment) -> None:
161
- self.attachments.append(attachment)
162
-
163
- def add_param(self, key: str, value: str) -> None:
164
- self.params[key] = value
165
-
166
- def add_param_groups(self, values: List[str]) -> None:
167
- self.param_groups.append(values)
168
-
169
- def set_relation(self, relation: Relation) -> None:
170
- self.relations = relation
171
-
172
- def get_status(self) -> Optional[str]:
173
- return self.execution.status
174
-
175
- def get_id(self) -> str:
176
- return self.id
177
-
178
- def get_title(self) -> str:
179
- return self.title
180
-
181
- def get_field(self, name: str) -> Optional[Type[Field]]:
182
- if name in self.fields:
183
- return self.fields[name]
184
- return None
185
-
186
- def get_testops_id(self) -> Optional[int]:
187
- return self.testops_id
188
-
189
- def get_duration(self) -> int:
190
- return self.execution.duration
191
-
192
- def set_run_id(self, run_id: str) -> None:
193
- self.run_id = run_id
194
-
195
- @classmethod
196
- def convert_from_result(cls, result: Result, testops_id: Optional[int] = None):
197
- int_result = cls(result.title, result.signature)
198
-
199
- int_result.id = result.id
200
- int_result.title = result.title
201
- int_result.signature = result.signature
202
- int_result.run_id = result.run_id
203
- int_result.testops_id = testops_id
204
- int_result.execution = copy.deepcopy(result.execution)
205
- int_result.fields = result.fields
206
- int_result.attachments = result.attachments
207
- int_result.steps = result.steps
208
- int_result.params = result.params
209
- int_result.author = result.author
210
- int_result.relations = result.relations
211
- int_result.muted = result.muted
212
- int_result.message = result.message
213
-
214
- return int_result
@@ -1,5 +1,3 @@
1
- import json
2
-
3
1
  from typing import Optional, List
4
2
 
5
3
  from .basemodel import BaseModel
@@ -71,6 +69,7 @@ class Run(BaseModel):
71
69
  "duration": result["execution"]["duration"],
72
70
  "thread": result["execution"]["thread"]
73
71
  }
72
+ self._extract_path_from_relations(result)
74
73
  self.results.append(compact_result)
75
74
  self.execution.track(result)
76
75
  self.stats.track(result)
@@ -79,3 +78,16 @@ class Run(BaseModel):
79
78
 
80
79
  def add_host_data(self, host_data: dict):
81
80
  self.host_data = host_data
81
+
82
+ def _extract_path_from_relations(self, relations_dict):
83
+
84
+ titles = []
85
+ if "relations" in relations_dict and "suite" in relations_dict["relations"]:
86
+ if "data" in relations_dict["relations"]["suite"]:
87
+ data_list = relations_dict["relations"]["suite"]["data"]
88
+ titles = [item["title"] for item in data_list if "title" in item]
89
+
90
+ path = "/".join(titles)
91
+
92
+ if path and path not in self.suites:
93
+ self.suites.append(path)
@@ -20,6 +20,7 @@ class StepTextData(BaseModel):
20
20
  def __init__(self, action: str, expected_result: Optional[str] = None):
21
21
  self.action = action
22
22
  self.expected_result = expected_result
23
+ self.input_data = None
23
24
 
24
25
 
25
26
  class StepAssertData(BaseModel):
@@ -78,6 +79,7 @@ class StepExecution(BaseModel):
78
79
  self.status = status
79
80
  self.end_time = end_time
80
81
  self.duration = duration
82
+ self.attachments = []
81
83
 
82
84
  def set_status(self, status: Optional[str]):
83
85
  if status in ['passed', 'failed', 'skipped', 'blocked', 'untested']:
@@ -89,6 +91,9 @@ class StepExecution(BaseModel):
89
91
  self.end_time = time.time()
90
92
  self.duration = int((self.end_time - self.start_time) * 1000)
91
93
 
94
+ def add_attachment(self, attachment: Attachment):
95
+ self.attachments.append(attachment)
96
+
92
97
 
93
98
  class Step(BaseModel):
94
99
  def __init__(self,
@@ -107,7 +112,6 @@ class Step(BaseModel):
107
112
  self.data = data
108
113
  self.parent_id = parent_id
109
114
  self.execution = StepExecution()
110
- self.attachments = []
111
115
  self.steps = []
112
116
 
113
117
  def set_parent_id(self, parent_id: Optional[str]):
@@ -132,4 +136,4 @@ class Step(BaseModel):
132
136
  self.steps = steps
133
137
 
134
138
  def add_attachment(self, attachment: Attachment):
135
- self.attachments.append(attachment)
139
+ self.execution.add_attachment(attachment)
@@ -6,7 +6,7 @@ from ..logger import Logger
6
6
  from .report import QaseReport
7
7
  from .testops import QaseTestOps
8
8
 
9
- from ..models import InternalResult, Result, Attachment, Runtime
9
+ from ..models import Result, Attachment, Runtime
10
10
  from ..models.config.qaseconfig import Mode
11
11
  from typing import Union, List
12
12
 
@@ -84,20 +84,8 @@ class QaseCoreReporter:
84
84
  try:
85
85
  ts = time.time()
86
86
  self.logger.log_debug(f"Adding result {result}")
87
- ids = result.get_testops_id()
88
-
89
- if ids is None:
90
- int_result = InternalResult.convert_from_result(result)
91
- self.reporter.add_result(int_result)
92
- else:
93
- first = True
94
- for testops_id in ids:
95
- int_result = InternalResult.convert_from_result(result, testops_id)
96
- if not first:
97
- int_result.execution.duration = 0
98
- else:
99
- first = False
100
- self.reporter.add_result(int_result)
87
+
88
+ self.reporter.add_result(result)
101
89
 
102
90
  self.logger.log_debug(f"Result {result.get_title()} added")
103
91
  self.overhead += time.time() - ts
@@ -3,7 +3,7 @@ import os
3
3
  import shutil
4
4
  import json
5
5
  import re
6
- from ..models import InternalResult, Run, Attachment
6
+ from ..models import Result, Run, Attachment
7
7
  from .. import QaseUtils, Logger
8
8
  from ..models.config.connection import Format
9
9
  from ..models.config.qaseconfig import QaseConfig
@@ -40,8 +40,7 @@ class QaseReport:
40
40
  def complete_worker(self):
41
41
  pass
42
42
 
43
- def add_result(self, result: InternalResult):
44
- result.set_run_id(self.run_id)
43
+ def add_result(self, result: Result):
45
44
  for attachment in result.attachments:
46
45
  self._persist_attachment(attachment)
47
46
 
@@ -84,14 +83,14 @@ class QaseReport:
84
83
 
85
84
  def _persist_attachments_in_steps(self, steps: list):
86
85
  for step in steps:
87
- if step.attachments:
88
- for attachment in step.attachments:
86
+ if step.execution.attachments:
87
+ for attachment in step.execution.attachments:
89
88
  self._persist_attachment(attachment)
90
89
  if step.steps:
91
90
  self._persist_attachments_in_steps(step.steps)
92
91
 
93
92
  # Method saves result to a file
94
- def _store_result(self, result: InternalResult):
93
+ def _store_result(self, result: Result):
95
94
  self._store_object(result, self.report_path + "/results/", result.id)
96
95
 
97
96
  def _check_report_path(self):
@@ -2,11 +2,11 @@ import threading
2
2
  import urllib.parse
3
3
 
4
4
  from datetime import datetime
5
- from typing import List
5
+ from typing import List, Union
6
6
  from .. import Logger, ReporterException
7
- from ..client.api_v1_client import ApiV1Client
7
+ from ..client.api_v2_client import ApiV2Client
8
8
  from ..client.base_api_client import BaseApiClient
9
- from ..models import InternalResult
9
+ from ..models import Result
10
10
  from ..models.config.qaseconfig import QaseConfig
11
11
 
12
12
  DEFAULT_BATCH_SIZE = 200
@@ -69,10 +69,7 @@ class QaseTestOps:
69
69
  self.client.get_project(self.project_code)
70
70
 
71
71
  def _prepare_client(self) -> BaseApiClient:
72
- if self.config.testops.use_v2:
73
- from ..client.api_v2_client import ApiV2Client
74
- return ApiV2Client(self.config, self.logger)
75
- return ApiV1Client(self.config, self.logger)
72
+ return ApiV2Client(self.config, self.logger)
76
73
 
77
74
  def _send_results_threaded(self, results):
78
75
  try:
@@ -129,9 +126,9 @@ class QaseTestOps:
129
126
  if len(self.results) > 0:
130
127
  self._send_results()
131
128
 
132
- def add_result(self, result: InternalResult) -> None:
129
+ def add_result(self, result: Result) -> None:
133
130
  if result.get_status() == 'failed':
134
- self.__show_link(result.testops_id, result.title)
131
+ self.__show_link(result.testops_ids, result.title)
135
132
  self.results.append(result)
136
133
  if len(self.results) >= self.batch_size:
137
134
  self._send_results()
@@ -142,15 +139,14 @@ class QaseTestOps:
142
139
  def set_results(self, results) -> None:
143
140
  self.results = results
144
141
 
145
- def __show_link(self, id, title: str):
146
- link = self.__prepare_link(id, title)
142
+ def __show_link(self, ids: Union[None, List[int]], title: str):
143
+ link = self.__prepare_link(ids, title)
147
144
  self.logger.log(f"See why this test failed: {link}", "info")
148
145
 
149
- def __prepare_link(self, id, title: str):
146
+ def __prepare_link(self, ids: Union[None, List[int]], title: str):
150
147
  link = f"{self.__baseUrl}/run/{self.project_code}/dashboard/{self.run_id}?source=logs&status=%5B2%5D&search="
151
- if id:
152
- return f"{link}{id}`"
153
-
148
+ if ids is not None and len(ids) > 0:
149
+ return f"{link}{ids[0]}"
154
150
  return f"{link}{urllib.parse.quote_plus(title)}"
155
151
 
156
152
  @staticmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qase-python-commons
3
- Version: 3.3.2
3
+ Version: 3.4.1
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.1.1
26
- Requires-Dist: qase-api-v2-client~=1.1.0
25
+ Requires-Dist: qase-api-client~=1.2.0
26
+ Requires-Dist: qase-api-v2-client~=1.2.0
27
27
  Requires-Dist: more_itertools
28
28
  Provides-Extra: testing
29
29
  Requires-Dist: pytest; extra == "testing"
@@ -1,7 +1,7 @@
1
1
  certifi>=2024.2.2
2
2
  attrs>=23.2.0
3
- qase-api-client~=1.1.1
4
- qase-api-v2-client~=1.1.0
3
+ qase-api-client~=1.2.0
4
+ qase-api-v2-client~=1.2.0
5
5
  more_itertools
6
6
 
7
7
  [testing]
@@ -1,302 +0,0 @@
1
- from datetime import datetime, timezone
2
- from typing import Dict, Union
3
-
4
- import certifi
5
- from qase.api_client_v1 import ApiClient, ProjectsApi, Project, EnvironmentsApi, RunsApi, AttachmentsApi, \
6
- AttachmentGet, RunCreate, ResultsApi, ResultcreateBulk, AuthorsApi
7
- from qase.api_client_v1.configuration import Configuration
8
- from .. import Logger
9
- from .base_api_client import BaseApiClient
10
- from ..exceptions.reporter import ReporterException
11
- from ..models import Attachment, InternalResult, Step
12
- from ..models.config.framework import Video, Trace
13
- from ..models.config.qaseconfig import QaseConfig
14
- from ..models.step import StepType
15
-
16
-
17
- class ApiV1Client(BaseApiClient):
18
- def __init__(self, config: QaseConfig, logger: Logger):
19
- self.logger = logger
20
- self.config = config
21
- self.__authors = {}
22
-
23
- try:
24
- self.logger.log_debug("Preparing API client")
25
- configuration = Configuration()
26
- configuration.api_key['TokenAuth'] = self.config.testops.api.token
27
- configuration.ssl_ca_cert = certifi.where()
28
- host = self.config.testops.api.host
29
- if host == 'qase.io':
30
- configuration.host = f'https://api.{host}/v1'
31
- self.web = f'https://app.{host}'
32
- else:
33
- configuration.host = f'https://api-{host}/v1'
34
- self.web = f'https://{host}'
35
-
36
- self.client = ApiClient(configuration)
37
- self.logger.log_debug("API client prepared")
38
- except Exception as e:
39
- self.logger.log(f"Error at preparing API client: {e}", "error")
40
- raise ReporterException(e)
41
-
42
- def get_project(self, project_code: str) -> Union[Project, None]:
43
- try:
44
- self.logger.log_debug(f"Getting project {project_code}")
45
- response = ProjectsApi(self.client).get_project(code=project_code)
46
- if hasattr(response, 'result'):
47
- self.logger.log_debug(f"Project {project_code} found: {response.result.to_json()}")
48
- return response.result
49
- raise ReporterException("Unable to find given project code")
50
- except Exception as e:
51
- self.logger.log("Exception when calling ProjectApi->get_project: %s\n" % e, "error")
52
- raise ReporterException("Exception when calling ProjectApi")
53
-
54
- def get_environment(self, environment: str, project_code: str) -> Union[str, None]:
55
- try:
56
- self.logger.log_debug(f"Getting environment {environment}")
57
- api_instance = EnvironmentsApi(self.client)
58
- response = api_instance.get_environments(code=project_code)
59
- if hasattr(response, 'result') and hasattr(response.result, 'entities'):
60
- for env in response.result.entities:
61
- if env.slug == environment:
62
- self.logger.log_debug(f"Environment {environment} found: {env.to_json()}")
63
- return env.id
64
- self.logger.log_debug(f"Environment {environment} not found")
65
- return None
66
- except Exception as e:
67
- self.logger.log("Exception when calling EnvironmentsApi->get_environments: %s\n" % e, "error")
68
- raise ReporterException(e)
69
-
70
- def complete_run(self, project_code: str, run_id: str) -> None:
71
- api_runs = RunsApi(self.client)
72
- self.logger.log_debug(f"Completing run {run_id}")
73
- res = api_runs.get_run(project_code, run_id).result
74
- if res.status == 1:
75
- self.logger.log_debug(f"Run {run_id} already completed")
76
- return
77
- try:
78
- api_runs.complete_run(project_code, run_id)
79
- self.logger.log(f"Test run link: {self.web}/run/{project_code}/dashboard/{run_id}", "info")
80
- except Exception as e:
81
- self.logger.log(f"Error at completing run {run_id}: {e}", "error")
82
- raise ReporterException(e)
83
-
84
- def _upload_attachment(self, project_code: str, attachment: Attachment) -> Union[AttachmentGet, None]:
85
- try:
86
- self.logger.log_debug(f"Uploading attachment {attachment.id} for project {project_code}")
87
- attach_api = AttachmentsApi(self.client)
88
- response = attach_api.upload_attachment(project_code, file=[attachment.get_for_upload()])
89
-
90
- return response.result
91
-
92
- except Exception as e:
93
- self.logger.log(f"Error at uploading attachment: {e}", "error")
94
- return None
95
-
96
- def create_test_run(self, project_code: str, title: str, description: str, plan_id=None,
97
- environment_id=None) -> str:
98
- kwargs = dict(
99
- title=title,
100
- description=description,
101
- environment_id=(int(environment_id) if environment_id else None),
102
- plan_id=(int(plan_id) if plan_id else plan_id),
103
- is_autotest=True,
104
- start_time=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
105
- )
106
- self.logger.log_debug(f"Creating test run with parameters: {kwargs}")
107
- try:
108
- result = RunsApi(self.client).create_run(
109
- code=project_code,
110
- run_create=RunCreate(**{k: v for k, v in kwargs.items() if v is not None})
111
- )
112
-
113
- return result.result.id
114
-
115
- except Exception as e:
116
- self.logger.log(f"Error at creating test run: {e}", "error")
117
- raise ReporterException(e)
118
-
119
- def check_test_run(self, project_code: str, run_id: int) -> bool:
120
- api_runs = RunsApi(self.client)
121
- run = api_runs.get_run(code=project_code, id=run_id)
122
- if run.result.id:
123
- return True
124
- return False
125
-
126
- def send_results(self, project_code: str, run_id: str, results: []) -> None:
127
- api_results = ResultsApi(self.client)
128
- results_to_send = [self._prepare_result(project_code, result) for result in results]
129
- self.logger.log_debug(f"Sending results for run {run_id}: {results_to_send}")
130
- api_results.create_result_bulk(
131
- code=project_code,
132
- id=run_id,
133
- resultcreate_bulk=ResultcreateBulk(
134
- results=results_to_send
135
- )
136
- )
137
- self.logger.log_debug(f"Results for run {run_id} sent successfully")
138
-
139
- def _prepare_result(self, project_code: str, result: InternalResult) -> Dict:
140
- attached = []
141
- if result.attachments:
142
- for attachment in result.attachments:
143
- if self.__should_skip_attachment(attachment, result):
144
- continue
145
- attach_id = self._upload_attachment(project_code, attachment)
146
- if attach_id:
147
- attached.extend(attach_id)
148
-
149
- steps = []
150
- for step in result.steps:
151
- prepared = self._prepare_step(project_code, step)
152
- steps.append(prepared)
153
-
154
- case_data = {
155
- "title": result.get_title(),
156
- "description": result.get_field('description'),
157
- "preconditions": result.get_field('preconditions'),
158
- "postconditions": result.get_field('postconditions'),
159
- }
160
-
161
- for key, param in result.params.items():
162
- # Hack to match old TestOps API
163
- if param == "":
164
- result.params[key] = "empty"
165
-
166
- if result.get_field('severity'):
167
- case_data["severity"] = result.get_field('severity')
168
-
169
- if result.get_field('priority'):
170
- case_data["priority"] = result.get_field('priority')
171
-
172
- if result.get_field('layer'):
173
- case_data["layer"] = result.get_field('layer')
174
-
175
- suite = None
176
- if result.relations is not None and result.relations.suite is not None and len(
177
- result.relations.suite.data) != 0:
178
- suites = []
179
-
180
- for raw in result.relations.suite.data:
181
- suites.append(raw.title)
182
-
183
- suite = "\t".join(suites)
184
-
185
- if result.get_field('suite'):
186
- suite = result.get_field('suite')
187
-
188
- root_suite = self.config.root_suite
189
- if root_suite:
190
- suite = f"{root_suite}\t{suite}"
191
-
192
- if suite:
193
- case_data["suite_title"] = suite
194
-
195
- result_model = {
196
- "status": result.execution.status,
197
- "stacktrace": result.execution.stacktrace,
198
- "time_ms": result.execution.duration,
199
- "comment": result.message,
200
- "attachments": [attach.hash for attach in attached],
201
- "steps": steps,
202
- "param": result.params,
203
- "param_groups": result.param_groups,
204
- "defect": self.config.testops.defect,
205
- "case": case_data
206
- }
207
-
208
- test_ops_id = result.get_testops_id()
209
-
210
- if test_ops_id:
211
- result_model["case_id"] = test_ops_id
212
-
213
- if result.get_field('author'):
214
- author_id = self._get_author_id(result.get_field('author'))
215
- if author_id:
216
- result_model["author_id"] = author_id
217
-
218
- self.logger.log_debug(f"Prepared result: {result_model}")
219
-
220
- return result_model
221
-
222
- def _prepare_step(self, project_code: str, step: Step) -> Dict:
223
- prepared_children = []
224
-
225
- try:
226
- prepared_step = {"time": step.execution.duration, "status": step.execution.status}
227
-
228
- if step.execution.status == 'untested':
229
- prepared_step["status"] = 'passed'
230
-
231
- if step.execution.status == 'skipped':
232
- prepared_step["status"] = 'blocked'
233
-
234
- if step.step_type == StepType.TEXT:
235
- prepared_step['action'] = step.data.action
236
- if step.data.expected_result:
237
- prepared_step['expected_result'] = step.data.expected_result
238
-
239
- if step.step_type == StepType.REQUEST:
240
- prepared_step['action'] = step.data.request_method + " " + step.data.request_url
241
- if step.data.request_body:
242
- step.attachments.append(
243
- Attachment(file_name='request_body.txt', content=step.data.request_body, mime_type='text/plain',
244
- temporary=True))
245
- if step.data.request_headers:
246
- step.attachments.append(
247
- Attachment(file_name='request_headers.txt', content=step.data.request_headers,
248
- mime_type='text/plain', temporary=True))
249
- if step.data.response_body:
250
- step.attachments.append(Attachment(file_name='response_body.txt', content=step.data.response_body,
251
- mime_type='text/plain', temporary=True))
252
- if step.data.response_headers:
253
- step.attachments.append(
254
- Attachment(file_name='response_headers.txt', content=step.data.response_headers,
255
- mime_type='text/plain', temporary=True))
256
-
257
- if step.step_type == StepType.GHERKIN:
258
- prepared_step['action'] = step.data.keyword
259
-
260
- if step.step_type == StepType.SLEEP:
261
- prepared_step['action'] = f"Sleep for {step.data.duration} seconds"
262
-
263
- if step.attachments:
264
- uploaded_attachments = []
265
- for file in step.attachments:
266
- attach_id = self._upload_attachment(project_code, file)
267
- if attach_id:
268
- uploaded_attachments.extend(attach_id)
269
- prepared_step['attachments'] = [attach.hash for attach in uploaded_attachments]
270
-
271
- if step.steps:
272
- for substep in step.steps:
273
- prepared_children.append(self._prepare_step(project_code, substep))
274
- prepared_step["steps"] = prepared_children
275
- return prepared_step
276
- except Exception as e:
277
- self.logger.log(f"Error at preparing step: {e}", "error")
278
- raise ReporterException(e)
279
-
280
- def _get_author_id(self, author: str) -> Union[str, None]:
281
- if author in self.__authors:
282
- return self.__authors[author]
283
-
284
- author_api = AuthorsApi(self.client)
285
- authors = author_api.get_authors(search=author)
286
- if authors.result.total == 0:
287
- return None
288
-
289
- self.__authors[author] = authors.result.entities[0].author_id
290
-
291
- return authors.result.entities[0].author_id
292
-
293
- def __should_skip_attachment(self, attachment, result):
294
- if (self.config.framework.playwright.video == Video.failed and
295
- result.execution.status != 'failed' and
296
- attachment.file_name == 'video.webm'):
297
- return True
298
- if (self.config.framework.playwright.trace == Trace.failed and
299
- result.execution.status != 'failed' and
300
- attachment.file_name == 'trace.zip'):
301
- return True
302
- return False
@@ -1,7 +0,0 @@
1
- import json
2
-
3
-
4
- class BaseModel:
5
- def __str__(self) -> str:
6
- return json.dumps(self, default=lambda o: o.__dict__ if hasattr(o, '__dict__') else str(o), indent=4,
7
- sort_keys=True)