qase-python-commons 3.0.3__tar.gz → 3.1.0b2__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.
Files changed (46) hide show
  1. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/PKG-INFO +2 -2
  2. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/pyproject.toml +2 -2
  3. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/client/api_v1_client.py +19 -10
  4. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/client/api_v2_client.py +13 -7
  5. qase_python_commons-3.1.0b2/src/qase/commons/config.py +219 -0
  6. qase_python_commons-3.1.0b2/src/qase/commons/models/config/api.py +15 -0
  7. qase_python_commons-3.1.0b2/src/qase/commons/models/config/batch.py +13 -0
  8. qase_python_commons-3.1.0b2/src/qase/commons/models/config/connection.py +24 -0
  9. qase_python_commons-3.1.0b2/src/qase/commons/models/config/framework.py +18 -0
  10. qase_python_commons-3.1.0b2/src/qase/commons/models/config/plan.py +8 -0
  11. qase_python_commons-3.1.0b2/src/qase/commons/models/config/qaseconfig.py +65 -0
  12. qase_python_commons-3.1.0b2/src/qase/commons/models/config/report.py +21 -0
  13. qase_python_commons-3.1.0b2/src/qase/commons/models/config/run.py +23 -0
  14. qase_python_commons-3.1.0b2/src/qase/commons/models/config/testops.py +31 -0
  15. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/reporters/core.py +30 -28
  16. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/reporters/report.py +12 -10
  17. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/reporters/testops.py +15 -14
  18. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase_python_commons.egg-info/PKG-INFO +2 -2
  19. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase_python_commons.egg-info/SOURCES.txt +9 -0
  20. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase_python_commons.egg-info/requires.txt +1 -1
  21. qase_python_commons-3.0.3/src/qase/commons/config.py +0 -66
  22. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/README.md +0 -0
  23. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/setup.cfg +0 -0
  24. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/__init__.py +0 -0
  25. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/client/base_api_client.py +0 -0
  26. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/exceptions/reporter.py +0 -0
  27. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/loader.py +0 -0
  28. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/logger.py +0 -0
  29. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/models/__init__.py +0 -0
  30. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/models/attachment.py +0 -0
  31. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/models/basemodel.py +0 -0
  32. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/models/relation.py +0 -0
  33. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/models/result.py +0 -0
  34. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/models/run.py +0 -0
  35. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/models/runtime.py +0 -0
  36. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/models/step.py +0 -0
  37. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/models/suite.py +0 -0
  38. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/profilers/__init__.py +0 -0
  39. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/profilers/db.py +0 -0
  40. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/profilers/network.py +0 -0
  41. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/profilers/sleep.py +0 -0
  42. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/reporters/__init__.py +0 -0
  43. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/utils.py +0 -0
  44. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase/commons/validators/base.py +0 -0
  45. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase_python_commons.egg-info/dependency_links.txt +0 -0
  46. {qase_python_commons-3.0.3 → qase_python_commons-3.1.0b2}/src/qase_python_commons.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qase-python-commons
3
- Version: 3.0.3
3
+ Version: 3.1.0b2
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/master/qase-python-commons
@@ -22,7 +22,7 @@ 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.0.0
25
+ Requires-Dist: qase-api-client~=1.0.1
26
26
  Requires-Dist: qase-api-v2-client~=1.0.0
27
27
  Requires-Dist: more_itertools
28
28
  Provides-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.0.3"
7
+ version = "3.1.0b2"
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,7 +30,7 @@ requires-python = ">=3.7"
30
30
  dependencies = [
31
31
  "certifi>=2024.2.2",
32
32
  "attrs>=23.2.0",
33
- "qase-api-client~=1.0.0",
33
+ "qase-api-client~=1.0.1",
34
34
  "qase-api-v2-client~=1.0.0",
35
35
  "more_itertools"
36
36
  ]
@@ -4,30 +4,31 @@ import certifi
4
4
  from qase.api_client_v1 import ApiClient, ProjectsApi, Project, EnvironmentsApi, RunsApi, AttachmentsApi, \
5
5
  AttachmentGet, RunCreate, ResultsApi, ResultcreateBulk
6
6
  from qase.api_client_v1.configuration import Configuration
7
- from .. import ConfigManager, Logger
7
+ from .. import Logger
8
8
  from .base_api_client import BaseApiClient
9
9
  from ..exceptions.reporter import ReporterException
10
10
  from ..models import Attachment, Result, Step
11
+ from ..models.config.qaseconfig import QaseConfig
11
12
  from ..models.step import StepType
12
13
 
13
14
 
14
15
  class ApiV1Client(BaseApiClient):
15
- def __init__(self, config: ConfigManager, logger: Logger):
16
+ def __init__(self, config: QaseConfig, logger: Logger):
16
17
  self.logger = logger
17
18
  self.config = config
18
19
 
19
20
  try:
20
21
  self.logger.log_debug("Preparing API client")
21
22
  configuration = Configuration()
