qase-python-commons 2.0.12__tar.gz → 3.0.0__tar.gz

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 (40) hide show
  1. {qase_python_commons-2.0.12/src/qase_python_commons.egg-info → qase_python_commons-3.0.0}/PKG-INFO +14 -3
  2. {qase_python_commons-2.0.12 → qase_python_commons-3.0.0}/pyproject.toml +15 -3
  3. qase_python_commons-3.0.0/src/qase/commons/__init__.py +14 -0
  4. qase_python_commons-3.0.0/src/qase/commons/config.py +61 -0
  5. qase_python_commons-3.0.0/src/qase/commons/exceptions/reporter.py +2 -0
  6. {qase_python_commons-2.0.12/src/qaseio → qase_python_commons-3.0.0/src/qase}/commons/loader.py +5 -4
  7. qase_python_commons-3.0.0/src/qase/commons/logger.py +28 -0
  8. qase_python_commons-3.0.0/src/qase/commons/models/__init__.py +17 -0
  9. {qase_python_commons-2.0.12/src/qaseio → qase_python_commons-3.0.0/src/qase}/commons/models/attachment.py +13 -10
  10. {qase_python_commons-2.0.12/src/qaseio → qase_python_commons-3.0.0/src/qase}/commons/models/relation.py +2 -1
  11. {qase_python_commons-2.0.12/src/qaseio → qase_python_commons-3.0.0/src/qase}/commons/models/result.py +34 -29
  12. {qase_python_commons-2.0.12/src/qaseio → qase_python_commons-3.0.0/src/qase}/commons/models/run.py +11 -8
  13. qase_python_commons-3.0.0/src/qase/commons/models/runtime.py +43 -0
  14. {qase_python_commons-2.0.12/src/qaseio → qase_python_commons-3.0.0/src/qase}/commons/models/step.py +42 -23
  15. {qase_python_commons-2.0.12/src/qaseio → qase_python_commons-3.0.0/src/qase}/commons/models/suite.py +3 -2
  16. qase_python_commons-3.0.0/src/qase/commons/profilers/__init__.py +10 -0
  17. qase_python_commons-3.0.0/src/qase/commons/profilers/db.py +19 -0
  18. qase_python_commons-2.0.12/src/qaseio/commons/interceptor.py → qase_python_commons-3.0.0/src/qase/commons/profilers/network.py +37 -40
  19. qase_python_commons-3.0.0/src/qase/commons/profilers/sleep.py +54 -0
  20. qase_python_commons-3.0.0/src/qase/commons/reporters/__init__.py +9 -0
  21. qase_python_commons-3.0.0/src/qase/commons/reporters/core.py +187 -0
  22. {qase_python_commons-2.0.12/src/qaseio/commons → qase_python_commons-3.0.0/src/qase/commons/reporters}/report.py +48 -40
  23. qase_python_commons-3.0.0/src/qase/commons/reporters/testops.py +369 -0
  24. {qase_python_commons-2.0.12/src/qaseio → qase_python_commons-3.0.0/src/qase}/commons/utils.py +48 -6
  25. qase_python_commons-3.0.0/src/qase/commons/validators/base.py +6 -0
  26. {qase_python_commons-2.0.12 → qase_python_commons-3.0.0/src/qase_python_commons.egg-info}/PKG-INFO +14 -3
  27. qase_python_commons-3.0.0/src/qase_python_commons.egg-info/SOURCES.txt +30 -0
  28. {qase_python_commons-2.0.12 → qase_python_commons-3.0.0}/src/qase_python_commons.egg-info/requires.txt +1 -1
  29. qase_python_commons-3.0.0/src/qase_python_commons.egg-info/top_level.txt +1 -0
  30. qase_python_commons-2.0.12/AUTHORS.rst +0 -5
  31. qase_python_commons-2.0.12/src/qase_python_commons.egg-info/SOURCES.txt +0 -23
  32. qase_python_commons-2.0.12/src/qase_python_commons.egg-info/top_level.txt +0 -1
  33. qase_python_commons-2.0.12/src/qaseio/commons/__init__.py +0 -15
  34. qase_python_commons-2.0.12/src/qaseio/commons/config.py +0 -50
  35. qase_python_commons-2.0.12/src/qaseio/commons/models/__init__.py +0 -17
  36. qase_python_commons-2.0.12/src/qaseio/commons/models/runtime.py +0 -29
  37. qase_python_commons-2.0.12/src/qaseio/commons/testops.py +0 -399
  38. {qase_python_commons-2.0.12 → qase_python_commons-3.0.0}/README.md +0 -0
  39. {qase_python_commons-2.0.12 → qase_python_commons-3.0.0}/setup.cfg +0 -0
  40. {qase_python_commons-2.0.12 → qase_python_commons-3.0.0}/src/qase_python_commons.egg-info/dependency_links.txt +0 -0
