robotframework-openapitools 0.3.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.
- OpenApiDriver/__init__.py +45 -41
- OpenApiDriver/openapi_executors.py +83 -49
- OpenApiDriver/openapi_reader.py +114 -116
- OpenApiDriver/openapidriver.libspec +209 -133
- OpenApiDriver/openapidriver.py +31 -296
- OpenApiLibCore/__init__.py +39 -13
- OpenApiLibCore/annotations.py +10 -0
- OpenApiLibCore/data_generation/__init__.py +10 -0
- OpenApiLibCore/data_generation/body_data_generation.py +250 -0
- OpenApiLibCore/data_generation/data_generation_core.py +233 -0
- OpenApiLibCore/data_invalidation.py +294 -0
- OpenApiLibCore/dto_base.py +75 -129
- OpenApiLibCore/dto_utils.py +125 -85
- OpenApiLibCore/localized_faker.py +88 -0
- OpenApiLibCore/models.py +723 -0
- OpenApiLibCore/oas_cache.py +14 -13
- OpenApiLibCore/openapi_libcore.libspec +363 -322
- OpenApiLibCore/openapi_libcore.py +388 -1903
- OpenApiLibCore/parameter_utils.py +97 -0
- OpenApiLibCore/path_functions.py +215 -0
- OpenApiLibCore/path_invalidation.py +42 -0
- OpenApiLibCore/protocols.py +38 -0
- OpenApiLibCore/request_data.py +246 -0
- OpenApiLibCore/resource_relations.py +55 -0
- OpenApiLibCore/validation.py +380 -0
- OpenApiLibCore/value_utils.py +216 -481
- openapi_libgen/__init__.py +3 -0
- openapi_libgen/command_line.py +75 -0
- openapi_libgen/generator.py +82 -0
- openapi_libgen/parsing_utils.py +30 -0
- openapi_libgen/spec_parser.py +154 -0
- openapi_libgen/templates/__init__.jinja +3 -0
- openapi_libgen/templates/library.jinja +30 -0
- robotframework_openapitools-1.0.0.dist-info/METADATA +249 -0
- robotframework_openapitools-1.0.0.dist-info/RECORD +40 -0
- {robotframework_openapitools-0.3.0.dist-info → robotframework_openapitools-1.0.0.dist-info}/WHEEL +1 -1
- robotframework_openapitools-1.0.0.dist-info/entry_points.txt +3 -0
- roboswag/__init__.py +0 -9
- roboswag/__main__.py +0 -3
- roboswag/auth.py +0 -44
- roboswag/cli.py +0 -80
- roboswag/core.py +0 -85
- roboswag/generate/__init__.py +0 -1
- roboswag/generate/generate.py +0 -121
- roboswag/generate/models/__init__.py +0 -0
- roboswag/generate/models/api.py +0 -219
- roboswag/generate/models/definition.py +0 -28
- roboswag/generate/models/endpoint.py +0 -68
- roboswag/generate/models/parameter.py +0 -25
- roboswag/generate/models/response.py +0 -8
- roboswag/generate/models/tag.py +0 -16
- roboswag/generate/models/utils.py +0 -60
- roboswag/generate/templates/api_init.jinja +0 -15
- roboswag/generate/templates/models.jinja +0 -7
- roboswag/generate/templates/paths.jinja +0 -68
- roboswag/logger.py +0 -33
- roboswag/validate/__init__.py +0 -6
- roboswag/validate/core.py +0 -3
- roboswag/validate/schema.py +0 -21
- roboswag/validate/text_response.py +0 -14
- robotframework_openapitools-0.3.0.dist-info/METADATA +0 -41
- robotframework_openapitools-0.3.0.dist-info/RECORD +0 -41
- {robotframework_openapitools-0.3.0.dist-info → robotframework_openapitools-1.0.0.dist-info}/LICENSE +0 -0
OpenApiLibCore/models.py
ADDED
@@ -0,0 +1,723 @@
|
|
1
|
+
import base64
|
2
|
+
from abc import abstractmethod
|
3
|
+
from collections import ChainMap
|
4
|
+
from functools import cached_property
|
5
|
+
from random import choice, randint, uniform
|
6
|
+
from sys import float_info
|
7
|
+
from typing import (
|
8
|
+
Generator,
|
9
|
+
Generic,
|
10
|
+
Literal,
|
11
|
+
Mapping,
|
12
|
+
TypeAlias,
|
13
|
+
TypeVar,
|
14
|
+
)
|
15
|
+
|
16
|
+
import rstr
|
17
|
+
from pydantic import BaseModel, Field, RootModel
|
18
|
+
from robot.api import logger
|
19
|
+
|
20
|
+
from OpenApiLibCore.annotations import JSON
|
21
|
+
from OpenApiLibCore.localized_faker import FAKE, fake_string
|
22
|
+
|
23
|
+
EPSILON = float_info.epsilon
|
24
|
+
|
25
|
+
O = TypeVar("O")
|
26
|
+
|
27
|
+
|
28
|
+
class SchemaBase(BaseModel, Generic[O], frozen=True):
|
29
|
+
readOnly: bool = False
|
30
|
+
writeOnly: bool = False
|
31
|
+
|
32
|
+
@abstractmethod
|
33
|
+
def get_valid_value(self) -> JSON: ...
|
34
|
+
|
35
|
+
@abstractmethod
|
36
|
+
def get_values_out_of_bounds(self, current_value: O) -> list[O]: ...
|
37
|
+
|
38
|
+
@abstractmethod
|
39
|
+
def get_invalid_value_from_const_or_enum(self) -> O: ...
|
40
|
+
|
41
|
+
|
42
|
+
class NullSchema(SchemaBase[None], frozen=True):
|
43
|
+
type: Literal["null"] = "null"
|
44
|
+
|
45
|
+
def get_valid_value(self) -> None:
|
46
|
+
return None
|
47
|
+
|
48
|
+
def get_values_out_of_bounds(self, current_value: None) -> list[None]:
|
49
|
+
raise ValueError
|
50
|
+
|
51
|
+
def get_invalid_value_from_const_or_enum(self) -> None:
|
52
|
+
raise ValueError
|
53
|
+
|
54
|
+
@property
|
55
|
+
def can_be_invalidated(self) -> bool:
|
56
|
+
return False
|
57
|
+
|
58
|
+
@property
|
59
|
+
def annotation_string(self) -> str:
|
60
|
+
return "None"
|
61
|
+
|
62
|
+
|
63
|
+
class BooleanSchema(SchemaBase[bool], frozen=True):
|
64
|
+
type: Literal["boolean"] = "boolean"
|
65
|
+
const: bool | None = None
|
66
|
+
nullable: bool = False
|
67
|
+
|
68
|
+
def get_valid_value(self) -> bool:
|
69
|
+
if self.const is not None:
|
70
|
+
return self.const
|
71
|
+
return choice([True, False])
|
72
|
+
|
73
|
+
def get_values_out_of_bounds(self, current_value: bool) -> list[bool]:
|
74
|
+
raise ValueError
|
75
|
+
|
76
|
+
def get_invalid_value_from_const_or_enum(self) -> bool:
|
77
|
+
if self.const is not None:
|
78
|
+
return not self.const
|
79
|
+
raise ValueError
|
80
|
+
|
81
|
+
@property
|
82
|
+
def can_be_invalidated(self) -> bool:
|
83
|
+
return True
|
84
|
+
|
85
|
+
@property
|
86
|
+
def annotation_string(self) -> str:
|
87
|
+
return "bool"
|
88
|
+
|
89
|
+
|
90
|
+
class StringSchema(SchemaBase[str], frozen=True):
|
91
|
+
type: Literal["string"] = "string"
|
92
|
+
format: str = ""
|
93
|
+
pattern: str = ""
|
94
|
+
maxLength: int | None = None
|
95
|
+
minLength: int | None = None
|
96
|
+
const: str | None = None
|
97
|
+
enum: list[str] | None = None
|
98
|
+
nullable: bool = False
|
99
|
+
|
100
|
+
def get_valid_value(self) -> bytes | str:
|
101
|
+
"""Generate a random string within the min/max length in the schema, if specified."""
|
102
|
+
if self.const is not None:
|
103
|
+
return self.const
|
104
|
+
if self.enum is not None:
|
105
|
+
return choice(self.enum)
|
106
|
+
# if a pattern is provided, format and min/max length can be ignored
|
107
|
+
if pattern := self.pattern:
|
108
|
+
return rstr.xeger(pattern)
|
109
|
+
minimum = self.minLength if self.minLength is not None else 0
|
110
|
+
maximum = self.maxLength if self.maxLength is not None else 36
|
111
|
+
maximum = max(minimum, maximum)
|
112
|
+
format_ = self.format if self.format else "uuid"
|
113
|
+
# byte is a special case due to the required encoding
|
114
|
+
if format_ == "byte":
|
115
|
+
data = FAKE.uuid()
|
116
|
+
return base64.b64encode(data.encode("utf-8"))
|
117
|
+
value = fake_string(string_format=format_)
|
118
|
+
while len(value) < minimum:
|
119
|
+
# use fake.name() to ensure the returned string uses the provided locale
|
120
|
+
value = value + FAKE.name()
|
121
|
+
if len(value) > maximum:
|
122
|
+
value = value[:maximum]
|
123
|
+
return value
|
124
|
+
|
125
|
+
def get_values_out_of_bounds(self, current_value: str) -> list[str]:
|
126
|
+
invalid_values: list[str] = []
|
127
|
+
if self.minLength:
|
128
|
+
invalid_values.append(current_value[0 : self.minLength - 1])
|
129
|
+
# if there is a maximum length, send 1 character more
|
130
|
+
if self.maxLength:
|
131
|
+
invalid_string_value = current_value if current_value else "x"
|
132
|
+
# add random characters from the current value to prevent adding new characters
|
133
|
+
while len(invalid_string_value) <= self.maxLength:
|
134
|
+
invalid_string_value += choice(invalid_string_value)
|
135
|
+
invalid_values.append(invalid_string_value)
|
136
|
+
if invalid_values:
|
137
|
+
return invalid_values
|
138
|
+
raise ValueError
|
139
|
+
|
140
|
+
def get_invalid_value_from_const_or_enum(self) -> str:
|
141
|
+
valid_values = []
|
142
|
+
if self.const is not None:
|
143
|
+
valid_values = [self.const]
|
144
|
+
if self.enum is not None:
|
145
|
+
valid_values = self.enum
|
146
|
+
|
147
|
+
if not valid_values:
|
148
|
+
raise ValueError
|
149
|
+
|
150
|
+
invalid_value = ""
|
151
|
+
for value in valid_values:
|
152
|
+
invalid_value += value + value
|
153
|
+
|
154
|
+
return invalid_value
|
155
|
+
|
156
|
+
@property
|
157
|
+
def can_be_invalidated(self) -> bool:
|
158
|
+
if (
|
159
|
+
self.maxLength is not None
|
160
|
+
or self.minLength is not None
|
161
|
+
or self.const is not None
|
162
|
+
or self.enum is not None
|
163
|
+
):
|
164
|
+
return True
|
165
|
+
return False
|
166
|
+
|
167
|
+
@property
|
168
|
+
def annotation_string(self) -> str:
|
169
|
+
return "str"
|
170
|
+
|
171
|
+
|
172
|
+
class IntegerSchema(SchemaBase[int], frozen=True):
|
173
|
+
type: Literal["integer"] = "integer"
|
174
|
+
format: str = "int32"
|
175
|
+
maximum: int | None = None
|
176
|
+
exclusiveMaximum: int | bool | None = None
|
177
|
+
minimum: int | None = None
|
178
|
+
exclusiveMinimum: int | bool | None = None
|
179
|
+
multipleOf: int | None = None # TODO: implement support
|
180
|
+
const: int | None = None
|
181
|
+
enum: list[int] | None = None
|
182
|
+
nullable: bool = False
|
183
|
+
|
184
|
+
@cached_property
|
185
|
+
def _max_int(self) -> int:
|
186
|
+
if self.format == "int64":
|
187
|
+
return 9223372036854775807
|
188
|
+
return 2147483647
|
189
|
+
|
190
|
+
@cached_property
|
191
|
+
def _min_int(self) -> int:
|
192
|
+
if self.format == "int64":
|
193
|
+
return -9223372036854775808
|
194
|
+
return -2147483648
|
195
|
+
|
196
|
+
@cached_property
|
197
|
+
def _max_value(self) -> int:
|
198
|
+
# OAS 3.0: exclusiveMinimum/Maximum is a bool in combination with minimum/maximum
|
199
|
+
# OAS 3.1: exclusiveMinimum/Maximum is an integer
|
200
|
+
if isinstance(self.exclusiveMaximum, int) and not isinstance(
|
201
|
+
self.exclusiveMaximum, bool
|
202
|
+
):
|
203
|
+
return self.exclusiveMaximum - 1
|
204
|
+
|
205
|
+
if isinstance(self.maximum, int):
|
206
|
+
if self.exclusiveMaximum is True:
|
207
|
+
return self.maximum - 1
|
208
|
+
return self.maximum
|
209
|
+
|
210
|
+
return self._max_int
|
211
|
+
|
212
|
+
@cached_property
|
213
|
+
def _min_value(self) -> int:
|
214
|
+
# OAS 3.0: exclusiveMinimum/Maximum is a bool in combination with minimum/maximum
|
215
|
+
# OAS 3.1: exclusiveMinimum/Maximum is an integer
|
216
|
+
if isinstance(self.exclusiveMinimum, int) and not isinstance(
|
217
|
+
self.exclusiveMinimum, bool
|
218
|
+
):
|
219
|
+
return self.exclusiveMinimum + 1
|
220
|
+
|
221
|
+
if isinstance(self.minimum, int):
|
222
|
+
if self.exclusiveMinimum is True:
|
223
|
+
return self.minimum + 1
|
224
|
+
return self.minimum
|
225
|
+
|
226
|
+
return self._min_int
|
227
|
+
|
228
|
+
def get_valid_value(self) -> int:
|
229
|
+
"""Generate a random int within the min/max range of the schema, if specified."""
|
230
|
+
if self.const is not None:
|
231
|
+
return self.const
|
232
|
+
if self.enum is not None:
|
233
|
+
return choice(self.enum)
|
234
|
+
|
235
|
+
return randint(self._min_value, self._max_value)
|
236
|
+
|
237
|
+
def get_values_out_of_bounds(self, current_value: int) -> list[int]: # pylint: disable=unused-argument
|
238
|
+
invalid_values: list[int] = []
|
239
|
+
|
240
|
+
if self._min_value > self._min_int:
|
241
|
+
invalid_values.append(self._min_value - 1)
|
242
|
+
|
243
|
+
if self._max_value < self._max_int:
|
244
|
+
invalid_values.append(self._max_value + 1)
|
245
|
+
|
246
|
+
if invalid_values:
|
247
|
+
return invalid_values
|
248
|
+
|
249
|
+
raise ValueError
|
250
|
+
|
251
|
+
def get_invalid_value_from_const_or_enum(self) -> int:
|
252
|
+
valid_values = []
|
253
|
+
if self.const is not None:
|
254
|
+
valid_values = [self.const]
|
255
|
+
if self.enum is not None:
|
256
|
+
valid_values = self.enum
|
257
|
+
|
258
|
+
if not valid_values:
|
259
|
+
raise ValueError
|
260
|
+
|
261
|
+
invalid_value = 0
|
262
|
+
for value in valid_values:
|
263
|
+
invalid_value += abs(value) + abs(value)
|
264
|
+
|
265
|
+
return invalid_value
|
266
|
+
|
267
|
+
@property
|
268
|
+
def can_be_invalidated(self) -> bool:
|
269
|
+
return True
|
270
|
+
|
271
|
+
@property
|
272
|
+
def annotation_string(self) -> str:
|
273
|
+
return "int"
|
274
|
+
|
275
|
+
|
276
|
+
class NumberSchema(SchemaBase[float], frozen=True):
|
277
|
+
type: Literal["number"] = "number"
|
278
|
+
maximum: int | float | None = None
|
279
|
+
exclusiveMaximum: int | float | bool | None = None
|
280
|
+
minimum: int | float | None = None
|
281
|
+
exclusiveMinimum: int | float | bool | None = None
|
282
|
+
multipleOf: int | None = None # TODO: implement support
|
283
|
+
const: int | float | None = None
|
284
|
+
enum: list[int | float] | None = None
|
285
|
+
nullable: bool = False
|
286
|
+
|
287
|
+
@cached_property
|
288
|
+
def _max_float(self) -> float:
|
289
|
+
return 9223372036854775807.0
|
290
|
+
|
291
|
+
@cached_property
|
292
|
+
def _min_float(self) -> float:
|
293
|
+
return -9223372036854775808.0
|
294
|
+
|
295
|
+
@cached_property
|
296
|
+
def _max_value(self) -> float:
|
297
|
+
# OAS 3.0: exclusiveMinimum/Maximum is a bool in combination with minimum/maximum
|
298
|
+
# OAS 3.1: exclusiveMinimum/Maximum is an integer or a float
|
299
|
+
if isinstance(self.exclusiveMaximum, (int, float)) and not isinstance(
|
300
|
+
self.exclusiveMaximum, bool
|
301
|
+
):
|
302
|
+
return self.exclusiveMaximum - 0.0000000001
|
303
|
+
|
304
|
+
if isinstance(self.maximum, (int, float)):
|
305
|
+
if self.exclusiveMaximum is True:
|
306
|
+
return self.maximum - 0.0000000001
|
307
|
+
return self.maximum
|
308
|
+
|
309
|
+
return self._max_float
|
310
|
+
|
311
|
+
@cached_property
|
312
|
+
def _min_value(self) -> float:
|
313
|
+
# OAS 3.0: exclusiveMinimum/Maximum is a bool in combination with minimum/maximum
|
314
|
+
# OAS 3.1: exclusiveMinimum/Maximum is an integer or a float
|
315
|
+
if isinstance(self.exclusiveMinimum, (int, float)) and not isinstance(
|
316
|
+
self.exclusiveMinimum, bool
|
317
|
+
):
|
318
|
+
return self.exclusiveMinimum + 0.0000000001
|
319
|
+
|
320
|
+
if isinstance(self.minimum, (int, float)):
|
321
|
+
if self.exclusiveMinimum is True:
|
322
|
+
return self.minimum + 0.0000000001
|
323
|
+
return self.minimum
|
324
|
+
|
325
|
+
return self._min_float
|
326
|
+
|
327
|
+
def get_valid_value(self) -> float:
|
328
|
+
"""Generate a random float within the min/max range of the schema, if specified."""
|
329
|
+
if self.const is not None:
|
330
|
+
return self.const
|
331
|
+
if self.enum is not None:
|
332
|
+
return choice(self.enum)
|
333
|
+
|
334
|
+
return uniform(self._min_value, self._max_value)
|
335
|
+
|
336
|
+
def get_values_out_of_bounds(self, current_value: float) -> list[float]: # pylint: disable=unused-argument
|
337
|
+
invalid_values: list[float] = []
|
338
|
+
|
339
|
+
if self._min_value > self._min_float:
|
340
|
+
invalid_values.append(self._min_value - 0.000000001)
|
341
|
+
|
342
|
+
if self._max_value < self._max_float:
|
343
|
+
invalid_values.append(self._max_value + 0.000000001)
|
344
|
+
|
345
|
+
if invalid_values:
|
346
|
+
return invalid_values
|
347
|
+
|
348
|
+
raise ValueError
|
349
|
+
|
350
|
+
def get_invalid_value_from_const_or_enum(self) -> float:
|
351
|
+
valid_values = []
|
352
|
+
if self.const is not None:
|
353
|
+
valid_values = [self.const]
|
354
|
+
if self.enum is not None:
|
355
|
+
valid_values = self.enum
|
356
|
+
|
357
|
+
if not valid_values:
|
358
|
+
raise ValueError
|
359
|
+
|
360
|
+
invalid_value = 0.0
|
361
|
+
for value in valid_values:
|
362
|
+
invalid_value += abs(value) + abs(value)
|
363
|
+
|
364
|
+
return invalid_value
|
365
|
+
|
366
|
+
@property
|
367
|
+
def can_be_invalidated(self) -> bool:
|
368
|
+
return True
|
369
|
+
|
370
|
+
@property
|
371
|
+
def annotation_string(self) -> str:
|
372
|
+
return "float"
|
373
|
+
|
374
|
+
|
375
|
+
class ArraySchema(SchemaBase[list[JSON]], frozen=True):
|
376
|
+
type: Literal["array"] = "array"
|
377
|
+
items: "SchemaObjectTypes"
|
378
|
+
maxItems: int | None = None
|
379
|
+
minItems: int | None = None
|
380
|
+
uniqueItems: bool = False
|
381
|
+
const: list[JSON] | None = None
|
382
|
+
enum: list[list[JSON]] | None = None
|
383
|
+
nullable: bool = False
|
384
|
+
|
385
|
+
def get_valid_value(self) -> list[JSON]:
|
386
|
+
if self.const is not None:
|
387
|
+
return self.const
|
388
|
+
|
389
|
+
if self.enum is not None:
|
390
|
+
return choice(self.enum)
|
391
|
+
|
392
|
+
minimum = self.minItems if self.minItems is not None else 0
|
393
|
+
maximum = self.maxItems if self.maxItems is not None else 1
|
394
|
+
maximum = max(minimum, maximum)
|
395
|
+
|
396
|
+
value: list[JSON] = []
|
397
|
+
for _ in range(maximum):
|
398
|
+
item_value = self.items.get_valid_value()
|
399
|
+
value.append(item_value)
|
400
|
+
return value
|
401
|
+
|
402
|
+
def get_values_out_of_bounds(self, current_value: list[JSON]) -> list[list[JSON]]:
|
403
|
+
invalid_values: list[list[JSON]] = []
|
404
|
+
|
405
|
+
if self.minItems:
|
406
|
+
invalid_value = current_value[0 : self.minItems - 1]
|
407
|
+
invalid_values.append(invalid_value)
|
408
|
+
|
409
|
+
if self.maxItems is not None:
|
410
|
+
invalid_value = []
|
411
|
+
if not current_value:
|
412
|
+
current_value = self.get_valid_value()
|
413
|
+
|
414
|
+
if not current_value:
|
415
|
+
current_value = [self.items.get_valid_value()]
|
416
|
+
|
417
|
+
while len(invalid_value) <= self.maxItems:
|
418
|
+
invalid_value.append(choice(current_value))
|
419
|
+
invalid_values.append(invalid_value)
|
420
|
+
|
421
|
+
if invalid_values:
|
422
|
+
return invalid_values
|
423
|
+
|
424
|
+
raise ValueError
|
425
|
+
|
426
|
+
def get_invalid_value_from_const_or_enum(self) -> list[JSON]:
|
427
|
+
valid_values = []
|
428
|
+
if self.const is not None:
|
429
|
+
valid_values = [self.const]
|
430
|
+
if self.enum is not None:
|
431
|
+
valid_values = self.enum
|
432
|
+
|
433
|
+
if not valid_values:
|
434
|
+
raise ValueError
|
435
|
+
|
436
|
+
invalid_value = []
|
437
|
+
for value in valid_values:
|
438
|
+
invalid_value.extend(value)
|
439
|
+
invalid_value.extend(value)
|
440
|
+
|
441
|
+
return invalid_value
|
442
|
+
|
443
|
+
@property
|
444
|
+
def can_be_invalidated(self) -> bool:
|
445
|
+
if (
|
446
|
+
self.maxItems is not None
|
447
|
+
or self.minItems is not None
|
448
|
+
or self.uniqueItems
|
449
|
+
or self.const is not None
|
450
|
+
or self.enum is not None
|
451
|
+
):
|
452
|
+
return True
|
453
|
+
if isinstance(self.items, (BooleanSchema, IntegerSchema, NumberSchema)):
|
454
|
+
return True
|
455
|
+
return False
|
456
|
+
|
457
|
+
@property
|
458
|
+
def annotation_string(self) -> str:
|
459
|
+
return f"list[{self.items.annotation_string}]"
|
460
|
+
|
461
|
+
|
462
|
+
class PropertiesMapping(RootModel[dict[str, "SchemaObjectTypes"]], frozen=True): ...
|
463
|
+
|
464
|
+
|
465
|
+
class ObjectSchema(SchemaBase[dict[str, JSON]], frozen=True):
|
466
|
+
type: Literal["object"] = "object"
|
467
|
+
properties: PropertiesMapping | None = None
|
468
|
+
additionalProperties: "bool | SchemaObjectTypes" = True
|
469
|
+
required: list[str] = []
|
470
|
+
maxProperties: int | None = None
|
471
|
+
minProperties: int | None = None
|
472
|
+
const: dict[str, JSON] | None = None
|
473
|
+
enum: list[dict[str, JSON]] | None = None
|
474
|
+
nullable: bool = False
|
475
|
+
|
476
|
+
def get_valid_value(self) -> dict[str, JSON]:
|
477
|
+
raise NotImplementedError
|
478
|
+
|
479
|
+
def get_values_out_of_bounds(
|
480
|
+
self, current_value: Mapping[str, JSON]
|
481
|
+
) -> list[dict[str, JSON]]:
|
482
|
+
raise ValueError
|
483
|
+
|
484
|
+
def get_invalid_value_from_const_or_enum(self) -> dict[str, JSON]:
|
485
|
+
valid_values = []
|
486
|
+
if self.const is not None:
|
487
|
+
valid_values = [self.const]
|
488
|
+
if self.enum is not None:
|
489
|
+
valid_values = self.enum
|
490
|
+
|
491
|
+
if not valid_values:
|
492
|
+
raise ValueError
|
493
|
+
|
494
|
+
# This invalidation will not work for a const and may not work for
|
495
|
+
# an enum. In that case a different invalidation approach will be used.
|
496
|
+
invalid_value = {**valid_values[0]}
|
497
|
+
for value in valid_values:
|
498
|
+
for key in invalid_value.keys():
|
499
|
+
invalid_value[key] = value.get(key)
|
500
|
+
if invalid_value not in valid_values:
|
501
|
+
return invalid_value
|
502
|
+
|
503
|
+
raise ValueError
|
504
|
+
|
505
|
+
@property
|
506
|
+
def can_be_invalidated(self) -> bool:
|
507
|
+
if (
|
508
|
+
self.required
|
509
|
+
or self.maxProperties is not None
|
510
|
+
or self.minProperties is not None
|
511
|
+
or self.const is not None
|
512
|
+
or self.enum is not None
|
513
|
+
):
|
514
|
+
return True
|
515
|
+
return False
|
516
|
+
|
517
|
+
@property
|
518
|
+
def annotation_string(self) -> str:
|
519
|
+
return "dict[str, Any]"
|
520
|
+
|
521
|
+
|
522
|
+
ResolvedSchemaObjectTypes: TypeAlias = (
|
523
|
+
NullSchema
|
524
|
+
| BooleanSchema
|
525
|
+
| StringSchema
|
526
|
+
| IntegerSchema
|
527
|
+
| NumberSchema
|
528
|
+
| ArraySchema
|
529
|
+
| ObjectSchema
|
530
|
+
)
|
531
|
+
|
532
|
+
|
533
|
+
class UnionTypeSchema(SchemaBase[JSON], frozen=True):
|
534
|
+
allOf: list["SchemaObjectTypes"] = []
|
535
|
+
anyOf: list["SchemaObjectTypes"] = []
|
536
|
+
oneOf: list["SchemaObjectTypes"] = []
|
537
|
+
|
538
|
+
def get_valid_value(self) -> JSON:
|
539
|
+
chosen_schema = choice(self.resolved_schemas)
|
540
|
+
return chosen_schema.get_valid_value()
|
541
|
+
|
542
|
+
def get_values_out_of_bounds(self, current_value: JSON) -> list[JSON]:
|
543
|
+
raise ValueError
|
544
|
+
|
545
|
+
@property
|
546
|
+
def resolved_schemas(self) -> list[ResolvedSchemaObjectTypes]:
|
547
|
+
return list(self._get_resolved_schemas())
|
548
|
+
|
549
|
+
def _get_resolved_schemas(self) -> Generator[ResolvedSchemaObjectTypes, None, None]:
|
550
|
+
if self.allOf:
|
551
|
+
properties_list: list[PropertiesMapping] = []
|
552
|
+
additional_properties_list = []
|
553
|
+
required_list = []
|
554
|
+
max_properties_list = []
|
555
|
+
min_properties_list = []
|
556
|
+
nullable_list = []
|
557
|
+
|
558
|
+
for schema in self.allOf:
|
559
|
+
if not isinstance(schema, ObjectSchema):
|
560
|
+
raise NotImplementedError("allOf only supported for ObjectSchemas")
|
561
|
+
|
562
|
+
if schema.const is not None:
|
563
|
+
raise ValueError("allOf and models with a const are not compatible")
|
564
|
+
|
565
|
+
if schema.enum:
|
566
|
+
raise ValueError("allOf and models with enums are not compatible")
|
567
|
+
|
568
|
+
if schema.properties:
|
569
|
+
properties_list.append(schema.properties)
|
570
|
+
additional_properties_list.append(schema.additionalProperties)
|
571
|
+
required_list += schema.required
|
572
|
+
max_properties_list.append(schema.maxProperties)
|
573
|
+
min_properties_list.append(schema.minProperties)
|
574
|
+
nullable_list.append(schema.nullable)
|
575
|
+
|
576
|
+
properties_dicts = [mapping.root for mapping in properties_list]
|
577
|
+
properties = dict(ChainMap(*properties_dicts))
|
578
|
+
|
579
|
+
if True in additional_properties_list:
|
580
|
+
additional_properties_value: bool | SchemaObjectTypes = True
|
581
|
+
else:
|
582
|
+
additional_properties_types = []
|
583
|
+
for additional_properties_item in additional_properties_list:
|
584
|
+
if isinstance(
|
585
|
+
additional_properties_item, ResolvedSchemaObjectTypes
|
586
|
+
):
|
587
|
+
additional_properties_types.append(additional_properties_item)
|
588
|
+
if not additional_properties_types:
|
589
|
+
additional_properties_value = False
|
590
|
+
else:
|
591
|
+
additional_properties_value = UnionTypeSchema(
|
592
|
+
anyOf=additional_properties_types,
|
593
|
+
)
|
594
|
+
|
595
|
+
max_properties = [max for max in max_properties_list if max is not None]
|
596
|
+
min_properties = [min for min in min_properties_list if min is not None]
|
597
|
+
max_propeties_value = max(max_properties) if max_properties else None
|
598
|
+
min_propeties_value = min(min_properties) if min_properties else None
|
599
|
+
|
600
|
+
merged_schema = ObjectSchema(
|
601
|
+
type="object",
|
602
|
+
properties=properties,
|
603
|
+
additionalProperties=additional_properties_value,
|
604
|
+
required=required_list,
|
605
|
+
maxProperties=max_propeties_value,
|
606
|
+
minProperties=min_propeties_value,
|
607
|
+
nullable=all(nullable_list),
|
608
|
+
)
|
609
|
+
yield merged_schema
|
610
|
+
else:
|
611
|
+
for schema in self.anyOf + self.oneOf:
|
612
|
+
if isinstance(schema, ResolvedSchemaObjectTypes):
|
613
|
+
yield schema
|
614
|
+
else:
|
615
|
+
yield from schema.resolved_schemas
|
616
|
+
|
617
|
+
def get_invalid_value_from_const_or_enum(self) -> JSON:
|
618
|
+
raise ValueError
|
619
|
+
|
620
|
+
@property
|
621
|
+
def annotation_string(self) -> str:
|
622
|
+
unique_annotations = {s.annotation_string for s in self.resolved_schemas}
|
623
|
+
return " | ".join(unique_annotations)
|
624
|
+
|
625
|
+
|
626
|
+
SchemaObjectTypes: TypeAlias = ResolvedSchemaObjectTypes | UnionTypeSchema
|
627
|
+
|
628
|
+
|
629
|
+
class ParameterObject(BaseModel):
|
630
|
+
name: str
|
631
|
+
in_: str = Field(..., alias="in")
|
632
|
+
required: bool = False
|
633
|
+
description: str = ""
|
634
|
+
schema_: SchemaObjectTypes | None = Field(None, alias="schema")
|
635
|
+
|
636
|
+
|
637
|
+
class MediaTypeObject(BaseModel):
|
638
|
+
schema_: SchemaObjectTypes | None = Field(None, alias="schema")
|
639
|
+
|
640
|
+
|
641
|
+
class RequestBodyObject(BaseModel):
|
642
|
+
content: dict[str, MediaTypeObject]
|
643
|
+
required: bool = False
|
644
|
+
description: str = ""
|
645
|
+
|
646
|
+
@cached_property
|
647
|
+
def schema_(self) -> SchemaObjectTypes | None:
|
648
|
+
if not self.mime_type:
|
649
|
+
return None
|
650
|
+
|
651
|
+
if len(self._json_schemas) > 1:
|
652
|
+
logger.info(
|
653
|
+
f"Multiple JSON media types defined for requestBody, "
|
654
|
+
f"using the first candidate from {self.content}"
|
655
|
+
)
|
656
|
+
return self._json_schemas[self.mime_type]
|
657
|
+
|
658
|
+
@cached_property
|
659
|
+
def mime_type(self) -> str | None:
|
660
|
+
if not self._json_schemas:
|
661
|
+
return None
|
662
|
+
|
663
|
+
return next(iter(self._json_schemas))
|
664
|
+
|
665
|
+
@cached_property
|
666
|
+
def _json_schemas(self) -> dict[str, SchemaObjectTypes]:
|
667
|
+
json_schemas = {
|
668
|
+
mime_type: media_type.schema_
|
669
|
+
for mime_type, media_type in self.content.items()
|
670
|
+
if "json" in mime_type and media_type.schema_ is not None
|
671
|
+
}
|
672
|
+
return json_schemas
|
673
|
+
|
674
|
+
|
675
|
+
class HeaderObject(BaseModel): ...
|
676
|
+
|
677
|
+
|
678
|
+
class LinkObject(BaseModel): ...
|
679
|
+
|
680
|
+
|
681
|
+
class ResponseObject(BaseModel):
|
682
|
+
description: str
|
683
|
+
content: dict[str, MediaTypeObject] = {}
|
684
|
+
headers: dict[str, HeaderObject] = {}
|
685
|
+
links: dict[str, LinkObject] = {}
|
686
|
+
|
687
|
+
|
688
|
+
class OperationObject(BaseModel):
|
689
|
+
operationId: str | None = None
|
690
|
+
summary: str = ""
|
691
|
+
description: str = ""
|
692
|
+
tags: list[str] = []
|
693
|
+
parameters: list[ParameterObject] | None = None
|
694
|
+
requestBody: RequestBodyObject | None = None
|
695
|
+
responses: dict[str, ResponseObject] = {}
|
696
|
+
|
697
|
+
|
698
|
+
class PathItemObject(BaseModel):
|
699
|
+
get: OperationObject | None = None
|
700
|
+
post: OperationObject | None = None
|
701
|
+
patch: OperationObject | None = None
|
702
|
+
put: OperationObject | None = None
|
703
|
+
delete: OperationObject | None = None
|
704
|
+
summary: str = ""
|
705
|
+
description: str = ""
|
706
|
+
parameters: list[ParameterObject] | None = None
|
707
|
+
|
708
|
+
def get_operations(self) -> dict[str, OperationObject]:
|
709
|
+
return {
|
710
|
+
k: v for k, v in self.__dict__.items() if isinstance(v, OperationObject)
|
711
|
+
}
|
712
|
+
|
713
|
+
|
714
|
+
class InfoObject(BaseModel):
|
715
|
+
title: str
|
716
|
+
version: str
|
717
|
+
summary: str = ""
|
718
|
+
description: str = ""
|
719
|
+
|
720
|
+
|
721
|
+
class OpenApiObject(BaseModel):
|
722
|
+
info: InfoObject
|
723
|
+
paths: dict[str, PathItemObject]
|