robotframework-openapitools 0.4.0__py3-none-any.whl → 1.0.0__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 (63) hide show
  1. OpenApiDriver/__init__.py +45 -41
  2. OpenApiDriver/openapi_executors.py +78 -49
  3. OpenApiDriver/openapi_reader.py +114 -116
  4. OpenApiDriver/openapidriver.libspec +209 -133
  5. OpenApiDriver/openapidriver.py +31 -296
  6. OpenApiLibCore/__init__.py +39 -13
  7. OpenApiLibCore/annotations.py +10 -0
  8. OpenApiLibCore/data_generation/__init__.py +10 -0
  9. OpenApiLibCore/data_generation/body_data_generation.py +250 -0
  10. OpenApiLibCore/data_generation/data_generation_core.py +233 -0
  11. OpenApiLibCore/data_invalidation.py +294 -0
  12. OpenApiLibCore/dto_base.py +67 -130
  13. OpenApiLibCore/dto_utils.py +125 -85
  14. OpenApiLibCore/localized_faker.py +88 -0
  15. OpenApiLibCore/models.py +723 -0
  16. OpenApiLibCore/oas_cache.py +14 -13
  17. OpenApiLibCore/openapi_libcore.libspec +355 -330
  18. OpenApiLibCore/openapi_libcore.py +385 -1953
  19. OpenApiLibCore/parameter_utils.py +97 -0
  20. OpenApiLibCore/path_functions.py +215 -0
  21. OpenApiLibCore/path_invalidation.py +42 -0
  22. OpenApiLibCore/protocols.py +38 -0
  23. OpenApiLibCore/request_data.py +246 -0
  24. OpenApiLibCore/resource_relations.py +55 -0
  25. OpenApiLibCore/validation.py +380 -0
  26. OpenApiLibCore/value_utils.py +216 -481
  27. openapi_libgen/__init__.py +3 -0
  28. openapi_libgen/command_line.py +75 -0
  29. openapi_libgen/generator.py +82 -0
  30. openapi_libgen/parsing_utils.py +30 -0
  31. openapi_libgen/spec_parser.py +154 -0
  32. openapi_libgen/templates/__init__.jinja +3 -0
  33. openapi_libgen/templates/library.jinja +30 -0
  34. robotframework_openapitools-1.0.0.dist-info/METADATA +249 -0
  35. robotframework_openapitools-1.0.0.dist-info/RECORD +40 -0
  36. {robotframework_openapitools-0.4.0.dist-info → robotframework_openapitools-1.0.0.dist-info}/WHEEL +1 -1
  37. robotframework_openapitools-1.0.0.dist-info/entry_points.txt +3 -0
  38. roboswag/__init__.py +0 -9
  39. roboswag/__main__.py +0 -3
  40. roboswag/auth.py +0 -44
  41. roboswag/cli.py +0 -80
  42. roboswag/core.py +0 -85
  43. roboswag/generate/__init__.py +0 -1
  44. roboswag/generate/generate.py +0 -121
  45. roboswag/generate/models/__init__.py +0 -0
  46. roboswag/generate/models/api.py +0 -219
  47. roboswag/generate/models/definition.py +0 -28
  48. roboswag/generate/models/endpoint.py +0 -68
  49. roboswag/generate/models/parameter.py +0 -25
  50. roboswag/generate/models/response.py +0 -8
  51. roboswag/generate/models/tag.py +0 -16
  52. roboswag/generate/models/utils.py +0 -60
  53. roboswag/generate/templates/api_init.jinja +0 -15
  54. roboswag/generate/templates/models.jinja +0 -7
  55. roboswag/generate/templates/paths.jinja +0 -68
  56. roboswag/logger.py +0 -33
  57. roboswag/validate/__init__.py +0 -6
  58. roboswag/validate/core.py +0 -3
  59. roboswag/validate/schema.py +0 -21
  60. roboswag/validate/text_response.py +0 -14
  61. robotframework_openapitools-0.4.0.dist-info/METADATA +0 -42
  62. robotframework_openapitools-0.4.0.dist-info/RECORD +0 -41
  63. {robotframework_openapitools-0.4.0.dist-info → robotframework_openapitools-1.0.0.dist-info}/LICENSE +0 -0