@@ -1,17 +1,28 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qase-python-commons
3
- Version: 2.0.12
3
+ Version: 3.0.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/master/qase-python-commons
7
7
  Classifier: Development Status :: 5 - Production/Stable
8
8
  Classifier: Programming Language :: Python
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Topic :: Software Development :: Quality Assurance
12
+ Classifier: Topic :: Software Development :: Testing
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.7
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
9
21
  Requires-Python: >=3.7
10
22
  Description-Content-Type: text/markdown
11
- License-File: AUTHORS.rst
12
23
  Requires-Dist: certifi>=2024.2.2
13
- Requires-Dist: qaseio<5.0.0,>=4.0.2
14
24
  Requires-Dist: attrs>=23.2.0
25
+ Requires-Dist: qaseio
15
26
  Requires-Dist: more_itertools
16
27
  Provides-Extra: testing
17
28
  Requires-Dist: pytest; extra == "testing"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qase-python-commons"
7
- version = "2.0.12"
7
+ version = "3.0.0"
8
8
  description = "A library for Qase TestOps and Qase Report"
9
9
  readme = "README.md"
10
10
  authors = [{name = "Qase Team", email = "support@qase.io"}]
@@ -12,13 +12,25 @@ license = {file = "LICENSE"}
12
12
  classifiers = [
13
13
  "Development Status :: 5 - Production/Stable",
14
14
  "Programming Language :: Python",
15
+ 'Intended Audience :: Developers',
16
+ 'License :: OSI Approved :: Apache Software License',
17
+ 'Topic :: Software Development :: Quality Assurance',
18
+ 'Topic :: Software Development :: Testing',
19
+ 'Programming Language :: Python :: 3',
20
+ 'Programming Language :: Python :: 3 :: Only',
21
+ 'Programming Language :: Python :: 3.7',
22
+ 'Programming Language :: Python :: 3.8',
23
+ 'Programming Language :: Python :: 3.9',
24
+ 'Programming Language :: Python :: 3.10',
25
+ 'Programming Language :: Python :: 3.11',
26
+ 'Programming Language :: Python :: 3.12',
15
27
  ]
16
28
  urls = {"Homepage" = "https://github.com/qase-tms/qase-python/tree/master/qase-python-commons"}
17
29
  requires-python = ">=3.7"
18
30
  dependencies = [
19
31
  "certifi>=2024.2.2",
20
- "qaseio>=4.0.2,<5.0.0",
21
32
  "attrs>=23.2.0",
33
+ "qaseio",
22
34
  "more_itertools"
23
35
  ]
24
36
 
@@ -66,7 +78,7 @@ exclude = [".tox", "build", "dist", ".eggs", "docs/conf.py"]
66
78
  branch = true
67
79
 
68
80
  [tool.coverage.paths]
69
- source = ["src/qaseio/commons"]
81
+ source = ["src/qase/commons"]
70
82
 
71
83
  [tool.coverage.report]
72
84
  # Regexes for lines to exclude from consideration