22
- configuration.api_key['TokenAuth'] = self.config.get('testops.api.token')
23
+ configuration.api_key['TokenAuth'] = self.config.testops.api.token
23
24
  configuration.ssl_ca_cert = certifi.where()
24
- host = self.config.get('testops.api.host', 'qase.io')
25
- if self.config.get('testops.api.enterprise', False, bool):
26
- configuration.host = f'https://api-{host}/v1'
27
- self.web = f'https://{host}'
28
- else:
25
+ host = self.config.testops.api.host
26
+ if host == 'qase.io':
29
27
  configuration.host = f'https://api.{host}/v1'
30
28
  self.web = f'https://app.{host}'
29
+ else:
30
+ configuration.host = f'https://api-{host}/v1'
31
+ self.web = f'https://{host}'
31
32
 
32
33
  self.client = ApiClient(configuration)
33
34
  self.logger.log_debug("API client prepared")
@@ -165,8 +166,16 @@ class ApiV1Client(BaseApiClient):
165
166
  if result.get_field('layer'):
166
167
  case_data["layer"] = result.get_field('layer')
167
168
 
169
+ suite = None
168
170
  if result.get_suite_title():
169
- case_data["suite_title"] = "\t".join(result.get_suite_title().split("."))
171
+ suite = "\t".join(result.get_suite_title().split("."))
172
+
173
+ root_suite = self.config.root_suite
174
+ if root_suite:
175
+ suite = f"{root_suite}\t{suite}"
176
+
177
+ if suite:
178
+ case_data["suite"] = suite
170
179
 
171
180
  result_model = {
172
181
  "status": result.execution.status,
@@ -176,7 +185,7 @@ class ApiV1Client(BaseApiClient):
176
185
  "attachments": [attach.hash for attach in attached],
177
186
  "steps": steps,
178
187
  "param": result.params,
179
- "defect": self.config.get('testops.defect', False, bool),
188
+ "defect": self.config.testops.defect,
180
189
  }
181
190
 
182
191
  test_ops_id = result.get_testops_id()
@@ -14,26 +14,29 @@ from qase.api_client_v2.models.result_step_status import ResultStepStatus
14
14
  from qase.api_client_v2.models.result_steps_type import ResultStepsType
15
15
 
16
16
  from .api_v1_client import ApiV1Client
17
- from .. import ConfigManager, Logger
17
+ from .. import Logger
18
18
  from ..exceptions.reporter import ReporterException
19
19
  from ..models import Attachment, Result
20
+ from ..models.config.qaseconfig import QaseConfig
20
21
  from ..models.step import StepType, Step
21
22
 
22
23
 
23
24
  class ApiV2Client(ApiV1Client):
24
- def __init__(self, config: ConfigManager, logger: Logger):
25
+ def __init__(self, config: QaseConfig, logger: Logger):
25
26
  ApiV1Client.__init__(self, config, logger)
26
27
 
27
28
  try:
28
29
  self.logger.log_debug("Preparing API V2 client")
29
30
  configuration = Configuration()
30
- configuration.api_key['TokenAuth'] = self.config.get('testops.api.token')
31
+ configuration.api_key['TokenAuth'] = self.config.testops.api.token
31
32
  configuration.ssl_ca_cert = certifi.where()
32
- host = self.config.get('testops.api.host', 'qase.io')
33
- if self.config.get('testops.api.enterprise', False, bool):
34
- configuration.host = f'https://api-{host}/v2'
35
- else:
33
+ host = self.config.testops.api.host
34
+ if host == 'qase.io':
36
35
  configuration.host = f'https://api.{host}/v2'
36
+ self.web = f'https://app.{host}'
37
+ else:
38
+ configuration.host = f'https://api-{host}/v2'
39
+ self.web = f'https://{host}'
37
40
 
38
41
  self.client_v2 = ApiClient(configuration)
39
42
  self.logger.log_debug("API V2 client prepared")
@@ -83,6 +86,9 @@ class ApiV2Client(ApiV1Client):
83
86
 
84
87
  if result.get_suite_title():
85
88
  data = []
89
+ root_suite = self.config.root_suite
90
+ if root_suite:
91
+ data.append(RelationSuiteItem(title=root_suite))
86
92
 
87
93
  for suite in result.get_suite_title().split("."):
88
94
  data.append(RelationSuiteItem(title=suite))
