robotframework-openapitools 0.4.0__py3-none-any.whl → 1.0.0b2__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 (60) hide show
  1. OpenApiDriver/__init__.py +44 -41
  2. OpenApiDriver/openapi_executors.py +40 -39
  3. OpenApiDriver/openapi_reader.py +115 -116
  4. OpenApiDriver/openapidriver.libspec +71 -61
  5. OpenApiDriver/openapidriver.py +25 -19
  6. OpenApiLibCore/__init__.py +13 -11
  7. OpenApiLibCore/annotations.py +3 -0
  8. OpenApiLibCore/data_generation/__init__.py +12 -0
  9. OpenApiLibCore/data_generation/body_data_generation.py +269 -0
  10. OpenApiLibCore/data_generation/data_generation_core.py +240 -0
  11. OpenApiLibCore/data_invalidation.py +281 -0
  12. OpenApiLibCore/dto_base.py +29 -35
  13. OpenApiLibCore/dto_utils.py +97 -85
  14. OpenApiLibCore/oas_cache.py +14 -13
  15. OpenApiLibCore/openapi_libcore.libspec +346 -193
  16. OpenApiLibCore/openapi_libcore.py +389 -1702
  17. OpenApiLibCore/parameter_utils.py +91 -0
  18. OpenApiLibCore/path_functions.py +215 -0
  19. OpenApiLibCore/path_invalidation.py +44 -0
  20. OpenApiLibCore/protocols.py +30 -0
  21. OpenApiLibCore/request_data.py +281 -0
  22. OpenApiLibCore/resource_relations.py +54 -0
  23. OpenApiLibCore/validation.py +497 -0
  24. OpenApiLibCore/value_utils.py +528 -481
  25. openapi_libgen/__init__.py +46 -0
  26. openapi_libgen/command_line.py +87 -0
  27. openapi_libgen/parsing_utils.py +26 -0
  28. openapi_libgen/spec_parser.py +221 -0
  29. openapi_libgen/templates/__init__.jinja +3 -0
  30. openapi_libgen/templates/library.jinja +30 -0
  31. robotframework_openapitools-1.0.0b2.dist-info/METADATA +237 -0
  32. robotframework_openapitools-1.0.0b2.dist-info/RECORD +37 -0
  33. {robotframework_openapitools-0.4.0.dist-info → robotframework_openapitools-1.0.0b2.dist-info}/WHEEL +1 -1
  34. robotframework_openapitools-1.0.0b2.dist-info/entry_points.txt +3 -0
  35. roboswag/__init__.py +0 -9
  36. roboswag/__main__.py +0 -3
  37. roboswag/auth.py +0 -44
  38. roboswag/cli.py +0 -80
  39. roboswag/core.py +0 -85
  40. roboswag/generate/__init__.py +0 -1
  41. roboswag/generate/generate.py +0 -121
  42. roboswag/generate/models/__init__.py +0 -0
  43. roboswag/generate/models/api.py +0 -219
  44. roboswag/generate/models/definition.py +0 -28
  45. roboswag/generate/models/endpoint.py +0 -68
  46. roboswag/generate/models/parameter.py +0 -25
  47. roboswag/generate/models/response.py +0 -8
  48. roboswag/generate/models/tag.py +0 -16
  49. roboswag/generate/models/utils.py +0 -60
  50. roboswag/generate/templates/api_init.jinja +0 -15
  51. roboswag/generate/templates/models.jinja +0 -7
  52. roboswag/generate/templates/paths.jinja +0 -68
  53. roboswag/logger.py +0 -33
  54. roboswag/validate/__init__.py +0 -6
  55. roboswag/validate/core.py +0 -3
  56. roboswag/validate/schema.py +0 -21
  57. roboswag/validate/text_response.py +0 -14
  58. robotframework_openapitools-0.4.0.dist-info/METADATA +0 -42
  59. robotframework_openapitools-0.4.0.dist-info/RECORD +0 -41
  60. {robotframework_openapitools-0.4.0.dist-info → robotframework_openapitools-1.0.0b2.dist-info}/LICENSE +0 -0
