qase-python-commons 3.1.3__py3-none-any.whl → 4.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of qase-python-commons might be problematic. Click here for more details.

Files changed (36) hide show
  1. qase/__init__.py +3 -0
  2. qase/commons/client/api_v1_client.py +169 -143
  3. qase/commons/client/api_v2_client.py +77 -23
  4. qase/commons/client/base_api_client.py +12 -1
  5. qase/commons/config.py +159 -20
  6. qase/commons/logger.py +82 -13
  7. qase/commons/models/__init__.py +0 -2
  8. qase/commons/models/attachment.py +11 -8
  9. qase/commons/models/basemodel.py +12 -3
  10. qase/commons/models/config/framework.py +61 -0
  11. qase/commons/models/config/qaseconfig.py +34 -0
  12. qase/commons/models/config/run.py +19 -0
  13. qase/commons/models/config/testops.py +45 -3
  14. qase/commons/models/external_link.py +41 -0
  15. qase/commons/models/relation.py +16 -6
  16. qase/commons/models/result.py +16 -31
  17. qase/commons/models/run.py +17 -2
  18. qase/commons/models/runtime.py +15 -1
  19. qase/commons/models/step.py +43 -11
  20. qase/commons/profilers/__init__.py +4 -3
  21. qase/commons/profilers/db.py +965 -5
  22. qase/commons/profilers/network.py +5 -1
  23. qase/commons/reporters/core.py +50 -9
  24. qase/commons/reporters/report.py +11 -6
  25. qase/commons/reporters/testops.py +56 -22
  26. qase/commons/status_mapping/__init__.py +12 -0
  27. qase/commons/status_mapping/status_mapping.py +237 -0
  28. qase/commons/util/__init__.py +9 -0
  29. qase/commons/util/host_data.py +140 -0
  30. qase/commons/utils.py +95 -0
  31. {qase_python_commons-3.1.3.dist-info → qase_python_commons-4.1.3.dist-info}/METADATA +16 -11
  32. qase_python_commons-4.1.3.dist-info/RECORD +45 -0
  33. {qase_python_commons-3.1.3.dist-info → qase_python_commons-4.1.3.dist-info}/WHEEL +1 -1
  34. qase/commons/models/suite.py +0 -13
  35. qase_python_commons-3.1.3.dist-info/RECORD +0 -40
  36. {qase_python_commons-3.1.3.dist-info → qase_python_commons-4.1.3.dist-info}/top_level.txt +0 -0
@@ -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,14 +47,19 @@ 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
23
- self.use_v2 = False
61
+ self.status_filter = []
62
+ self.show_public_report_link = False
24
63
 
25
64
  def set_project(self, project: str):
26
65
  self.project = project
@@ -28,5 +67,8 @@ class TestopsConfig(BaseModel):
28
67
  def set_defect(self, defect):
29
68
  self.defect = QaseUtils.parse_bool(defect)
30
69
 
31
- def set_use_v2(self, use_v2):
32
- self.use_v2 = QaseUtils.parse_bool(use_v2)
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}")
@@ -1,13 +1,23 @@
1
1
  from .basemodel import BaseModel
2
2
 
3
3
 
4
- class RelationSuite(BaseModel):
5
- def __init__(self, suite_id: int, title: str) -> None:
6
- self.suite_id = suite_id
4
+ class SuiteData(BaseModel):
5
+ def __init__(self, title: str) -> None:
6
+ self.public_id = None
7
7
  self.title = title
8
8
 
9
9
 
10
+ class RelationSuite(BaseModel):
11
+ def __init__(self) -> None:
12
+ self.data = []
13
+
14
+ def add_data(self, data: SuiteData) -> None:
15
+ self.data.append(data)
16
+
17
+
10
18
  class Relation(BaseModel):
11
- def __init__(self, type: str, data: RelationSuite):
12
- self.type = type
13
- self.data = data
19
+ def __init__(self):
20
+ self.suite = RelationSuite()
21
+
22
+ def add_suite(self, suite: SuiteData) -> None:
23
+ self.suite.add_data(suite)
@@ -4,7 +4,6 @@ import uuid
4
4
  from typing import Type, Optional, Union, Dict, List
5
5
  from .basemodel import BaseModel
6
6
  from .step import Step
7
- from .suite import Suite
8
7
  from .attachment import Attachment
9
8
  from .relation import Relation
10
9
  from .. import QaseUtils
@@ -26,7 +25,7 @@ class Execution(BaseModel):
26
25
  stacktrace: Optional[str] = None,
27
26
  thread: Optional[str] = QaseUtils.get_thread_name()
28
27
  ):
29
- self.start_time = time.time()
28
+ self.start_time = QaseUtils.get_real_time()
30
29
  self.status = status
31
30
  self.end_time = end_time
32
31
  self.duration = duration