OpenApiDriver/__init__.py CHANGED
@@ -1,41 +1,45 @@
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
+
34
+ __all__ = [
35
+ "IGNORE",
36
+ "Dto",
37
+ "IdDependency",
38
+ "IdReference",
39
+ "OpenApiDriver",
40
+ "PathPropertiesConstraint",
41
+ "PropertyValueConstraint",
42
+ "ResourceRelation",
43
+ "UniquePropertyValueConstraint",
44
+ "ValidationLevel",
45
+ ]
@@ -1,52 +1,70 @@
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
5
+ from os import getenv
4
6
  from pathlib import Path
5
7
  from random import choice
6
- from typing import Any, Dict, List, Optional, Tuple, Union
8
+ from types import MappingProxyType
7
9
 
8
10
  from requests import Response
9
11
  from requests.auth import AuthBase
10
12
  from requests.cookies import RequestsCookieJar as CookieJar
11
- from robot.api import SkipExecution
13
+ from robot.api import logger
12
14
  from robot.api.deco import keyword, library
15
+ from robot.api.exceptions import SkipExecution
13
16
  from robot.libraries.BuiltIn import BuiltIn
14
17
 
15
- from OpenApiLibCore import OpenApiLibCore, RequestData, RequestValues, ValidationLevel
18
+ from OpenApiLibCore import (
19
+ KEYWORD_NAMES as LIBCORE_KEYWORD_NAMES,
20
+ )
21
+ from OpenApiLibCore import (
22
+ OpenApiLibCore,
23
+ RequestData,
24
+ RequestValues,
25
+ ValidationLevel,
26
+ )
27
+ from OpenApiLibCore.annotations import JSON
16
28
 
17
29
  run_keyword = BuiltIn().run_keyword
30
+ default_str_mapping: Mapping[str, str] = MappingProxyType({})
18
31
 
19
32
 
20
- logger = getLogger(__name__)
33
+ KEYWORD_NAMES = [
34
+ "test_unauthorized",
35
+ "test_forbidden",
36
+ "test_invalid_url",
37
+ "test_endpoint",
38
+ ]
21
39
 
22
40
 
23
41
  @library(scope="SUITE", doc_format="ROBOT")
24
- class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-attributes
42
+ class OpenApiExecutors(OpenApiLibCore):
25
43
  """Main class providing the keywords and core logic to perform endpoint validations."""
26
44
 
