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.
Files changed (35) hide show
  1. qase/__init__.py +3 -0
  2. qase/commons/client/api_v1_client.py +269 -175
  3. qase/commons/client/api_v2_client.py +163 -26
  4. qase/commons/client/base_api_client.py +23 -6
  5. qase/commons/config.py +162 -23
  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 +17 -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 +9 -0
  19. qase/commons/models/step.py +45 -12
  20. qase/commons/profilers/__init__.py +4 -3
  21. qase/commons/profilers/db.py +965 -5
  22. qase/commons/reporters/core.py +60 -10
  23. qase/commons/reporters/report.py +11 -6
  24. qase/commons/reporters/testops.py +56 -27
  25. qase/commons/status_mapping/__init__.py +12 -0
  26. qase/commons/status_mapping/status_mapping.py +237 -0
  27. qase/commons/util/__init__.py +9 -0
  28. qase/commons/util/host_data.py +147 -0
  29. qase/commons/utils.py +95 -0
  30. {qase_python_commons-3.1.9.dist-info → qase_python_commons-4.1.9.dist-info}/METADATA +16 -11
  31. qase_python_commons-4.1.9.dist-info/RECORD +45 -0
  32. {qase_python_commons-3.1.9.dist-info → qase_python_commons-4.1.9.dist-info}/WHEEL +1 -1
  33. qase/commons/models/suite.py +0 -13
  34. qase_python_commons-3.1.9.dist-info/RECORD +0 -40
  35. {qase_python_commons-3.1.9.dist-info → qase_python_commons-4.1.9.dist-info}/top_level.txt +0 -0
qase/commons/logger.py CHANGED
@@ -1,32 +1,101 @@
1
+ import os
1
2
  import datetime
3
+ import threading
4
+ from typing import Optional
2
5
 
3
- import os
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
- def __init__(self, debug: bool = False, prefix: str = '', dir: str = os.path.join('.', 'logs')) -> None:
14
+ _log_file = None
15
+
16
+ def __init__(self, debug: bool = False, prefix: str = '', dir: str = os.path.join('.', 'logs'),
17
+ logging_options: Optional[LoggingOptions] = None) -> None:
8
18
  self.debug = debug
9
- if self.debug:
10
- filename = f'{prefix}_{self._get_timestamp()}.log'
11
- if not os.path.exists(dir):
12
- os.makedirs(dir)
13
- self.log_file = os.path.join(dir, f'{filename}')
14
- with open(self.log_file, 'w', encoding='utf-8'):
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:
37
+ if Logger._log_file is None:
38
+ timestamp = self._get_timestamp()
39
+ filename = f'{prefix}_{timestamp}.log'
40
+ if not os.path.exists(dir):
41
+ os.makedirs(dir)
42
+ Logger._log_file = os.path.join(dir, filename)
43
+
44
+ with open(Logger._log_file, 'a', encoding='utf-8'):
15
45
  pass
16
46
 
47
+ self.lock = threading.Lock()
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
+
17
68
  def log(self, message: str, level: str = 'info'):
18
69
  time_str = self._get_timestamp("%H:%M:%S")
19
70
  log = f"[Qase][{time_str}][{level}] {message}\n"
20
- print(log)
21
- if self.debug:
22
- with open(self.log_file, 'a', encoding='utf-8') as f:
23
- f.write(log)
71
+
72
+ # Console output
73
+ if self.logging_options.console:
74
+ try:
75
+ print(log, end='')
76
+ except (OSError, IOError):
77
+ pass
78
+
79
+ # File output
80
+ if self.logging_options.file:
81
+ with self.lock:
82
+ with open(Logger._log_file, 'a', encoding='utf-8') as f:
83
+ f.write(log)
24
84
 
25
85
  def log_debug(self, message: str):
26
86
  if self.debug:
27
87
  self.log(message, 'debug')
28
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
+
29
98
  @staticmethod
30
- def _get_timestamp(fmt: str = "%Y%m%d_%H_%M_%S"):
99
+ def _get_timestamp(fmt: str = "%Y%m%d"):
31
100
  now = datetime.datetime.now()
32
101
  return now.strftime(fmt)
@@ -1,7 +1,6 @@
1
1
  from .result import Result, Field
2
2
  from .run import Run
3
3
  from .attachment import Attachment
4
- from .suite import Suite
5
4
  from .relation import Relation
6
5
  from .step import Step
7
6
  from .runtime import Runtime
@@ -10,7 +9,6 @@ __all__ = [
10
9
  Result,
11
10
  Run,
12
11
  Attachment,
13
- Suite,
14
12
  Relation,
15
13
  Step,
16
14
  Runtime,
@@ -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) -> BytesIO:
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
- content = BytesIO(fc.read())
37
+ filedata = fc.read()
36
38
  else:
37
39
  if isinstance(self.content, str):
38
- content = BytesIO(bytes(self.content, 'utf-8'))
40
+ filedata = bytes(self.content, 'utf-8')
39
41
  elif isinstance(self.content, bytes):
40
- content = BytesIO(self.content)
41
- content.name = self.file_name
42
- content.mime = self.mime_type
43
-
44
- return content
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,7 +1,16 @@
1
1
  import json
2
2
 
3
+ from enum import Enum
4
+
3
5
 
4
6
  class BaseModel:
5
- def __str__(self) -> str:
6
- return json.dumps(self, default=lambda o: o.__dict__ if hasattr(o, '__dict__') else str(o), indent=4,
7
- sort_keys=True)
7
+ def __str__(self, enum_as_name=False) -> str:
8
+ def serialize(o):
9
+ if isinstance(o, Enum):
10
+ return o.name if enum_as_name else o.value
11
+ elif hasattr(o, '__dict__'):
12
+ return o.__dict__
13
+ else:
14
+ return str(o)
15
+
16
+ return json.dumps(self, default=serialize, indent=4, sort_keys=True)
@@ -17,11 +17,28 @@ class Trace(Enum):
17
17
  failed = "retain-on-failure"
18
18
 
19
19
 
20
+ class XFailStatus(BaseModel):
21
+ xfail: str = None
22
+ xpass: str = None
23
+
24
+ def __init__(self):
25
+ self.xfail = 'skipped'
26
+ self.xpass = 'passed'
27
+
28
+ def set_xfail(self, value: str):
29
+ self.xfail = value
30
+
31
+ def set_xpass(self, value: str):
32
+ self.xpass = value
33
+
34
+
20
35
  class PytestConfig(BaseModel):
21
36
  capture_logs: bool = None
37
+ xfail_status: XFailStatus = None
22
38
 
23
39
  def __init__(self):
24
40
  self.capture_logs = False
41
+ self.xfail_status = XFailStatus()
25
42
 
26
43
  def set_capture_logs(self, capture_logs):
27
44
  self.capture_logs = QaseUtils.parse_bool(capture_logs)
@@ -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,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,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 = {}