qase-python-commons 3.4.1__py3-none-any.whl → 4.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of qase-python-commons might be problematic. Click here for more details.
- qase/__init__.py +3 -0
- qase/commons/client/api_v1_client.py +153 -5
- qase/commons/client/api_v2_client.py +34 -5
- qase/commons/client/base_api_client.py +12 -1
- qase/commons/config.py +139 -15
- qase/commons/logger.py +63 -7
- qase/commons/models/attachment.py +11 -8
- qase/commons/models/config/qaseconfig.py +34 -0
- qase/commons/models/config/run.py +19 -0
- qase/commons/models/config/testops.py +46 -0
- qase/commons/models/external_link.py +41 -0
- qase/commons/models/result.py +10 -4
- qase/commons/models/run.py +3 -0
- qase/commons/models/runtime.py +9 -0
- qase/commons/models/step.py +37 -9
- qase/commons/profilers/__init__.py +4 -3
- qase/commons/profilers/db.py +965 -5
- qase/commons/reporters/core.py +42 -6
- qase/commons/reporters/report.py +2 -2
- qase/commons/reporters/testops.py +48 -10
- qase/commons/status_mapping/__init__.py +12 -0
- qase/commons/status_mapping/status_mapping.py +237 -0
- qase/commons/utils.py +95 -0
- {qase_python_commons-3.4.1.dist-info → qase_python_commons-4.1.3.dist-info}/METADATA +12 -7
- qase_python_commons-4.1.3.dist-info/RECORD +45 -0
- {qase_python_commons-3.4.1.dist-info → qase_python_commons-4.1.3.dist-info}/WHEEL +1 -1
- qase_python_commons-3.4.1.dist-info/RECORD +0 -41
- {qase_python_commons-3.4.1.dist-info → qase_python_commons-4.1.3.dist-info}/top_level.txt +0 -0
|
@@ -29,16 +29,19 @@ class Attachment(BaseModel):
|
|
|
29
29
|
def get_id(self) -> str:
|
|
30
30
|
return self.id
|
|
31
31
|
|
|
32
|
-
def get_for_upload(self) ->
|
|
32
|
+
def get_for_upload(self) -> tuple:
|
|
33
|
+
"""Returns attachment data in format expected by new API client: (filename, filedata)"""
|
|
34
|
+
|
|
33
35
|
if self.file_path:
|
|
34
36
|
with open(self.file_path, "rb") as fc:
|
|
35
|
-
|
|
37
|
+
filedata = fc.read()
|
|
36
38
|
else:
|
|
37
39
|
if isinstance(self.content, str):
|
|
38
|
-
|
|
40
|
+
filedata = bytes(self.content, 'utf-8')
|
|
39
41
|
elif isinstance(self.content, bytes):
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
filedata = self.content
|
|
43
|
+
else:
|
|
44
|
+
# Handle case where content is not str or bytes (e.g., JSON serialized)
|
|
45
|
+
filedata = bytes(self.content, 'utf-8')
|
|
46
|
+
|
|
47
|
+
return (self.file_name, filedata)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
+
from typing import List, Dict, Optional
|
|
2
3
|
|
|
3
4
|
from .framework import Framework
|
|
4
5
|
from .report import ReportConfig
|
|
@@ -13,6 +14,21 @@ class Mode(Enum):
|
|
|
13
14
|
off = "off"
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
class LoggingConfig(BaseModel):
|
|
18
|
+
console: Optional[bool] = None
|
|
19
|
+
file: Optional[bool] = None
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.console = None
|
|
23
|
+
self.file = None
|
|
24
|
+
|
|
25
|
+
def set_console(self, console: bool):
|
|
26
|
+
self.console = console
|
|
27
|
+
|
|
28
|
+
def set_file(self, file: bool):
|
|
29
|
+
self.file = file
|
|
30
|
+
|
|
31
|
+
|
|
16
32
|
class ExecutionPlan(BaseModel):
|
|
17
33
|
path: str = None
|
|
18
34
|
|
|
@@ -34,6 +50,9 @@ class QaseConfig(BaseModel):
|
|
|
34
50
|
report: ReportConfig = None
|
|
35
51
|
profilers: list = None
|
|
36
52
|
framework: Framework = None
|
|
53
|
+
exclude_params: list = None
|
|
54
|
+
status_mapping: Dict[str, str] = None
|
|
55
|
+
logging: LoggingConfig = None
|
|
37
56
|
|
|
38
57
|
def __init__(self):
|
|
39
58
|
self.mode = Mode.off
|
|
@@ -44,6 +63,9 @@ class QaseConfig(BaseModel):
|
|
|
44
63
|
self.execution_plan = ExecutionPlan()
|
|
45
64
|
self.framework = Framework()
|
|
46
65
|
self.profilers = []
|
|
66
|
+
self.exclude_params = []
|
|
67
|
+
self.status_mapping = {}
|
|
68
|
+
self.logging = LoggingConfig()
|
|
47
69
|
|
|
48
70
|
def set_mode(self, mode: str):
|
|
49
71
|
if any(mode == e.value for e in Mode.__members__.values()):
|
|
@@ -64,3 +86,15 @@ class QaseConfig(BaseModel):
|
|
|
64
86
|
|
|
65
87
|
def set_debug(self, debug):
|
|
66
88
|
self.debug = QaseUtils.parse_bool(debug)
|
|
89
|
+
|
|
90
|
+
def set_exclude_params(self, exclude_params: List[str]):
|
|
91
|
+
self.exclude_params = exclude_params
|
|
92
|
+
|
|
93
|
+
def set_status_mapping(self, status_mapping: Dict[str, str]):
|
|
94
|
+
self.status_mapping = status_mapping
|
|
95
|
+
|
|
96
|
+
def set_logging(self, logging_config: dict):
|
|
97
|
+
if logging_config.get("console") is not None:
|
|
98
|
+
self.logging.set_console(QaseUtils.parse_bool(logging_config.get("console")))
|
|
99
|
+
if logging_config.get("file") is not None:
|
|
100
|
+
self.logging.set_file(QaseUtils.parse_bool(logging_config.get("file")))
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
1
2
|
from ..basemodel import BaseModel
|
|
3
|
+
from ..external_link import ExternalLinkConfig
|
|
2
4
|
from ... import QaseUtils
|
|
3
5
|
|
|
4
6
|
|
|
@@ -7,9 +9,14 @@ class RunConfig(BaseModel):
|
|
|
7
9
|
description: str = None
|
|
8
10
|
complete: bool = None
|
|
9
11
|
id: int = None
|
|
12
|
+
tags: List[str] = None
|
|
13
|
+
external_link: Optional[ExternalLinkConfig] = None
|
|
14
|
+
|
|
10
15
|
|
|
11
16
|
def __init__(self):
|
|
12
17
|
self.complete = True
|
|
18
|
+
self.tags = []
|
|
19
|
+
self.external_link = None
|
|
13
20
|
|
|
14
21
|
def set_title(self, title: str):
|
|
15
22
|
self.title = title
|
|
@@ -22,3 +29,15 @@ class RunConfig(BaseModel):
|
|
|
22
29
|
|
|
23
30
|
def set_id(self, id: int):
|
|
24
31
|
self.id = id
|
|
32
|
+
|
|
33
|
+
def set_tags(self, tags: List[str]):
|
|
34
|
+
self.tags = tags
|
|
35
|
+
|
|
36
|
+
def set_external_link(self, external_link: dict):
|
|
37
|
+
"""Set external link configuration from dictionary"""
|
|
38
|
+
if external_link:
|
|
39
|
+
self.external_link = ExternalLinkConfig()
|
|
40
|
+
if 'type' in external_link:
|
|
41
|
+
self.external_link.set_type(external_link['type'])
|
|
42
|
+
if 'link' in external_link:
|
|
43
|
+
self.external_link.set_link(external_link['link'])
|
|
@@ -4,6 +4,40 @@ from .plan import PlanConfig
|
|
|
4
4
|
from .run import RunConfig
|
|
5
5
|
from ..basemodel import BaseModel
|
|
6
6
|
from ... import QaseUtils
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConfigurationValue(BaseModel):
|
|
11
|
+
name: str = None
|
|
12
|
+
value: str = None
|
|
13
|
+
|
|
14
|
+
def __init__(self, name: str = None, value: str = None):
|
|
15
|
+
self.name = name
|
|
16
|
+
self.value = value
|
|
17
|
+
|
|
18
|
+
def set_name(self, name: str):
|
|
19
|
+
self.name = name
|
|
20
|
+
|
|
21
|
+
def set_value(self, value: str):
|
|
22
|
+
self.value = value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ConfigurationsConfig(BaseModel):
|
|
26
|
+
values: List[ConfigurationValue] = None
|
|
27
|
+
create_if_not_exists: bool = None
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
self.values = []
|
|
31
|
+
self.create_if_not_exists = False
|
|
32
|
+
|
|
33
|
+
def set_values(self, values: List[ConfigurationValue]):
|
|
34
|
+
self.values = values
|
|
35
|
+
|
|
36
|
+
def set_create_if_not_exists(self, create_if_not_exists):
|
|
37
|
+
self.create_if_not_exists = QaseUtils.parse_bool(create_if_not_exists)
|
|
38
|
+
|
|
39
|
+
def add_value(self, name: str, value: str):
|
|
40
|
+
self.values.append(ConfigurationValue(name=name, value=value))
|
|
7
41
|
|
|
8
42
|
|
|
9
43
|
class TestopsConfig(BaseModel):
|
|
@@ -13,16 +47,28 @@ class TestopsConfig(BaseModel):
|
|
|
13
47
|
run: RunConfig = None
|
|
14
48
|
plan: PlanConfig = None
|
|
15
49
|
batch: BatchConfig = None
|
|
50
|
+
configurations: ConfigurationsConfig = None
|
|
51
|
+
status_filter: List[str] = None
|
|
52
|
+
show_public_report_link: bool = None
|
|
16
53
|
|
|
17
54
|
def __init__(self):
|
|
18
55
|
self.api = ApiConfig()
|
|
19
56
|
self.run = RunConfig()
|
|
20
57
|
self.batch = BatchConfig()
|
|
21
58
|
self.plan = PlanConfig()
|
|
59
|
+
self.configurations = ConfigurationsConfig()
|
|
22
60
|
self.defect = False
|
|
61
|
+
self.status_filter = []
|
|
62
|
+
self.show_public_report_link = False
|
|
23
63
|
|
|
24
64
|
def set_project(self, project: str):
|
|
25
65
|
self.project = project
|
|
26
66
|
|
|
27
67
|
def set_defect(self, defect):
|
|
28
68
|
self.defect = QaseUtils.parse_bool(defect)
|
|
69
|
+
|
|
70
|
+
def set_status_filter(self, status_filter: List[str]):
|
|
71
|
+
self.status_filter = status_filter
|
|
72
|
+
|
|
73
|
+
def set_show_public_report_link(self, show_public_report_link):
|
|
74
|
+
self.show_public_report_link = QaseUtils.parse_bool(show_public_report_link)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from .basemodel import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ExternalLinkType(Enum):
|
|
7
|
+
"""External link types supported by Qase TestOps"""
|
|
8
|
+
JIRA_CLOUD = 'jiraCloud'
|
|
9
|
+
JIRA_SERVER = 'jiraServer'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ExternalLinkConfig(BaseModel):
|
|
13
|
+
"""Configuration for external link"""
|
|
14
|
+
type: ExternalLinkType = None
|
|
15
|
+
link: str = None
|
|
16
|
+
|
|
17
|
+
def __init__(self, type: ExternalLinkType = None, link: str = None):
|
|
18
|
+
self.type = type
|
|
19
|
+
self.link = link
|
|
20
|
+
|
|
21
|
+
def set_type(self, type: str):
|
|
22
|
+
"""Set external link type from string"""
|
|
23
|
+
if type == 'jiraCloud':
|
|
24
|
+
self.type = ExternalLinkType.JIRA_CLOUD
|
|
25
|
+
elif type == 'jiraServer':
|
|
26
|
+
self.type = ExternalLinkType.JIRA_SERVER
|
|
27
|
+
else:
|
|
28
|
+
raise ValueError(f"Invalid external link type: {type}. Supported types: jiraCloud, jiraServer")
|
|
29
|
+
|
|
30
|
+
def set_link(self, link: str):
|
|
31
|
+
"""Set external link URL or identifier"""
|
|
32
|
+
self.link = link
|
|
33
|
+
|
|
34
|
+
def to_api_type(self) -> str:
|
|
35
|
+
"""Convert to API enum value"""
|
|
36
|
+
if self.type == ExternalLinkType.JIRA_CLOUD:
|
|
37
|
+
return 'jira-cloud'
|
|
38
|
+
elif self.type == ExternalLinkType.JIRA_SERVER:
|
|
39
|
+
return 'jira-server'
|
|
40
|
+
else:
|
|
41
|
+
raise ValueError(f"Invalid external link type: {self.type}")
|
qase/commons/models/result.py
CHANGED
|
@@ -25,7 +25,7 @@ class Execution(BaseModel):
|
|
|
25
25
|
stacktrace: Optional[str] = None,
|
|
26
26
|
thread: Optional[str] = QaseUtils.get_thread_name()
|
|
27
27
|
):
|
|
28
|
-
self.start_time =
|
|
28
|
+
self.start_time = QaseUtils.get_real_time()
|
|
29
29
|
self.status = status
|
|
30
30
|
self.end_time = end_time
|
|
31
31
|
self.duration = duration
|
|
@@ -33,16 +33,22 @@ class Execution(BaseModel):
|
|
|
33
33
|
self.thread = thread
|
|
34
34
|
|
|
35
35
|
def set_status(self, status: Optional[str]):
|
|
36
|
-
if status
|
|
36
|
+
if status is None:
|
|
37
37
|
self.status = status
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
# Convert to lowercase for validation
|
|
41
|
+
status_lower = status.lower()
|
|
42
|
+
if status_lower in ['passed', 'failed', 'skipped', 'untested', 'invalid', 'disabled', 'blocked']:
|
|
43
|
+
self.status = status_lower
|
|
38
44
|
else:
|
|
39
|
-
raise ValueError('Step status must be one of: passed, failed, skipped, untested')
|
|
45
|
+
raise ValueError('Step status must be one of: passed, failed, skipped, untested, invalid, disabled, blocked')
|
|
40
46
|
|
|
41
47
|
def get_status(self):
|
|
42
48
|
return self.status
|
|
43
49
|
|
|
44
50
|
def complete(self):
|
|
45
|
-
self.end_time =
|
|
51
|
+
self.end_time = QaseUtils.get_real_time()
|
|
46
52
|
self.duration = (int)((self.end_time - self.start_time) * 1000)
|
|
47
53
|
|
|
48
54
|
|
qase/commons/models/run.py
CHANGED
|
@@ -24,6 +24,7 @@ class RunStats(BaseModel):
|
|
|
24
24
|
self.failed = 0
|
|
25
25
|
self.skipped = 0
|
|
26
26
|
self.broken = 0
|
|
27
|
+
self.invalid = 0
|
|
27
28
|
self.muted = 0
|
|
28
29
|
self.total = 0
|
|
29
30
|
|
|
@@ -37,6 +38,8 @@ class RunStats(BaseModel):
|
|
|
37
38
|
self.skipped += 1
|
|
38
39
|
elif status == "broken":
|
|
39
40
|
self.broken += 1
|
|
41
|
+
elif status == "invalid":
|
|
42
|
+
self.invalid += 1
|
|
40
43
|
self.total += 1
|
|
41
44
|
if result.get('muted', False):
|
|
42
45
|
self.muted += 1
|
qase/commons/models/runtime.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from . import Result
|
|
1
2
|
from .step import Step
|
|
2
3
|
from .attachment import Attachment
|
|
3
4
|
|
|
@@ -42,6 +43,14 @@ class Runtime:
|
|
|
42
43
|
except Exception as e:
|
|
43
44
|
raise QaseRuntimeException(e)
|
|
44
45
|
|
|
46
|
+
|
|
47
|
+
def add_param(self, key: str, value: str):
|
|
48
|
+
"""
|
|
49
|
+
Add a parameter to the current result.
|
|
50
|
+
"""
|
|
51
|
+
if self.result is not None:
|
|
52
|
+
self.result.params[key] = value
|
|
53
|
+
|
|
45
54
|
def clear(self):
|
|
46
55
|
self.result = None
|
|
47
56
|
self.steps = {}
|
qase/commons/models/step.py
CHANGED
|
@@ -5,6 +5,7 @@ from enum import Enum
|
|
|
5
5
|
from typing import Optional, Union, Dict, List, Type
|
|
6
6
|
from .attachment import Attachment
|
|
7
7
|
from .basemodel import BaseModel
|
|
8
|
+
from .. import QaseUtils
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class StepType(Enum):
|
|
@@ -43,10 +44,20 @@ class StepRequestData(BaseModel):
|
|
|
43
44
|
self.response_body = None
|
|
44
45
|
self.status_code = None
|
|
45
46
|
if isinstance(request_body, bytes):
|
|
46
|
-
|
|
47
|
+
try:
|
|
48
|
+
request_body = request_body.decode('utf-8')
|
|
49
|
+
except UnicodeDecodeError:
|
|
50
|
+
# For binary data (like file uploads), keep as base64 encoded string
|
|
51
|
+
import base64
|
|
52
|
+
request_body = base64.b64encode(request_body).decode('ascii')
|
|
47
53
|
self.request_body = request_body
|
|
48
54
|
if isinstance(request_headers, bytes):
|
|
49
|
-
|
|
55
|
+
try:
|
|
56
|
+
request_headers = request_headers.decode('utf-8')
|
|
57
|
+
except UnicodeDecodeError:
|
|
58
|
+
# For binary headers, keep as base64 encoded string
|
|
59
|
+
import base64
|
|
60
|
+
request_headers = base64.b64encode(request_headers).decode('ascii')
|
|
50
61
|
self.request_headers = request_headers
|
|
51
62
|
self.request_method = request_method
|
|
52
63
|
self.request_url = request_url
|
|
@@ -56,16 +67,33 @@ class StepRequestData(BaseModel):
|
|
|
56
67
|
self.status_code = status_code
|
|
57
68
|
|
|
58
69
|
if isinstance(response_body, bytes):
|
|
59
|
-
|
|
70
|
+
try:
|
|
71
|
+
response_body = response_body.decode('utf-8')
|
|
72
|
+
except UnicodeDecodeError:
|
|
73
|
+
# For binary data (like file downloads), keep as base64 encoded string
|
|
74
|
+
import base64
|
|
75
|
+
response_body = base64.b64encode(response_body).decode('ascii')
|
|
60
76
|
self.response_body = response_body
|
|
61
77
|
if isinstance(response_headers, bytes):
|
|
62
|
-
|
|
78
|
+
try:
|
|
79
|
+
response_headers = response_headers.decode('utf-8')
|
|
80
|
+
except UnicodeDecodeError:
|
|
81
|
+
# For binary headers, keep as base64 encoded string
|
|
82
|
+
import base64
|
|
83
|
+
response_headers = base64.b64encode(response_headers).decode('ascii')
|
|
63
84
|
self.response_headers = response_headers
|
|
64
85
|
|
|
65
86
|
|
|
66
87
|
class StepDbQueryData(BaseModel):
|
|
67
|
-
def __init__(self, query: str, expected_result: str
|
|
88
|
+
def __init__(self, query: str, expected_result: str = None,
|
|
89
|
+
database_type: str = None, execution_time: float = None,
|
|
90
|
+
rows_affected: int = None, connection_info: str = None):
|
|
68
91
|
self.query = query
|
|
92
|
+
self.expected_result = expected_result
|
|
93
|
+
self.database_type = database_type
|
|
94
|
+
self.execution_time = execution_time
|
|
95
|
+
self.rows_affected = rows_affected
|
|
96
|
+
self.connection_info = connection_info
|
|
69
97
|
|
|
70
98
|
|
|
71
99
|
class StepSleepData(BaseModel):
|
|
@@ -75,20 +103,20 @@ class StepSleepData(BaseModel):
|
|
|
75
103
|
|
|
76
104
|
class StepExecution(BaseModel):
|
|
77
105
|
def __init__(self, status: Optional[str] = 'untested', end_time: int = 0, duration: int = 0):
|
|
78
|
-
self.start_time =
|
|
106
|
+
self.start_time = QaseUtils.get_real_time()
|
|
79
107
|
self.status = status
|
|
80
108
|
self.end_time = end_time
|
|
81
109
|
self.duration = duration
|
|
82
110
|
self.attachments = []
|
|
83
111
|
|
|
84
112
|
def set_status(self, status: Optional[str]):
|
|
85
|
-
if status in ['passed', 'failed', 'skipped', 'blocked', 'untested']:
|
|
113
|
+
if status in ['passed', 'failed', 'skipped', 'blocked', 'untested', 'invalid']:
|
|
86
114
|
self.status = status
|
|
87
115
|
else:
|
|
88
|
-
raise ValueError('Step status must be one of: passed, failed, skipped, blocked, untested')
|
|
116
|
+
raise ValueError('Step status must be one of: passed, failed, skipped, blocked, untested, invalid')
|
|
89
117
|
|
|
90
118
|
def complete(self):
|
|
91
|
-
self.end_time =
|
|
119
|
+
self.end_time = QaseUtils.get_real_time()
|
|
92
120
|
self.duration = int((self.end_time - self.start_time) * 1000)
|
|
93
121
|
|
|
94
122
|
def add_attachment(self, attachment: Attachment):
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from .network import NetworkProfiler, NetworkProfilerSingleton
|
|
2
2
|
from .sleep import SleepProfiler
|
|
3
|
-
from .db import
|
|
3
|
+
from .db import DatabaseProfiler, DatabaseProfilerSingleton
|
|
4
4
|
|
|
5
5
|
__all__ = [
|
|
6
6
|
NetworkProfiler,
|
|
7
7
|
NetworkProfilerSingleton,
|
|
8
8
|
SleepProfiler,
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
DatabaseProfiler,
|
|
10
|
+
DatabaseProfilerSingleton
|
|
11
|
+
]
|