robotframework-openapitools 1.0.0b3__py3-none-any.whl → 1.0.0b5__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 (34) hide show
  1. OpenApiDriver/openapi_executors.py +15 -11
  2. OpenApiDriver/openapi_reader.py +12 -13
  3. OpenApiDriver/openapidriver.libspec +5 -42
  4. OpenApiLibCore/__init__.py +0 -2
  5. OpenApiLibCore/annotations.py +8 -1
  6. OpenApiLibCore/data_generation/__init__.py +0 -2
  7. OpenApiLibCore/data_generation/body_data_generation.py +54 -73
  8. OpenApiLibCore/data_generation/data_generation_core.py +75 -82
  9. OpenApiLibCore/data_invalidation.py +38 -25
  10. OpenApiLibCore/dto_base.py +48 -105
  11. OpenApiLibCore/dto_utils.py +31 -3
  12. OpenApiLibCore/localized_faker.py +88 -0
  13. OpenApiLibCore/models.py +723 -0
  14. OpenApiLibCore/openapi_libcore.libspec +48 -284
  15. OpenApiLibCore/openapi_libcore.py +54 -71
  16. OpenApiLibCore/parameter_utils.py +20 -14
  17. OpenApiLibCore/path_functions.py +10 -10
  18. OpenApiLibCore/path_invalidation.py +5 -7
  19. OpenApiLibCore/protocols.py +13 -5
  20. OpenApiLibCore/request_data.py +67 -102
  21. OpenApiLibCore/resource_relations.py +6 -5
  22. OpenApiLibCore/validation.py +50 -167
  23. OpenApiLibCore/value_utils.py +46 -358
  24. openapi_libgen/__init__.py +0 -46
  25. openapi_libgen/command_line.py +7 -19
  26. openapi_libgen/generator.py +84 -0
  27. openapi_libgen/parsing_utils.py +9 -5
  28. openapi_libgen/spec_parser.py +41 -114
  29. {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/METADATA +2 -1
  30. robotframework_openapitools-1.0.0b5.dist-info/RECORD +40 -0
  31. robotframework_openapitools-1.0.0b3.dist-info/RECORD +0 -37
  32. {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/LICENSE +0 -0
  33. {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/WHEEL +0 -0
  34. {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/entry_points.txt +0 -0
@@ -5,7 +5,6 @@ test and constraints / restrictions on properties of the resources.
5
5
  """
6
6
 
7
7
  from abc import ABC
8
- from copy import deepcopy
9
8
  from dataclasses import dataclass, fields
10
9
  from random import choice, shuffle
11
10
  from typing import Any
@@ -14,77 +13,13 @@ from uuid import uuid4
14
13
  from robot.api import logger
15
14
 
16
15
  from OpenApiLibCore import value_utils
16
+ from OpenApiLibCore.models import NullSchema, ObjectSchema, UnionTypeSchema
17
+ from OpenApiLibCore.parameter_utils import get_oas_name_from_safe_name
17
18
 
18
19
  NOT_SET = object()
19
20
  SENTINEL = object()
20
21
 
21
22
 
22
- def resolve_schema(schema: dict[str, Any]) -> dict[str, Any]:
23
- """
24
- Helper function to resolve allOf, anyOf and oneOf instances in a schema.
25
-
26
- The schemas are used to generate values for headers, query parameters and json
27
- bodies to be able to make requests.
28
- """
29
- # Schema is mutable, so deepcopy to prevent mutation of original schema argument
30
- resolved_schema = deepcopy(schema)
31
-
32
- # allOf / anyOf / oneOf may be nested, so recursively resolve the dict-typed values
33
- for key, value in resolved_schema.items():
34
- if isinstance(value, dict):
35
- resolved_schema[key] = resolve_schema(value)
36
-
37
- # When handling allOf there should no duplicate keys, so the schema parts can
38
- # just be merged after resolving the individual parts
39
- if schema_parts := resolved_schema.pop("allOf", None):
40
- for schema_part in schema_parts:
41
- resolved_part = resolve_schema(schema_part)
42
- resolved_schema = merge_schemas(resolved_schema, resolved_part)
43
- # Handling anyOf and oneOf requires extra logic to deal with the "type" information.
44
- # Some properties / parameters may be of different types and each type may have its
45
- # own restrictions e.g. a parameter that accepts an enum value (string) or an
46
- # integer value within a certain range.
47
- # Since the library needs all this information for different purposes, the
48
- # schema_parts cannot be merged, so a helper property / key "types" is introduced.
49
- any_of = resolved_schema.pop("anyOf", [])
50
- one_of = resolved_schema.pop("oneOf", [])
51
- schema_parts = any_of if any_of else one_of
52
-
53
- for schema_part in schema_parts:
54
- resolved_part = resolve_schema(schema_part)
55
- if isinstance(resolved_part, dict) and "type" in resolved_part.keys():
56
- if "types" in resolved_schema.keys():
57
- resolved_schema["types"].append(resolved_part)
58
- else:
59
- resolved_schema["types"] = [resolved_part]
60
- else:
61
- resolved_schema = merge_schemas(resolved_schema, resolved_part)
62
-
63
- return resolved_schema
64
-
65
-
66
- def merge_schemas(first: dict[str, Any], second: dict[str, Any]) -> dict[str, Any]:
67
- """Helper method to merge two schemas, recursively."""
68
- merged_schema = deepcopy(first)
69
- for key, value in second.items():
70
- # for existing keys, merge dict and list values, leave others unchanged
71
- if key in merged_schema.keys():
72
- if isinstance(value, dict):
73
- # if the key holds a dict, merge the values (e.g. 'properties')
74
- merged_schema[key].update(value)
75
- elif isinstance(value, list):
76
- # if the key holds a list, extend the values (e.g. 'required')
77
- merged_schema[key].extend(value)
78
- elif value != merged_schema[key]:
79
- logger.debug(
80
- f"key '{key}' with value '{merged_schema[key]}'"
81
- f" not updated to '{value}'"
82
- )
83
- else:
84
- merged_schema[key] = value
85
- return merged_schema
86
-
87
-
88
23
  class ResourceRelation(ABC):
89
24
  """ABC for all resource relations or restrictions within the API."""
90
25
 
@@ -148,17 +83,17 @@ class Dto(ABC):
148
83
  """Base class for the Dto class."""
149
84
 
150
85
  @staticmethod
151
- def get_parameter_relations() -> list[ResourceRelation]:
86
+ def get_path_relations() -> list[PathPropertiesConstraint]:
152
87
  """Return the list of Relations for the header and query parameters."""
153
88
  return []
154
89
 
155
- def get_parameter_relations_for_error_code(
90
+ def get_path_relations_for_error_code(
156
91
  self, error_code: int
157
- ) -> list[ResourceRelation]:
92
+ ) -> list[PathPropertiesConstraint]:
158
93
  """Return the list of Relations associated with the given error_code."""
159
- relations: list[ResourceRelation] = [
94
+ relations: list[PathPropertiesConstraint] = [
160
95
  r
161
- for r in self.get_parameter_relations()
96
+ for r in self.get_path_relations()
162
97
  if r.error_code == error_code
163
98
  or (
164
99
  getattr(r, "invalid_value_error_code", None) == error_code
@@ -168,15 +103,17 @@ class Dto(ABC):
168
103
  return relations
169
104
 
170
105
  @staticmethod
171
- def get_relations() -> list[ResourceRelation]:
172
- """Return the list of Relations for the (json) body."""
106
+ def get_parameter_relations() -> list[ResourceRelation]:
107
+ """Return the list of Relations for the header and query parameters."""
173
108
  return []
174
109
 
175
- def get_relations_for_error_code(self, error_code: int) -> list[ResourceRelation]:
110
+ def get_parameter_relations_for_error_code(
111
+ self, error_code: int
112
+ ) -> list[ResourceRelation]:
176
113
  """Return the list of Relations associated with the given error_code."""
177
114
  relations: list[ResourceRelation] = [
178
115
  r
179
- for r in self.get_relations()
116
+ for r in self.get_parameter_relations()
180
117
  if r.error_code == error_code
181
118
  or (
182
119
  getattr(r, "invalid_value_error_code", None) == error_code
@@ -185,6 +122,11 @@ class Dto(ABC):
185
122
  ]
186
123
  return relations
187
124
 
125
+ @staticmethod
126
+ def get_relations() -> list[ResourceRelation]:
127
+ """Return the list of Relations for the (json) body."""
128
+ return []
129
+
188
130
  def get_body_relations_for_error_code(
189
131
  self, error_code: int
190
132
  ) -> list[ResourceRelation]:
@@ -192,29 +134,31 @@ class Dto(ABC):
192
134
  Return the list of Relations associated with the given error_code that are
193
135
  applicable to the body / payload of the request.
194
136
  """
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)]
137
+ relations: list[ResourceRelation] = [
138
+ r
139
+ for r in self.get_relations()
140
+ if r.error_code == error_code
141
+ or (
142
+ getattr(r, "invalid_value_error_code", None) == error_code
143
+ and getattr(r, "invalid_value", None) != NOT_SET
144
+ )
145
+ ]
146
+ return relations
197
147
 
198
148
  def get_invalidated_data(
199
149
  self,
200
- schema: dict[str, Any],
150
+ schema: ObjectSchema,
201
151
  status_code: int,
202
152
  invalid_property_default_code: int,
203
153
  ) -> dict[str, Any]:
204
154
  """Return a data set with one of the properties set to an invalid value or type."""
205
155
  properties: dict[str, Any] = self.as_dict()
206
156
 
207
- schema = resolve_schema(schema)
208
-
209
- relations = self.get_relations_for_error_code(error_code=status_code)
210
- # filter PathProperyConstraints since in that case no data can be invalidated
211
- relations = [
212
- r for r in relations if not isinstance(r, PathPropertiesConstraint)
213
- ]
157
+ relations = self.get_body_relations_for_error_code(error_code=status_code)
214
158
  property_names = [r.property_name for r in relations]
215
- if status_code == invalid_property_default_code and schema.get("properties"):
159
+ if status_code == invalid_property_default_code:
216
160
  # add all properties defined in the schema, including optional properties
217
- property_names.extend((schema["properties"].keys()))
161
+ property_names.extend((schema.properties.root.keys())) # type: ignore[union-attr]
218
162
  if not property_names:
219
163
  raise ValueError(
220
164
  f"No property can be invalidated to cause status_code {status_code}"
@@ -232,8 +176,8 @@ class Dto(ABC):
232
176
  if id_dependencies:
233
177
  invalid_id = uuid4().hex
234
178
  logger.debug(
235
- f"Breaking IdDependency for status_code {status_code}: replacing "
236
- f"{properties[property_name]} with {invalid_id}"
179
+ f"Breaking IdDependency for status_code {status_code}: setting "
180
+ f"{property_name} to {invalid_id}"
237
181
  )
238
182
  properties[property_name] = invalid_id
239
183
  return properties
@@ -256,31 +200,30 @@ class Dto(ABC):
256
200
  )
257
201
  return properties
258
202
 
259
- value_schema = schema["properties"][property_name]
260
- value_schema = resolve_schema(value_schema)
261
-
262
- # Filter "type": "null" from the possible types since this indicates an
263
- # optional / nullable property that can only be invalidated by sending
264
- # invalid data of a non-null type
265
- if value_schemas := value_schema.get("types"):
266
- if len(value_schemas) > 1:
267
- value_schemas = [
268
- schema for schema in value_schemas if schema["type"] != "null"
269
- ]
270
- value_schema = choice(value_schemas)
203
+ value_schema = schema.properties.root[property_name] # type: ignore[union-attr]
204
+ if isinstance(value_schema, UnionTypeSchema):
205
+ # Filter "type": "null" from the possible types since this indicates an
206
+ # optional / nullable property that can only be invalidated by sending
207
+ # invalid data of a non-null type
208
+ non_null_schemas = [
209
+ s
210
+ for s in value_schema.resolved_schemas
211
+ if not isinstance(s, NullSchema)
212
+ ]
213
+ value_schema = choice(non_null_schemas)
271
214
 
272
215
  # there may not be a current_value when invalidating an optional property
273
216
  current_value = properties.get(property_name, SENTINEL)
274
217
  if current_value is SENTINEL:
275
218
  # the current_value isn't very relevant as long as the type is correct
276
219
  # so no logic to handle Relations / objects / arrays here
277
- property_type = value_schema["type"]
220
+ property_type = value_schema.type
278
221
  if property_type == "object":
279
222
  current_value = {}
280
223
  elif property_type == "array":
281
224
  current_value = []
282
225
  else:
283
- current_value = value_utils.get_valid_value(value_schema)
226
+ current_value = value_schema.get_valid_value()
284
227
 
285
228
  values_from_constraint = [
286
229
  r.values[0]
@@ -311,7 +254,7 @@ class Dto(ABC):
311
254
  field_name = field.name
312
255
  if field_name not in self.__dict__:
313
256
  continue
314
- original_name = field.metadata["original_property_name"]
257
+ original_name = get_oas_name_from_safe_name(field_name)
315
258
  result[original_name] = getattr(self, field_name)
316
259
 
317
260
  return result
@@ -7,7 +7,11 @@ from typing import Any, Callable, Type, overload
7
7
  from robot.api import logger
8
8
 
9
9
  from OpenApiLibCore.dto_base import Dto
10
- from OpenApiLibCore.protocols import GetDtoClassType, GetIdPropertyNameType
10
+ from OpenApiLibCore.protocols import (
11
+ GetDtoClassType,
12
+ GetIdPropertyNameType,
13
+ GetPathDtoClassType,
14
+ )
11
15
 
12
16
 
13
17
  @dataclass
@@ -49,6 +53,30 @@ class GetDtoClass:
49
53
  return DefaultDto
50
54
 
51
55
 
56
+ def get_path_dto_class(mappings_module_name: str) -> GetPathDtoClassType:
57
+ return GetPathDtoClass(mappings_module_name=mappings_module_name)
58
+
59
+
60
+ class GetPathDtoClass:
61
+ """Callable class to return Dtos from user-implemented mappings file."""
62
+
63
+ def __init__(self, mappings_module_name: str) -> None:
64
+ try:
65
+ mappings_module = import_module(mappings_module_name)
66
+ self.dto_mapping: dict[str, Type[Dto]] = mappings_module.PATH_MAPPING
67
+ except (ImportError, AttributeError, ValueError) as exception:
68
+ if mappings_module_name != "no mapping":
69
+ logger.error(f"PATH_MAPPING was not imported: {exception}")
70
+ self.dto_mapping = {}
71
+
72
+ def __call__(self, path: str) -> Type[Dto]:
73
+ try:
74
+ return self.dto_mapping[path]
75
+ except KeyError:
76
+ logger.debug(f"No Dto mapping for {path}.")
77
+ return DefaultDto
78
+
79
+
52
80
  def get_id_property_name(mappings_module_name: str) -> GetIdPropertyNameType:
53
81
  return GetIdPropertyName(mappings_module_name=mappings_module_name)
54
82
 
@@ -86,11 +114,11 @@ class GetIdPropertyName:
86
114
 
87
115
 
88
116
  @overload
89
- def dummy_transformer(valid_id: str) -> str: ...
117
+ def dummy_transformer(valid_id: str) -> str: ... # pragma: no cover
90
118
 
91
119
 
92
120
  @overload
93
- def dummy_transformer(valid_id: int) -> int: ...
121
+ def dummy_transformer(valid_id: int) -> int: ... # pragma: no cover
94
122
 
95
123
 
96
124
  def dummy_transformer(valid_id: Any) -> Any:
@@ -0,0 +1,88 @@
1
+ import datetime
2
+ from typing import Callable
3
+
4
+ import faker
5
+
6
+
7
+ def fake_string(string_format: str) -> str:
8
+ """
9
+ Generate a random string based on the provided format if the format is supported.
10
+ """
11
+ # format names may contain -, which is invalid in Python naming
12
+ string_format = string_format.replace("-", "_")
13
+ fake_generator = getattr(FAKE, string_format, FAKE.uuid)
14
+ value: str = fake_generator()
15
+ if isinstance(value, datetime.datetime):
16
+ return value.strftime("%Y-%m-%dT%H:%M:%SZ")
17
+ return value
18
+
19
+
20
+ class LocalizedFaker:
21
+ """Class to support setting a locale post-init."""
22
+
23
+ # pylint: disable=missing-function-docstring
24
+ def __init__(self) -> None:
25
+ self.fake = faker.Faker()
26
+
27
+ def set_locale(self, locale: str | list[str]) -> None:
28
+ """Update the fake attribute with a Faker instance with the provided locale."""
29
+ self.fake = faker.Faker(locale)
30
+
31
+ @property
32
+ def date(self) -> Callable[[], str]:
33
+ return self.fake.date
34
+
35
+ @property
36
+ def date_time(self) -> Callable[[], datetime.datetime]:
37
+ return self.fake.date_time
38
+
39
+ @property
40
+ def password(self) -> Callable[[], str]:
41
+ return self.fake.password
42
+
43
+ @property
44
+ def binary(self) -> Callable[[], bytes]:
45
+ return self.fake.binary
46
+
47
+ @property
48
+ def email(self) -> Callable[[], str]:
49
+ return self.fake.safe_email
50
+
51
+ @property
52
+ def uuid(self) -> Callable[[], str]:
53
+ return self.fake.uuid4
54
+
55
+ @property
56
+ def uri(self) -> Callable[[], str]:
57
+ return self.fake.uri
58
+
59
+ @property
60
+ def url(self) -> Callable[[], str]:
61
+ return self.fake.url
62
+
63
+ @property
64
+ def hostname(self) -> Callable[[], str]:
65
+ return self.fake.hostname
66
+
67
+ @property
68
+ def ipv4(self) -> Callable[[], str]:
69
+ return self.fake.ipv4
70
+
71
+ @property
72
+ def ipv6(self) -> Callable[[], str]:
73
+ return self.fake.ipv6
74
+
75
+ @property
76
+ def name(self) -> Callable[[], str]:
77
+ return self.fake.name
78
+
79
+ @property
80
+ def text(self) -> Callable[[], str]:
81
+ return self.fake.text
82
+
83
+ @property
84
+ def description(self) -> Callable[[], str]:
85
+ return self.fake.text
86
+
87
+
88
+ FAKE = LocalizedFaker()