qase-python-commons 5.1.2__tar.gz → 5.1.4__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.
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/PKG-INFO +46 -1
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/README.md +45 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/pyproject.toml +1 -1
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/client/api_v1_client.py +3 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/client/api_v2_client.py +8 -1
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/config.py +23 -0
- qase_python_commons-5.1.4/src/qase/commons/models/config/api.py +39 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/config/batch.py +2 -2
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/reporters/testops.py +35 -8
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/reporters/testops_multi.py +39 -8
- qase_python_commons-5.1.4/src/qase/commons/retry.py +94 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase_python_commons.egg-info/PKG-INFO +46 -1
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase_python_commons.egg-info/SOURCES.txt +1 -0
- qase_python_commons-5.1.2/src/qase/commons/models/config/api.py +0 -15
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/setup.cfg +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/__init__.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/__init__.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/client/base_api_client.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/exceptions/reporter.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/loader.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/logger.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/__init__.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/attachment.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/basemodel.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/config/connection.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/config/framework.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/config/plan.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/config/qaseconfig.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/config/report.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/config/run.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/config/testops.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/external_link.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/relation.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/result.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/run.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/runtime.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/step.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/profilers/__init__.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/profilers/db.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/profilers/network.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/profilers/sleep.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/reporters/__init__.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/reporters/core.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/reporters/report.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/status_mapping/__init__.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/status_mapping/status_mapping.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/util/__init__.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/util/host_data.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/util/token_masker.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/utils.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/validators/base.py +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase_python_commons.egg-info/dependency_links.txt +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase_python_commons.egg-info/requires.txt +0 -0
- {qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/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: 5.1.
|
|
3
|
+
Version: 5.1.4
|
|
4
4
|
Summary: A library for Qase TestOps and Qase Report
|
|
5
5
|
Author-email: Qase Team <support@qase.io>
|
|
6
6
|
Project-URL: Homepage, https://github.com/qase-tms/qase-python/tree/main/qase-python-commons
|
|
@@ -65,6 +65,7 @@ Core library for all Qase Python reporters. Contains the complete configuration
|
|
|
65
65
|
- [Status Filtering](#status-filtering)
|
|
66
66
|
- [External Links](#external-links)
|
|
67
67
|
- [Test Run Configurations](#test-run-configurations)
|
|
68
|
+
- [Upload Reliability](#upload-reliability)
|
|
68
69
|
|
|
69
70
|
---
|
|
70
71
|
|
|
@@ -137,6 +138,9 @@ The reporter mode is set via the `mode` option:
|
|
|
137
138
|
|-------------|-------------|---------------------|---------|----------|
|
|
138
139
|
| API token | `testops.api.token` | `QASE_TESTOPS_API_TOKEN` | — | Yes* |
|
|
139
140
|
| API host | `testops.api.host` | `QASE_TESTOPS_API_HOST` | `qase.io` | No |
|
|
141
|
+
| Request timeout, seconds | `testops.api.timeout` | `QASE_TESTOPS_API_TIMEOUT` | `30` | No |
|
|
142
|
+
| Upload attempts | `testops.api.retries` | `QASE_TESTOPS_API_RETRIES` | `3` | No |
|
|
143
|
+
| Retry backoff base, seconds | `testops.api.retryBackoff` | `QASE_TESTOPS_API_RETRY_BACKOFF` | `2` | No |
|
|
140
144
|
| Project code | `testops.project` | `QASE_TESTOPS_PROJECT` | — | Yes* |
|
|
141
145
|
| Test run ID | `testops.run.id` | `QASE_TESTOPS_RUN_ID` | — | No |
|
|
142
146
|
| Test run title | `testops.run.title` | `QASE_TESTOPS_RUN_TITLE` | `Automated run <date>` | No |
|
|
@@ -296,6 +300,9 @@ export QASE_TESTOPS_API_TOKEN="<token>"
|
|
|
296
300
|
export QASE_TESTOPS_PROJECT="DEMO"
|
|
297
301
|
export QASE_TESTOPS_RUN_TITLE="Automated Run"
|
|
298
302
|
export QASE_TESTOPS_RUN_COMPLETE="true"
|
|
303
|
+
export QASE_TESTOPS_API_TIMEOUT="30"
|
|
304
|
+
export QASE_TESTOPS_API_RETRIES="3"
|
|
305
|
+
export QASE_TESTOPS_API_RETRY_BACKOFF="2"
|
|
299
306
|
|
|
300
307
|
# Pytest
|
|
301
308
|
export QASE_PYTEST_CAPTURE_LOGS="true"
|
|
@@ -409,6 +416,44 @@ Creates or finds configurations in Qase TestOps:
|
|
|
409
416
|
}
|
|
410
417
|
```
|
|
411
418
|
|
|
419
|
+
### Upload Reliability
|
|
420
|
+
|
|
421
|
+
Results are uploaded in batches from background threads. A batch that fails on
|
|
422
|
+
a transient error is retried rather than dropped.
|
|
423
|
+
|
|
424
|
+
| Setting | Meaning |
|
|
425
|
+
|---------|---------|
|
|
426
|
+
| `testops.api.timeout` | Per-request timeout in seconds. Without it a stalled connection blocks the session at teardown. |
|
|
427
|
+
| `testops.api.retries` | Total attempts per batch, not retries on top of the first. `3` means three tries; `0` sends once and never retries. |
|
|
428
|
+
| `testops.api.retryBackoff` | Base of the exponential delay: attempt *n* waits `retryBackoff ** n` seconds. |
|
|
429
|
+
|
|
430
|
+
Retried: connection resets, timeouts and other transport failures, plus HTTP
|
|
431
|
+
408, 429 and 5xx. Not retried: 400, 401, 403, 404, 413, 422 and 507 — a second
|
|
432
|
+
attempt fails identically and only adds load.
|
|
433
|
+
|
|
434
|
+
When a 429 carries a `Retry-After` header, that value replaces the computed
|
|
435
|
+
backoff. Qase sends roughly 60 seconds, so a run that hits the rate limit takes
|
|
436
|
+
longer to finish rather than losing the batch.
|
|
437
|
+
|
|
438
|
+
**If a batch cannot be delivered after all attempts**, the reporter logs an
|
|
439
|
+
error naming how many results were lost and **does not mark the run complete**.
|
|
440
|
+
An open run is the signal that its data is incomplete; a completed run over
|
|
441
|
+
partial results would look trustworthy and not be. In `testops_multi` mode this
|
|
442
|
+
is per project — one project's failure does not stop the others completing.
|
|
443
|
+
|
|
444
|
+
```json
|
|
445
|
+
{
|
|
446
|
+
"testops": {
|
|
447
|
+
"api": {
|
|
448
|
+
"token": "<token>",
|
|
449
|
+
"timeout": 30,
|
|
450
|
+
"retries": 3,
|
|
451
|
+
"retryBackoff": 2
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
412
457
|
---
|
|
413
458
|
|
|
414
459
|
## Requirements
|
|
@@ -30,6 +30,7 @@ Core library for all Qase Python reporters. Contains the complete configuration
|
|
|
30
30
|
- [Status Filtering](#status-filtering)
|
|
31
31
|
- [External Links](#external-links)
|
|
32
32
|
- [Test Run Configurations](#test-run-configurations)
|
|
33
|
+
- [Upload Reliability](#upload-reliability)
|
|
33
34
|
|
|
34
35
|
---
|
|
35
36
|
|
|
@@ -102,6 +103,9 @@ The reporter mode is set via the `mode` option:
|
|
|
102
103
|
|-------------|-------------|---------------------|---------|----------|
|
|
103
104
|
| API token | `testops.api.token` | `QASE_TESTOPS_API_TOKEN` | — | Yes* |
|
|
104
105
|
| API host | `testops.api.host` | `QASE_TESTOPS_API_HOST` | `qase.io` | No |
|
|
106
|
+
| Request timeout, seconds | `testops.api.timeout` | `QASE_TESTOPS_API_TIMEOUT` | `30` | No |
|
|
107
|
+
| Upload attempts | `testops.api.retries` | `QASE_TESTOPS_API_RETRIES` | `3` | No |
|
|
108
|
+
| Retry backoff base, seconds | `testops.api.retryBackoff` | `QASE_TESTOPS_API_RETRY_BACKOFF` | `2` | No |
|
|
105
109
|
| Project code | `testops.project` | `QASE_TESTOPS_PROJECT` | — | Yes* |
|
|
106
110
|
| Test run ID | `testops.run.id` | `QASE_TESTOPS_RUN_ID` | — | No |
|
|
107
111
|
| Test run title | `testops.run.title` | `QASE_TESTOPS_RUN_TITLE` | `Automated run <date>` | No |
|
|
@@ -261,6 +265,9 @@ export QASE_TESTOPS_API_TOKEN="<token>"
|
|
|
261
265
|
export QASE_TESTOPS_PROJECT="DEMO"
|
|
262
266
|
export QASE_TESTOPS_RUN_TITLE="Automated Run"
|
|
263
267
|
export QASE_TESTOPS_RUN_COMPLETE="true"
|
|
268
|
+
export QASE_TESTOPS_API_TIMEOUT="30"
|
|
269
|
+
export QASE_TESTOPS_API_RETRIES="3"
|
|
270
|
+
export QASE_TESTOPS_API_RETRY_BACKOFF="2"
|
|
264
271
|
|
|
265
272
|
# Pytest
|
|
266
273
|
export QASE_PYTEST_CAPTURE_LOGS="true"
|
|
@@ -374,6 +381,44 @@ Creates or finds configurations in Qase TestOps:
|
|
|
374
381
|
}
|
|
375
382
|
```
|
|
376
383
|
|
|
384
|
+
### Upload Reliability
|
|
385
|
+
|
|
386
|
+
Results are uploaded in batches from background threads. A batch that fails on
|
|
387
|
+
a transient error is retried rather than dropped.
|
|
388
|
+
|
|
389
|
+
| Setting | Meaning |
|
|
390
|
+
|---------|---------|
|
|
391
|
+
| `testops.api.timeout` | Per-request timeout in seconds. Without it a stalled connection blocks the session at teardown. |
|
|
392
|
+
| `testops.api.retries` | Total attempts per batch, not retries on top of the first. `3` means three tries; `0` sends once and never retries. |
|
|
393
|
+
| `testops.api.retryBackoff` | Base of the exponential delay: attempt *n* waits `retryBackoff ** n` seconds. |
|
|
394
|
+
|
|
395
|
+
Retried: connection resets, timeouts and other transport failures, plus HTTP
|
|
396
|
+
408, 429 and 5xx. Not retried: 400, 401, 403, 404, 413, 422 and 507 — a second
|
|
397
|
+
attempt fails identically and only adds load.
|
|
398
|
+
|
|
399
|
+
When a 429 carries a `Retry-After` header, that value replaces the computed
|
|
400
|
+
backoff. Qase sends roughly 60 seconds, so a run that hits the rate limit takes
|
|
401
|
+
longer to finish rather than losing the batch.
|
|
402
|
+
|
|
403
|
+
**If a batch cannot be delivered after all attempts**, the reporter logs an
|
|
404
|
+
error naming how many results were lost and **does not mark the run complete**.
|
|
405
|
+
An open run is the signal that its data is incomplete; a completed run over
|
|
406
|
+
partial results would look trustworthy and not be. In `testops_multi` mode this
|
|
407
|
+
is per project — one project's failure does not stop the others completing.
|
|
408
|
+
|
|
409
|
+
```json
|
|
410
|
+
{
|
|
411
|
+
"testops": {
|
|
412
|
+
"api": {
|
|
413
|
+
"token": "<token>",
|
|
414
|
+
"timeout": 30,
|
|
415
|
+
"retries": 3,
|
|
416
|
+
"retryBackoff": 2
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
377
422
|
---
|
|
378
423
|
|
|
379
424
|
## Requirements
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "qase-python-commons"
|
|
7
|
-
version = "5.1.
|
|
7
|
+
version = "5.1.4"
|
|
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"}]
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/client/api_v1_client.py
RENAMED
|
@@ -26,6 +26,9 @@ class ApiV1Client(BaseApiClient):
|
|
|
26
26
|
configuration = Configuration()
|
|
27
27
|
configuration.api_key['TokenAuth'] = self.config.testops.api.token
|
|
28
28
|
configuration.ssl_ca_cert = certifi.where()
|
|
29
|
+
# See the note in ApiV2Client: urllib3's default retry policy is
|
|
30
|
+
# pinned off so it cannot multiply with the application-level one.
|
|
31
|
+
configuration.retries = 0
|
|
29
32
|
host = self.config.testops.api.host
|
|
30
33
|
if host == 'qase.io':
|
|
31
34
|
configuration.host = f'https://api.{host}/v1'
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/client/api_v2_client.py
RENAMED
|
@@ -36,6 +36,11 @@ class ApiV2Client(ApiV1Client):
|
|
|
36
36
|
configuration = Configuration()
|
|
37
37
|
configuration.api_key['TokenAuth'] = self.config.testops.api.token
|
|
38
38
|
configuration.ssl_ca_cert = certifi.where()
|
|
39
|
+
# Pin urllib3's own retry policy off: the reporter retries at the
|
|
40
|
+
# application level, and leaving this at None lets urllib3's
|
|
41
|
+
# defaults multiply with ours -- a 3-attempt policy would issue
|
|
42
|
+
# 9 requests, the opposite of what is wanted on a 429.
|
|
43
|
+
configuration.retries = 0
|
|
39
44
|
host = self.config.testops.api.host
|
|
40
45
|
if host == 'qase.io':
|
|
41
46
|
configuration.host = f'https://api.{host}/v2'
|
|
@@ -131,7 +136,8 @@ class ApiV2Client(ApiV1Client):
|
|
|
131
136
|
run_id_int = int(run_id) if isinstance(run_id, str) else run_id
|
|
132
137
|
self.logger.log_debug(f"Sending results for run {run_id_int}: {results_to_send}")
|
|
133
138
|
api_results.create_results_v2(project_code, run_id_int,
|
|
134
|
-
create_results_request_v2=CreateResultsRequestV2(results=results_to_send)
|
|
139
|
+
create_results_request_v2=CreateResultsRequestV2(results=results_to_send),
|
|
140
|
+
_request_timeout=self.config.testops.api.timeout)
|
|
135
141
|
self.logger.log_debug(f"Results for run {run_id_int} sent successfully")
|
|
136
142
|
|
|
137
143
|
def _prepare_result(self, project_code: str, result: Result) -> ResultCreate:
|
|
@@ -158,6 +164,7 @@ class ApiV2Client(ApiV1Client):
|
|
|
158
164
|
result.params[key] = "empty"
|
|
159
165
|
|
|
160
166
|
result_model_v2 = ResultCreate(
|
|
167
|
+
id=result.id,
|
|
161
168
|
title=result.get_title(),
|
|
162
169
|
signature=result.signature,
|
|
163
170
|
testops_ids=result.get_testops_ids(),
|
|
@@ -109,6 +109,20 @@ class ConfigManager:
|
|
|
109
109
|
self.config.testops.api.set_token(
|
|
110
110
|
api.get("token"))
|
|
111
111
|
|
|
112
|
+
if api.get("timeout"):
|
|
113
|
+
self.config.testops.api.set_timeout(
|
|
114
|
+
api.get("timeout"))
|
|
115
|
+
|
|
116
|
+
# `is not None`, not truthiness: 0 is a valid value
|
|
117
|
+
# for both, meaning "no retries" / "no backoff".
|
|
118
|
+
if api.get("retries") is not None:
|
|
119
|
+
self.config.testops.api.set_retries(
|
|
120
|
+
api.get("retries"))
|
|
121
|
+
|
|
122
|
+
if api.get("retryBackoff") is not None:
|
|
123
|
+
self.config.testops.api.set_retry_backoff(
|
|
124
|
+
api.get("retryBackoff"))
|
|
125
|
+
|
|
112
126
|
if testops.get("project"):
|
|
113
127
|
self.config.testops.set_project(
|
|
114
128
|
testops.get("project"))
|
|
@@ -288,6 +302,15 @@ class ConfigManager:
|
|
|
288
302
|
if key == 'QASE_TESTOPS_API_TOKEN':
|
|
289
303
|
self.config.testops.api.set_token(value)
|
|
290
304
|
|
|
305
|
+
if key == 'QASE_TESTOPS_API_TIMEOUT':
|
|
306
|
+
self.config.testops.api.set_timeout(value)
|
|
307
|
+
|
|
308
|
+
if key == 'QASE_TESTOPS_API_RETRIES':
|
|
309
|
+
self.config.testops.api.set_retries(value)
|
|
310
|
+
|
|
311
|
+
if key == 'QASE_TESTOPS_API_RETRY_BACKOFF':
|
|
312
|
+
self.config.testops.api.set_retry_backoff(value)
|
|
313
|
+
|
|
291
314
|
if key == 'QASE_TESTOPS_PROJECT':
|
|
292
315
|
self.config.testops.set_project(value)
|
|
293
316
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from ..basemodel import BaseModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ApiConfig(BaseModel):
|
|
5
|
+
token: str = None
|
|
6
|
+
host: str = None
|
|
7
|
+
timeout: int = None
|
|
8
|
+
retries: int = None
|
|
9
|
+
retry_backoff: int = None
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self.host = "qase.io"
|
|
13
|
+
self.timeout = 30
|
|
14
|
+
self.retries = 3
|
|
15
|
+
self.retry_backoff = 2
|
|
16
|
+
|
|
17
|
+
def set_token(self, token: str):
|
|
18
|
+
self.token = token
|
|
19
|
+
|
|
20
|
+
def set_host(self, host: str):
|
|
21
|
+
self.host = host
|
|
22
|
+
|
|
23
|
+
def set_timeout(self, timeout: int):
|
|
24
|
+
timeout = int(timeout)
|
|
25
|
+
if timeout <= 0:
|
|
26
|
+
raise ValueError("API timeout should be greater than 0")
|
|
27
|
+
self.timeout = timeout
|
|
28
|
+
|
|
29
|
+
def set_retries(self, retries: int):
|
|
30
|
+
retries = int(retries)
|
|
31
|
+
if retries < 0:
|
|
32
|
+
raise ValueError("API retries should be 0 or greater")
|
|
33
|
+
self.retries = retries
|
|
34
|
+
|
|
35
|
+
def set_retry_backoff(self, retry_backoff: int):
|
|
36
|
+
retry_backoff = int(retry_backoff)
|
|
37
|
+
if retry_backoff < 0:
|
|
38
|
+
raise ValueError("API retry backoff should be 0 or greater")
|
|
39
|
+
self.retry_backoff = retry_backoff
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/config/batch.py
RENAMED
|
@@ -8,6 +8,6 @@ class BatchConfig(BaseModel):
|
|
|
8
8
|
self.size = 200
|
|
9
9
|
|
|
10
10
|
def set_size(self, size: int):
|
|
11
|
-
if size >
|
|
12
|
-
raise ValueError("Batch size should be greater than 0 and
|
|
11
|
+
if size > 200 or size == 0:
|
|
12
|
+
raise ValueError("Batch size should be greater than 0 and no greater than 200")
|
|
13
13
|
self.size = size
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/reporters/testops.py
RENAMED
|
@@ -8,6 +8,7 @@ from .. import Logger, ReporterException
|
|
|
8
8
|
from ..client.base_api_client import BaseApiClient
|
|
9
9
|
from ..models import Result
|
|
10
10
|
from ..models.config.qaseconfig import QaseConfig
|
|
11
|
+
from ..retry import send_with_retry
|
|
11
12
|
|
|
12
13
|
DEFAULT_BATCH_SIZE = 200
|
|
13
14
|
DEFAULT_THREAD_COUNT = 4
|
|
@@ -32,10 +33,12 @@ class QaseTestOps:
|
|
|
32
33
|
self.complete_after_run = self.config.testops.run.complete
|
|
33
34
|
self.environment = None
|
|
34
35
|
|
|
35
|
-
self.batch_size = min(
|
|
36
|
+
self.batch_size = min(200, max(1, int(self.config.testops.batch.size or DEFAULT_BATCH_SIZE)))
|
|
36
37
|
self.send_semaphore = threading.Semaphore(DEFAULT_THREAD_COUNT) # Semaphore to limit concurrent sends
|
|
37
38
|
self.lock = threading.Lock()
|
|
38
39
|
self.count_running_threads = 0
|
|
40
|
+
# Results in batches that could not be delivered after all retries.
|
|
41
|
+
self.lost_results = 0
|
|
39
42
|
|
|
40
43
|
environment = self.config.environment
|
|
41
44
|
if environment:
|
|
@@ -70,15 +73,28 @@ class QaseTestOps:
|
|
|
70
73
|
|
|
71
74
|
def _send_results_threaded(self, results):
|
|
72
75
|
try:
|
|
73
|
-
|
|
76
|
+
send_with_retry(
|
|
77
|
+
lambda: self.client.send_results(self.project_code, self.run_id, results),
|
|
78
|
+
attempts=self.config.testops.api.retries,
|
|
79
|
+
backoff=self.config.testops.api.retry_backoff,
|
|
80
|
+
logger=self.logger,
|
|
81
|
+
)
|
|
74
82
|
with self.lock:
|
|
75
83
|
self.processed.extend(results)
|
|
76
84
|
except Exception as e:
|
|
85
|
+
# Account for the batch here rather than re-raising. The raise only
|
|
86
|
+
# ever surfaced as a PytestUnhandledThreadExceptionWarning that
|
|
87
|
+
# nothing acted on, which is how the loss stayed invisible.
|
|
77
88
|
with self.lock:
|
|
78
|
-
self.
|
|
79
|
-
|
|
89
|
+
self.lost_results += len(results)
|
|
90
|
+
self.logger.log(
|
|
91
|
+
f"Failed to send {len(results)} results for run {self.run_id} "
|
|
92
|
+
f"after {self.config.testops.api.retries} attempt(s): {e}",
|
|
93
|
+
"error",
|
|
94
|
+
)
|
|
80
95
|
finally:
|
|
81
|
-
self.
|
|
96
|
+
with self.lock:
|
|
97
|
+
self.count_running_threads -= 1
|
|
82
98
|
self.send_semaphore.release() # Release semaphore whether success or exception
|
|
83
99
|
|
|
84
100
|
def _send_results(self) -> None:
|
|
@@ -101,7 +117,8 @@ class QaseTestOps:
|
|
|
101
117
|
if results_to_send:
|
|
102
118
|
# Acquire semaphore before starting the send operation
|
|
103
119
|
self.send_semaphore.acquire()
|
|
104
|
-
self.
|
|
120
|
+
with self.lock:
|
|
121
|
+
self.count_running_threads += 1
|
|
105
122
|
|
|
106
123
|
# Start a new thread for sending results
|
|
107
124
|
send_thread = threading.Thread(target=self._send_results_threaded, args=(results_to_send,))
|
|
@@ -136,7 +153,17 @@ class QaseTestOps:
|
|
|
136
153
|
|
|
137
154
|
while self.count_running_threads > 0:
|
|
138
155
|
time.sleep(DEFAULT_THREAD_POLL_INTERVAL)
|
|
139
|
-
|
|
156
|
+
|
|
157
|
+
if self.lost_results > 0:
|
|
158
|
+
# Leaving the run open is the signal. A run marked complete over
|
|
159
|
+
# partial data looks trustworthy and is not.
|
|
160
|
+
self.logger.log(
|
|
161
|
+
f"{self.lost_results} result(s) were not delivered to Qase. "
|
|
162
|
+
f"The run will not be marked complete so the gap stays visible.",
|
|
163
|
+
"error",
|
|
164
|
+
)
|
|
165
|
+
return
|
|
166
|
+
|
|
140
167
|
if self.complete_after_run:
|
|
141
168
|
self.logger.log_debug("Completing run")
|
|
142
169
|
self.client.complete_run(self.project_code, self.run_id)
|
|
@@ -158,7 +185,7 @@ class QaseTestOps:
|
|
|
158
185
|
if len(self.results) > 0:
|
|
159
186
|
self._send_results()
|
|
160
187
|
while self.count_running_threads > 0:
|
|
161
|
-
|
|
188
|
+
time.sleep(DEFAULT_THREAD_POLL_INTERVAL)
|
|
162
189
|
self.logger.log_debug("Worker completed")
|
|
163
190
|
|
|
164
191
|
def add_result(self, result: Result) -> None:
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/reporters/testops_multi.py
RENAMED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import threading
|
|
2
|
+
import time
|
|
2
3
|
import urllib.parse
|
|
3
4
|
import copy
|
|
4
5
|
|
|
@@ -8,9 +9,11 @@ from .. import Logger, ReporterException
|
|
|
8
9
|
from ..client.base_api_client import BaseApiClient
|
|
9
10
|
from ..models import Result
|
|
10
11
|
from ..models.config.qaseconfig import QaseConfig
|
|
12
|
+
from ..retry import send_with_retry
|
|
11
13
|
|
|
12
14
|
DEFAULT_BATCH_SIZE = 200
|
|
13
15
|
DEFAULT_THREAD_COUNT = 4
|
|
16
|
+
DEFAULT_THREAD_POLL_INTERVAL = 0.05
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
class QaseTestOpsMulti:
|
|
@@ -22,7 +25,7 @@ class QaseTestOpsMulti:
|
|
|
22
25
|
self.client = client
|
|
23
26
|
|
|
24
27
|
self.multi_config = config.testops_multi
|
|
25
|
-
self.batch_size = min(
|
|
28
|
+
self.batch_size = min(200, max(1, int(self.config.testops.batch.size or DEFAULT_BATCH_SIZE)))
|
|
26
29
|
self.send_semaphore = threading.Semaphore(DEFAULT_THREAD_COUNT)
|
|
27
30
|
self.lock = threading.Lock()
|
|
28
31
|
self.count_running_threads = 0
|
|
@@ -44,6 +47,9 @@ class QaseTestOpsMulti:
|
|
|
44
47
|
self.project_results: Dict[str, List[Result]] = {project.code: [] for project in self.multi_config.projects}
|
|
45
48
|
self.project_runs: Dict[str, int] = {} # Store run_id as int
|
|
46
49
|
self.processed: Dict[str, List[Result]] = {project.code: [] for project in self.multi_config.projects}
|
|
50
|
+
# Results in batches that could not be delivered after all retries,
|
|
51
|
+
# counted per project so one project's loss does not hide another's.
|
|
52
|
+
self.lost_results: Dict[str, int] = {project.code: 0 for project in self.multi_config.projects}
|
|
47
53
|
|
|
48
54
|
# Initialize environment for each project
|
|
49
55
|
for project_code, project_data in self.project_configs.items():
|
|
@@ -101,15 +107,28 @@ class QaseTestOpsMulti:
|
|
|
101
107
|
try:
|
|
102
108
|
# Convert run_id to str for send_results (it will convert to int internally for API)
|
|
103
109
|
run_id_str = str(run_id) if isinstance(run_id, int) else run_id
|
|
104
|
-
|
|
110
|
+
send_with_retry(
|
|
111
|
+
lambda: self.client.send_results(project_code, run_id_str, results),
|
|
112
|
+
attempts=self.config.testops.api.retries,
|
|
113
|
+
backoff=self.config.testops.api.retry_backoff,
|
|
114
|
+
logger=self.logger,
|
|
115
|
+
)
|
|
105
116
|
with self.lock:
|
|
106
117
|
self.processed[project_code].extend(results)
|
|
107
118
|
except Exception as e:
|
|
119
|
+
# Account for the batch here rather than re-raising. The raise only
|
|
120
|
+
# ever surfaced as a PytestUnhandledThreadExceptionWarning that
|
|
121
|
+
# nothing acted on, which is how the loss stayed invisible.
|
|
108
122
|
with self.lock:
|
|
109
|
-
self.
|
|
110
|
-
|
|
123
|
+
self.lost_results[project_code] = self.lost_results.get(project_code, 0) + len(results)
|
|
124
|
+
self.logger.log(
|
|
125
|
+
f"Failed to send {len(results)} results for project {project_code}, "
|
|
126
|
+
f"run {run_id} after {self.config.testops.api.retries} attempt(s): {e}",
|
|
127
|
+
"error",
|
|
128
|
+
)
|
|
111
129
|
finally:
|
|
112
|
-
self.
|
|
130
|
+
with self.lock:
|
|
131
|
+
self.count_running_threads -= 1
|
|
113
132
|
self.send_semaphore.release()
|
|
114
133
|
|
|
115
134
|
def _send_results_for_project(self, project_code: str) -> None:
|
|
@@ -141,7 +160,8 @@ class QaseTestOpsMulti:
|
|
|
141
160
|
if results_to_send:
|
|
142
161
|
# Acquire semaphore before starting the send operation
|
|
143
162
|
self.send_semaphore.acquire()
|
|
144
|
-
self.
|
|
163
|
+
with self.lock:
|
|
164
|
+
self.count_running_threads += 1
|
|
145
165
|
|
|
146
166
|
# Start a new thread for sending results
|
|
147
167
|
# run_id is stored as int, convert to str for thread (will be converted back to int in send_results)
|
|
@@ -230,10 +250,21 @@ class QaseTestOpsMulti:
|
|
|
230
250
|
|
|
231
251
|
# Wait for all send operations to complete
|
|
232
252
|
while self.count_running_threads > 0:
|
|
233
|
-
|
|
253
|
+
time.sleep(DEFAULT_THREAD_POLL_INTERVAL)
|
|
234
254
|
|
|
235
255
|
# Complete all test runs
|
|
236
256
|
for project_code, project_data in self.project_configs.items():
|
|
257
|
+
if self.lost_results.get(project_code, 0) > 0:
|
|
258
|
+
# `continue`, not `return`: one project's loss must not stop
|
|
259
|
+
# the others completing. Leaving this run open is the signal.
|
|
260
|
+
self.logger.log(
|
|
261
|
+
f"{self.lost_results[project_code]} result(s) were not delivered for "
|
|
262
|
+
f"project {project_code}. Its run will not be marked complete so the "
|
|
263
|
+
f"gap stays visible.",
|
|
264
|
+
"error",
|
|
265
|
+
)
|
|
266
|
+
continue
|
|
267
|
+
|
|
237
268
|
if project_data.get('complete_after_run'):
|
|
238
269
|
run_id = self.project_runs.get(project_code)
|
|
239
270
|
if run_id:
|
|
@@ -258,7 +289,7 @@ class QaseTestOpsMulti:
|
|
|
258
289
|
if any(len(results) > 0 for results in self.project_results.values()):
|
|
259
290
|
self._send_results()
|
|
260
291
|
while self.count_running_threads > 0:
|
|
261
|
-
|
|
292
|
+
time.sleep(DEFAULT_THREAD_POLL_INTERVAL)
|
|
262
293
|
self.logger.log_debug("Worker completed")
|
|
263
294
|
|
|
264
295
|
def add_result(self, result: Result) -> None:
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Bounded retry for result uploads.
|
|
2
|
+
|
|
3
|
+
Retries transport failures and the HTTP statuses that can succeed on a second
|
|
4
|
+
attempt. Never retries a status that would be rejected identically -- an
|
|
5
|
+
oversized batch, an invalid payload, an expired token -- where retrying only
|
|
6
|
+
delays the error and adds load.
|
|
7
|
+
|
|
8
|
+
Safe only because ResultCreate.id is populated: a request that was accepted
|
|
9
|
+
but whose response was lost is indistinguishable from one that never arrived,
|
|
10
|
+
and the idempotency key is what stops the retry creating duplicates.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import time
|
|
14
|
+
from typing import Callable, Optional
|
|
15
|
+
|
|
16
|
+
RETRYABLE_STATUSES = frozenset({408, 429, 500, 502, 503, 504})
|
|
17
|
+
|
|
18
|
+
_TRANSPORT_ERRORS = (ConnectionError, OSError, TimeoutError)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _urllib3_errors():
|
|
22
|
+
try:
|
|
23
|
+
from urllib3.exceptions import HTTPError
|
|
24
|
+
|
|
25
|
+
return (HTTPError,)
|
|
26
|
+
except ImportError: # pragma: no cover - urllib3 is a hard dependency
|
|
27
|
+
return ()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_retryable(exc: BaseException) -> bool:
|
|
31
|
+
"""True when another attempt could plausibly succeed."""
|
|
32
|
+
status = getattr(exc, "status", None)
|
|
33
|
+
if status is not None:
|
|
34
|
+
try:
|
|
35
|
+
return int(status) in RETRYABLE_STATUSES
|
|
36
|
+
except (TypeError, ValueError):
|
|
37
|
+
return False
|
|
38
|
+
return isinstance(exc, _TRANSPORT_ERRORS + _urllib3_errors())
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def retry_after_seconds(exc: BaseException) -> Optional[float]:
|
|
42
|
+
"""Seconds requested by a Retry-After header, when there is one.
|
|
43
|
+
|
|
44
|
+
Qase answers 429 with roughly 60 seconds, which no short backoff ladder
|
|
45
|
+
survives, so honouring the header matters more than the ladder itself.
|
|
46
|
+
"""
|
|
47
|
+
headers = getattr(exc, "headers", None)
|
|
48
|
+
if not headers:
|
|
49
|
+
return None
|
|
50
|
+
try:
|
|
51
|
+
value = headers.get("Retry-After")
|
|
52
|
+
except AttributeError:
|
|
53
|
+
return None
|
|
54
|
+
if value is None:
|
|
55
|
+
return None
|
|
56
|
+
try:
|
|
57
|
+
return float(value)
|
|
58
|
+
except (TypeError, ValueError):
|
|
59
|
+
# The RFC also allows an HTTP-date. We do not parse it: falling back to
|
|
60
|
+
# the computed backoff is safer than guessing at a clock skew.
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def send_with_retry(
|
|
65
|
+
send: Callable[[], None],
|
|
66
|
+
attempts: int,
|
|
67
|
+
backoff: int,
|
|
68
|
+
sleep: Callable[[float], None] = time.sleep,
|
|
69
|
+
logger=None,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Call send(), retrying retryable failures. Re-raises the last exception.
|
|
72
|
+
|
|
73
|
+
`attempts` is the total number of tries, not the number of retries on top
|
|
74
|
+
of the first: a configured `retries: 3` means three attempts in all, and
|
|
75
|
+
`retries: 0` still sends once but never retries.
|
|
76
|
+
"""
|
|
77
|
+
attempts = max(1, attempts)
|
|
78
|
+
for attempt in range(1, attempts + 1):
|
|
79
|
+
try:
|
|
80
|
+
send()
|
|
81
|
+
return
|
|
82
|
+
except Exception as exc:
|
|
83
|
+
if attempt == attempts or not is_retryable(exc):
|
|
84
|
+
raise
|
|
85
|
+
delay = retry_after_seconds(exc)
|
|
86
|
+
if delay is None:
|
|
87
|
+
delay = backoff ** attempt
|
|
88
|
+
if logger:
|
|
89
|
+
logger.log(
|
|
90
|
+
f"Upload attempt {attempt}/{attempts} failed ({exc}); "
|
|
91
|
+
f"retrying in {delay}s",
|
|
92
|
+
"warning",
|
|
93
|
+
)
|
|
94
|
+
sleep(delay)
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase_python_commons.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qase-python-commons
|
|
3
|
-
Version: 5.1.
|
|
3
|
+
Version: 5.1.4
|
|
4
4
|
Summary: A library for Qase TestOps and Qase Report
|
|
5
5
|
Author-email: Qase Team <support@qase.io>
|
|
6
6
|
Project-URL: Homepage, https://github.com/qase-tms/qase-python/tree/main/qase-python-commons
|
|
@@ -65,6 +65,7 @@ Core library for all Qase Python reporters. Contains the complete configuration
|
|
|
65
65
|
- [Status Filtering](#status-filtering)
|
|
66
66
|
- [External Links](#external-links)
|
|
67
67
|
- [Test Run Configurations](#test-run-configurations)
|
|
68
|
+
- [Upload Reliability](#upload-reliability)
|
|
68
69
|
|
|
69
70
|
---
|
|
70
71
|
|
|
@@ -137,6 +138,9 @@ The reporter mode is set via the `mode` option:
|
|
|
137
138
|
|-------------|-------------|---------------------|---------|----------|
|
|
138
139
|
| API token | `testops.api.token` | `QASE_TESTOPS_API_TOKEN` | — | Yes* |
|
|
139
140
|
| API host | `testops.api.host` | `QASE_TESTOPS_API_HOST` | `qase.io` | No |
|
|
141
|
+
| Request timeout, seconds | `testops.api.timeout` | `QASE_TESTOPS_API_TIMEOUT` | `30` | No |
|
|
142
|
+
| Upload attempts | `testops.api.retries` | `QASE_TESTOPS_API_RETRIES` | `3` | No |
|
|
143
|
+
| Retry backoff base, seconds | `testops.api.retryBackoff` | `QASE_TESTOPS_API_RETRY_BACKOFF` | `2` | No |
|
|
140
144
|
| Project code | `testops.project` | `QASE_TESTOPS_PROJECT` | — | Yes* |
|
|
141
145
|
| Test run ID | `testops.run.id` | `QASE_TESTOPS_RUN_ID` | — | No |
|
|
142
146
|
| Test run title | `testops.run.title` | `QASE_TESTOPS_RUN_TITLE` | `Automated run <date>` | No |
|
|
@@ -296,6 +300,9 @@ export QASE_TESTOPS_API_TOKEN="<token>"
|
|
|
296
300
|
export QASE_TESTOPS_PROJECT="DEMO"
|
|
297
301
|
export QASE_TESTOPS_RUN_TITLE="Automated Run"
|
|
298
302
|
export QASE_TESTOPS_RUN_COMPLETE="true"
|
|
303
|
+
export QASE_TESTOPS_API_TIMEOUT="30"
|
|
304
|
+
export QASE_TESTOPS_API_RETRIES="3"
|
|
305
|
+
export QASE_TESTOPS_API_RETRY_BACKOFF="2"
|
|
299
306
|
|
|
300
307
|
# Pytest
|
|
301
308
|
export QASE_PYTEST_CAPTURE_LOGS="true"
|
|
@@ -409,6 +416,44 @@ Creates or finds configurations in Qase TestOps:
|
|
|
409
416
|
}
|
|
410
417
|
```
|
|
411
418
|
|
|
419
|
+
### Upload Reliability
|
|
420
|
+
|
|
421
|
+
Results are uploaded in batches from background threads. A batch that fails on
|
|
422
|
+
a transient error is retried rather than dropped.
|
|
423
|
+
|
|
424
|
+
| Setting | Meaning |
|
|
425
|
+
|---------|---------|
|
|
426
|
+
| `testops.api.timeout` | Per-request timeout in seconds. Without it a stalled connection blocks the session at teardown. |
|
|
427
|
+
| `testops.api.retries` | Total attempts per batch, not retries on top of the first. `3` means three tries; `0` sends once and never retries. |
|
|
428
|
+
| `testops.api.retryBackoff` | Base of the exponential delay: attempt *n* waits `retryBackoff ** n` seconds. |
|
|
429
|
+
|
|
430
|
+
Retried: connection resets, timeouts and other transport failures, plus HTTP
|
|
431
|
+
408, 429 and 5xx. Not retried: 400, 401, 403, 404, 413, 422 and 507 — a second
|
|
432
|
+
attempt fails identically and only adds load.
|
|
433
|
+
|
|
434
|
+
When a 429 carries a `Retry-After` header, that value replaces the computed
|
|
435
|
+
backoff. Qase sends roughly 60 seconds, so a run that hits the rate limit takes
|
|
436
|
+
longer to finish rather than losing the batch.
|
|
437
|
+
|
|
438
|
+
**If a batch cannot be delivered after all attempts**, the reporter logs an
|
|
439
|
+
error naming how many results were lost and **does not mark the run complete**.
|
|
440
|
+
An open run is the signal that its data is incomplete; a completed run over
|
|
441
|
+
partial results would look trustworthy and not be. In `testops_multi` mode this
|
|
442
|
+
is per project — one project's failure does not stop the others completing.
|
|
443
|
+
|
|
444
|
+
```json
|
|
445
|
+
{
|
|
446
|
+
"testops": {
|
|
447
|
+
"api": {
|
|
448
|
+
"token": "<token>",
|
|
449
|
+
"timeout": 30,
|
|
450
|
+
"retries": 3,
|
|
451
|
+
"retryBackoff": 2
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
412
457
|
---
|
|
413
458
|
|
|
414
459
|
## Requirements
|
|
@@ -1,15 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/client/base_api_client.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/exceptions/reporter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/attachment.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/basemodel.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/config/connection.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/config/framework.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/config/plan.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/config/qaseconfig.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/config/report.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/config/run.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/config/testops.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/models/external_link.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/profilers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/profilers/network.py
RENAMED
|
File without changes
|
|
File without changes
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/reporters/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/reporters/report.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/status_mapping/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{qase_python_commons-5.1.2 → qase_python_commons-5.1.4}/src/qase/commons/util/token_masker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|