@@ -0,0 +1,14 @@
1
+ from .utils import QaseUtils, StringFormatter
2
+ from .config import ConfigManager
3
+ from .loader import TestOpsPlanLoader
4
+ from .logger import Logger
5
+ from .exceptions.reporter import ReporterException
6
+
7
+ __all__ = [
8
+ QaseUtils,
9
+ StringFormatter,
10
+ ConfigManager,
11
+ TestOpsPlanLoader,
12
+ Logger,
13
+ ReporterException
14
+ ]
@@ -0,0 +1,61 @@
1
+ import os
2
+ import json
3
+ from .logger import Logger
4
+
5
+
6
+ class ConfigManager:
7
+
8
+ def __init__(self, config_file='./qase.config.json', env_vars_prefix='QASE_'):
9
+ self.logger = Logger()
10
+ self.config = {}
11
+ self.parseBool = lambda d: d in ("y", "yes", "true", "1", 1, True)
12
+
13
+ try:
14
+ if os.path.exists(config_file):
15
+ with open(config_file, "r") as file:
16
+ self.config = json.load(file)
17
+ except Exception as e:
18
+ self.logger.log("Failed to load config from file", "error")
19
+
20
+ # Load from env vars
21
+ try:
22
+ for key, value in os.environ.items():
23
+ if key.startswith(env_vars_prefix):
24
+ self._set_config(key[len(env_vars_prefix):].lower().replace('_', '.'), value)
25
+ except Exception as e:
26
+ self.logger.log("Failed to load config from env vars {e}", "error")
27
+
28
+ def get(self, key, default=None, value_type=None):
29
+ # Use _get_config method to get the value. If None, return default.
30
+ value = self._get_config(key)
31
+ if value_type and value_type == bool:
32
+ return self.parseBool(value or default)
33
+ return value or default
34
+
35
+ def validate_config(self):
36
+ if self.validator:
37
+ self.validator.validate(self.config)
38
+
39
+ def set(self, key, value):
40
+ self._set_config(key, value)
41
+
42
+ def _get_keys(self, config, prefix=""):
43
+ for key, value in config.items():
44
+ if isinstance(value, dict):
45
+ yield from self._get_keys(value, f"{prefix}{key}.")
46
+ else:
47
+ yield f"{prefix}{key}"
48
+
49
+ def _set_config(self, key, value, delimiter="."):
50
+ keys = key.split(delimiter)
51
+ config = self.config
52
+ for key in keys[:-1]:
53
+ config = config.setdefault(key, {})
54
+ config[keys[-1]] = value
55
+
56
+ def _get_config(self, key):
57
+ keys = key.split(".")
58
+ config = self.config
59
+ for key in keys[:-1]:
60
+ config = config.get(key, {})
61
+ return config.get(keys[-1], None)
@@ -0,0 +1,2 @@
1
+ class ReporterException(Exception):
2
+ pass
@@ -2,10 +2,12 @@ from qaseio.api_client import ApiClient
2
2
  from qaseio.configuration import Configuration
3
3
  from qaseio.api.plans_api import PlansApi
4
4
  from qaseio.rest import ApiException
5
+
5
6
  import certifi
6
7
 
8
+
7
9
  class TestOpsPlanLoader:
8
- def __init__(self, api_token, host = 'qase.io'):
10
+ def __init__(self, api_token, host='qase.io'):
9
11
  configuration = Configuration()
10
12
  configuration.api_key['TokenAuth'] = api_token
11
13
  configuration.host = f'https://api.{host}/v1'
@@ -16,13 +18,12 @@ class TestOpsPlanLoader:
16
18
  configuration.ssl_ca_cert = certifi.where()
17
19
 
18
20
  def load(self, code: str, plan_id: int) -> list:
19
- api_instance = PlansApi(self.client)
20
21
  try:
21
- response = api_instance.get_plan(code=code, id=plan_id)
22
+ response = PlansApi(self.client).get_plan(code=code, id=plan_id)
22
23
  if hasattr(response, 'result'):
23
24
  self.case_list = [c.case_id for c in response.result.cases]
24
25
  return self.case_list
25
26
  raise ValueError("Unable to find given plan")
26
27
  except ApiException as e:
27
28
  print("Unable to load test plan data: %s\n" % e)
