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.
- OpenApiDriver/openapi_executors.py +15 -11
- OpenApiDriver/openapi_reader.py +12 -13
- OpenApiDriver/openapidriver.libspec +5 -42
- OpenApiLibCore/__init__.py +0 -2
- OpenApiLibCore/annotations.py +8 -1
- OpenApiLibCore/data_generation/__init__.py +0 -2
- OpenApiLibCore/data_generation/body_data_generation.py +54 -73
- OpenApiLibCore/data_generation/data_generation_core.py +75 -82
- OpenApiLibCore/data_invalidation.py +38 -25
- OpenApiLibCore/dto_base.py +48 -105
- OpenApiLibCore/dto_utils.py +31 -3
- OpenApiLibCore/localized_faker.py +88 -0
- OpenApiLibCore/models.py +723 -0
- OpenApiLibCore/openapi_libcore.libspec +48 -284
- OpenApiLibCore/openapi_libcore.py +54 -71
- OpenApiLibCore/parameter_utils.py +20 -14
- OpenApiLibCore/path_functions.py +10 -10
- OpenApiLibCore/path_invalidation.py +5 -7
- OpenApiLibCore/protocols.py +13 -5
- OpenApiLibCore/request_data.py +67 -102
- OpenApiLibCore/resource_relations.py +6 -5
- OpenApiLibCore/validation.py +50 -167
- OpenApiLibCore/value_utils.py +46 -358
- openapi_libgen/__init__.py +0 -46
- openapi_libgen/command_line.py +7 -19
- openapi_libgen/generator.py +84 -0
- openapi_libgen/parsing_utils.py +9 -5
- openapi_libgen/spec_parser.py +41 -114
- {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/METADATA +2 -1
- robotframework_openapitools-1.0.0b5.dist-info/RECORD +40 -0
- robotframework_openapitools-1.0.0b3.dist-info/RECORD +0 -37
- {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/LICENSE +0 -0
- {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/WHEEL +0 -0
- {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/entry_points.txt +0 -0
OpenApiLibCore/dto_base.py
CHANGED
@@ -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
|
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
|
90
|
+
def get_path_relations_for_error_code(
|
156
91
|
self, error_code: int
|
157
|
-
) -> list[
|
92
|
+
) -> list[PathPropertiesConstraint]:
|
158
93
|
"""Return the list of Relations associated with the given error_code."""
|
159
|
-
relations: list[
|
94
|
+
relations: list[PathPropertiesConstraint] = [
|
160
95
|
r
|
161
|
-
for r in self.
|
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
|
172
|
-
"""Return the list of Relations for the
|
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
|
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.
|
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
|
-
|
196
|
-
|
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:
|
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
|
-
|
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
|
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
|
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}:
|
236
|
-
f"{
|
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
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
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
|
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 =
|
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 =
|
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
|
OpenApiLibCore/dto_utils.py
CHANGED
@@ -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
|
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()
|