qase-python-commons 4.0.0__py3-none-any.whl → 4.1.0__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/config.py +40 -3
- qase/commons/logger.py +63 -7
- qase/commons/models/config/qaseconfig.py +29 -1
- qase/commons/models/result.py +8 -2
- qase/commons/reporters/core.py +34 -1
- qase/commons/status_mapping/__init__.py +12 -0
- qase/commons/status_mapping/status_mapping.py +237 -0
- {qase_python_commons-4.0.0.dist-info → qase_python_commons-4.1.0.dist-info}/METADATA +8 -3
- {qase_python_commons-4.0.0.dist-info → qase_python_commons-4.1.0.dist-info}/RECORD +12 -9
- {qase_python_commons-4.0.0.dist-info → qase_python_commons-4.1.0.dist-info}/WHEEL +0 -0
- {qase_python_commons-4.0.0.dist-info → qase_python_commons-4.1.0.dist-info}/top_level.txt +0 -0
qase/__init__.py
ADDED
qase/commons/config.py
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import json
|
|
3
|
-
from .logger import Logger
|
|
3
|
+
from .logger import Logger, LoggingOptions
|
|
4
4
|
from .models.config.qaseconfig import QaseConfig, Mode
|
|
5
|
+
from .utils import QaseUtils
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class ConfigManager:
|
|
8
9
|
|
|
9
10
|
def __init__(self, config_file='./qase.config.json'):
|
|
10
|
-
self.logger = Logger()
|
|
11
11
|
self.__config_file = config_file
|
|
12
12
|
self.config = QaseConfig()
|
|
13
13
|
|
|
14
|
+
# Initialize temporary logger for error handling during config loading
|
|
15
|
+
self.logger = Logger(debug=False)
|
|
16
|
+
|
|
14
17
|
self.__load_file_config()
|
|
15
18
|
self.__load_env_config()
|
|
19
|
+
|
|
20
|
+
# Re-initialize logger with proper logging options after config is loaded
|
|
21
|
+
logging_options = LoggingOptions(
|
|
22
|
+
console=self.config.logging.console if self.config.logging.console is not None else True,
|
|
23
|
+
file=self.config.logging.file if self.config.logging.file is not None else self.config.debug
|
|
24
|
+
)
|
|
25
|
+
self.logger = Logger(debug=self.config.debug, logging_options=logging_options)
|
|
16
26
|
|
|
17
27
|
def validate_config(self):
|
|
18
28
|
errors: list[str] = []
|
|
@@ -63,6 +73,11 @@ class ConfigManager:
|
|
|
63
73
|
config.get("excludeParams")
|
|
64
74
|
)
|
|
65
75
|
|
|
76
|
+
if config.get("statusMapping"):
|
|
77
|
+
self.config.set_status_mapping(
|
|
78
|
+
config.get("statusMapping")
|
|
79
|
+
)
|
|
80
|
+
|
|
66
81
|
if config.get("executionPlan"):
|
|
67
82
|
execution_plan = config.get("executionPlan")
|
|
68
83
|
if execution_plan.get("path"):
|
|
@@ -196,6 +211,9 @@ class ConfigManager:
|
|
|
196
211
|
xfail_status.get("xpass")
|
|
197
212
|
)
|
|
198
213
|
|
|
214
|
+
if config.get("logging"):
|
|
215
|
+
self.config.set_logging(config.get("logging"))
|
|
216
|
+
|
|
199
217
|
except Exception as e:
|
|
200
218
|
self.logger.log("Failed to load config from file", "error")
|
|
201
219
|
|
|
@@ -224,6 +242,19 @@ class ConfigManager:
|
|
|
224
242
|
self.config.set_exclude_params(
|
|
225
243
|
[param.strip() for param in value.split(',')])
|
|
226
244
|
|
|
245
|
+
if key == 'QASE_STATUS_MAPPING':
|
|
246
|
+
# Parse status mapping from environment variable
|
|
247
|
+
# Format: "source1=target1,source2=target2"
|
|
248
|
+
if value:
|
|
249
|
+
mapping_dict = {}
|
|
250
|
+
pairs = value.split(',')
|
|
251
|
+
for pair in pairs:
|
|
252
|
+
pair = pair.strip()
|
|
253
|
+
if pair and '=' in pair:
|
|
254
|
+
source_status, target_status = pair.split('=', 1)
|
|
255
|
+
mapping_dict[source_status.strip()] = target_status.strip()
|
|
256
|
+
self.config.set_status_mapping(mapping_dict)
|
|
257
|
+
|
|
227
258
|
if key == 'QASE_EXECUTION_PLAN_PATH':
|
|
228
259
|
self.config.execution_plan.set_path(value)
|
|
229
260
|
|
|
@@ -308,5 +339,11 @@ class ConfigManager:
|
|
|
308
339
|
if key == 'QASE_PYTEST_XFAIL_STATUS_XPASS':
|
|
309
340
|
self.config.framework.pytest.xfail_status.set_xpass(value)
|
|
310
341
|
|
|
342
|
+
if key == 'QASE_LOGGING_CONSOLE':
|
|
343
|
+
self.config.logging.set_console(QaseUtils.parse_bool(value))
|
|
344
|
+
|
|
345
|
+
if key == 'QASE_LOGGING_FILE':
|
|
346
|
+
self.config.logging.set_file(QaseUtils.parse_bool(value))
|
|
347
|
+
|
|
311
348
|
except Exception as e:
|
|
312
|
-
self.logger.log("Failed to load config from env vars {e}", "error")
|
|
349
|
+
self.logger.log(f"Failed to load config from env vars {e}", "error")
|
qase/commons/logger.py
CHANGED
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import datetime
|
|
3
3
|
import threading
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LoggingOptions:
|
|
8
|
+
def __init__(self, console: bool = True, file: bool = False):
|
|
9
|
+
self.console = console
|
|
10
|
+
self.file = file
|
|
4
11
|
|
|
5
12
|
|
|
6
13
|
class Logger:
|
|
7
14
|
_log_file = None
|
|
8
15
|
|
|
9
|
-
def __init__(self, debug: bool = False, prefix: str = '', dir: str = os.path.join('.', 'logs')
|
|
16
|
+
def __init__(self, debug: bool = False, prefix: str = '', dir: str = os.path.join('.', 'logs'),
|
|
17
|
+
logging_options: Optional[LoggingOptions] = None) -> None:
|
|
10
18
|
self.debug = debug
|
|
11
|
-
|
|
19
|
+
self.prefix = prefix
|
|
20
|
+
self.dir = dir
|
|
21
|
+
|
|
22
|
+
# Initialize logging options
|
|
23
|
+
if logging_options is None:
|
|
24
|
+
# Default behavior: console always enabled, file enabled only in debug mode
|
|
25
|
+
self.logging_options = LoggingOptions(
|
|
26
|
+
console=True,
|
|
27
|
+
file=debug
|
|
28
|
+
)
|
|
29
|
+
else:
|
|
30
|
+
self.logging_options = logging_options
|
|
31
|
+
|
|
32
|
+
# Override with environment variables if set
|
|
33
|
+
self._load_env_logging_options()
|
|
34
|
+
|
|
35
|
+
# Setup file logging if enabled
|
|
36
|
+
if self.logging_options.file:
|
|
12
37
|
if Logger._log_file is None:
|
|
13
38
|
timestamp = self._get_timestamp()
|
|
14
39
|
filename = f'{prefix}_{timestamp}.log'
|
|
@@ -21,16 +46,38 @@ class Logger:
|
|
|
21
46
|
|
|
22
47
|
self.lock = threading.Lock()
|
|
23
48
|
|
|
49
|
+
def _load_env_logging_options(self):
|
|
50
|
+
"""Load logging options from environment variables"""
|
|
51
|
+
# QASE_LOGGING_CONSOLE
|
|
52
|
+
console_env = os.environ.get('QASE_LOGGING_CONSOLE')
|
|
53
|
+
if console_env is not None:
|
|
54
|
+
self.logging_options.console = console_env.lower() in ('true', '1', 'yes', 'on')
|
|
55
|
+
|
|
56
|
+
# QASE_LOGGING_FILE
|
|
57
|
+
file_env = os.environ.get('QASE_LOGGING_FILE')
|
|
58
|
+
if file_env is not None:
|
|
59
|
+
self.logging_options.file = file_env.lower() in ('true', '1', 'yes', 'on')
|
|
60
|
+
|
|
61
|
+
# Legacy QASE_DEBUG support
|
|
62
|
+
debug_env = os.environ.get('QASE_DEBUG')
|
|
63
|
+
if debug_env is not None and debug_env.lower() in ('true', '1', 'yes', 'on'):
|
|
64
|
+
# When debug is enabled via env, enable file logging if not explicitly disabled
|
|
65
|
+
if not hasattr(self.logging_options, 'file') or self.logging_options.file is None:
|
|
66
|
+
self.logging_options.file = True
|
|
67
|
+
|
|
24
68
|
def log(self, message: str, level: str = 'info'):
|
|
25
69
|
time_str = self._get_timestamp("%H:%M:%S")
|
|
26
70
|
log = f"[Qase][{time_str}][{level}] {message}\n"
|
|
27
71
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
72
|
+
# Console output
|
|
73
|
+
if self.logging_options.console:
|
|
74
|
+
try:
|
|
75
|
+
print(log, end='')
|
|
76
|
+
except (OSError, IOError):
|
|
77
|
+
pass
|
|
32
78
|
|
|
33
|
-
|
|
79
|
+
# File output
|
|
80
|
+
if self.logging_options.file:
|
|
34
81
|
with self.lock:
|
|
35
82
|
with open(Logger._log_file, 'a', encoding='utf-8') as f:
|
|
36
83
|
f.write(log)
|
|
@@ -39,6 +86,15 @@ class Logger:
|
|
|
39
86
|
if self.debug:
|
|
40
87
|
self.log(message, 'debug')
|
|
41
88
|
|
|
89
|
+
def log_error(self, message: str):
|
|
90
|
+
self.log(message, 'error')
|
|
91
|
+
|
|
92
|
+
def log_warning(self, message: str):
|
|
93
|
+
self.log(message, 'warning')
|
|
94
|
+
|
|
95
|
+
def log_info(self, message: str):
|
|
96
|
+
self.log(message, 'info')
|
|
97
|
+
|
|
42
98
|
@staticmethod
|
|
43
99
|
def _get_timestamp(fmt: str = "%Y%m%d"):
|
|
44
100
|
now = datetime.datetime.now()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
-
from typing import List
|
|
2
|
+
from typing import List, Dict, Optional
|
|
3
3
|
|
|
4
4
|
from .framework import Framework
|
|
5
5
|
from .report import ReportConfig
|
|
@@ -14,6 +14,21 @@ class Mode(Enum):
|
|
|
14
14
|
off = "off"
|
|
15
15
|
|
|
16
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
|
+
|
|
17
32
|
class ExecutionPlan(BaseModel):
|
|
18
33
|
path: str = None
|
|
19
34
|
|
|
@@ -36,6 +51,8 @@ class QaseConfig(BaseModel):
|
|
|
36
51
|
profilers: list = None
|
|
37
52
|
framework: Framework = None
|
|
38
53
|
exclude_params: list = None
|
|
54
|
+
status_mapping: Dict[str, str] = None
|
|
55
|
+
logging: LoggingConfig = None
|
|
39
56
|
|
|
40
57
|
def __init__(self):
|
|
41
58
|
self.mode = Mode.off
|
|
@@ -47,6 +64,8 @@ class QaseConfig(BaseModel):
|
|
|
47
64
|
self.framework = Framework()
|
|
48
65
|
self.profilers = []
|
|
49
66
|
self.exclude_params = []
|
|
67
|
+
self.status_mapping = {}
|
|
68
|
+
self.logging = LoggingConfig()
|
|
50
69
|
|
|
51
70
|
def set_mode(self, mode: str):
|
|
52
71
|
if any(mode == e.value for e in Mode.__members__.values()):
|
|
@@ -70,3 +89,12 @@ class QaseConfig(BaseModel):
|
|
|
70
89
|
|
|
71
90
|
def set_exclude_params(self, exclude_params: List[str]):
|
|
72
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")))
|
qase/commons/models/result.py
CHANGED
|
@@ -33,10 +33,16 @@ 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, invalid')
|
|
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
|
qase/commons/reporters/core.py
CHANGED
|
@@ -11,6 +11,7 @@ from ..models.config.qaseconfig import Mode
|
|
|
11
11
|
from typing import Union, List
|
|
12
12
|
|
|
13
13
|
from ..util import get_host_info
|
|
14
|
+
from ..status_mapping.status_mapping import StatusMapping
|
|
14
15
|
|
|
15
16
|
"""
|
|
16
17
|
CoreReporter is a facade for all reporters and it is used to initialize and manage them.
|
|
@@ -23,11 +24,17 @@ class QaseCoreReporter:
|
|
|
23
24
|
reporter_name: Union[str, None] = None):
|
|
24
25
|
config.validate_config()
|
|
25
26
|
self.config = config.config
|
|
26
|
-
|
|
27
|
+
# Use the logger from ConfigManager instead of creating a new one
|
|
28
|
+
self.logger = config.logger
|
|
27
29
|
self._execution_plan = None
|
|
28
30
|
self.profilers = []
|
|
29
31
|
self.overhead = 0
|
|
30
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
|
+
|
|
31
38
|
# self._selective_execution_setup()
|
|
32
39
|
self.fallback = self._fallback_setup()
|
|
33
40
|
|
|
@@ -87,6 +94,9 @@ class QaseCoreReporter:
|
|
|
87
94
|
ts = time.time()
|
|
88
95
|
self.logger.log_debug(f"Adding result {result}")
|
|
89
96
|
|
|
97
|
+
# Apply status mapping before adding result
|
|
98
|
+
self._apply_status_mapping(result)
|
|
99
|
+
|
|
90
100
|
self.reporter.add_result(result)
|
|
91
101
|
|
|
92
102
|
self.logger.log_debug(f"Result {result.get_title()} added")
|
|
@@ -208,3 +218,26 @@ class QaseCoreReporter:
|
|
|
208
218
|
if self.config.fallback == Mode.report:
|
|
209
219
|
return QaseReport(config=self.config, logger=self.logger)
|
|
210
220
|
return None
|
|
221
|
+
|
|
222
|
+
def _apply_status_mapping(self, result: Result) -> None:
|
|
223
|
+
"""
|
|
224
|
+
Apply status mapping to a test result.
|
|
225
|
+
|
|
226
|
+
This method applies the configured status mapping to the result's execution status.
|
|
227
|
+
The mapping is applied before the result is sent to the reporter.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
result: Test result to apply status mapping to
|
|
231
|
+
"""
|
|
232
|
+
if self.status_mapping.is_empty():
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
original_status = result.get_status()
|
|
236
|
+
if not original_status:
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
mapped_status = self.status_mapping.apply_mapping(original_status)
|
|
240
|
+
|
|
241
|
+
if mapped_status != original_status:
|
|
242
|
+
result.execution.set_status(mapped_status)
|
|
243
|
+
self.logger.log_debug(f"Status mapped for '{result.get_title()}': {original_status} -> {mapped_status}")
|
|
@@ -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()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qase-python-commons
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.1.0
|
|
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
|
|
@@ -21,7 +21,7 @@ Requires-Python: >=3.9
|
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
Requires-Dist: certifi>=2024.2.2
|
|
23
23
|
Requires-Dist: attrs>=23.2.0
|
|
24
|
-
Requires-Dist: qase-api-client~=2.0.
|
|
24
|
+
Requires-Dist: qase-api-client~=2.0.1
|
|
25
25
|
Requires-Dist: qase-api-v2-client~=2.0.0
|
|
26
26
|
Requires-Dist: more_itertools
|
|
27
27
|
Provides-Extra: testing
|
|
@@ -36,7 +36,12 @@ Requires-Dist: urllib3; extra == "testing"
|
|
|
36
36
|
|
|
37
37
|
## Description
|
|
38
38
|
|
|
39
|
-
This package contains reporters for Qase TestOps and Qase Report that are used in
|
|
39
|
+
This package contains reporters for Qase TestOps and Qase Report that are used in following projects:
|
|
40
|
+
|
|
41
|
+
- [qase-pytest](https://github.com/qase-tms/qase-python/tree/main/qase-pytest)
|
|
42
|
+
- [qase-robotframework](https://github.com/qase-tms/qase-python/tree/main/qase-robotframework)
|
|
43
|
+
- [qase-behave](https://github.com/qase-tms/qase-python/tree/main/qase-behave)
|
|
44
|
+
- [qase-tavern](https://github.com/qase-tms/qase-python/tree/main/qase-tavern)
|
|
40
45
|
|
|
41
46
|
## How to install
|
|
42
47
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
qase/__init__.py,sha256=FLefSJnT6MTsTfZSDjMbMImI6674fAoJ5ZT3PNWGcRo,37
|
|
1
2
|
qase/commons/__init__.py,sha256=3HI65PJES4Q6YvtkSuRPh6tZboTETJo8wbdHlNYaePU,323
|
|
2
|
-
qase/commons/config.py,sha256=
|
|
3
|
+
qase/commons/config.py,sha256=ivunk-6IBfsXyRf-_Y_Je_K8boD4gDupv8v2Yf3nm2M,15162
|
|
3
4
|
qase/commons/loader.py,sha256=-MMY4HgSI6q1xq3NaJoq_w4liM73qdFKjYLVCT1E7Pc,1064
|
|
4
|
-
qase/commons/logger.py,sha256=
|
|
5
|
+
qase/commons/logger.py,sha256=V_QTSDWNnUgd0j58rydYYN1AYOu3wa9T2fWjpOyxyuA,3430
|
|
5
6
|
qase/commons/utils.py,sha256=utPRoYyThLs2tgD1lmjkwJ9KZuE7ZjliUiZkJJWFca0,3330
|
|
6
7
|
qase/commons/client/api_v1_client.py,sha256=_dpvldRonALKubInPU03hYhyGJqxeq3DiIuyjDeOngg,11543
|
|
7
8
|
qase/commons/client/api_v2_client.py,sha256=GsIrXJcBw6GtzvJjbjMYa0tvUIxEEe4pALRN2LBMKPM,9043
|
|
@@ -12,7 +13,7 @@ qase/commons/models/attachment.py,sha256=cGfB0BaTDVfA7euJubKvi2cXXNvlSm_dy5x-ak3
|
|
|
12
13
|
qase/commons/models/basemodel.py,sha256=0j8E-LE6hxAKQPYLNM9qThor9s2ZndZys_kibeoLImo,426
|
|
13
14
|
qase/commons/models/external_link.py,sha256=bdXj7pNHHkfb3dcYtXN8sNp-HFxHvZeYz8QIPGcvX0w,1317
|
|
14
15
|
qase/commons/models/relation.py,sha256=HymHeh1uBcoQnTs4Vra7WJ_KFkhryj5o7cShjoGQImI,511
|
|
15
|
-
qase/commons/models/result.py,sha256=
|
|
16
|
+
qase/commons/models/result.py,sha256=8Tp-R34vxZhxmgqQErr2vH8ie-ZidDNqxZy6Xty4uqg,4019
|
|
16
17
|
qase/commons/models/run.py,sha256=ANbljW1mgua4JF1wGZG9S-jnUpI7FC4F15sAQOoNnko,3096
|
|
17
18
|
qase/commons/models/runtime.py,sha256=h0kJcPYRo8dglIQVFyzh-cFpWb3bAaQaIq5wV8VfClM,1508
|
|
18
19
|
qase/commons/models/step.py,sha256=svH2jvxiJRBugCjKdifXzOKli9OzQQvn8lsPhzd20yY,5433
|
|
@@ -21,7 +22,7 @@ qase/commons/models/config/batch.py,sha256=X0H8SVOCCD2pV6LSMqjI-tIjRcLifnrM5Mare
|
|
|
21
22
|
qase/commons/models/config/connection.py,sha256=wK2fGjc0G0rMVVhPnjw_t_M1YWZwANlhwl-awmI7XSo,516
|
|
22
23
|
qase/commons/models/config/framework.py,sha256=sSWKQp18zxiS_79_XDH2hkHLpYEQnyH3bRquaT8XYYY,1803
|
|
23
24
|
qase/commons/models/config/plan.py,sha256=JbAY7qfGXYreXOLa32OLxw6z0paeCCf87-2b1m8xkks,137
|
|
24
|
-
qase/commons/models/config/qaseconfig.py,sha256=
|
|
25
|
+
qase/commons/models/config/qaseconfig.py,sha256=oWc03PKV8-jb3JNgs_tqQP-22wsKZW25eSaAacYl4no,2838
|
|
25
26
|
qase/commons/models/config/report.py,sha256=g3Z2B3Tewaasjc1TMj2mkXxz0Zc1C39sHeTXH0MRM2Y,497
|
|
26
27
|
qase/commons/models/config/run.py,sha256=UGE5PA_EyiFpfIev-TOob1BocxT1vdUdWW3ELMD58zQ,1239
|
|
27
28
|
qase/commons/models/config/testops.py,sha256=rJ9wW-VMt-5XaoPdRUKeM9rlV0eTiRINEhEulFVczv0,1893
|
|
@@ -30,13 +31,15 @@ qase/commons/profilers/db.py,sha256=Am1tvvLgJq4_A8JsuSeBGf47BD2lnSX-5KiMjSgr-Ko,
|
|
|
30
31
|
qase/commons/profilers/network.py,sha256=zKNBnTQG4BMg8dn8O--tQzQLpu-qs5ADhHEnqIas0gM,4950
|
|
31
32
|
qase/commons/profilers/sleep.py,sha256=HT6h0R-2XHZAoBYRxS2T_KC8RrnEoVjP7MXusaE4Nec,1624
|
|
32
33
|
qase/commons/reporters/__init__.py,sha256=J0aNLzb_MPPT_zF8BtX_w9nj_U7Ad06RGpyWK5Pxq1o,169
|
|
33
|
-
qase/commons/reporters/core.py,sha256=
|
|
34
|
+
qase/commons/reporters/core.py,sha256=FJx1kxXkUz9vwkSAXapkNS3vF97Igvlq7Yyjp-4BpeI,9539
|
|
34
35
|
qase/commons/reporters/report.py,sha256=ZLwtVn5gjwgJFtfbpLUO-vW3M3skEq3AhKJwtmM0nUw,4810
|
|
35
36
|
qase/commons/reporters/testops.py,sha256=uph_8upIt5bu4ncqyK7ehXR9NqtpZ34wGUBFLVJKOpE,7481
|
|
37
|
+
qase/commons/status_mapping/__init__.py,sha256=NPsWKDC7rGSCYfVunnGnHxL0_HmKWH7RBaMjWTH_Mtk,322
|
|
38
|
+
qase/commons/status_mapping/status_mapping.py,sha256=kohSLNK6_qbMSP-M8gxHTTmOECgzDE3XvLqOzidPlYI,7213
|
|
36
39
|
qase/commons/util/__init__.py,sha256=0sRRfrMOIPCHpk9tXM94Pj10qrk18B61qEcbLpRjw_I,74
|
|
37
40
|
qase/commons/util/host_data.py,sha256=n8o5PDs8kELCZZ5GR7Jug6LsgZHWJudU7iRmZHRdrlw,5264
|
|
38
41
|
qase/commons/validators/base.py,sha256=wwSn-4YiuXtfGMGnSKgo9Vm5hAKevVmmfd2Ro6Q7MYQ,173
|
|
39
|
-
qase_python_commons-4.
|
|
40
|
-
qase_python_commons-4.
|
|
41
|
-
qase_python_commons-4.
|
|
42
|
-
qase_python_commons-4.
|
|
42
|
+
qase_python_commons-4.1.0.dist-info/METADATA,sha256=JcshiFrQAEhDrqLCi3QflCIJcPMfDF4qLsEqLt-ansE,1982
|
|
43
|
+
qase_python_commons-4.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
44
|
+
qase_python_commons-4.1.0.dist-info/top_level.txt,sha256=Mn5aFk7H7Uia4s1NRDsvebu8vCrFy9nOuRIBfkIY5kQ,5
|
|
45
|
+
qase_python_commons-4.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|