28
- return []
29
+ return []
@@ -0,0 +1,28 @@
1
+ import datetime
2
+
3
+ import os
4
+
5
+
6
+ class Logger:
7
+ def __init__(self, debug: bool = False, prefix: str = '', dir: str = './logs') -> None:
8
+ 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'):
15
+ pass
16
+
17
+ def log(self, message: str, level: str = 'info'):
18
+ time_str = self._get_timestamp("%H:%M:%S")
19
+ log = f"[Qase][{time_str}][{level}] {message}\n"
20
+ print(log)
21
+ if self.debug:
22
+ with open(self.log_file, 'a') as f:
23
+ f.write(log)
24
+
25
+ @staticmethod
26
+ def _get_timestamp(format: str = "%Y%m%d_%H:%M:%S"):
27
+ now = datetime.datetime.now()
28
+ return now.strftime(format)
@@ -0,0 +1,17 @@
1
+ from .result import Result
2
+ from .run import Run
3
+ from .attachment import Attachment
4
+ from .suite import Suite
5
+ from .relation import Relation
6
+ from .step import Step
7
+ from .runtime import Runtime
8
+
9
+ __all__ = [
10
+ Result,
11
+ Run,
12
+ Attachment,
13
+ Suite,
14
+ Relation,
15
+ Step,
16
+ Runtime
17
+ ]
@@ -1,28 +1,32 @@
1
1
  import os
2
2
  import uuid
3
- from typing import Optional
3
+ from typing import Optional, Union
4
4
  from io import BytesIO, StringIO
5
5
  import json
6
+ import pathlib
7
+
6
8
 
7
9
  class Attachment:
8
- def __init__(self,
10
+ def __init__(self,
9
11
  file_name: str,
10
12
  mime_type: str,
11
13
  content: Optional[str] = None,
12
- file_path: Optional[str] = None):
14
+ file_path: Optional[str] = None,
15
+ temporary: bool = False):
13
16
  self.file_name = file_name
14
17
  self.mime_type = mime_type
15
18
  if (not content) and (not file_path):
16
19
  raise ValueError('Either content or file_path must be provided.')
17
20
  self.file_path = file_path
18
21
  self.content = content
22
+ self.temporary = temporary
19
23
 
20
24
  if (not isinstance(content, str)) and (not isinstance(content, bytes)):
21
25
  self.content = json.dumps(self.content, default=lambda o: o.__dict__, sort_keys=False, indent=4)
22
26
 
23
27
  self.size = self._get_size(content)
24
28
  self.id = str(uuid.uuid4())
25
-
29
+
26
30
  def _get_size(self, content):
27
31
  if self.file_path:
28
32
  return os.path.getsize(self.file_path)
@@ -30,17 +34,16 @@ class Attachment:
30
34
  return len(content)
31
35
  else:
32
36
  return 0
33
-
34
- def get_id(self):
37
+
38
+ def get_id(self) -> str:
35
39
  return self.id
36
-
40
+
37
41
  def get_for_upload(self) -> BytesIO:
38
42
  if self.file_path:
39
- with open(self.file_path, "rb") as fc:
40
- content = BytesIO(fc.read())
43
+ return pathlib.Path(os.path.abspath(self.file_path))
41
44
  else:
42
45
  if isinstance(self.content, str):
43
- content = BytesIO(bytes(self.content, 'utf-8'))
46
+ content = BytesIO(self.content.encode('utf-8'))
44
47
  elif isinstance(self.content, bytes):
45
48
  content = BytesIO(self.content)
46
49
  content.name = self.file_name
@@ -3,7 +3,8 @@ class RelationSuite(object):
3
3
  self.suite_id = suite_id
4
4
  self.title = title
5
5
 
6
+
6
7
  class Relation(object):
7
8
  def __init__(self, type: str, data: RelationSuite):
8
9
  self.type = type
9
- self.data = data
10
+ self.data = data
@@ -3,27 +3,29 @@ from pathlib import PosixPath
3
3
  import time
4
4
  import uuid
5
5
  import json
6
- from qaseio.commons.models.step import Step
7
- from qaseio.commons.models.suite import Suite
8
- from qaseio.commons.models.relation import Relation
9
- from qaseio.commons.models.attachment import Attachment
10
- from qaseio.commons.utils import QaseUtils
6
+ from .step import Step
7
+ from .suite import Suite
8
+ from .attachment import Attachment
9
+ from .relation import Relation
10
+ from .. import QaseUtils
11
+
11
12
 
12
13
  class Field:
