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
@@ -0,0 +1,281 @@
1
+ """
2
+ Module holding the functions related to invalidation of valid data (generated
3
+ to make 2xx requests) to support testing for 4xx responses.
4
+ """
5
+
6
+ from copy import deepcopy
7
+ from random import choice
8
+ from typing import Any
9
+
10
+ from requests import Response
11
+ from robot.api import logger
12
+ from robot.libraries.BuiltIn import BuiltIn
13
+
14
+ from OpenApiLibCore.dto_base import (
15
+ NOT_SET,
16
+ Dto,
17
+ IdReference,
18
+ PathPropertiesConstraint,
19
+ PropertyValueConstraint,
20
+ UniquePropertyValueConstraint,
21
+ resolve_schema,
22
+ )
23
+ from OpenApiLibCore.request_data import RequestData
24
+ from OpenApiLibCore.value_utils import IGNORE, get_invalid_value, get_valid_value
25
+
26
+ run_keyword = BuiltIn().run_keyword
27
+
28
+
29
+ def get_invalid_json_data(
30
+ url: str,
31
+ method: str,
32
+ status_code: int,
33
+ request_data: RequestData,
34
+ invalid_property_default_response: int,
35
+ ) -> dict[str, Any]:
36
+ method = method.lower()
37
+ data_relations = request_data.dto.get_relations_for_error_code(status_code)
38
+ data_relations = [
39
+ r for r in data_relations if not isinstance(r, PathPropertiesConstraint)
40
+ ]
41
+ if not data_relations:
42
+ if not request_data.dto_schema:
43
+ raise ValueError(
44
+ "Failed to invalidate: no data_relations and empty schema."
45
+ )
46
+ json_data = request_data.dto.get_invalidated_data(
47
+ schema=request_data.dto_schema,
48
+ status_code=status_code,
49
+ invalid_property_default_code=invalid_property_default_response,
50
+ )
51
+ return json_data
52
+ resource_relation = choice(data_relations)
53
+ if isinstance(resource_relation, UniquePropertyValueConstraint):
54
+ json_data = run_keyword(
55
+ "get_json_data_with_conflict",
56
+ url,
57
+ method,
58
+ request_data.dto,
59
+ status_code,
60
+ )
61
+ elif isinstance(resource_relation, IdReference):
62
+ run_keyword("ensure_in_use", url, resource_relation)
63
+ json_data = request_data.dto.as_dict()
64
+ else:
65
+ json_data = request_data.dto.get_invalidated_data(
66
+ schema=request_data.dto_schema,
67
+ status_code=status_code,
68
+ invalid_property_default_code=invalid_property_default_response,
69
+ )
70
+ return json_data
71
+
72
+
73
+ def get_invalidated_parameters(
74
+ status_code: int, request_data: RequestData, invalid_property_default_response: int
75
+ ) -> tuple[dict[str, Any], dict[str, str]]:
76
+ if not request_data.parameters:
77
+ raise ValueError("No params or headers to invalidate.")
78
+
79
+ # ensure the status_code can be triggered
80
+ relations = request_data.dto.get_parameter_relations_for_error_code(status_code)
81
+ relations_for_status_code = [
82
+ r
83
+ for r in relations
84
+ if isinstance(r, PropertyValueConstraint)
85
+ and (status_code in (r.error_code, r.invalid_value_error_code))
86
+ ]
87
+ parameters_to_ignore = {
88
+ r.property_name
89
+ for r in relations_for_status_code
90
+ if r.invalid_value_error_code == status_code and r.invalid_value == IGNORE
91
+ }
92
+ relation_property_names = {r.property_name for r in relations_for_status_code}
93
+ if not relation_property_names:
94
+ if status_code != invalid_property_default_response:
95
+ raise ValueError(f"No relations to cause status_code {status_code} found.")
96
+
97
+ # ensure we're not modifying mutable properties
98
+ params = deepcopy(request_data.params)
99
+ headers = deepcopy(request_data.headers)
100
+
101
+ if status_code == invalid_property_default_response:
102
+ # take the params and headers that can be invalidated based on data type
103
+ # and expand the set with properties that can be invalided by relations
104
+ parameter_names = set(request_data.params_that_can_be_invalidated).union(
105
+ request_data.headers_that_can_be_invalidated
106
+ )
107
+ parameter_names.update(relation_property_names)
108
+ if not parameter_names:
109
+ raise ValueError(
110
+ "None of the query parameters and headers can be invalidated."
111
+ )
112
+ else:
113
+ # non-default status_codes can only be the result of a Relation
114
+ parameter_names = relation_property_names
115
+
116
+ # Dto mappings may contain generic mappings for properties that are not present
117
+ # in this specific schema
118
+ request_data_parameter_names = [p.get("name") for p in request_data.parameters]
119
+ additional_relation_property_names = {
120
+ n for n in relation_property_names if n not in request_data_parameter_names
121
+ }
122
+ if additional_relation_property_names:
123
+ logger.warn(
124
+ f"get_parameter_relations_for_error_code yielded properties that are "
125
+ f"not defined in the schema: {additional_relation_property_names}\n"
126
+ f"These properties will be ignored for parameter invalidation."
127
+ )
128
+ parameter_names = parameter_names - additional_relation_property_names
129
+
130
+ if not parameter_names:
131
+ raise ValueError(
132
+ f"No parameter can be changed to cause status_code {status_code}."
133
+ )
134
+
135
+ parameter_names = parameter_names - parameters_to_ignore
136
+ parameter_to_invalidate = choice(tuple(parameter_names))
137
+
138
+ # check for invalid parameters in the provided request_data
139
+ try:
140
+ [parameter_data] = [
141
+ data
142
+ for data in request_data.parameters
143
+ if data["name"] == parameter_to_invalidate
144
+ ]
145
+ except Exception:
146
+ raise ValueError(
147
+ f"{parameter_to_invalidate} not found in provided parameters."
148
+ ) from None
149
+
150
+ # get the invalid_value for the chosen parameter
151
+ try:
152
+ [invalid_value_for_error_code] = [
153
+ r.invalid_value
154
+ for r in relations_for_status_code
155
+ if r.property_name == parameter_to_invalidate
156
+ and r.invalid_value_error_code == status_code
157
+ ]
158
+ except ValueError:
159
+ invalid_value_for_error_code = NOT_SET
160
+
161
+ # get the constraint values if available for the chosen parameter
162
+ try:
163
+ [values_from_constraint] = [
164
+ r.values
165
+ for r in relations_for_status_code
166
+ if r.property_name == parameter_to_invalidate
167
+ ]
168
+ except ValueError:
169
+ values_from_constraint = []
170
+
171
+ # if the parameter was not provided, add it to params / headers
172
+ params, headers = ensure_parameter_in_parameters(
173
+ parameter_to_invalidate=parameter_to_invalidate,
174
+ params=params,
175
+ headers=headers,
176
+ parameter_data=parameter_data,
177
+ values_from_constraint=values_from_constraint,
178
+ )
179
+
180
+ # determine the invalid_value
181
+ if invalid_value_for_error_code != NOT_SET:
182
+ invalid_value = invalid_value_for_error_code
183
+ else:
184
+ if parameter_to_invalidate in params.keys():
185
+ valid_value = params[parameter_to_invalidate]
186
+ else:
187
+ valid_value = headers[parameter_to_invalidate]
188
+
189
+ value_schema = resolve_schema(parameter_data["schema"])
190
+ invalid_value = get_invalid_value(
191
+ value_schema=value_schema,
192
+ current_value=valid_value,
193
+ values_from_constraint=values_from_constraint,
194
+ )
195
+ logger.debug(f"{parameter_to_invalidate} changed to {invalid_value}")
196
+
197
+ # update the params / headers and return
198
+ if parameter_to_invalidate in params.keys():
199
+ params[parameter_to_invalidate] = invalid_value
200
+ else:
201
+ headers[parameter_to_invalidate] = str(invalid_value)
202
+ return params, headers
203
+
204
+
205
+ def ensure_parameter_in_parameters(
206
+ parameter_to_invalidate: str,
207
+ params: dict[str, Any],
208
+ headers: dict[str, str],
209
+ parameter_data: dict[str, Any],
210
+ values_from_constraint: list[Any],
211
+ ) -> tuple[dict[str, Any], dict[str, str]]:
212
+ """
213
+ Returns the params, headers tuple with parameter_to_invalidate with a valid
214
+ value to params or headers if not originally present.
215
+ """
216
+ if (
217
+ parameter_to_invalidate not in params.keys()
218
+ and parameter_to_invalidate not in headers.keys()
219
+ ):
220
+ if values_from_constraint:
221
+ valid_value = choice(values_from_constraint)
222
+ else:
223
+ parameter_schema = resolve_schema(parameter_data["schema"])
224
+ valid_value = get_valid_value(parameter_schema)
225
+ if (
226
+ parameter_data["in"] == "query"
227
+ and parameter_to_invalidate not in params.keys()
228
+ ):
229
+ params[parameter_to_invalidate] = valid_value
230
+ if (
231
+ parameter_data["in"] == "header"
232
+ and parameter_to_invalidate not in headers.keys()
233
+ ):
234
+ headers[parameter_to_invalidate] = str(valid_value)
235
+ return params, headers
236
+
237
+
238
+ def get_json_data_with_conflict(
239
+ url: str, base_url: str, method: str, dto: Dto, conflict_status_code: int
240
+ ) -> dict[str, Any]:
241
+ method = method.lower()
242
+ json_data = dto.as_dict()
243
+ unique_property_value_constraints = [
244
+ r for r in dto.get_relations() if isinstance(r, UniquePropertyValueConstraint)
245
+ ]
246
+ for relation in unique_property_value_constraints:
247
+ json_data[relation.property_name] = relation.value
248
+ # create a new resource that the original request will conflict with
249
+ if method in ["patch", "put"]:
250
+ post_url_parts = url.split("/")[:-1]
251
+ post_url = "/".join(post_url_parts)
252
+ # the PATCH or PUT may use a different dto than required for POST
253
+ # so a valid POST dto must be constructed
254
+ path = post_url.replace(base_url, "")
255
+ request_data: RequestData = run_keyword("get_request_data", path, "post")
256
+ post_json = request_data.dto.as_dict()
257
+ for key in post_json.keys():
258
+ if key in json_data:
259
+ post_json[key] = json_data.get(key)
260
+ else:
261
+ post_url = url
262
+ post_json = json_data
263
+ path = post_url.replace(base_url, "")
264
+ request_data = run_keyword("get_request_data", path, "post")
265
+
266
+ response: Response = run_keyword(
267
+ "authorized_request",
268
+ post_url,
269
+ "post",
270
+ request_data.params,
271
+ request_data.headers,
272
+ post_json,
273
+ )
274
+ # conflicting resource may already exist
275
+ assert response.ok or response.status_code == conflict_status_code, (
276
+ f"get_json_data_with_conflict received {response.status_code}: {response.json()}"
277
+ )
278
+ return json_data
279
+ raise ValueError(
280
+ f"No UniquePropertyValueConstraint in the get_relations list on dto {dto}."
281
+ )
@@ -7,20 +7,19 @@ test and constraints / restrictions on properties of the resources.
7
7
  from abc import ABC