@@ -0,0 +1,219 @@
1
+ import os
2
+ import json
3
+ from .logger import Logger
4
+ from .models.config.qaseconfig import QaseConfig, Mode
5
+
6
+
7
+ class ConfigManager:
8
+
9
+ def __init__(self, config_file='./qase.config.json'):
10
+ self.logger = Logger()
11
+ self.__config_file = config_file
12
+ self.config = QaseConfig()
13
+ self.parse_bool = lambda d: d in ("y", "yes", "true", "1", 1, True)
14
+
15
+ self.__load_file_config()
16
+ self.__load_env_config()
17
+
18
+ def validate_config(self):
19
+ errors: list[str] = []
20
+ if self.config.mode is Mode.testops or self.config.fallback is Mode.testops:
21
+ if self.config.testops.api.token is None:
22
+ errors.append("Testops token is not set")
23
+
24
+ if self.config.testops.project is None:
25
+ errors.append("Testops project is not set")
26
+
27
+ if len(errors) > 0:
28
+ self.logger.log("Config validation failed", "error")
29
+ for error in errors:
30
+ self.logger.log(error, "error")
31
+ raise Exception("Config validation failed")
32
+
33
+ def __str__(self):
34
+ return json.dumps(self.config, indent=4, sort_keys=True)
35
+
36
+ def __load_file_config(self):
37
+ try:
38
+ if os.path.exists(self.__config_file):
39
+ with open(self.__config_file, "r") as file:
40
+ config = json.load(file)
41
+
42
+ if config.get("mode"):
43
+ self.config.set_mode(config.get("mode"))
44
+
45
+ if config.get("fallback"):
46
+ self.config.set_fallback(config.get("fallback"))
47
+
48
+ if config.get("environment"):
49
+ self.config.set_environment(config.get("environment"))
50
+
51
+ if config.get("rootSuite"):
52
+ self.config.set_root_suite(config.get("rootSuite"))
53
+
54
+ if config.get("profilers"):
55
+ self.config.set_profilers(config.get("profilers"))
56
+
57
+ if config.get("debug"):
58
+ self.config.set_debug(
59
+ self.parse_bool(config.get("debug"))
60
+ )
61
+
62
+ if config.get("executionPlan"):
63
+ execution_plan = config.get("executionPlan")
64
+ if execution_plan.get("path"):
65
+ self.config.execution_plan.set_path(execution_plan.get("path"))
66
+
67
+ if config.get("testops"):
68
+ testops = config.get("testops")
69
+
70
+ if testops.get("api"):
71
+ api = testops.get("api")
72
+
73
+ if api.get("host"):
74
+ self.config.testops.api.set_host(api.get("host"))
75
+
76
+ if api.get("token"):
77
+ self.config.testops.api.set_token(api.get("token"))
78
+
79
+ if testops.get("project"):
80
+ self.config.testops.set_project(testops.get("project"))
81
+
82
+ if testops.get("defect"):
83
+ self.config.testops.set_defect(
84
+ self.parse_bool(testops.get("defect"))
85
+ )
86
+
87
+ if testops.get("useV2"):
88
+ self.config.testops.set_use_v2(
89
+ self.parse_bool(testops.get("useV2"))
90
+ )
91
+
92
+ if testops.get("plan"):
93
+ plan = testops.get("plan")
94
+
95
+ if plan.get("id"):
96
+ self.config.testops.plan.set_id(plan.get("id"))
97
+
98
+ if testops.get("run"):
99
+ run = testops.get("run")
100
+
101
+ if run.get("id"):
102
+ self.config.testops.run.set_id(run.get("id"))
103
+
104
+ if run.get("title"):
105
+ self.config.testops.run.set_title(run.get("title"))
106
+
107
+ if run.get("description"):
108
+ self.config.testops.run.set_description(run.get("description"))
109
+
110
+ if run.get("complete"):
111
+ self.config.testops.run.set_complete(
112
+ self.parse_bool(run.get("complete"))
113
+ )
114
+
115
+ if testops.get("batch"):
116
+ batch = testops.get("batch")
117
+
118
+ if batch.get("size"):
119
+ self.config.testops.batch.set_size(batch.get("size"))
120
+
121
+ if config.get("report"):
122
+ report = config.get("report")
123
+
124
+ if report.get("driver"):
125
+ self.config.report.set_driver(report.get("driver"))
126
+
127
+ if report.get("connection"):
128
+ connection = report.get("connection")
129
+
130
+ if connection.get("path"):
131
+ self.config.report.connection.set_path(connection.get("path"))
132
+
133
+ if connection.get("format"):
134
+ self.config.report.connection.set_format(
135
+ connection.get("format")
136
+ )
137
+
138
+ if config.get("framework"):
139
+ framework = config.get("framework")
140
+
141
+ if framework.get("pytest"):
142
+ pytest = framework.get("pytest")
143
+
144
+ if pytest.get("captureLogs"):
145
+ self.config.framework.pytest.set_capture_logs(
146
+ self.parse_bool(pytest.get("captureLogs"))
147
+ )
148
+
149
+ except Exception as e:
150
+ self.logger.log("Failed to load config from file", "error")
151
+
152
+ def __load_env_config(self):
153
+ try:
154
+ for key, value in os.environ.items():
155
+ if key == 'QASE_MODE':
156
+ self.config.set_mode(value)
157
+
158
+ if key == 'QASE_FALLBACK':
159
+ self.config.set_fallback(value)
160
+
161
+ if key == 'QASE_ENVIRONMENT':
162
+ self.config.set_environment(value)
163
+
164
+ if key == 'QASE_ROOT_SUITE':
165
+ self.config.set_root_suite(value)
166
+
167
+ if key == 'QASE_PROFILERS':
168
+ self.config.set_profilers(value.split(','))
169
+
170
+ if key == 'QASE_DEBUG':
171
+ self.config.set_debug(self.parse_bool(value))
172
+
173
+ if key == 'QASE_EXECUTION_PLAN_PATH':
174
+ self.config.execution_plan.set_path(value)
175
+
176
+ if key == 'QASE_TESTOPS_API_HOST':
177
+ self.config.testops.api.set_host(value)
178
+
179
+ if key == 'QASE_TESTOPS_API_TOKEN':
180
+ self.config.testops.api.set_token(value)
181
+
182
+ if key == 'QASE_TESTOPS_PROJECT':
183
+ self.config.testops.set_project(value)
184
+
185
+ if key == 'QASE_TESTOPS_DEFECT':
186
+ self.config.testops.set_defect(self.parse_bool(value))
187
+
188
+ if key == 'QASE_TESTOPS_PLAN_ID':
189
+ self.config.testops.plan.set_id(value)
190
+
191
+ if key == 'QASE_TESTOPS_RUN_ID':
192
+ self.config.testops.run.set_id(value)
193
+
194
+ if key == 'QASE_TESTOPS_RUN_TITLE':
195
+ self.config.testops.run.set_title(value)
196
+
197
+ if key == 'QASE_TESTOPS_RUN_DESCRIPTION':
198
+ self.config.testops.run.set_description(value)
199
+
200
+ if key == 'QASE_TESTOPS_RUN_COMPLETE':
201
+ self.config.testops.run.set_complete(self.parse_bool(value))
202
+
203
+ if key == 'QASE_TESTOPS_BATCH_SIZE':
204
+ self.config.testops.batch.set_size(value)
205
+
206
+ if key == 'QASE_REPORT_DRIVER':
207
+ self.config.report.set_driver(value)
208
+
209
+ if key == 'QASE_REPORT_CONNECTION_PATH':
210
+ self.config.report.connection.set_path(value)
211
+
212
+ if key == 'QASE_REPORT_CONNECTION_FORMAT':
213
+ self.config.report.connection.set_format(value)
214
+
215
+ if key == 'QASE_PYTEST_CAPTURE_LOGS':
216
+ self.config.framework.pytest.set_capture_logs(self.parse_bool(value))
217
+
218
+ except Exception as e:
219
+ self.logger.log("Failed to load config from env vars {e}", "error")
@@ -0,0 +1,15 @@
1
+ from ..basemodel import BaseModel
2
+
3
+
4
+ class ApiConfig(BaseModel):
5
+ token: str = None
6
+ host: str = None
7
+
8
+ def __init__(self):
9
+ self.host = "qase.io"
10
+
11
+ def set_token(self, token: str):
12
+ self.token = token
13
+
14
+ def set_host(self, host: str):
15
+ self.host = host
@@ -0,0 +1,13 @@
1
+ from ..basemodel import BaseModel
2
+
3
+
4
+ class BatchConfig(BaseModel):
5
+ size: int = None
6
+
7
+ def __init__(self):
8
+ self.size = 200
9
+
10
+ def set_size(self, size: int):
11
+ if size > 2000 or size == 0:
12
+ raise ValueError("Batch size should be greater than 0 and less than 2000")
13
+ self.size = size
@@ -0,0 +1,24 @@
1
+ from enum import Enum
2
+
3
+ from ..basemodel import BaseModel
4
+
5
+
6
+ class Format(Enum):
7
+ json = "json"
8
+ jsonp = "jsonp"
9
+
10
+
11
+ class ConnectionConfig(BaseModel):
12
+ path: str = None
13
+ format: Format = None
14
+
15
+ def __init__(self):
16
+ self.format = Format.json
17
+ self.path = "./build/qase-report"
18
+
19
+ def set_path(self, path: str):
20
+ self.path = path
21
+
22
+ def set_format(self, format: str):
23
+ if any(format == e.value for e in Format.__members__.values()):
24
+ self.format = Format[format]
@@ -0,0 +1,18 @@
1
+ from ..basemodel import BaseModel
2
+
3
+
4
+ class PytestConfig(BaseModel):
5
+ capture_logs: bool = None
6
+
7
+ def __init__(self):
8
+ self.capture_logs = False
9
+
10
+ def set_capture_logs(self, capture_logs: bool):
11
+ self.capture_logs = capture_logs
12
+
13
+
14
+ class Framework(BaseModel):
15
+ pytest: PytestConfig = None
16
+
17
+ def __init__(self):
18
+ self.pytest = PytestConfig()
@@ -0,0 +1,8 @@
1
+ from ..basemodel import BaseModel
2
+
3
+
4
+ class PlanConfig(BaseModel):
5
+ id: int = None
6
+
7
+ def set_id(self, id: int):
8
+ self.id = id
@@ -0,0 +1,65 @@
1
+ from enum import Enum
2
+
3
+ from .framework import Framework
4
+ from .report import ReportConfig
5
+ from .testops import TestopsConfig
6
+ from ..basemodel import BaseModel
7
+
8
+
9
+ class Mode(Enum):
10
+ testops = "testops"
11
+ report = "report"
12
+ off = "off"
13
+
14
+
15
+ class ExecutionPlan(BaseModel):
16
+ path: str = None
17
+
18
+ def __init__(self):
19
+ self.path = "./build/qase-execution-plan.json"
20
+
21
+ def set_path(self, path: str):
22
+ self.path = path
23
+
24
+
25
+ class QaseConfig(BaseModel):
26
+ mode: Mode = None
27
+ fallback: Mode = None
28
+ environment: str = None
29
+ root_suite: str = None
30
+ debug: bool = None
31
+ execution_plan: ExecutionPlan = None
32
+ testops: TestopsConfig = None
33
+ report: ReportConfig = None
34
+ profilers: list = None
35
+ framework: Framework = None
36
+
37
+ def __init__(self):
38
+ self.mode = Mode.off
39
+ self.fallback = Mode.off
40
+ self.debug = False
41
+ self.testops = TestopsConfig()
42
+ self.report = ReportConfig()
43
+ self.execution_plan = ExecutionPlan()
44
+ self.framework = Framework()
45
+ self.profilers = []
46
+
47
+ def set_mode(self, mode: str):
48
+ if any(mode == e.value for e in Mode.__members__.values()):
49
+ self.mode = Mode[mode]
50
+
51
+ def set_fallback(self, fallback: str):
52
+ if any(fallback == e.value for e in Mode.__members__.values()):
53
+ self.fallback = Mode[fallback]
54
+
55
+ def set_environment(self, environment: str):
56
+ self.environment = environment
57
+
58
+ def set_profilers(self, profilers: list):
59
+ self.profilers = profilers
60
+
61
+ def set_root_suite(self, root_suite: str):
62
+ self.root_suite = root_suite
63
+
64
+ def set_debug(self, debug: bool):
65
+ self.debug = debug
@@ -0,0 +1,21 @@
1
+ from enum import Enum
2
+
3
+ from .connection import ConnectionConfig
4
+ from ..basemodel import BaseModel
5
+
6
+
7
+ class Driver(Enum):
8
+ local = "local"
9
+
10
+
11
+ class ReportConfig(BaseModel):
12
+ driver: Driver = None
13
+ connection: ConnectionConfig = None
14
+
15
+ def __init__(self):
16
+ self.driver = Driver.local
17
+ self.connection = ConnectionConfig()
18
+
19
+ def set_driver(self, driver: str):
20
+ if any(driver == e.value for e in Driver.__members__.values()):
21
+ self.driver = Driver[driver]
@@ -0,0 +1,23 @@
1
+ from ..basemodel import BaseModel
2
+
3
+
4
+ class RunConfig(BaseModel):
5
+ title: str = None
6
+ description: str = None
7
+ complete: bool = None
8
+ id: int = None
9
+
10
+ def __init__(self):
11
+ self.complete = True
12
+
13
+ def set_title(self, title: str):
14
+ self.title = title
15
+
16
+ def set_description(self, description: str):
17
+ self.description = description
18
+
19
+ def set_complete(self, complete: bool):
20
+ self.complete = complete
21
+
22
+ def set_id(self, id: int):
23
+ self.id = id
@@ -0,0 +1,31 @@
1
+ from .api import ApiConfig
2
+ from .batch import BatchConfig
3
+ from .plan import PlanConfig
4
+ from .run import RunConfig
5
+ from ..basemodel import BaseModel
6
+
7
+
8
+ class TestopsConfig(BaseModel):
9
+ project: str = None
10
+ defect: bool = None
11
+ api: ApiConfig = None
12
+ run: RunConfig = None
13
+ plan: PlanConfig = None
14
+ batch: BatchConfig = None
15
+
16
+ def __init__(self):
17
+ self.api = ApiConfig()
18
+ self.run = RunConfig()
19
+ self.batch = BatchConfig()
20
+ self.plan = PlanConfig()
21
+ self.defect = False
22
+ self.use_v2 = False
23
+
24
+ def set_project(self, project: str):
25
+ self.project = project
26
+
27
+ def set_defect(self, defect: bool):
28
+ self.defect = defect
29
+
30
+ def set_use_v2(self, use_v2: bool):
31
+ self.use_v2 = use_v2
@@ -2,13 +2,14 @@ import os
2
2
  import json