13
- def __init__(self,
14
+ def __init__(self,
14
15
  name: str,
15
16
  value: Union[str, list]):
16
17
  self.name = name
17
18
  self.value = value
18
19
 
20
+
19
21
  class Execution(object):
20
- def __init__(self,
21
- status: Optional[str] = None,
22
- end_time: int = 0,
23
- duration: int = 0,
24
- stacktrace: Optional[str] = None,
25
- thread: Optional[str] = QaseUtils.get_thread_name()
26
- ):
22
+ def __init__(self,
23
+ status: Optional[str] = None,
24
+ end_time: int = 0,
25
+ duration: int = 0,
26
+ stacktrace: Optional[str] = None,
27
+ thread: Optional[str] = QaseUtils.get_thread_name()
28
+ ):
27
29
  self.start_time = time.time()
28
30
  self.status = status
29
31
  self.end_time = end_time
@@ -32,11 +34,11 @@ class Execution(object):
32
34
  self.thread = thread
33
35
 
34
36
  def set_status(self, status: Optional[str]):
35
- if (status in ['passed', 'failed', 'skipped', 'untested']):
37
+ if status in ['passed', 'failed', 'skipped', 'untested']:
36
38
  self.status = status
37
39
  else:
38
40
  raise ValueError('Step status must be one of: passed, failed, skipped, untested')
39
-
41
+
40
42
  def get_status(self):
41
43
  return self.status
42
44
 
@@ -44,15 +46,16 @@ class Execution(object):
44
46
  self.end_time = time.time()
45
47
  self.duration = (int)((self.end_time - self.start_time) * 1000)
46
48
 
49
+
47
50
  class Request(object):
48
51
  def __init__(self,
49
- method: str,
50
- url: str,
51
- status: int,
52
- request_headers: Dict[str, str],
53
- request_body: str,
54
- response_headers: Dict[str, str],
55
- response_body: str):
52
+ method: str,
53
+ url: str,
54
+ status: int,
55
+ request_headers: Dict[str, str],
56
+ request_body: str,
57
+ response_headers: Dict[str, str],
58
+ response_body: str):
56
59
  self.method = method
57
60
  self.url = url
58
61
  self.status = status
@@ -61,6 +64,7 @@ class Request(object):
61
64
  self.response_headers = response_headers
62
65
  self.response_body = response_body
63
66
 
67
+
64
68
  class Result(object):
65
69
  def __init__(self, title: str, signature: str) -> None:
66
70
  self.id: str = str(uuid.uuid4())
@@ -82,7 +86,7 @@ class Result(object):
82
86
 
83
87
  def add_message(self, message: str) -> None:
84
88
  self.message = message
85
-
89
+
86
90
  def add_field(self, field: Type[Field]) -> None:
87
91
  self.fields[field.name] = field.value
88
92
 
@@ -106,24 +110,24 @@ class Result(object):
106
110
 
107
111
  def get_status(self) -> Optional[str]:
108
112
  return self.execution.status
109
-
113
+
110
114
  def get_id(self) -> str:
111
115
  return self.id
112
-
116
+
113
117
  def get_title(self) -> str:
114
118
  return self.title
115
-
119
+
116
120
  def get_field(self, name: str) -> Optional[Type[Field]]:
117
121
  if name in self.fields:
118
122
  return self.fields[name]
119
123
  return None
120
-
124
+
121
125
  def get_testops_id(self) -> Optional[int]:
122
126
  if self.testops_id is None:
123
127
  # Hack for old API
124
128
  return 0
125
129
  return self.testops_id
126
-
130
+
127
131
  def get_duration(self) -> int:
128
132
  return self.execution.duration
129
133
 
@@ -135,4 +139,5 @@ class Result(object):
135
139
  self.run_id = run_id
136
140
 
137
141
  def to_json(self) -> str:
138
- return json.dumps(self, default=lambda o: o.__str__() if isinstance(o, PosixPath) else o.__dict__, sort_keys=False, indent=4)
142
+ return json.dumps(self, default=lambda o: o.__str__() if isinstance(o, PosixPath) else o.__dict__,
143
+ sort_keys=False, indent=4)
@@ -1,6 +1,7 @@
1
1
  import json