@@ -34,16 +33,22 @@ class Execution(BaseModel):
34
33
  self.thread = thread
35
34
 
36
35
  def set_status(self, status: Optional[str]):
37
- if status in ['passed', 'failed', 'skipped', 'untested']:
36
+ if status is None:
38
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
39
44
  else:
40
- 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')
41
46
 
42
47
  def get_status(self):
43
48
  return self.status
44
49
 
45
50
  def complete(self):
46
- self.end_time = time.time()
51
+ self.end_time = QaseUtils.get_real_time()
47
52
  self.duration = (int)((self.end_time - self.start_time) * 1000)
48
53
 
49
54
 
@@ -70,20 +75,16 @@ class Result(BaseModel):
70
75
  self.id: str = str(uuid.uuid4())
71
76
  self.title: str = title
72
77
  self.signature: str = signature
73
- self.run_id: Optional[str] = None
74
- self.testops_id: Optional[int] = None
78
+ self.testops_ids: Optional[List[int]] = None
75
79
  self.execution: Type[Execution] = Execution()
76
80
  self.fields: Dict[Type[Field]] = {}
77
81
  self.attachments: List[Attachment] = []
78
82
  self.steps: List[Type[Step]] = []
79
83
  self.params: Optional[dict] = {}
80
84
  self.param_groups: Optional[List[List[str]]] = []
81
- self.author: Optional[str] = None
82
- self.relations: List[Type[Relation]] = []
85
+ self.relations: Type[Relation] = None
83
86
  self.muted: bool = False
84
87
  self.message: Optional[str] = None
85
- self.suite: Optional[Type[Suite]] = None
86
- QaseUtils.get_host_data()
87
88
 
88
89
  def add_message(self, message: str) -> None:
89
90
  self.message = message
@@ -97,20 +98,14 @@ class Result(BaseModel):
97
98
  def add_attachment(self, attachment: Attachment) -> None:
98
99
  self.attachments.append(attachment)
99
100
 
100
- def add_relation(self, relation: Type[Relation]) -> None:
101
- self.relations.append(relation)
102
-
103
101
  def add_param(self, key: str, value: str) -> None:
104
102
  self.params[key] = value
105
103
 
106
104
  def add_param_groups(self, values: List[str]) -> None:
107
105
  self.param_groups.append(values)
108
106
 
109
- def add_relation(self, relation: Type[Relation]) -> None:
110
- self.relations.append(relation)
111
-
112
- def add_suite(self, suite: Type[Suite]) -> None:
113
- self.suite = suite
107
+ def set_relation(self, relation: Relation) -> None:
108
+ self.relations = relation
114
109
 
115
110
  def get_status(self) -> Optional[str]:
116
111
  return self.execution.status
@@ -126,18 +121,8 @@ class Result(BaseModel):
126
121
  return self.fields[name]
127
122
  return None
128
123
 
129
- def get_testops_id(self) -> Optional[int]:
130
- if self.testops_id is None:
131
- # Hack for old API
132
- return 0
133
- return self.testops_id
124
+ def get_testops_ids(self) -> Optional[List[int]]:
125
+ return self.testops_ids
134
126
 
135
127
  def get_duration(self) -> int:
136
128
  return self.execution.duration
137
-
138
- def get_suite_title(self) -> Optional[str]:
139
- if self.suite:
140
- return self.suite.title
141
-
142
- def set_run_id(self, run_id: str) -> None:
143
- self.run_id = run_id
@@ -1,5 +1,3 @@
1
- import json
2
-
3
1
  from typing import Optional, List
4
2
 
5
3
  from .basemodel import BaseModel
@@ -26,6 +24,7 @@ class RunStats(BaseModel):
26
24
  self.failed = 0
27
25
  self.skipped = 0
28
26
  self.broken = 0
27
+ self.invalid = 0
29
28
  self.muted = 0
30
29
  self.total = 0
31
30
 
@@ -39,6 +38,8 @@ class RunStats(BaseModel):
39
38
  self.skipped += 1
40
39
  elif status == "broken":
41
40
  self.broken += 1
41
+ elif status == "invalid":
42
+ self.invalid += 1
42
43
  self.total += 1
43
44
  if result.get('muted', False):
44
45
  self.muted += 1
@@ -71,6 +72,7 @@ class Run(BaseModel):
71
72
  "duration": result["execution"]["duration"],
72
73
  "thread": result["execution"]["thread"]
73
74
  }
75
+ self._extract_path_from_relations(result)
74
76
  self.results.append(compact_result)
75
77
  self.execution.track(result)
76
78
  self.stats.track(result)
@@ -79,3 +81,16 @@ class Run(BaseModel):
79
81
 
80
82
  def add_host_data(self, host_data: dict):
81
83
  self.host_data = host_data