3
3
  import time
4
4
 
5
- from ..config import ConfigManager as Config
5
+ from ..config import ConfigManager
6
6
  from ..logger import Logger
7
7
 
8
8
  from .report import QaseReport
9
9
  from .testops import QaseTestOps
10
10
 
11
11
  from ..models import Result, Attachment, Runtime
12
+ from ..models.config.qaseconfig import Mode
12
13
  from typing import Union
13
14
 
14
15
  """
@@ -18,31 +19,32 @@ from typing import Union
18
19
 
19
20
 
20
21
  class QaseCoreReporter:
21
- def __init__(self, config: Config):
22
- self.config = config
23
- self.logger = Logger(self.config.get('debug', False, bool))
22
+ def __init__(self, config: ConfigManager):
23
+ config.validate_config()
24
+ self.config = config.config
25
+ self.logger = Logger(self.config.debug)
24
26
  self.execution_plan = None
25
27
  self.profilers = []
26
28
  self.overhead = 0
27
29
 
28
- self._selective_execution_setup()
30
+ # self._selective_execution_setup()
29
31
  self.fallback = self._fallback_setup()
30
32
 
31
33
  self.logger.log_debug(f"Config: {self.config}")
32
34
 
33
35
  # Reading reporter mode from config file
34
- mode = config.get("mode", "off")
36
+ mode = self.config.mode
35
37
 
36
- if mode == 'testops':
38
+ if mode == Mode.testops:
37
39
  try:
38
40
  self._load_testops_plan()
39
- self.reporter = QaseTestOps(config=config, logger=self.logger)
41
+ self.reporter = QaseTestOps(config=self.config, logger=self.logger)
40
42
  except Exception as e:
41
43
  self.logger.log('Failed to initialize TestOps reporter. Using fallback.', 'info')
42
44
  self.logger.log(e, 'error')
43
45
  self.reporter = self.fallback
44
- elif mode == 'report':
45
- self.reporter = QaseReport(config=config, logger=self.logger)
46
+ elif mode == Mode.report:
47
+ self.reporter = QaseReport(config=self.config, logger=self.logger)
46
48
  else:
47
49
  self.reporter = None
48
50
 
@@ -103,14 +105,14 @@ class QaseCoreReporter:
103
105
  self._run_fallback()
104
106
 
105
107
  def setup_profilers(self, runtime: Runtime) -> None:
106
- profilers = self.config.get("profilers", [])
108
+ profilers = self.config.profilers
107
109
 
108
110
  for profiler in profilers:
109
111
  if profiler == "network":
110
112
  # Lazy import
111
113
  from ..profilers import NetworkProfilerSingleton
112
114
  NetworkProfilerSingleton.init(runtime=runtime,
113
- skip_domain=self.config.get("testops.api.host", "qase.io"))
115
+ skip_domain=self.config.testops.api.host)
114
116
  self.profilers.append(NetworkProfilerSingleton.get_instance())
115
117
  if profiler == "sleep":
116
118
  from ..profilers import SleepProfiler
@@ -166,33 +168,33 @@ class QaseCoreReporter:
166
168
 
167
169
  def _load_testops_plan(self) -> None:
168
170
  try:
169
- if self.config.get("testops.plan.id", None) is not None:
171
+ if self.config.testops.plan.id is not None:
170
172
  from .. import TestOpsPlanLoader
171
173
 
172
174
  # Load test plan data from Qase TestOps
173
175
  loader = TestOpsPlanLoader(
174
- api_token=self.config.get("testops.api.token"),
175
- host=self.config.get("testops.api.host", "qase.io"),
176
+ api_token=self.config.testops.api.token,
177
+ host=self.config.testops.api.host
176
178
  )
177
- self.execution_plan = loader.load(self.config.get("testops.project"),
178
- int(self.config.get("testops.plan.id")))
179
+ self.execution_plan = loader.load(self.config.testops.project,
180
+ int(self.config.testops.plan.id))
179
181
  except Exception as e:
180
182
  self.logger.log('Failed to load test plan from Qase TestOps', 'info')
181
183
  self.logger.log(e, 'error')
182
184
 
183
185
  # TODO: won't work, need to fix
184
- def _selective_execution_setup(self) -> list:
185
- # Load execution plan from file
186
- path = self.config.get("execution_plan.path", "qase_execution_plan.json")
187
- if not self.config.get("execution_plan.path", None) and path and os.path.isfile(path):
188
- with open('execution_plan.json') as f:
189
- return json.load(f)
190
-
191
- # Load execution plan from command line or env variable
192
- if self.config.get("execution_plan", None):
193
- return [int(n) for n in str(self.config.get("execution_plan").split(","))]
186
+ # def _selective_execution_setup(self) -> list:
187
+ # # Load execution plan from file
188
+ # path = self.config.execution_plan.path
189
+ # if not self.config.execution_plan.path and path and os.path.isfile(self.config.execution_plan.path):
190
+ # with open(self.config.execution_plan.path) as f:
191
+ # return json.load(f)
192
+ #
193
+ # # Load execution plan from command line or env variable
194
+ # if self.config.self.config.execution_plan.path:
195
+ # return [int(n) for n in str(self.config.self.config.execution_plan.path.split(","))]
194
196
 
195
197
  def _fallback_setup(self) -> Union[QaseReport, None]:
196
- if self.config.get("fallback", 'report'):
198
+ if self.config.fallback == Mode.report:
197
199
  return QaseReport(config=self.config, logger=self.logger)
198
200
  return None
@@ -4,13 +4,15 @@ import shutil
4
4
  import json
5
5
  import re
6
6
  from ..models import Result, Run, Attachment
7
- from .. import QaseUtils, ConfigManager, Logger
7
+ from .. import QaseUtils, Logger
8
+ from ..models.config.connection import Format
9
+ from ..models.config.qaseconfig import QaseConfig
8
10
 
9
11
 
10
12
  class QaseReport:
11
13
  def __init__(
12
14
  self,
13
- config: ConfigManager,
15
+ config: QaseConfig,
14
16
  logger: Logger
15
17
  ):
16
18
  self.duration = 0
@@ -19,13 +21,13 @@ class QaseReport:
19
21
  self.config = config
20
22
  self.logger = logger
21
23
 
22
- self.report_path = self.config.get("report.path", "./build/qase-report")
23
- self.format = self.config.get("report.format", "json")
24
+ self.report_path = self.config.report.connection.path
25
+ self.format = self.config.report.connection.format
24
26
 
25
27
  self.start_time = None
26
28
  self.end_time = None
27
29
  self.run_id = None
28
- self.environment = self.config.get("environment", None)
30
+ self.environment = self.config.environment
29
31
 
30
32
  def start_run(self):
31
33
  self._check_report_path()
@@ -118,17 +120,17 @@ class QaseReport:
118
120
 
119
121
  # Saves a model to a file
120
122
  def _store_object(self, object, path, filename):
121
- data = object.to_json()
122
- if self.format == 'jsonp':
123
+ data = object.__str__()
124
+ if self.format == Format.jsonp:
123
125
  data = f"qaseJsonp({data});"
124
- with open(f"{path}/{filename}.{self.format}", 'w', encoding='utf-8') as f:
126
+ with open(f"{path}/{filename}.{self.format.value}", 'w', encoding='utf-8') as f:
125
127
  f.write(data)
126
128
 
127
129
  def _read_object(self, source):
128
130
  data = source.read()
129
- if self.format == 'json':
131
+ if self.format == Format.json:
130
132
  return json.loads(data)
131
- elif self.format == 'jsonp':
133
+ elif self.format == Format.jsonp:
132
134
  jsonp_pattern = r'\w+\(\s*({[\s\S]*})\s*\);'
133
135
  match = re.search(jsonp_pattern, data)
134
136
  if match:
@@ -2,52 +2,53 @@ import threading
2
2
 
3
3
  from datetime import datetime
4
4
  from typing import List
5
- from .. import ConfigManager, Logger, ReporterException
5
+ from .. import Logger, ReporterException
6
6
  from ..client.api_v1_client import ApiV1Client
7
7
  from ..client.base_api_client import BaseApiClient
8
8
  from ..models import Result
9
+ from ..models.config.qaseconfig import QaseConfig
9
10
 
10
11
  DEFAULT_BATCH_SIZE = 200
12
+ DEFAULT_THREAD_COUNT = 4
11
13
 
12
14
 
13
15
  class QaseTestOps:
14
16
 
15
- def __init__(self, config: ConfigManager, logger: Logger) -> None:
17
+ def __init__(self, config: QaseConfig, logger: Logger) -> None:
16
18
  self.config = config
17
19
  self.logger = logger
18
20
 
19
21
  self.client = self._prepare_client()
20
22
 
21
- run_id = self.config.get('testops.run.id')
22
- plan_id = self.config.get('testops.plan.id')
23
+ run_id = self.config.testops.run.id
24
+ plan_id = self.config.testops.plan.id
23
25
 
24
- self.project_code = self.config.get('testops.project')
26
+ self.project_code = self.config.testops.project
25
27
  self.run_id = int(run_id) if run_id else run_id
26
28
  self.plan_id = int(plan_id) if plan_id else plan_id
27
- self.defect = self.config.get('testops.defect', False, bool)
28
- self.complete_after_run = self.config.get('testops.run.complete', True, bool)
29
+ self.defect = self.config.testops.defect
30
+ self.complete_after_run = self.config.testops.run.complete
29
31
  self.environment = None
30
32
 
31
- self.batch_size = min(2000, max(1, int(self.config.get('testops.batch.size', DEFAULT_BATCH_SIZE))))
32
- self.send_semaphore = threading.Semaphore(
33
- self.config.get('testops.batch.threads', 4)) # Semaphore to limit concurrent sends
33
+ self.batch_size = min(2000, max(1, int(self.config.testops.batch.size or DEFAULT_BATCH_SIZE)))
34
+ self.send_semaphore = threading.Semaphore(DEFAULT_THREAD_COUNT) # Semaphore to limit concurrent sends
34
35
  self.lock = threading.Lock()
35
36
  self.count_running_threads = 0
36
37
 
37
- environment = self.config.get('environment', None)
38
+ environment = self.config.environment
38
39
  if environment:
39
40
  if isinstance(environment, int) or (isinstance(environment, str) and environment.isnumeric()):
40
41
  self.environment = environment
41
42
  elif isinstance(environment, str):
42
43
  self.environment = self.client.get_environment(environment, self.project_code)
43
44
 
44
- run_title = self.config.get('testops.run.title', None)
45
+ run_title = self.config.testops.run.title
45
46
  if run_title and run_title != '':
46
47
  self.run_title = run_title
47
48
  else:
48
49
  self.run_title = "Automated Run {}".format(str(datetime.now()))
49
50
 
50
- run_description = self.config.get('testops.run.description', None)
51
+ run_description = self.config.testops.run.description
51
52
  if run_description and run_description != '':
52
53
  self.run_description = run_description
53
54
  else:
@@ -66,7 +67,7 @@ class QaseTestOps:
66
67
  self.client.get_project(self.project_code)
67
68
 
68
69
  def _prepare_client(self) -> BaseApiClient:
69
- if self.config.get('testops.usev2', False, bool):
70
+ if self.config.testops.use_v2:
70
71
  from ..client.api_v2_client import ApiV2Client
71
72
  return ApiV2Client(self.config, self.logger)
72
73
  return ApiV1Client(self.config, self.logger)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qase-python-commons
3
- Version: 3.0.3
3
+ Version: 3.1.0b2
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/master/qase-python-commons
@@ -22,7 +22,7 @@ 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.0.0
25
+ Requires-Dist: qase-api-client~=1.0.1
26
26
  Requires-Dist: qase-api-v2-client~=1.0.0
27
27
  Requires-Dist: more_itertools
28
28
  Provides-Extra: testing
@@ -18,6 +18,15 @@ src/qase/commons/models/run.py
18
18
  src/qase/commons/models/runtime.py
19
19
  src/qase/commons/models/step.py
20
20
  src/qase/commons/models/suite.py
21
+ src/qase/commons/models/config/api.py
22
+ src/qase/commons/models/config/batch.py
23
+ src/qase/commons/models/config/connection.py
24
+ src/qase/commons/models/config/framework.py
25
+ src/qase/commons/models/config/plan.py
26
+ src/qase/commons/models/config/qaseconfig.py
27
+ src/qase/commons/models/config/report.py
28
+ src/qase/commons/models/config/run.py
29
+ src/qase/commons/models/config/testops.py
21
30
  src/qase/commons/profilers/__init__.py
22
31
  src/qase/commons/profilers/db.py
23
32
  src/qase/commons/profilers/network.py
@@ -1,6 +1,6 @@
1
1
  certifi>=2024.2.2
2
2
  attrs>=23.2.0
3
- qase-api-client~=1.0.0
3
+ qase-api-client~=1.0.1
4
4
  qase-api-v2-client~=1.0.0
5
5
  more_itertools
6
6
 
@@ -1,66 +0,0 @@
1
- import os
2
- import json
3
- from .logger import Logger
4
-
5
-
6
- class ConfigManager:
7
-
8
- def __init__(self, config_file='./qase.config.json', env_vars_prefix='QASE_'):
9
- self.logger = Logger()
10
- self.config = {}
11
- self.parseBool = lambda d: d in ("y", "yes", "true", "1", 1, True)
12
-
13
- try:
14
- if os.path.exists(config_file):
15
- with open(config_file, "r") as file:
16
- def transform_keys(obj):
17
- return {k.lower(): v for k, v in obj.items()}
18
- self.config = json.load(file, object_hook=transform_keys)
19
- except Exception as e:
20
- self.logger.log("Failed to load config from file", "error")
21
-
22
- # Load from env vars
23
- try:
24
- for key, value in os.environ.items():
25
- if key.startswith(env_vars_prefix):
26
- self._set_config(key[len(env_vars_prefix):].lower().replace('_', '.'), value)
27
- except Exception as e:
28
- self.logger.log("Failed to load config from env vars {e}", "error")
29
-
30
- def get(self, key, default=None, value_type=None):
31
- # Use _get_config method to get the value. If None, return default.
32
- value = self._get_config(key)
33
- if value is not None and value_type is not None and value_type == bool:
34
- return self.parseBool(value)
35
- return value or default
36
-
37
- def validate_config(self):
38
- if self.validator:
39
- self.validator.validate(self.config)
40
-
41
- def set(self, key, value):
42
- self._set_config(key, value)
43
-
44
- def _get_keys(self, config, prefix=""):
45
- for key, value in config.items():
46
- if isinstance(value, dict):
47
- yield from self._get_keys(value, f"{prefix}{key}.")
48
- else:
49
- yield f"{prefix}{key}"
50
-
51
- def _set_config(self, key, value, delimiter="."):
52
- keys = key.split(delimiter)
53
- config = self.config
54
- for key in keys[:-1]:
55
- config = config.setdefault(key, {})
56
- config[keys[-1]] = value
57
-
58
- def _get_config(self, key):
59
- keys = key.split(".")
60
- config = self.config
61
- for key in keys[:-1]:
62
- config = config.get(key, {})
63
- return config.get(keys[-1], None)
64
-
65
- def __str__(self):
66
- return json.dumps(self.config, indent=4, sort_keys=True)