8
8
  from copy import deepcopy
9
9
  from dataclasses import dataclass, fields
10
- from logging import getLogger
11
10
  from random import choice, shuffle
12
- from typing import Any, Dict, List, Optional, Union
11
+ from typing import Any
13
12
  from uuid import uuid4
14
13
 
15
- from OpenApiLibCore import value_utils
14
+ from robot.api import logger
16
15
 
17
- logger = getLogger(__name__)
16
+ from OpenApiLibCore import value_utils
18
17
 
19
18
  NOT_SET = object()
20
19
  SENTINEL = object()
21
20
 
22
21
 
23
- def resolve_schema(schema: Dict[str, Any]) -> Dict[str, Any]:
22
+ def resolve_schema(schema: dict[str, Any]) -> dict[str, Any]:
24
23
  """
25
24
  Helper function to resolve allOf, anyOf and oneOf instances in a schema.
26
25
 
@@ -64,7 +63,7 @@ def resolve_schema(schema: Dict[str, Any]) -> Dict[str, Any]:
64
63
  return resolved_schema
65
64
 
66
65
 
67
- def merge_schemas(first: Dict[str, Any], second: Dict[str, Any]) -> Dict[str, Any]:
66
+ def merge_schemas(first: dict[str, Any], second: dict[str, Any]) -> dict[str, Any]:
68
67
  """Helper method to merge two schemas, recursively."""
69
68
  merged_schema = deepcopy(first)
70
69
  for key, value in second.items():
@@ -86,7 +85,7 @@ def merge_schemas(first: Dict[str, Any], second: Dict[str, Any]) -> Dict[str, An
86
85
  return merged_schema
87
86
 
88
87
 
89
- class ResourceRelation(ABC): # pylint: disable=too-few-public-methods
88
+ class ResourceRelation(ABC):
90
89
  """ABC for all resource relations or restrictions within the API."""
91
90
 
92
91
  property_name: str
@@ -95,10 +94,12 @@ class ResourceRelation(ABC): # pylint: disable=too-few-public-methods
95
94
 
96
95
  @dataclass
97
96
  class PathPropertiesConstraint(ResourceRelation):
98
- """The resolved path for the endpoint."""
97
+ """The value to be used as the ``path`` for related requests."""
99
98
 
100
99
  path: str
101
100
  property_name: str = "id"
101
+ invalid_value: Any = NOT_SET
102
+ invalid_value_error_code: int = 422
102
103
  error_code: int = 404
103
104
 
104
105
 
@@ -107,10 +108,11 @@ class PropertyValueConstraint(ResourceRelation):
107
108
  """The allowed values for property_name."""
108
109
 
109
110
  property_name: str
110
- values: List[Any]
111
+ values: list[Any]
111
112
  invalid_value: Any = NOT_SET
112
113
  invalid_value_error_code: int = 422
113
114
  error_code: int = 422
115
+ treat_as_mandatory: bool = False
114
116
 
115
117
 
116
118
  @dataclass
@@ -119,7 +121,7 @@ class IdDependency(ResourceRelation):
119
121
 
120
122
  property_name: str
121
123
  get_path: str
122
- operation_id: Optional[str] = None
124
+ operation_id: str = ""
123
125
  error_code: int = 422
124
126
 
125
127
 
@@ -141,27 +143,20 @@ class UniquePropertyValueConstraint(ResourceRelation):
141
143
  error_code: int = 422
142
144
 
143
145
 
144
- Relation = Union[
145
- IdDependency,
146
- IdReference,
147
- PathPropertiesConstraint,
148
- PropertyValueConstraint,
149
- UniquePropertyValueConstraint,
150
- ]
151
-
152
-
153
146
  @dataclass
154
147
  class Dto(ABC):
155
148
  """Base class for the Dto class."""
156
149
 
157
150
  @staticmethod
158
- def get_parameter_relations() -> List[Relation]:
151
+ def get_parameter_relations() -> list[ResourceRelation]:
159
152
  """Return the list of Relations for the header and query parameters."""
160
153
  return []
161
154
 
162
- def get_parameter_relations_for_error_code(self, error_code: int) -> List[Relation]:
155
+ def get_parameter_relations_for_error_code(
156
+ self, error_code: int
157
+ ) -> list[ResourceRelation]:
163
158
  """Return the list of Relations associated with the given error_code."""
164
- relations: List[Relation] = [
159
+ relations: list[ResourceRelation] = [
165
160
  r
166
161
  for r in self.get_parameter_relations()
167
162
  if r.error_code == error_code
@@ -173,13 +168,13 @@ class Dto(ABC):
173
168
  return relations
174
169
 
175
170
  @staticmethod
176
- def get_relations() -> List[Relation]:
171
+ def get_relations() -> list[ResourceRelation]:
177
172
  """Return the list of Relations for the (json) body."""
178
173
  return []
179
174
 
180
- def get_relations_for_error_code(self, error_code: int) -> List[Relation]:
175
+ def get_relations_for_error_code(self, error_code: int) -> list[ResourceRelation]:
181
176
  """Return the list of Relations associated with the given error_code."""
182
- relations: List[Relation] = [
177
+ relations: list[ResourceRelation] = [
183
178
  r
184
179
  for r in self.get_relations()
185
180
  if r.error_code == error_code
@@ -190,14 +185,24 @@ class Dto(ABC):
190
185
  ]
191
186
  return relations
192
187
 
188
+ def get_body_relations_for_error_code(
189
+ self, error_code: int
190
+ ) -> list[ResourceRelation]:
191
+ """
192
+ Return the list of Relations associated with the given error_code that are
193
+ applicable to the body / payload of the request.
194
+ """
195
+ all_relations = self.get_relations_for_error_code(error_code=error_code)
196
+ return [r for r in all_relations if not isinstance(r, PathPropertiesConstraint)]
197
+
193
198
  def get_invalidated_data(
194
199
  self,
195
- schema: Dict[str, Any],
200
+ schema: dict[str, Any],
196
201
  status_code: int,
197
202
  invalid_property_default_code: int,
198
- ) -> Dict[str, Any]:
203
+ ) -> dict[str, Any]:
199
204
  """Return a data set with one of the properties set to an invalid value or type."""
200
- properties: Dict[str, Any] = self.as_dict()
205
+ properties: dict[str, Any] = self.as_dict()
201
206
 
202
207
  schema = resolve_schema(schema)
203
208
 
@@ -207,18 +212,16 @@ class Dto(ABC):
207
212
  r for r in relations if not isinstance(r, PathPropertiesConstraint)
208
213
  ]
209
214
  property_names = [r.property_name for r in relations]
210
- if status_code == invalid_property_default_code:
215
+ if status_code == invalid_property_default_code and schema.get("properties"):
211
216
  # add all properties defined in the schema, including optional properties
212
217
  property_names.extend((schema["properties"].keys()))
213
- # remove duplicates
214
- property_names = list(set(property_names))
215
218
  if not property_names:
216
219
  raise ValueError(
217
220
  f"No property can be invalidated to cause status_code {status_code}"
218
221
  )
219
- # shuffle the property_names so different properties on the Dto are invalidated
220
- # when rerunning the test
221
- shuffle(property_names)
222
+ # Remove duplicates, then shuffle the property_names so different properties on
223
+ # the Dto are invalidated when rerunning the test.
224
+ shuffle(list(set(property_names)))
222
225
  for property_name in property_names:
223
226
  # if possible, invalidate a constraint but send otherwise valid data
224
227
  id_dependencies = [
@@ -227,12 +230,12 @@ class Dto(ABC):
227
230
  if isinstance(r, IdDependency) and r.property_name == property_name
228
231
  ]
229
232
  if id_dependencies:
230
- invalid_value = uuid4().hex
233
+ invalid_id = uuid4().hex
231
234
  logger.debug(
232
235
  f"Breaking IdDependency for status_code {status_code}: replacing "
233
- f"{properties[property_name]} with {invalid_value}"
236
+ f"{properties[property_name]} with {invalid_id}"
234
237
  )
235
- properties[property_name] = invalid_value
238
+ properties[property_name] = invalid_id
236
239
  return properties
237
240
 
238
241
  invalid_value_from_constraint = [
@@ -293,14 +296,14 @@ class Dto(ABC):
293
296
  )
294
297
  properties[property_name] = invalid_value
295
298
  logger.debug(
296
- f"Property {property_name} changed to {invalid_value} (received from "
299
+ f"Property {property_name} changed to {invalid_value!r} (received from "
297
300
  f"get_invalid_value)"
298
301
  )
299
302
  return properties
300
- logger.warning("get_invalidated_data returned unchanged properties")
303
+ logger.warn("get_invalidated_data returned unchanged properties")
301
304
  return properties # pragma: no cover
302
305
 
303
- def as_dict(self) -> Dict[Any, Any]:
306
+ def as_dict(self) -> dict[Any, Any]:
304
307
  """Return the dict representation of the Dto."""
305
308
  result = {}
306
309
 
@@ -1,85 +1,97 @@
1
- """Module for helper methods and classes used by the openapi_executors module."""
2
-
3
- from dataclasses import dataclass
4
- from importlib import import_module
5
- from logging import getLogger
6
- from typing import Callable, Dict, Tuple, Type, Union
7
-
8
- from OpenApiLibCore.dto_base import Dto
9
-
10
- logger = getLogger(__name__)
11
-
12
-
13
- @dataclass
14
- class _DefaultIdPropertyName:
15
- id_property_name: str = "id"
16
-
17
-
18
- DEFAULT_ID_PROPERTY_NAME = _DefaultIdPropertyName()
19
-
20
-
21
- @dataclass
22
- class DefaultDto(Dto):
23
- """A default Dto that can be instantiated."""
24
-
25
-
26
- # pylint: disable=invalid-name, too-few-public-methods
27
- class get_dto_class:
28
- """Callable class to return Dtos from user-implemented mappings file."""
29
-
30
- def __init__(self, mappings_module_name: str) -> None:
31
- try:
32
- mappings_module = import_module(mappings_module_name)
33
- self.dto_mapping: Dict[Tuple[str, str], Type[Dto]] = (
34
- mappings_module.DTO_MAPPING
35
- )
36
- except (ImportError, AttributeError, ValueError) as exception:
37
- if mappings_module_name != "no mapping":
38
- logger.error(f"DTO_MAPPING was not imported: {exception}")
39
- self.dto_mapping = {}
40
-
41
- def __call__(self, endpoint: str, method: str) -> Type[Dto]:
42
- try:
43
- return self.dto_mapping[(endpoint, method.lower())]
44
- except KeyError:
45
- logger.debug(f"No Dto mapping for {endpoint} {method}.")
46
- return DefaultDto
47
-
48
-
49
- # pylint: disable=invalid-name, too-few-public-methods
50
- class get_id_property_name:
51
- """
52
- Callable class to return the name of the property that uniquely identifies
53
- the resource from user-implemented mappings file.
54
- """
55
-
56
- def __init__(self, mappings_module_name: str) -> None:
57
- try:
58
- mappings_module = import_module(mappings_module_name)
59
- self.id_mapping: Dict[
60
- str,
61
- Union[
62
- str,
63
- Tuple[
64
- str, Callable[[Union[str, int, float]], Union[str, int, float]]
65
- ],
66
- ],
67
- ] = mappings_module.ID_MAPPING
68
- except (ImportError, AttributeError, ValueError) as exception:
69
- if mappings_module_name != "no mapping":
70
- logger.error(f"ID_MAPPING was not imported: {exception}")
71
- self.id_mapping = {}
72
-
73
- def __call__(
74
- self, endpoint: str
75
- ) -> Union[
76
- str, Tuple[str, Callable[[Union[str, int, float]], Union[str, int, float]]]
77
- ]:
78
- try:
79
- return self.id_mapping[endpoint]
80
- except KeyError:
81
- default_id_name = DEFAULT_ID_PROPERTY_NAME.id_property_name
82
- logger.debug(
83
- f"No id mapping for {endpoint} ('{default_id_name}' will be used)"
84
- )
85
- return default_id_name
1
+ """Module for helper methods and classes used by the openapi_executors module."""
2
+
3
+ from dataclasses import dataclass
4
+ from importlib import import_module
5
+ from typing import Any, Callable, Type, overload
6
+
7
+ from robot.api import logger
8
+
9
+ from OpenApiLibCore.dto_base import Dto
10
+ from OpenApiLibCore.protocols import GetDtoClassType, GetIdPropertyNameType
11
+
12
+
13
+ @dataclass
14
+ class _DefaultIdPropertyName:
15
+ id_property_name: str = "id"
16
+
17
+
18
+ DEFAULT_ID_PROPERTY_NAME = _DefaultIdPropertyName()
19
+
20
+
21
+ @dataclass
22
+ class DefaultDto(Dto):
23
+ """A default Dto that can be instantiated."""
24
+
25
+
26
+ def get_dto_class(mappings_module_name: str) -> GetDtoClassType:
27
+ return GetDtoClass(mappings_module_name=mappings_module_name)
28
+
29
+
30
+ class GetDtoClass:
31
+ """Callable class to return Dtos from user-implemented mappings file."""
32
+
33
+ def __init__(self, mappings_module_name: str) -> None:
34
+ try:
35
+ mappings_module = import_module(mappings_module_name)
36
+ self.dto_mapping: dict[tuple[str, str], Type[Dto]] = (
37
+ mappings_module.DTO_MAPPING
38
+ )
39
+ except (ImportError, AttributeError, ValueError) as exception:
40
+ if mappings_module_name != "no mapping":
41
+ logger.error(f"DTO_MAPPING was not imported: {exception}")
42
+ self.dto_mapping = {}
43
+
44
+ def __call__(self, path: str, method: str) -> Type[Dto]:
45
+ try:
46
+ return self.dto_mapping[(path, method.lower())]
47
+ except KeyError:
48
+ logger.debug(f"No Dto mapping for {path} {method}.")
49
+ return DefaultDto
50
+
51
+
52
+ def get_id_property_name(mappings_module_name: str) -> GetIdPropertyNameType:
53
+ return GetIdPropertyName(mappings_module_name=mappings_module_name)
54
+
55
+
56
+ class GetIdPropertyName:
57
+ """
58
+ Callable class to return the name of the property that uniquely identifies
59
+ the resource from user-implemented mappings file.
60
+ """
61
+
62
+ def __init__(self, mappings_module_name: str) -> None:
63
+ try:
64
+ mappings_module = import_module(mappings_module_name)
65
+ self.id_mapping: dict[
66
+ str,
67
+ str | tuple[str, Callable[[str], str] | Callable[[int], int]],
68
+ ] = mappings_module.ID_MAPPING
69
+ except (ImportError, AttributeError, ValueError) as exception:
70
+ if mappings_module_name != "no mapping":
71
+ logger.error(f"ID_MAPPING was not imported: {exception}")
72
+ self.id_mapping = {}
73
+
74
+ def __call__(
75
+ self, path: str
76
+ ) -> tuple[str, Callable[[str], str] | Callable[[int], int]]:
77
+ try:
78
+ value_or_mapping = self.id_mapping[path]
79
+ if isinstance(value_or_mapping, str):
80
+ return (value_or_mapping, dummy_transformer)
81
+ return value_or_mapping
82
+ except KeyError:
83
+ default_id_name = DEFAULT_ID_PROPERTY_NAME.id_property_name
84
+ logger.debug(f"No id mapping for {path} ('{default_id_name}' will be used)")
85
+ return (default_id_name, dummy_transformer)
86
+
87
+
88
+ @overload
89
+ def dummy_transformer(valid_id: str) -> str: ...
90
+
91
+
92
+ @overload
93
+ def dummy_transformer(valid_id: int) -> int: ...
94
+
95
+
96
+ def dummy_transformer(valid_id: Any) -> Any:
97
+ return valid_id