OpenApiDriver/__init__.py CHANGED
@@ -1,41 +1,44 @@
1
- """
2
- The OpenApiDriver package is intended to be used as a Robot Framework library.
3
- The following classes and constants are exposed to be used by the library user:
4
- - OpenApiDriver: The class to be used as a Library in the *** Settings *** section
5
- - IdDependency, IdReference, PathPropertiesConstraint, PropertyValueConstraint,
6
- UniquePropertyValueConstraint: Classes to be subclassed by the library user
7
- when implementing a custom mapping module (advanced use).
8
- - Dto, Relation: Base classes that can be used for type annotations.
9
- - IGNORE: A special constant that can be used as a value in the PropertyValueConstraint.
10
- """
11
-
12
- from importlib.metadata import version
13
-
14
- from OpenApiDriver.openapidriver import OpenApiDriver
15
- from OpenApiLibCore.dto_base import (
16
- Dto,
17
- IdDependency,
18
- IdReference,
19
- PathPropertiesConstraint,
20
- PropertyValueConstraint,
21
- Relation,
22
- UniquePropertyValueConstraint,
23
- )
24
- from OpenApiLibCore.value_utils import IGNORE
25
-
26
- try:
27
- __version__ = version("robotframework-openapidriver")
28
- except Exception: # pragma: no cover
29
- pass
30
-
31
- __all__ = [
32
- "Dto",
33
- "IdDependency",
34
- "IdReference",
35
- "PathPropertiesConstraint",
36
- "PropertyValueConstraint",
37
- "Relation",
38
- "UniquePropertyValueConstraint",
39
- "IGNORE",
40
- "OpenApiDriver",
41
- ]
1
+ # pylint: disable=invalid-name
2
+ """
3
+ The OpenApiDriver package is intended to be used as a Robot Framework library.
4
+ The following classes and constants are exposed to be used by the library user:
5
+ - OpenApiDriver: The class to be used as a Library in the *** Settings *** section
6
+ - IdDependency, IdReference, PathPropertiesConstraint, PropertyValueConstraint,
7
+ UniquePropertyValueConstraint: Classes to be subclassed by the library user
8
+ when implementing a custom mapping module (advanced use).
9
+ - Dto, Relation: Base classes that can be used for type annotations.
10
+ - IGNORE: A special constant that can be used as a value in the PropertyValueConstraint.
11
+ """
12
+
13
+ from importlib.metadata import version
14
+
15
+ from OpenApiDriver.openapidriver import OpenApiDriver
16
+ from OpenApiLibCore.dto_base import (
17
+ Dto,
18
+ IdDependency,
19
+ IdReference,
20
+ PathPropertiesConstraint,
21
+ PropertyValueConstraint,
22
+ ResourceRelation,
23
+ UniquePropertyValueConstraint,
24
+ )
25
+ from OpenApiLibCore.validation import ValidationLevel
26
+ from OpenApiLibCore.value_utils import IGNORE
27
+
28
+ try:
29
+ __version__ = version("robotframework-openapidriver")
30
+ except Exception: # pragma: no cover pylint: disable=broad-exception-caught
31
+ pass
32
+
33
+ __all__ = [
34
+ "IGNORE",
35
+ "Dto",
36
+ "IdDependency",
37
+ "IdReference",
38
+ "OpenApiDriver",
39
+ "PathPropertiesConstraint",
40
+ "PropertyValueConstraint",
41
+ "ResourceRelation",
42
+ "UniquePropertyValueConstraint",
43
+ "ValidationLevel",
44
+ ]
@@ -1,52 +1,53 @@
1
1
  """Module containing the classes to perform automatic OpenAPI contract validation."""
2
2
 
3
- from logging import getLogger
3
+ from collections.abc import Mapping, MutableMapping
4
+ from http import HTTPStatus
4
5
  from pathlib import Path
5
6
  from random import choice
6
- from typing import Any, Dict, List, Optional, Tuple, Union
7
+ from types import MappingProxyType
7
8
 
8
9
  from requests import Response
9
10
  from requests.auth import AuthBase
10
11
  from requests.cookies import RequestsCookieJar as CookieJar
11
- from robot.api import SkipExecution
12
+ from robot.api import logger
12
13
  from robot.api.deco import keyword, library
