robotframework-openapitools 0.3.0__py3-none-any.whl → 1.0.0b1__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 +48 -42
  3. OpenApiDriver/openapi_reader.py +115 -116
  4. OpenApiDriver/openapidriver.libspec +72 -62
  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 +43 -40
  13. OpenApiLibCore/dto_utils.py +97 -85
  14. OpenApiLibCore/oas_cache.py +14 -13
  15. OpenApiLibCore/openapi_libcore.libspec +361 -188
  16. OpenApiLibCore/openapi_libcore.py +392 -1645
  17. OpenApiLibCore/parameter_utils.py +89 -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 +275 -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 +212 -0
  29. openapi_libgen/templates/__init__.jinja +3 -0
  30. openapi_libgen/templates/library.jinja +30 -0
  31. robotframework_openapitools-1.0.0b1.dist-info/METADATA +237 -0
  32. robotframework_openapitools-1.0.0b1.dist-info/RECORD +37 -0
  33. {robotframework_openapitools-0.3.0.dist-info → robotframework_openapitools-1.0.0b1.dist-info}/WHEEL +1 -1
  34. robotframework_openapitools-1.0.0b1.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.3.0.dist-info/METADATA +0 -41
  59. robotframework_openapitools-0.3.0.dist-info/RECORD +0 -41
  60. {robotframework_openapitools-0.3.0.dist-info → robotframework_openapitools-1.0.0b1.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
@@ -118,8 +119,9 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
118
119
  Perform a request for the provided 'path' and 'method' where the url for
119
120
  the `path` is invalidated.
120
121
 
121
- This keyword will be `SKIPPED` if the path contains no parts that
122
- can be invalidated.
122
+ This keyword will be `SKIPPED` if the path contains no parts
123
+ that can be invalidated and there is no mapping for a
124
+ PathPropertiesConstraint for the `expected_status_code`.
123
125
 
124
126
  The optional `expected_status_code` parameter (default: 404) can be set to the
125
127
  expected status code for APIs that do not return a 404 on invalid urls.
@@ -129,9 +131,13 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
129
131
  parameters are send with the request. The `require_body_for_invalid_url`
130
132
  parameter can be set to `True` if needed.
131
133
  """
132
- valid_url: str = run_keyword("get_valid_url", path, method)
134
+ valid_url: str = run_keyword("get_valid_url", path)
133
135
 
134
- if not (url := run_keyword("get_invalidated_url", valid_url)):
136
+ if not (
137
+ url := run_keyword(
138
+ "get_invalidated_url", valid_url, path, expected_status_code
139
+ )
140
+ ):
135
141
  raise SkipExecution(
136
142
  f"Path {path} does not contain resource references that "
137
143
  f"can be invalidated."
@@ -139,7 +145,7 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
139
145
 
140
146
  params, headers, json_data = None, None, None
141
147
  if self.require_body_for_invalid_url:
142
- request_data = self.get_request_data(method=method, endpoint=path)
148
+ request_data: RequestData = run_keyword("get_request_data", path, method)
143
149
  params = request_data.params
144
150
  headers = request_data.headers
145
151
  dto = request_data.dto
@@ -164,11 +170,11 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
164
170
  The keyword calls other keywords to generate the neccesary data to perform
165
171
  the desired operation and validate the response against the openapi document.
166
172
  """
167
- json_data: Optional[Dict[str, Any]] = None
168
- original_data = None
173
+ json_data: dict[str, JSON] = {}
174
+ original_data = {}
169
175
 
170
- url: str = run_keyword("get_valid_url", path, method)
171
- 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)
172
178
  params = request_data.params
173
179
  headers = request_data.headers
174
180
  if request_data.has_body:
@@ -177,7 +183,7 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
177
183
  if method == "PATCH":
178
184
  original_data = self.get_original_data(url=url)
179
185
  # in case of a status code indicating an error, ensure the error occurs
180
- if status_code >= 400:
186
+ if status_code >= int(HTTPStatus.BAD_REQUEST):
181
187
  invalidation_keyword_data = {
182
188
  "get_invalid_json_data": [
183
189
  "get_invalid_json_data",
@@ -194,7 +200,7 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
194
200
  }
195
201
  invalidation_keywords = []
196
202
 
197
- if request_data.dto.get_relations_for_error_code(status_code):
203
+ if request_data.dto.get_body_relations_for_error_code(status_code):
198
204
  invalidation_keywords.append("get_invalid_json_data")
199
205
  if request_data.dto.get_parameter_relations_for_error_code(status_code):
200
206
  invalidation_keywords.append("get_invalidated_parameters")
@@ -248,20 +254,20 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
248
254
  ),
249
255
  original_data,
250
256
  )
251
- if status_code < 300 and (
257
+ if status_code < int(HTTPStatus.MULTIPLE_CHOICES) and (
252
258
  request_data.has_optional_properties
253
259
  or request_data.has_optional_params
254
260
  or request_data.has_optional_headers
255
261
  ):
256
262
  logger.info("Performing request without optional properties and parameters")
257
- url = run_keyword("get_valid_url", path, method)
258
- 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)
259
265
  params = request_data.get_required_params()
260
266
  headers = request_data.get_required_headers()
261
267
  json_data = (
262
- 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 {}
263
269
  )
264
- original_data = None
270
+ original_data = {}
265
271
  if method == "PATCH":
266
272
  original_data = self.get_original_data(url=url)
267
273
  run_keyword(
@@ -278,15 +284,15 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
278
284
  original_data,
279
285
  )
280
286
 
281
- def get_original_data(self, url: str) -> Optional[Dict[str, Any]]:
287
+ def get_original_data(self, url: str) -> dict[str, JSON]:
282
288
  """
283
289
  Attempt to GET the current data for the given url and return it.
284
290
 
285
- If the GET request fails, None is returned.
291
+ If the GET request fails, an empty dict is returned.
286
292
  """
287
- original_data = None
288
- path = self.get_parameterized_endpoint_from_url(url)
289
- 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")
290
296
  get_params = get_request_data.params
291
297
  get_headers = get_request_data.headers
292
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}"]