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
@@ -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]