2
2
  from typing import Optional, List
3
3
 
4
+
4
5
  class RunExecution(object):
5
6
  def __init__(self,
6
7
  start_time: float,
@@ -9,12 +10,13 @@ class RunExecution(object):
9
10
  ) -> None:
10
11
  self.start_time = start_time
11
12
  self.end_time = end_time
12
- self.duration = int((end_time - start_time)*1000)
13
+ self.duration = int((end_time - start_time) * 1000)
13
14
  self.cumulative_duration = cumulative_duration
14
15
 
15
16
  def track(self, result: dict):
16
17
  self.cumulative_duration += result["execution"]["duration"]
17
18
 
19
+
18
20
  class RunStats(object):
19
21
  def __init__(self) -> None:
20
22
  self.passed = 0
@@ -23,7 +25,7 @@ class RunStats(object):
23
25
  self.broken = 0
24
26
  self.muted = 0
25
27
  self.total = 0
26
-
28
+
27
29
  def track(self, result: dict):
28
30
  status = result["execution"]["status"]
29
31
  if status == "passed":
@@ -37,10 +39,10 @@ class RunStats(object):
37
39
  self.total += 1
38
40
  if result.get('muted', False):
39
41
  self.muted += 1
40
-
42
+
41
43
 
42
44
  class Run(object):
43
- def __init__(self,
45
+ def __init__(self,
44
46
  title: str,
45
47
  start_time: float,
46
48
  end_time: float,
@@ -49,6 +51,7 @@ class Run(object):
49
51
  suites: List[str] = [],
50
52
  environment: Optional[str] = None
51
53
  ):
54
+ self.host_data = None
52
55
  self.title = title
53
56
  self.execution = RunExecution(start_time=start_time, end_time=end_time)
54
57
  self.stats = RunStats()
@@ -56,10 +59,10 @@ class Run(object):
56
59
  self.threads = threads
57
60
  self.suites = suites
58
61
  self.environment = environment
59
-
62
+
60
63
  def to_json(self) -> str:
61
64
  return json.dumps(self, default=lambda o: o.__dict__, sort_keys=False, indent=4)
62
-
65
+
63
66
  def add_result(self, result: dict):
64
67
  compact_result = {
65
68
  "id": result["id"],
@@ -71,8 +74,8 @@ class Run(object):
71
74
  self.results.append(compact_result)
72
75
  self.execution.track(result)
73
76
  self.stats.track(result)
74
- if (result["execution"]["thread"] not in self.threads):
77
+ if result["execution"]["thread"] not in self.threads:
75
78
  self.threads.append(result["execution"]["thread"])
76
79
 
77
80
  def add_host_data(self, host_data: dict):
78
- self.host_data = host_data
81
+ self.host_data = host_data
@@ -0,0 +1,43 @@
1
+ from .step import Step, StepTextData
2
+ from .attachment import Attachment
3
+
4
+
5
+ class QaseRuntimeException(Exception):
6
+ pass
7
+
8
+
9
+ class Runtime:
10
+ def __init__(self):
11
+ self.result = None
12
+ self.steps = {}
13
+ self.step_id = None
14
+
15
+ def add_step(self, step: Step):
16
+ try:
17
+ if self.step_id:
18
+ step.set_parent_id(self.step_id)
19
+
20
+ self.steps[step.id] = step
21
+ self.step_id = step.id
22
+ except Exception as e:
23
+ raise QaseRuntimeException(e)
24
+
25
+ def finish_step(self, id: str, status: str, data=None):
26
+ try:
27
+ self.steps[id].execution.set_status(status)
28
+ if data:
29
+ self.steps[id].set_data(data)
30
+
31
+ self.steps[id].execution.complete()
32
+ self.step_id = self.steps[id].parent_id
33
+ except Exception as e:
34
+ raise QaseRuntimeException(e)
35
+
36
+ def add_attachment(self, attachment: Attachment):
37
+ try:
38
+ if self.step_id:
39
+ self.steps[self.step_id].add_attachment(attachment)
40
+ else:
41
+ self.result.add_attachment(attachment)
42
+ except Exception as e:
43
+ raise QaseRuntimeException(e)