84
+
85
+ def _extract_path_from_relations(self, relations_dict):
86
+
87
+ titles = []
88
+ if "relations" in relations_dict and "suite" in relations_dict["relations"]:
89
+ if "data" in relations_dict["relations"]["suite"]:
90
+ data_list = relations_dict["relations"]["suite"]["data"]
91
+ titles = [item["title"] for item in data_list if "title" in item]
92
+
93
+ path = "/".join(titles)
94
+
95
+ if path and path not in self.suites:
96
+ self.suites.append(path)
@@ -1,4 +1,5 @@
1
- from .step import Step, StepTextData
1
+ from . import Result
2
+ from .step import Step
2
3
  from .attachment import Attachment
3
4
 
4
5
 
@@ -41,3 +42,16 @@ class Runtime:
41
42
  self.result.add_attachment(attachment)
42
43
  except Exception as e:
43
44
  raise QaseRuntimeException(e)
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
+
54
+ def clear(self):
55
+ self.result = None
56
+ self.steps = {}
57
+ self.step_id = None
@@ -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):
@@ -20,6 +21,7 @@ class StepTextData(BaseModel):
20
21
  def __init__(self, action: str, expected_result: Optional[str] = None):
21
22
  self.action = action
22
23
  self.expected_result = expected_result
24
+ self.input_data = None
23
25
 
24
26
 
25
27
  class StepAssertData(BaseModel):
@@ -42,10 +44,20 @@ class StepRequestData(BaseModel):
42
44
  self.response_body = None
43
45
  self.status_code = None
44
46
  if isinstance(request_body, bytes):
45
- request_body = request_body.decode('utf-8')
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')
46
53
  self.request_body = request_body
47
54
  if isinstance(request_headers, bytes):
48
- request_headers = request_headers.decode('utf-8')
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')
49
61
  self.request_headers = request_headers
50
62
  self.request_method = request_method
51
63
  self.request_url = request_url
@@ -55,16 +67,33 @@ class StepRequestData(BaseModel):
55
67
  self.status_code = status_code
56
68
 
57
69
  if isinstance(response_body, bytes):
58
- response_body = response_body.decode('utf-8')
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')
59
76
  self.response_body = response_body
60
77
  if isinstance(response_headers, bytes):
61
- response_headers = response_headers.decode('utf-8')
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')
62
84
  self.response_headers = response_headers
63
85
 
64
86
 
65
87
  class StepDbQueryData(BaseModel):
66
- 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):
67
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
68
97
 
69
98
 
70
99
  class StepSleepData(BaseModel):
@@ -74,21 +103,25 @@ class StepSleepData(BaseModel):
74
103
 
75
104
  class StepExecution(BaseModel):
76
105
  def __init__(self, status: Optional[str] = 'untested', end_time: int = 0, duration: int = 0):
77
- self.start_time = time.time()
106
+ self.start_time = QaseUtils.get_real_time()
78
107
  self.status = status
79
108
  self.end_time = end_time
80
109
  self.duration = duration
110
+ self.attachments = []
81
111
 
82
112
  def set_status(self, status: Optional[str]):
83
- if status in ['passed', 'failed', 'skipped', 'blocked', 'untested']:
113
+ if status in ['passed', 'failed', 'skipped', 'blocked', 'untested', 'invalid']:
84
114
  self.status = status
85
115
  else:
86
- 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')
87
117
 
88
118
  def complete(self):
89
- self.end_time = time.time()
119
+ self.end_time = QaseUtils.get_real_time()
90
120
  self.duration = int((self.end_time - self.start_time) * 1000)
91
121
 
122
+ def add_attachment(self, attachment: Attachment):
123
+ self.attachments.append(attachment)
124
+
92
125
 
93
126
  class Step(BaseModel):
94
127
  def __init__(self,
@@ -107,7 +140,6 @@ class Step(BaseModel):
107
140
  self.data = data
108
141
  self.parent_id = parent_id
109
142
  self.execution = StepExecution()
110
- self.attachments = []
111
143
  self.steps = []
112
144
 
113
145
  def set_parent_id(self, parent_id: Optional[str]):
@@ -132,4 +164,4 @@ class Step(BaseModel):
132
164
  self.steps = steps
133
165
 
134
166
  def add_attachment(self, attachment: Attachment):
135
- self.attachments.append(attachment)
167
+ self.execution.add_attachment(attachment)
@@ -1,10 +1,11 @@
1
1
  from .network import NetworkProfiler, NetworkProfilerSingleton
2
2
  from .sleep import SleepProfiler
3
- from .db import DbProfiler
3
+ from .db import DatabaseProfiler, DatabaseProfilerSingleton
4
4
 
5
5
  __all__ = [
6
6
  NetworkProfiler,
7
7
  NetworkProfilerSingleton,
8
8
  SleepProfiler,
9
- DbProfiler
10
- ]
9
+ DatabaseProfiler,
10
+ DatabaseProfilerSingleton
11
+ ]