qase-python-commons 3.1.9__py3-none-any.whl → 4.1.9__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.
- qase/__init__.py +3 -0
- qase/commons/client/api_v1_client.py +269 -175
- qase/commons/client/api_v2_client.py +163 -26
- qase/commons/client/base_api_client.py +23 -6
- qase/commons/config.py +162 -23
- qase/commons/logger.py +82 -13
- qase/commons/models/__init__.py +0 -2
- qase/commons/models/attachment.py +11 -8
- qase/commons/models/basemodel.py +12 -3
- qase/commons/models/config/framework.py +17 -0
- qase/commons/models/config/qaseconfig.py +34 -0
- qase/commons/models/config/run.py +19 -0
- qase/commons/models/config/testops.py +45 -3
- qase/commons/models/external_link.py +41 -0
- qase/commons/models/relation.py +16 -6
- qase/commons/models/result.py +16 -31
- qase/commons/models/run.py +17 -2
- qase/commons/models/runtime.py +9 -0
- qase/commons/models/step.py +45 -12
- qase/commons/profilers/__init__.py +4 -3
- qase/commons/profilers/db.py +965 -5
- qase/commons/reporters/core.py +60 -10
- qase/commons/reporters/report.py +11 -6
- qase/commons/reporters/testops.py +56 -27
- qase/commons/status_mapping/__init__.py +12 -0
- qase/commons/status_mapping/status_mapping.py +237 -0
- qase/commons/util/__init__.py +9 -0
- qase/commons/util/host_data.py +147 -0
- qase/commons/utils.py +95 -0
- {qase_python_commons-3.1.9.dist-info → qase_python_commons-4.1.9.dist-info}/METADATA +16 -11
- qase_python_commons-4.1.9.dist-info/RECORD +45 -0
- {qase_python_commons-3.1.9.dist-info → qase_python_commons-4.1.9.dist-info}/WHEEL +1 -1
- qase/commons/models/suite.py +0 -13
- qase_python_commons-3.1.9.dist-info/RECORD +0 -40
- {qase_python_commons-3.1.9.dist-info → qase_python_commons-4.1.9.dist-info}/top_level.txt +0 -0
qase/commons/reporters/core.py
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import json
|
|
3
1
|
import time
|
|
4
2
|
|
|
5
3
|
from ..config import ConfigManager
|
|
@@ -12,6 +10,9 @@ from ..models import Result, Attachment, Runtime
|
|
|
12
10
|
from ..models.config.qaseconfig import Mode
|
|
13
11
|
from typing import Union, List
|
|
14
12
|
|
|
13
|
+
from ..util import get_host_info
|
|
14
|
+
from ..status_mapping.status_mapping import StatusMapping
|
|
15
|
+
|
|
15
16
|
"""
|
|
16
17
|
CoreReporter is a facade for all reporters and it is used to initialize and manage them.
|
|
17
18
|
It is also used to pass configuration and logger to reporters, handle fallback logic and error handling.
|
|
@@ -19,26 +20,45 @@ from typing import Union, List
|
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class QaseCoreReporter:
|
|
22
|
-
def __init__(self, config: ConfigManager
|
|
23
|
+
def __init__(self, config: ConfigManager, framework: Union[str, None] = None,
|
|
24
|
+
reporter_name: Union[str, None] = None):
|
|
23
25
|
config.validate_config()
|
|
24
26
|
self.config = config.config
|
|
25
|
-
|
|
27
|
+
# Use the logger from ConfigManager instead of creating a new one
|
|
28
|
+
self.logger = config.logger
|
|
26
29
|
self._execution_plan = None
|
|
27
30
|
self.profilers = []
|
|
28
31
|
self.overhead = 0
|
|
29
32
|
|
|
33
|
+
# Initialize status mapping
|
|
34
|
+
self.status_mapping = StatusMapping.from_dict(self.config.status_mapping)
|
|
35
|
+
if not self.status_mapping.is_empty():
|
|
36
|
+
self.logger.log_debug(f"Status mapping initialized: {self.status_mapping}")
|
|
37
|
+
|
|
30
38
|
# self._selective_execution_setup()
|
|
31
39
|
self.fallback = self._fallback_setup()
|
|
32
40
|
|
|
33
41
|
self.logger.log_debug(f"Config: {self.config}")
|
|
34
42
|
|
|
43
|
+
host_data = get_host_info(framework, reporter_name)
|
|
44
|
+
self.logger.log_debug(f"Host data: {host_data}")
|
|
45
|
+
|
|
46
|
+
# Store framework and reporter_name for passing to reporters
|
|
47
|
+
self.framework = framework
|
|
48
|
+
self.reporter_name = reporter_name
|
|
49
|
+
self.host_data = host_data
|
|
50
|
+
|
|
35
51
|
# Reading reporter mode from config file
|
|
36
52
|
mode = self.config.mode
|
|
37
53
|
|
|
38
54
|
if mode == Mode.testops:
|
|
39
55
|
try:
|
|
40
56
|
self._load_testops_plan()
|
|
41
|
-
|
|
57
|
+
# Create API client with host_data for headers
|
|
58
|
+
from ..client.api_v2_client import ApiV2Client
|
|
59
|
+
api_client = ApiV2Client(self.config, self.logger, host_data=host_data,
|
|
60
|
+
framework=framework, reporter_name=reporter_name)
|
|
61
|
+
self.reporter = QaseTestOps(config=self.config, logger=self.logger, client=api_client)
|
|
42
62
|
except Exception as e:
|
|
43
63
|
self.logger.log('Failed to initialize TestOps reporter. Using fallback.', 'info')
|
|
44
64
|
self.logger.log(e, 'error')
|
|
@@ -61,14 +81,15 @@ class QaseCoreReporter:
|
|
|
61
81
|
self.logger.log('Failed to start run, disabling reporting', 'info')
|
|
62
82
|
self.logger.log(e, 'error')
|
|
63
83
|
self.reporter = None
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
return None
|
|
64
87
|
|
|
65
88
|
def complete_run(self) -> None:
|
|
66
89
|
if self.reporter:
|
|
67
90
|
try:
|
|
68
91
|
ts = time.time()
|
|
69
|
-
self.logger.log_debug("Completing run")
|
|
70
92
|
self.reporter.complete_run()
|
|
71
|
-
self.logger.log_debug("Run completed")
|
|
72
93
|
self.overhead += time.time() - ts
|
|
73
94
|
self.logger.log(f"Overhead for Qase Report: {round(self.overhead * 1000)}ms", 'info')
|
|
74
95
|
except Exception as e:
|
|
@@ -81,7 +102,12 @@ class QaseCoreReporter:
|
|
|
81
102
|
try:
|
|
82
103
|
ts = time.time()
|
|
83
104
|
self.logger.log_debug(f"Adding result {result}")
|
|
105
|
+
|
|
106
|
+
# Apply status mapping before adding result
|
|
107
|
+
self._apply_status_mapping(result)
|
|
108
|
+
|
|
84
109
|
self.reporter.add_result(result)
|
|
110
|
+
|
|
85
111
|
self.logger.log_debug(f"Result {result.get_title()} added")
|
|
86
112
|
self.overhead += time.time() - ts
|
|
87
113
|
except Exception as e:
|
|
@@ -118,8 +144,9 @@ class QaseCoreReporter:
|
|
|
118
144
|
from ..profilers import SleepProfiler
|
|
119
145
|
self.profilers.append(SleepProfiler(runtime=runtime))
|
|
120
146
|
if profiler == "db":
|
|
121
|
-
from ..profilers import
|
|
122
|
-
|
|
147
|
+
from ..profilers import DatabaseProfilerSingleton
|
|
148
|
+
DatabaseProfilerSingleton.init(runtime=runtime)
|
|
149
|
+
self.profilers.append(DatabaseProfilerSingleton.get_instance())
|
|
123
150
|
|
|
124
151
|
def enable_profilers(self) -> None:
|
|
125
152
|
if self.reporter:
|
|
@@ -180,7 +207,7 @@ class QaseCoreReporter:
|
|
|
180
207
|
host=self.config.testops.api.host
|
|
181
208
|
)
|
|
182
209
|
self._execution_plan = loader.load(self.config.testops.project,
|
|
183
|
-
|
|
210
|
+
int(self.config.testops.plan.id))
|
|
184
211
|
except Exception as e:
|
|
185
212
|
self.logger.log('Failed to load test plan from Qase TestOps', 'info')
|
|
186
213
|
self.logger.log(e, 'error')
|
|
@@ -201,3 +228,26 @@ class QaseCoreReporter:
|
|
|
201
228
|
if self.config.fallback == Mode.report:
|
|
202
229
|
return QaseReport(config=self.config, logger=self.logger)
|
|
203
230
|
return None
|
|
231
|
+
|
|
232
|
+
def _apply_status_mapping(self, result: Result) -> None:
|
|
233
|
+
"""
|
|
234
|
+
Apply status mapping to a test result.
|
|
235
|
+
|
|
236
|
+
This method applies the configured status mapping to the result's execution status.
|
|
237
|
+
The mapping is applied before the result is sent to the reporter.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
result: Test result to apply status mapping to
|
|
241
|
+
"""
|
|
242
|
+
if self.status_mapping.is_empty():
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
original_status = result.get_status()
|
|
246
|
+
if not original_status:
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
mapped_status = self.status_mapping.apply_mapping(original_status)
|
|
250
|
+
|
|
251
|
+
if mapped_status != original_status:
|
|
252
|
+
result.execution.set_status(mapped_status)
|
|
253
|
+
self.logger.log_debug(f"Status mapped for '{result.get_title()}': {original_status} -> {mapped_status}")
|
qase/commons/reporters/report.py
CHANGED
|
@@ -31,17 +31,16 @@ class QaseReport:
|
|
|
31
31
|
|
|
32
32
|
def start_run(self):
|
|
33
33
|
self._check_report_path()
|
|
34
|
-
self.start_time = str(
|
|
34
|
+
self.start_time = str(QaseUtils.get_real_time())
|
|
35
35
|
|
|
36
36
|
def complete_run(self):
|
|
37
|
-
self.end_time = str(
|
|
37
|
+
self.end_time = str(QaseUtils.get_real_time())
|
|
38
38
|
self._compile_report()
|
|
39
39
|
|
|
40
40
|
def complete_worker(self):
|
|
41
41
|
pass
|
|
42
42
|
|
|
43
43
|
def add_result(self, result: Result):
|
|
44
|
-
result.set_run_id(self.run_id)
|
|
45
44
|
for attachment in result.attachments:
|
|
46
45
|
self._persist_attachment(attachment)
|
|
47
46
|
|
|
@@ -68,18 +67,24 @@ class QaseReport:
|
|
|
68
67
|
mode = "w"
|
|
69
68
|
if isinstance(attachment.content, bytes):
|
|
70
69
|
mode = "wb"
|
|
71
|
-
|
|
70
|
+
|
|
71
|
+
file_path = f"{self.report_path}/attachments/{attachment.id}-{attachment.file_name}"
|
|
72
|
+
with open(file_path, mode) as f:
|
|
72
73
|
f.write(attachment.content)
|
|
73
74
|
# Clear content to save memory and avoid double writing
|
|
74
75
|
attachment.content = None
|
|
76
|
+
attachment.file_path = file_path
|
|
77
|
+
|
|
75
78
|
elif attachment.file_path:
|
|
79
|
+
file_path = f"{self.report_path}/attachments/{attachment.id}-{attachment.file_name}"
|
|
76
80
|
shutil.copy2(os.path.abspath(attachment.file_path),
|
|
77
81
|
f"{self.report_path}/attachments/{attachment.id}-{attachment.file_name}")
|
|
82
|
+
attachment.file_path = file_path
|
|
78
83
|
|
|
79
84
|
def _persist_attachments_in_steps(self, steps: list):
|
|
80
85
|
for step in steps:
|
|
81
|
-
if step.attachments:
|
|
82
|
-
for attachment in step.attachments:
|
|
86
|
+
if step.execution.attachments:
|
|
87
|
+
for attachment in step.execution.attachments:
|
|
83
88
|
self._persist_attachment(attachment)
|
|
84
89
|
if step.steps:
|
|
85
90
|
self._persist_attachments_in_steps(step.steps)
|
|
@@ -2,9 +2,8 @@ import threading
|
|
|
2
2
|
import urllib.parse
|
|
3
3
|
|
|
4
4
|
from datetime import datetime
|
|
5
|
-
from typing import List
|
|
5
|
+
from typing import List, Union
|
|
6
6
|
from .. import Logger, ReporterException
|
|
7
|
-
from ..client.api_v1_client import ApiV1Client
|
|
8
7
|
from ..client.base_api_client import BaseApiClient
|
|
9
8
|
from ..models import Result
|
|
10
9
|
from ..models.config.qaseconfig import QaseConfig
|
|
@@ -15,12 +14,11 @@ DEFAULT_THREAD_COUNT = 4
|
|
|
15
14
|
|
|
16
15
|
class QaseTestOps:
|
|
17
16
|
|
|
18
|
-
def __init__(self, config: QaseConfig, logger: Logger) -> None:
|
|
17
|
+
def __init__(self, config: QaseConfig, logger: Logger, client: BaseApiClient) -> None:
|
|
19
18
|
self.config = config
|
|
20
19
|
self.logger = logger
|
|
21
20
|
self.__baseUrl = self.__get_host(config.testops.api.host)
|
|
22
|
-
|
|
23
|
-
self.client = self._prepare_client()
|
|
21
|
+
self.client = client
|
|
24
22
|
|
|
25
23
|
run_id = self.config.testops.run.id
|
|
26
24
|
plan_id = self.config.testops.plan.id
|
|
@@ -68,12 +66,6 @@ class QaseTestOps:
|
|
|
68
66
|
"""Verify that project exists in TestOps"""
|
|
69
67
|
self.client.get_project(self.project_code)
|
|
70
68
|
|
|
71
|
-
def _prepare_client(self) -> BaseApiClient:
|
|
72
|
-
if self.config.testops.use_v2:
|
|
73
|
-
from ..client.api_v2_client import ApiV2Client
|
|
74
|
-
return ApiV2Client(self.config, self.logger)
|
|
75
|
-
return ApiV1Client(self.config, self.logger)
|
|
76
|
-
|
|
77
69
|
def _send_results_threaded(self, results):
|
|
78
70
|
try:
|
|
79
71
|
self.client.send_results(self.project_code, self.run_id, results)
|
|
@@ -89,15 +81,34 @@ class QaseTestOps:
|
|
|
89
81
|
|
|
90
82
|
def _send_results(self) -> None:
|
|
91
83
|
if self.results:
|
|
92
|
-
#
|
|
93
|
-
self.send_semaphore.acquire()
|
|
94
|
-
self.count_running_threads += 1
|
|
84
|
+
# Filter results by status if status_filter is configured
|
|
95
85
|
results_to_send = self.results.copy()
|
|
86
|
+
|
|
87
|
+
if self.config.testops.status_filter and len(self.config.testops.status_filter) > 0:
|
|
88
|
+
filtered_results = []
|
|
89
|
+
for result in results_to_send:
|
|
90
|
+
result_status = result.get_status()
|
|
91
|
+
if result_status and result_status not in self.config.testops.status_filter:
|
|
92
|
+
filtered_results.append(result)
|
|
93
|
+
else:
|
|
94
|
+
self.logger.log_debug(f"Filtering out result '{result.title}' with status '{result_status}'")
|
|
95
|
+
|
|
96
|
+
results_to_send = filtered_results
|
|
97
|
+
self.logger.log_debug(f"Filtered {len(self.results) - len(results_to_send)} results by status filter")
|
|
98
|
+
|
|
99
|
+
if results_to_send:
|
|
100
|
+
# Acquire semaphore before starting the send operation
|
|
101
|
+
self.send_semaphore.acquire()
|
|
102
|
+
self.count_running_threads += 1
|
|
103
|
+
|
|
104
|
+
# Start a new thread for sending results
|
|
105
|
+
send_thread = threading.Thread(target=self._send_results_threaded, args=(results_to_send,))
|
|
106
|
+
send_thread.start()
|
|
107
|
+
else:
|
|
108
|
+
self.logger.log("No results to send after filtering", "info")
|
|
109
|
+
|
|
110
|
+
# Clear results regardless of filtering
|
|
96
111
|
self.results = []
|
|
97
|
-
|
|
98
|
-
# Start a new thread for sending results
|
|
99
|
-
send_thread = threading.Thread(target=self._send_results_threaded, args=(results_to_send,))
|
|
100
|
-
send_thread.start()
|
|
101
112
|
else:
|
|
102
113
|
self.logger.log("No results to send", "info")
|
|
103
114
|
|
|
@@ -120,18 +131,37 @@ class QaseTestOps:
|
|
|
120
131
|
def complete_run(self) -> None:
|
|
121
132
|
if len(self.results) > 0:
|
|
122
133
|
self._send_results()
|
|
134
|
+
|
|
135
|
+
while self.count_running_threads > 0:
|
|
136
|
+
pass
|
|
137
|
+
|
|
123
138
|
if self.complete_after_run:
|
|
124
|
-
|
|
125
|
-
pass
|
|
139
|
+
self.logger.log_debug("Completing run")
|
|
126
140
|
self.client.complete_run(self.project_code, self.run_id)
|
|
141
|
+
self.logger.log_debug("Run completed")
|
|
142
|
+
|
|
143
|
+
# Enable public report if configured
|
|
144
|
+
if self.config.testops.show_public_report_link:
|
|
145
|
+
try:
|
|
146
|
+
self.logger.log_debug("Enabling public report")
|
|
147
|
+
public_url = self.client.enable_public_report(self.project_code, self.run_id)
|
|
148
|
+
if public_url:
|
|
149
|
+
self.logger.log(f"Public report link: {public_url}", "info")
|
|
150
|
+
else:
|
|
151
|
+
self.logger.log("Failed to generate public report link", "warning")
|
|
152
|
+
except Exception as e:
|
|
153
|
+
self.logger.log(f"Failed to generate public report link: {e}", "warning")
|
|
127
154
|
|
|
128
155
|
def complete_worker(self) -> None:
|
|
129
156
|
if len(self.results) > 0:
|
|
130
157
|
self._send_results()
|
|
158
|
+
while self.count_running_threads > 0:
|
|
159
|
+
pass
|
|
160
|
+
self.logger.log_debug("Worker completed")
|
|
131
161
|
|
|
132
162
|
def add_result(self, result: Result) -> None:
|
|
133
163
|
if result.get_status() == 'failed':
|
|
134
|
-
self.__show_link(result.
|
|
164
|
+
self.__show_link(result.testops_ids, result.title)
|
|
135
165
|
self.results.append(result)
|
|
136
166
|
if len(self.results) >= self.batch_size:
|
|
137
167
|
self._send_results()
|
|
@@ -142,15 +172,14 @@ class QaseTestOps:
|
|
|
142
172
|
def set_results(self, results) -> None:
|
|
143
173
|
self.results = results
|
|
144
174
|
|
|
145
|
-
def __show_link(self,
|
|
146
|
-
link = self.__prepare_link(
|
|
175
|
+
def __show_link(self, ids: Union[None, List[int]], title: str):
|
|
176
|
+
link = self.__prepare_link(ids, title)
|
|
147
177
|
self.logger.log(f"See why this test failed: {link}", "info")
|
|
148
178
|
|
|
149
|
-
def __prepare_link(self,
|
|
179
|
+
def __prepare_link(self, ids: Union[None, List[int]], title: str):
|
|
150
180
|
link = f"{self.__baseUrl}/run/{self.project_code}/dashboard/{self.run_id}?source=logs&status=%5B2%5D&search="
|
|
151
|
-
if
|
|
152
|
-
return f"{link}{
|
|
153
|
-
|
|
181
|
+
if ids is not None and len(ids) > 0:
|
|
182
|
+
return f"{link}{self.project_code}-{ids[0]}"
|
|
154
183
|
return f"{link}{urllib.parse.quote_plus(title)}"
|
|
155
184
|
|
|
156
185
|
@staticmethod
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities package for Qase Python Commons.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .status_mapping import StatusMapping, StatusMappingError, create_status_mapping_from_config, create_status_mapping_from_env
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
'StatusMapping',
|
|
9
|
+
'StatusMappingError',
|
|
10
|
+
'create_status_mapping_from_config',
|
|
11
|
+
'create_status_mapping_from_env'
|
|
12
|
+
]
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Status mapping utilities for Qase Python Commons.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to map test result statuses from one value to another
|
|
5
|
+
based on configuration. This is useful for standardizing status values across different
|
|
6
|
+
testing frameworks or for custom status transformations.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Dict, Optional, List
|
|
10
|
+
import os
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StatusMappingError(Exception):
|
|
15
|
+
"""Exception raised when status mapping encounters an error."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class StatusMapping:
|
|
20
|
+
"""
|
|
21
|
+
Handles mapping of test result statuses.
|
|
22
|
+
|
|
23
|
+
This class provides functionality to:
|
|
24
|
+
- Parse status mapping from configuration
|
|
25
|
+
- Validate status mappings
|
|
26
|
+
- Apply status mappings to test results
|
|
27
|
+
- Support both JSON configuration and environment variables
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
# Valid statuses that can be mapped
|
|
31
|
+
VALID_STATUSES = {
|
|
32
|
+
'passed', 'failed', 'skipped', 'disabled', 'blocked', 'invalid'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
def __init__(self, mapping: Optional[Dict[str, str]] = None):
|
|
36
|
+
"""
|
|
37
|
+
Initialize StatusMapping with optional mapping dictionary.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
mapping: Dictionary mapping source status to target status
|
|
41
|
+
"""
|
|
42
|
+
self.mapping = mapping or {}
|
|
43
|
+
self.logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_dict(cls, mapping_dict: Dict[str, str]) -> 'StatusMapping':
|
|
47
|
+
"""
|
|
48
|
+
Create StatusMapping from dictionary.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
mapping_dict: Dictionary with status mappings
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
StatusMapping instance
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
StatusMappingError: If mapping contains invalid statuses
|
|
58
|
+
"""
|
|
59
|
+
instance = cls()
|
|
60
|
+
instance.set_mapping(mapping_dict)
|
|
61
|
+
return instance
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def from_env_string(cls, env_string: str) -> 'StatusMapping':
|
|
65
|
+
"""
|
|
66
|
+
Create StatusMapping from environment variable string.
|
|
67
|
+
|
|
68
|
+
Expected format: "source1=target1,source2=target2"
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
env_string: Environment variable string
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
StatusMapping instance
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
StatusMappingError: If string format is invalid
|
|
78
|
+
"""
|
|
79
|
+
instance = cls()
|
|
80
|
+
instance.parse_env_string(env_string)
|
|
81
|
+
return instance
|
|
82
|
+
|
|
83
|
+
def set_mapping(self, mapping_dict: Dict[str, str]) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Set status mapping from dictionary.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
mapping_dict: Dictionary with status mappings
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
StatusMappingError: If mapping contains invalid statuses
|
|
92
|
+
"""
|
|
93
|
+
if not isinstance(mapping_dict, dict):
|
|
94
|
+
raise StatusMappingError("Mapping must be a dictionary")
|
|
95
|
+
|
|
96
|
+
# Validate all statuses in the mapping
|
|
97
|
+
for source_status, target_status in mapping_dict.items():
|
|
98
|
+
if source_status not in self.VALID_STATUSES:
|
|
99
|
+
raise StatusMappingError(f"Invalid source status: {source_status}")
|
|
100
|
+
if target_status not in self.VALID_STATUSES:
|
|
101
|
+
raise StatusMappingError(f"Invalid target status: {target_status}")
|
|
102
|
+
|
|
103
|
+
self.mapping = mapping_dict.copy()
|
|
104
|
+
self.logger.debug(f"Status mapping set: {self.mapping}")
|
|
105
|
+
|
|
106
|
+
def parse_env_string(self, env_string: str) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Parse status mapping from environment variable string.
|
|
109
|
+
|
|
110
|
+
Expected format: "source1=target1,source2=target2"
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
env_string: Environment variable string
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
StatusMappingError: If string format is invalid
|
|
117
|
+
"""
|
|
118
|
+
if not env_string or not env_string.strip():
|
|
119
|
+
self.mapping = {}
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
mapping_dict = {}
|
|
123
|
+
pairs = env_string.split(',')
|
|
124
|
+
|
|
125
|
+
for pair in pairs:
|
|
126
|
+
pair = pair.strip()
|
|
127
|
+
if not pair:
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
if '=' not in pair:
|
|
131
|
+
raise StatusMappingError(f"Invalid mapping format: {pair}. Expected 'source=target'")
|
|
132
|
+
|
|
133
|
+
source_status, target_status = pair.split('=', 1)
|
|
134
|
+
source_status = source_status.strip()
|
|
135
|
+
target_status = target_status.strip()
|
|
136
|
+
|
|
137
|
+
if not source_status or not target_status:
|
|
138
|
+
raise StatusMappingError(f"Empty status in mapping: {pair}")
|
|
139
|
+
|
|
140
|
+
mapping_dict[source_status] = target_status
|
|
141
|
+
|
|
142
|
+
self.set_mapping(mapping_dict)
|
|
143
|
+
|
|
144
|
+
def apply_mapping(self, status: str) -> str:
|
|
145
|
+
"""
|
|
146
|
+
Apply status mapping to a given status.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
status: Original status
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Mapped status if mapping exists, otherwise original status
|
|
153
|
+
"""
|
|
154
|
+
if not status:
|
|
155
|
+
return status
|
|
156
|
+
|
|
157
|
+
if status in self.mapping:
|
|
158
|
+
mapped_status = self.mapping[status]
|
|
159
|
+
self.logger.debug(f"Status mapped: {status} -> {mapped_status}")
|
|
160
|
+
return mapped_status
|
|
161
|
+
|
|
162
|
+
return status
|
|
163
|
+
|
|
164
|
+
def get_mapping(self) -> Dict[str, str]:
|
|
165
|
+
"""
|
|
166
|
+
Get current status mapping.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Dictionary with current status mappings
|
|
170
|
+
"""
|
|
171
|
+
return self.mapping.copy()
|
|
172
|
+
|
|
173
|
+
def is_empty(self) -> bool:
|
|
174
|
+
"""
|
|
175
|
+
Check if mapping is empty.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
True if no mappings are defined
|
|
179
|
+
"""
|
|
180
|
+
return len(self.mapping) == 0
|
|
181
|
+
|
|
182
|
+
def validate(self) -> List[str]:
|
|
183
|
+
"""
|
|
184
|
+
Validate current mapping and return any issues.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
List of validation error messages
|
|
188
|
+
"""
|
|
189
|
+
errors = []
|
|
190
|
+
|
|
191
|
+
for source_status, target_status in self.mapping.items():
|
|
192
|
+
if source_status not in self.VALID_STATUSES:
|
|
193
|
+
errors.append(f"Invalid source status: {source_status}")
|
|
194
|
+
if target_status not in self.VALID_STATUSES:
|
|
195
|
+
errors.append(f"Invalid target status: {target_status}")
|
|
196
|
+
|
|
197
|
+
return errors
|
|
198
|
+
|
|
199
|
+
def __str__(self) -> str:
|
|
200
|
+
"""String representation of the mapping."""
|
|
201
|
+
return str(self.mapping)
|
|
202
|
+
|
|
203
|
+
def __repr__(self) -> str:
|
|
204
|
+
"""Detailed string representation."""
|
|
205
|
+
return f"StatusMapping({self.mapping})"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def create_status_mapping_from_config(config_value: Optional[Dict[str, str]]) -> StatusMapping:
|
|
209
|
+
"""
|
|
210
|
+
Create StatusMapping from configuration value.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
config_value: Configuration dictionary or None
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
StatusMapping instance
|
|
217
|
+
"""
|
|
218
|
+
if config_value is None:
|
|
219
|
+
return StatusMapping()
|
|
220
|
+
|
|
221
|
+
return StatusMapping.from_dict(config_value)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def create_status_mapping_from_env(env_var_name: str = 'STATUS_MAPPING') -> StatusMapping:
|
|
225
|
+
"""
|
|
226
|
+
Create StatusMapping from environment variable.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
env_var_name: Name of environment variable
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
StatusMapping instance
|
|
233
|
+
"""
|
|
234
|
+
env_value = os.getenv(env_var_name)
|
|
235
|
+
if env_value:
|
|
236
|
+
return StatusMapping.from_env_string(env_value)
|
|
237
|
+
return StatusMapping()
|