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.
- OpenApiDriver/__init__.py +44 -41
- OpenApiDriver/openapi_executors.py +40 -39
- OpenApiDriver/openapi_reader.py +115 -116
- OpenApiDriver/openapidriver.libspec +71 -61
- OpenApiDriver/openapidriver.py +25 -19
- OpenApiLibCore/__init__.py +13 -11
- OpenApiLibCore/annotations.py +3 -0
- OpenApiLibCore/data_generation/__init__.py +12 -0
- OpenApiLibCore/data_generation/body_data_generation.py +269 -0
- OpenApiLibCore/data_generation/data_generation_core.py +240 -0
- OpenApiLibCore/data_invalidation.py +281 -0
- OpenApiLibCore/dto_base.py +29 -35
- OpenApiLibCore/dto_utils.py +97 -85
- OpenApiLibCore/oas_cache.py +14 -13
- OpenApiLibCore/openapi_libcore.libspec +346 -193
- OpenApiLibCore/openapi_libcore.py +389 -1702
- OpenApiLibCore/parameter_utils.py +91 -0
- OpenApiLibCore/path_functions.py +215 -0
- OpenApiLibCore/path_invalidation.py +44 -0
- OpenApiLibCore/protocols.py +30 -0
- OpenApiLibCore/request_data.py +281 -0
- OpenApiLibCore/resource_relations.py +54 -0
- OpenApiLibCore/validation.py +497 -0
- OpenApiLibCore/value_utils.py +528 -481
- openapi_libgen/__init__.py +46 -0
- openapi_libgen/command_line.py +87 -0
- openapi_libgen/parsing_utils.py +26 -0
- openapi_libgen/spec_parser.py +221 -0
- openapi_libgen/templates/__init__.jinja +3 -0
- openapi_libgen/templates/library.jinja +30 -0
- robotframework_openapitools-1.0.0b2.dist-info/METADATA +237 -0
- robotframework_openapitools-1.0.0b2.dist-info/RECORD +37 -0
- {robotframework_openapitools-0.4.0.dist-info → robotframework_openapitools-1.0.0b2.dist-info}/WHEEL +1 -1
- robotframework_openapitools-1.0.0b2.dist-info/entry_points.txt +3 -0
- roboswag/__init__.py +0 -9
- roboswag/__main__.py +0 -3
- roboswag/auth.py +0 -44
- roboswag/cli.py +0 -80
- roboswag/core.py +0 -85
- roboswag/generate/__init__.py +0 -1
- roboswag/generate/generate.py +0 -121
- roboswag/generate/models/__init__.py +0 -0
- roboswag/generate/models/api.py +0 -219
- roboswag/generate/models/definition.py +0 -28
- roboswag/generate/models/endpoint.py +0 -68
- roboswag/generate/models/parameter.py +0 -25
- roboswag/generate/models/response.py +0 -8
- roboswag/generate/models/tag.py +0 -16
- roboswag/generate/models/utils.py +0 -60
- roboswag/generate/templates/api_init.jinja +0 -15
- roboswag/generate/templates/models.jinja +0 -7
- roboswag/generate/templates/paths.jinja +0 -68
- roboswag/logger.py +0 -33
- roboswag/validate/__init__.py +0 -6
- roboswag/validate/core.py +0 -3
- roboswag/validate/schema.py +0 -21
- roboswag/validate/text_response.py +0 -14
- robotframework_openapitools-0.4.0.dist-info/METADATA +0 -42
- robotframework_openapitools-0.4.0.dist-info/RECORD +0 -41
- {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
|
-
|
3
|
-
The
|
4
|
-
|
5
|
-
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
from
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
"
|
35
|
-
"
|
36
|
-
"
|
37
|
-
"
|
38
|
-
"
|
39
|
-
"
|
40
|
-
"
|
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
|
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
|
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
|
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):
|
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=
|
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:
|
35
|
+
mappings_path: str | Path = "",
|
35
36
|
invalid_property_default_response: int = 422,
|
36
37
|
default_id_property_name: str = "id",
|
37
|
-
faker_locale:
|
38
|
+
faker_locale: str | list[str] = "",
|
38
39
|
require_body_for_invalid_url: bool = False,
|
39
40
|
recursion_limit: int = 1,
|
40
|
-
recursion_default:
|
41
|
+
recursion_default: JSON = {},
|
41
42
|
username: str = "",
|
42
43
|
password: str = "",
|
43
44
|
security_token: str = "",
|
44
|
-
auth:
|
45
|
-
cert:
|
46
|
-
verify_tls:
|
47
|
-
extra_headers:
|
48
|
-
cookies:
|
49
|
-
proxies:
|
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
|
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 !=
|
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
|
109
|
+
url: str = run_keyword("get_valid_url", path)
|
109
110
|
response: Response = run_keyword("authorized_request", url, method)
|
110
|
-
if response.status_code !=
|
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
|
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,
|
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 =
|
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:
|
173
|
-
original_data =
|
173
|
+
json_data: dict[str, JSON] = {}
|
174
|
+
original_data = {}
|
174
175
|
|
175
|
-
url: str = run_keyword("get_valid_url", path
|
176
|
-
request_data: RequestData =
|
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 >=
|
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 <
|
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
|
263
|
-
request_data =
|
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
|
268
|
+
request_data.get_minimal_body_dict() if request_data.has_body else {}
|
268
269
|
)
|
269
|
-
original_data =
|
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) ->
|
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,
|
291
|
+
If the GET request fails, an empty dict is returned.
|
291
292
|
"""
|
292
|
-
original_data =
|
293
|
-
path = self.
|
294
|
-
get_request_data =
|
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(
|
OpenApiDriver/openapi_reader.py
CHANGED
@@ -1,116 +1,115 @@
|
|
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
self.
|
17
|
-
self.
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
self.
|
25
|
-
and self.
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
paths
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
#
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
#
|
57
|
-
|
58
|
-
|
59
|
-
response
|
60
|
-
or response in
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
"${
|
72
|
-
"${
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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}"]
|