14
+ from robot.api.exceptions import SkipExecution
13
15
  from robot.libraries.BuiltIn import BuiltIn
14
16
 
15
17
  from OpenApiLibCore import OpenApiLibCore, RequestData, RequestValues, ValidationLevel
18
+ from OpenApiLibCore.annotations import JSON
16
19
 
17
20
  run_keyword = BuiltIn().run_keyword
18
-
19
-
20
- logger = getLogger(__name__)
21
+ default_str_mapping: Mapping[str, str] = MappingProxyType({})
21
22
 
22
23
 
23
24
  @library(scope="SUITE", doc_format="ROBOT")
24
- class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-attributes
25
+ class OpenApiExecutors(OpenApiLibCore):
25
26
  """Main class providing the keywords and core logic to perform endpoint validations."""
26
27
 
27
- def __init__( # pylint: disable=too-many-arguments
28
+ def __init__( # noqa: PLR0913, pylint: disable=dangerous-default-value
28
29
  self,
29
30
  source: str,
30
31
  origin: str = "",
31
32
  base_path: str = "",
32
33
  response_validation: ValidationLevel = ValidationLevel.WARN,
33
34
  disable_server_validation: bool = True,
34
- mappings_path: Union[str, Path] = "",
35
+ mappings_path: str | Path = "",
35
36
  invalid_property_default_response: int = 422,
36
37
  default_id_property_name: str = "id",
37
- faker_locale: Optional[Union[str, List[str]]] = None,
38
+ faker_locale: str | list[str] = "",
38
39
  require_body_for_invalid_url: bool = False,
39
40
  recursion_limit: int = 1,
40
- recursion_default: Any = {},
41
+ recursion_default: JSON = {},
41
42
  username: str = "",
42
43
  password: str = "",
43
44
  security_token: str = "",
44
- auth: Optional[AuthBase] = None,
45
- cert: Optional[Union[str, Tuple[str, str]]] = None,
46
- verify_tls: Optional[Union[bool, str]] = True,
47
- extra_headers: Optional[Dict[str, str]] = None,
48
- cookies: Optional[Union[Dict[str, str], CookieJar]] = None,
49
- proxies: Optional[Dict[str, str]] = None,
45
+ auth: AuthBase | None = None,
46
+ cert: str | tuple[str, str] = "",
47
+ verify_tls: bool | str = True,
48
+ extra_headers: Mapping[str, str] = default_str_mapping,
49
+ cookies: MutableMapping[str, str] | CookieJar | None = None,
50
+ proxies: MutableMapping[str, str] | None = None,
50
51
  ) -> None:
51
52
  super().__init__(
52
53
  source=source,
@@ -84,13 +85,13 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
84
85
  > Note: No headers or (json) body are send with the request. For security
85
86
  reasons, the authorization validation should be checked first.
86
87
  """
87
- url: str = run_keyword("get_valid_url", path, method)
88
+ url: str = run_keyword("get_valid_url", path)
88
89
  response = self.session.request(
89
90
  method=method,
90
91
  url=url,
91
92
  verify=False,
92
93
  )
93
- if response.status_code != 401:
94
+ if response.status_code != int(HTTPStatus.UNAUTHORIZED):
94
95
  raise AssertionError(f"Response {response.status_code} was not 401.")
95
96
 
96
97
  @keyword
@@ -105,9 +106,9 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
105
106
  > Note: No headers or (json) body are send with the request. For security
106
107
  reasons, the access rights validation should be checked first.
107
108
  """
108
- url: str = run_keyword("get_valid_url", path, method)
109
+ url: str = run_keyword("get_valid_url", path)
109
110
  response: Response = run_keyword("authorized_request", url, method)
110
- if response.status_code != 403:
111
+ if response.status_code != int(HTTPStatus.FORBIDDEN):
111
112
  raise AssertionError(f"Response {response.status_code} was not 403.")
112
113
 
113
114
  @keyword
@@ -130,11 +131,11 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
130
131
  parameters are send with the request. The `require_body_for_invalid_url`
131
132
  parameter can be set to `True` if needed.
132
133
  """
133
- valid_url: str = run_keyword("get_valid_url", path, method)
134
+ valid_url: str = run_keyword("get_valid_url", path)
134
135
 
135
136
  if not (
136
137
  url := run_keyword(
137
- "get_invalidated_url", valid_url, path, method, expected_status_code
138
+ "get_invalidated_url", valid_url, path, expected_status_code
138
139
  )
139
140
  ):
140
141
  raise SkipExecution(
@@ -144,7 +145,7 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
144
145
 
145
146
  params, headers, json_data = None, None, None
146
147
  if self.require_body_for_invalid_url:
147
- request_data = self.get_request_data(method=method, endpoint=path)
148
+ request_data: RequestData = run_keyword("get_request_data", path, method)
148
149
  params = request_data.params
149
150
  headers = request_data.headers
150
151
  dto = request_data.dto
@@ -169,11 +170,11 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
169
170
  The keyword calls other keywords to generate the neccesary data to perform
170
171
  the desired operation and validate the response against the openapi document.
171
172
  """
172
- json_data: Optional[Dict[str, Any]] = None
173
- original_data = None
173
+ json_data: dict[str, JSON] = {}
174
+ original_data = {}
174
175
 
175
- url: str = run_keyword("get_valid_url", path, method)
176
- request_data: RequestData = self.get_request_data(method=method, endpoint=path)
176
+ url: str = run_keyword("get_valid_url", path)
177
+ request_data: RequestData = run_keyword("get_request_data", path, method)
177
178
  params = request_data.params
178
179
  headers = request_data.headers
179
180
  if request_data.has_body:
@@ -182,7 +183,7 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
182
183
  if method == "PATCH":
183
184
  original_data = self.get_original_data(url=url)
184
185
  # in case of a status code indicating an error, ensure the error occurs
185
- if status_code >= 400:
186
+ if status_code >= int(HTTPStatus.BAD_REQUEST):
186
187
  invalidation_keyword_data = {
187
188
  "get_invalid_json_data": [
188
189
  "get_invalid_json_data",
@@ -253,20 +254,20 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
253
254
  ),
254
255
  original_data,
255
256
  )
256
- if status_code < 300 and (
257
+ if status_code < int(HTTPStatus.MULTIPLE_CHOICES) and (
257
258
  request_data.has_optional_properties
258
259
  or request_data.has_optional_params
259
260
  or request_data.has_optional_headers
260
261
  ):
261
262
  logger.info("Performing request without optional properties and parameters")
262
- url = run_keyword("get_valid_url", path, method)
263
- request_data = self.get_request_data(method=method, endpoint=path)
263
+ url = run_keyword("get_valid_url", path)
264
+ request_data = run_keyword("get_request_data", path, method)
264
265
  params = request_data.get_required_params()
265
266
  headers = request_data.get_required_headers()
266
267
  json_data = (
267
- request_data.get_minimal_body_dict() if request_data.has_body else None
268
+ request_data.get_minimal_body_dict() if request_data.has_body else {}
268
269
  )
269
- original_data = None
270
+ original_data = {}
270
271
  if method == "PATCH":
271
272
  original_data = self.get_original_data(url=url)
272
273
  run_keyword(
@@ -283,15 +284,15 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
283
284
  original_data,
284
285
  )
285
286
 
286
- def get_original_data(self, url: str) -> Optional[Dict[str, Any]]:
287
+ def get_original_data(self, url: str) -> dict[str, JSON]:
287
288
  """
288
289
  Attempt to GET the current data for the given url and return it.
289
290
 
290
- If the GET request fails, None is returned.
291
+ If the GET request fails, an empty dict is returned.
291
292
  """
292
- original_data = None
293
- path = self.get_parameterized_endpoint_from_url(url)
294
- get_request_data = self.get_request_data(endpoint=path, method="GET")
293
+ original_data = {}
294
+ path = self.get_parameterized_path_from_url(url)
295
+ get_request_data: RequestData = run_keyword("get_request_data", path, "GET")
295
296
  get_params = get_request_data.params
296
297
  get_headers = get_request_data.headers
297
298
  response: Response = run_keyword(
@@ -1,116 +1,115 @@
1
- """Module holding the OpenApiReader reader_class implementation."""
2
-
3
- from typing import Any, Dict, List, Union
4
-
5
- from DataDriver.AbstractReaderClass import AbstractReaderClass
6
- from DataDriver.ReaderConfig import TestCaseData
7
-
8
-
9
- # pylint: disable=too-few-public-methods
10
- class Test:
11
- """
12
- Helper class to support ignoring endpoint responses when generating the test cases.
13
- """
14
-
15
- def __init__(self, path: str, method: str, response: Union[str, int]):
16
- self.path = path
17
- self.method = method.lower()
18
- self.response = str(response)
19
-
20
- def __eq__(self, other: Any) -> bool:
21
- if not isinstance(other, type(self)):
22
- return False
23
- return (
24
- self.path == other.path
25
- and self.method == other.method
26
- and self.response == other.response
27
- )
28
-
29
-
30
- class OpenApiReader(AbstractReaderClass):
31
- """Implementation of the reader_class used by DataDriver."""
32
-
33
- def get_data_from_source(self) -> List[TestCaseData]:
34
- test_data: List[TestCaseData] = []
35
-
36
- read_paths_method = getattr(self, "read_paths_method")
37
- paths: Dict[str, Any] = read_paths_method()
38
- self._filter_paths(paths)
39
-
40
- ignored_responses_ = [
41
- str(response) for response in getattr(self, "ignored_responses", [])
42
- ]
43
-
44
- ignored_tests = [Test(*test) for test in getattr(self, "ignored_testcases", [])]
45
-
46
- for path, path_item in paths.items():
47
- # by reseversing the items, post/put operations come before get and delete
48
- for item_name, item_data in reversed(path_item.items()):
49
- # this level of the OAS also contains data that's not related to a
50
- # path operation
51
- if item_name not in ["get", "put", "post", "delete", "patch"]:
52
- continue
53
- method, method_data = item_name, item_data
54
- tags_from_spec = method_data.get("tags", [])
55
- for response in method_data.get("responses"):
56
- # 'default' applies to all status codes that are not specified, in
57
- # which case we don't know what to expect and therefore can't verify
58
- if (
59
- response == "default"
60
- or response in ignored_responses_
61
- or Test(path, method, response) in ignored_tests
62
- ):
63
- continue
64
-
65
- tag_list = _get_tag_list(
66
- tags=tags_from_spec, method=method, response=response
67
- )
68
- test_data.append(
69
- TestCaseData(
70
- arguments={
71
- "${path}": path,
72
- "${method}": method.upper(),
73
- "${status_code}": response,
74
- },
75
- tags=tag_list,
76
- ),
77
- )
78
- return test_data
79
-
80
- def _filter_paths(self, paths: Dict[str, Any]) -> None:
81
- def matches_include_pattern(path: str) -> bool:
82
- for included_path in included_paths:
83
- if path == included_path:
84
- return True
85
- if included_path.endswith("*"):
86
- wildcard_include, _, _ = included_path.partition("*")
87
- if path.startswith(wildcard_include):
88
- return True
89
- return False
90
-
91
- def matches_ignore_pattern(path: str) -> bool:
92
- for ignored_path in ignored_paths:
93
- if path == ignored_path:
94
- return True
95
-
96
- if ignored_path.endswith("*"):
97
- wildcard_ignore, _, _ = ignored_path.partition("*")
98
- if path.startswith(wildcard_ignore):
99
- return True
100
- return False
101
-
102
- if included_paths := getattr(self, "included_paths", ()):
103
- path_list = list(paths.keys())
104
- for path in path_list:
105
- if not matches_include_pattern(path):
106
- paths.pop(path)
107
-
108
- if ignored_paths := getattr(self, "ignored_paths", ()):
109
- path_list = list(paths.keys())
110
- for path in path_list:
111
- if matches_ignore_pattern(path):
112
- paths.pop(path)
113
-
114
-
115
- def _get_tag_list(tags: List[str], method: str, response: str) -> List[str]:
116
- return [*tags, f"Method: {method.upper()}", f"Response: {response}"]
1
+ """Module holding the OpenApiReader reader_class implementation."""
2
+
3
+ from typing import Any
4
+
5
+ from DataDriver.AbstractReaderClass import AbstractReaderClass
6
+ from DataDriver.ReaderConfig import TestCaseData
7
+
8
+
9
+ class Test:
10
+ """
11
+ Helper class to support ignoring endpoint responses when generating the test cases.
12
+ """
13
+
14
+ def __init__(self, path: str, method: str, response: str | int) -> None:
15
+ self.path = path
16
+ self.method = method.lower()
17
+ self.response = str(response)
18
+
19
+ def __eq__(self, other: Any) -> bool:
20
+ if not isinstance(other, type(self)):
21
+ return False
22
+ return (
23
+ self.path == other.path
24
+ and self.method == other.method
25
+ and self.response == other.response
26
+ )
27
+
28
+
29
+ class OpenApiReader(AbstractReaderClass):
30
+ """Implementation of the reader_class used by DataDriver."""
31
+
32
+ def get_data_from_source(self) -> list[TestCaseData]:
33
+ test_data: list[TestCaseData] = []
34
+
35
+ read_paths_method = getattr(self, "read_paths_method")
36
+ paths: dict[str, Any] = read_paths_method()
37
+ self._filter_paths(paths)
38
+
39
+ ignored_responses_ = [
40
+ str(response) for response in getattr(self, "ignored_responses", [])
41
+ ]
42
+
43
+ ignored_tests = [Test(*test) for test in getattr(self, "ignored_testcases", [])]
44
+
45
+ for path, path_item in paths.items():
46
+ # by reseversing the items, post/put operations come before get and delete
47
+ for item_name, item_data in reversed(path_item.items()):
48
+ # this level of the OAS also contains data that's not related to a
49
+ # path operation
50
+ if item_name not in ["get", "put", "post", "delete", "patch"]:
51
+ continue
52
+ method, method_data = item_name, item_data
53
+ tags_from_spec = method_data.get("tags", [])
54
+ for response in method_data.get("responses"):
55
+ # 'default' applies to all status codes that are not specified, in
56
+ # which case we don't know what to expect and therefore can't verify
57
+ if (
58
+ response == "default"
59
+ or response in ignored_responses_
60
+ or Test(path, method, response) in ignored_tests
61
+ ):
62
+ continue
63
+
64
+ tag_list = _get_tag_list(
65
+ tags=tags_from_spec, method=method, response=response
66
+ )
67
+ test_data.append(
68
+ TestCaseData(
69
+ arguments={
70
+ "${path}": path,
71
+ "${method}": method.upper(),
72
+ "${status_code}": response,
73
+ },
74
+ tags=tag_list,
75
+ ),
76
+ )
77
+ return test_data
78
+
79
+ def _filter_paths(self, paths: dict[str, Any]) -> None:
80
+ def matches_include_pattern(path: str) -> bool:
81
+ for included_path in included_paths:
82
+ if path == included_path:
83
+ return True
84
+ if included_path.endswith("*"):
85
+ wildcard_include, _, _ = included_path.partition("*")
86
+ if path.startswith(wildcard_include):
87
+ return True
88
+ return False
89
+
90
+ def matches_ignore_pattern(path: str) -> bool:
91
+ for ignored_path in ignored_paths:
92
+ if path == ignored_path:
93
+ return True
94
+
95
+ if ignored_path.endswith("*"):
96
+ wildcard_ignore, _, _ = ignored_path.partition("*")
97
+ if path.startswith(wildcard_ignore):
98
+ return True
99
+ return False
100
+
101
+ if included_paths := getattr(self, "included_paths", ()):
102
+ path_list = list(paths.keys())
103
+ for path in path_list:
104
+ if not matches_include_pattern(path):
105
+ paths.pop(path)
106
+
107
+ if ignored_paths := getattr(self, "ignored_paths", ()):
108
+ path_list = list(paths.keys())
109
+ for path in path_list:
110
+ if matches_ignore_pattern(path):
111
+ paths.pop(path)
112
+
113
+
114
+ def _get_tag_list(tags: list[str], method: str, response: str) -> list[str]:
115
+ return [*tags, f"Method: {method.upper()}", f"Response: {response}"]