27
- def __init__( # pylint: disable=too-many-arguments
45
+ def __init__( # noqa: PLR0913, pylint: disable=dangerous-default-value
28
46
  self,
29
47
  source: str,
30
48
  origin: str = "",
31
49
  base_path: str = "",
32
50
  response_validation: ValidationLevel = ValidationLevel.WARN,
33
51
  disable_server_validation: bool = True,
34
- mappings_path: Union[str, Path] = "",
52
+ mappings_path: str | Path = "",
35
53
  invalid_property_default_response: int = 422,
36
54
  default_id_property_name: str = "id",
37
- faker_locale: Optional[Union[str, List[str]]] = None,
55
+ faker_locale: str | list[str] = "",
38
56
  require_body_for_invalid_url: bool = False,
39
57
  recursion_limit: int = 1,
40
- recursion_default: Any = {},
58
+ recursion_default: JSON = {},
41
59
  username: str = "",
42
60
  password: str = "",
43
61
  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,
62
+ auth: AuthBase | None = None,
63
+ cert: str | tuple[str, str] = "",
64
+ verify_tls: bool | str = True,
65
+ extra_headers: Mapping[str, str] = default_str_mapping,
66
+ cookies: MutableMapping[str, str] | CookieJar | None = None,
67
+ proxies: MutableMapping[str, str] | None = None,
50
68
  ) -> None:
51
69
  super().__init__(
52
70
  source=source,
@@ -84,13 +102,13 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
84
102
  > Note: No headers or (json) body are send with the request. For security
85
103
  reasons, the authorization validation should be checked first.
86
104
  """
87
- url: str = run_keyword("get_valid_url", path, method)
105
+ url: str = run_keyword("get_valid_url", path)
88
106
  response = self.session.request(
89
107
  method=method,
90
108
  url=url,
91
109
  verify=False,
92
110
  )
93
- if response.status_code != 401:
111
+ if response.status_code != int(HTTPStatus.UNAUTHORIZED):
94
112
  raise AssertionError(f"Response {response.status_code} was not 401.")
95
113
 
96
114
  @keyword
@@ -105,9 +123,9 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
105
123
  > Note: No headers or (json) body are send with the request. For security
106
124
  reasons, the access rights validation should be checked first.
107
125
  """
108
- url: str = run_keyword("get_valid_url", path, method)
126
+ url: str = run_keyword("get_valid_url", path)
109
127
  response: Response = run_keyword("authorized_request", url, method)
110
- if response.status_code != 403:
128
+ if response.status_code != int(HTTPStatus.FORBIDDEN):
111
129
  raise AssertionError(f"Response {response.status_code} was not 403.")
112
130
 
113
131
  @keyword
@@ -130,13 +148,17 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
130
148
  parameters are send with the request. The `require_body_for_invalid_url`
131
149
  parameter can be set to `True` if needed.
132
150
  """
133
- valid_url: str = run_keyword("get_valid_url", path, method)
151
+ valid_url: str = run_keyword("get_valid_url", path)
134
152
 
135
- if not (
136
- url := run_keyword(
137
- "get_invalidated_url", valid_url, path, method, expected_status_code
153
+ try:
154
+ url = run_keyword(
155
+ "get_invalidated_url", valid_url, path, expected_status_code
138
156
  )
139
- ):
157
+ except Exception as exception:
158
+ message = getattr(exception, "message", "")
159
+ if not message.startswith("ValueError"):
160
+ raise exception # pragma: no cover
161
+
140
162
  raise SkipExecution(
141
163
  f"Path {path} does not contain resource references that "
142
164
  f"can be invalidated."
@@ -144,7 +166,7 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
144
166
 
145
167
  params, headers, json_data = None, None, None
146
168
  if self.require_body_for_invalid_url:
147
- request_data = self.get_request_data(method=method, endpoint=path)
169
+ request_data: RequestData = run_keyword("get_request_data", path, method)
148
170
  params = request_data.params
149
171
  headers = request_data.headers
150
172
  dto = request_data.dto
@@ -169,11 +191,11 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
169
191
  The keyword calls other keywords to generate the neccesary data to perform
170
192
  the desired operation and validate the response against the openapi document.
171
193
  """
172
- json_data: Optional[Dict[str, Any]] = None
173
- original_data = None
194
+ json_data: dict[str, JSON] = {}
195
+ original_data = {}
174
196
 
175
- url: str = run_keyword("get_valid_url", path, method)
176
- request_data: RequestData = self.get_request_data(method=method, endpoint=path)
197
+ url: str = run_keyword("get_valid_url", path)
198
+ request_data: RequestData = run_keyword("get_request_data", path, method)
177
199
  params = request_data.params
178
200
  headers = request_data.headers
179
201
  if request_data.has_body:
@@ -182,10 +204,10 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
182
204
  if method == "PATCH":
183
205
  original_data = self.get_original_data(url=url)
184
206
  # in case of a status code indicating an error, ensure the error occurs
185
- if status_code >= 400:
207
+ if status_code >= int(HTTPStatus.BAD_REQUEST):
186
208
  invalidation_keyword_data = {
187
- "get_invalid_json_data": [
188
- "get_invalid_json_data",
209
+ "get_invalid_body_data": [
210
+ "get_invalid_body_data",
189
211
  url,
190
212
  method,
191
213
  status_code,
@@ -200,13 +222,13 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
200
222
  invalidation_keywords = []
201
223
 
202
224
  if request_data.dto.get_body_relations_for_error_code(status_code):
203
- invalidation_keywords.append("get_invalid_json_data")
225
+ invalidation_keywords.append("get_invalid_body_data")
204
226
  if request_data.dto.get_parameter_relations_for_error_code(status_code):
205
227
  invalidation_keywords.append("get_invalidated_parameters")
206
228
  if invalidation_keywords:
207
229
  if (
208
230
  invalidation_keyword := choice(invalidation_keywords)
209
- ) == "get_invalid_json_data":
231
+ ) == "get_invalid_body_data":
210
232
  json_data = run_keyword(
211
233
  *invalidation_keyword_data[invalidation_keyword]
212
234
  )
@@ -224,13 +246,13 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
224
246
  params, headers = run_keyword(
225
247
  *invalidation_keyword_data["get_invalidated_parameters"]
226
248
  )
227
- if request_data.dto_schema:
249
+ if request_data.body_schema:
228
250
  json_data = run_keyword(
229
- *invalidation_keyword_data["get_invalid_json_data"]
251
+ *invalidation_keyword_data["get_invalid_body_data"]
230
252
  )
231
- elif request_data.dto_schema:
253
+ elif request_data.body_schema:
232
254
  json_data = run_keyword(
233
- *invalidation_keyword_data["get_invalid_json_data"]
255
+ *invalidation_keyword_data["get_invalid_body_data"]
234
256
  )
235
257
  else:
236
258
  raise SkipExecution(
@@ -253,20 +275,20 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
253
275
  ),
254
276
  original_data,
255
277
  )
256
- if status_code < 300 and (
278
+ if status_code < int(HTTPStatus.MULTIPLE_CHOICES) and (
257
279
  request_data.has_optional_properties
258
280
  or request_data.has_optional_params
259
281
  or request_data.has_optional_headers
260
282
  ):
261
283
  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)
284
+ url = run_keyword("get_valid_url", path)
285
+ request_data = run_keyword("get_request_data", path, method)
264
286
  params = request_data.get_required_params()
265
287
  headers = request_data.get_required_headers()
266
288
  json_data = (
267
- request_data.get_minimal_body_dict() if request_data.has_body else None
289
+ request_data.get_minimal_body_dict() if request_data.has_body else {}
268
290
  )
269
- original_data = None
291
+ original_data = {}
270
292
  if method == "PATCH":
271
293
  original_data = self.get_original_data(url=url)
272
294
  run_keyword(
@@ -283,15 +305,15 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
283
305
  original_data,
284
306
  )
285
307
 
286
- def get_original_data(self, url: str) -> Optional[Dict[str, Any]]:
308
+ def get_original_data(self, url: str) -> dict[str, JSON]:
287
309
  """
288
310
  Attempt to GET the current data for the given url and return it.
289
311
 
290
- If the GET request fails, None is returned.
312
+ If the GET request fails, an empty dict is returned.
291
313
  """
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")
314
+ original_data = {}
315
+ path = self.get_parameterized_path_from_url(url)
316
+ get_request_data: RequestData = run_keyword("get_request_data", path, "GET")
295
317
  get_params = get_request_data.params
296
318
  get_headers = get_request_data.headers
297
319
  response: Response = run_keyword(
@@ -300,3 +322,10 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
300
322
  if response.ok:
301
323
  original_data = response.json()
302
324
  return original_data
325
+
326
+ @staticmethod
327
+ def get_keyword_names() -> list[str]:
328
+ """Curated keywords for libdoc and libspec."""
329
+ if getenv("HIDE_INHERITED_KEYWORDS") == "true":
330
+ return KEYWORD_NAMES
331
+ return KEYWORD_NAMES + LIBCORE_KEYWORD_NAMES
@@ -1,116 +1,114 @@
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 Sequence
4
+
5
+ from DataDriver.AbstractReaderClass import AbstractReaderClass
6
+ from DataDriver.ReaderConfig import TestCaseData
7
+
8
+ from OpenApiLibCore.models import PathItemObject
9
+
10
+
11
+ class Test:
12
+ """
13
+ Helper class to support ignoring endpoint responses when generating the test cases.
14
+ """
15
+
16
+ def __init__(self, path: str, method: str, response: str | int) -> None:
17
+ self.path = path
18
+ self.method = method.lower()
19
+ self.response = str(response)
20
+
21
+ def __eq__(self, other: object) -> bool:
22
+ if not isinstance(other, type(self)):
23
+ return False
24
+ return (
25
+ self.path == other.path
26
+ and self.method == other.method
27
+ and self.response == other.response
28
+ )
29
+
30
+
31
+ class OpenApiReader(AbstractReaderClass):
32
+ """Implementation of the reader_class used by DataDriver."""
33
+
34
+ def get_data_from_source(self) -> list[TestCaseData]:
35
+ test_data: list[TestCaseData] = []
36
+
37
+ read_paths_method = getattr(self, "read_paths_method")
38
+ paths: dict[str, PathItemObject] = read_paths_method()
39
+ self._filter_paths(paths)
40
+
41
+ ignored_responses_ = [
42
+ str(response) for response in getattr(self, "ignored_responses", [])
43
+ ]
44
+
45
+ ignored_tests = [Test(*test) for test in getattr(self, "ignored_testcases", [])]
46
+
47
+ for path, path_item in paths.items():
48
+ path_operations = path_item.get_operations()
49
+
50
+ # by reseversing the items, post/put operations come before get and delete
51
+ for method, operation_data in reversed(path_operations.items()):
52
+ tags_from_spec = operation_data.tags
53
+ for response in operation_data.responses.keys():
54
+ # 'default' applies to all status codes that are not specified, in
55
+ # which case we don't know what to expect and therefore can't verify
56
+ if (
57
+ response == "default"
58
+ or response in ignored_responses_
59
+ or Test(path, method, response) in ignored_tests
60
+ ):
61
+ continue
62
+
63
+ tag_list = _get_tag_list(
64
+ tags=tags_from_spec, method=method, response=response
65
+ )
66
+ test_data.append(
67
+ TestCaseData(
68
+ arguments={
69
+ "${path}": path,
70
+ "${method}": method.upper(),
71
+ "${status_code}": response,
72
+ },
73
+ tags=tag_list,
74
+ ),
75
+ )
76
+ return test_data
77
+
78
+ def _filter_paths(self, paths: dict[str, PathItemObject]) -> None:
79
+ def matches_include_pattern(path: str) -> bool:
80
+ for included_path in included_paths:
81
+ if path == included_path:
82
+ return True
83
+ if included_path.endswith("*"):
84
+ wildcard_include, _, _ = included_path.partition("*")
85
+ if path.startswith(wildcard_include):
86
+ return True
87
+ return False
88
+
89
+ def matches_ignore_pattern(path: str) -> bool:
90
+ for ignored_path in ignored_paths:
91
+ if path == ignored_path:
92
+ return True
93
+
94
+ if ignored_path.endswith("*"):
95
+ wildcard_ignore, _, _ = ignored_path.partition("*")
96
+ if path.startswith(wildcard_ignore):
97
+ return True
98
+ return False
99
+
100
+ if included_paths := getattr(self, "included_paths", ()):
101
+ path_list = list(paths.keys())
102
+ for path in path_list:
103
+ if not matches_include_pattern(path):
104
+ paths.pop(path)
105
+
106
+ if ignored_paths := getattr(self, "ignored_paths", ()):
107
+ path_list = list(paths.keys())
108
+ for path in path_list:
109
+ if matches_ignore_pattern(path):
110
+ paths.pop(path)
111
+
112
+
113
+ def _get_tag_list(tags: Sequence[str], method: str, response: str) -> list[str]:
114
+ return [*tags, f"Method: {method.upper()}", f"Response: {response}"]