jentic-openapi-datamodels 1.0.0a2__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.
@@ -0,0 +1,626 @@
1
+ """
2
+ OpenAPI 3.0.4 Schema Object model.
3
+
4
+ The Schema Object allows the definition of input and output data types.
5
+ These types can be objects, but also primitives and arrays.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Mapping, Sequence
11
+ from typing import Any
12
+
13
+ from jentic.apitools.openapi.datamodels.low.v30.discriminator import Discriminator
14
+ from jentic.apitools.openapi.datamodels.low.v30.external_documentation import (
15
+ ExternalDocumentation,
16
+ )
17
+ from jentic.apitools.openapi.datamodels.low.v30.reference import Reference
18
+ from jentic.apitools.openapi.datamodels.low.v30.specification_object import SpecificationObject
19
+ from jentic.apitools.openapi.datamodels.low.v30.xml import XML
20
+
21
+
22
+ __all__ = ["Schema"]
23
+
24
+
25
+ class Schema(SpecificationObject):
26
+ """
27
+ Represents a Schema Object from OpenAPI 3.0.4.
28
+
29
+ The Schema Object allows the definition of input and output data types.
30
+ Based on a subset of JSON Schema Draft 4/5 with OpenAPI-specific extensions.
31
+
32
+ Supports specification extensions (x-* fields).
33
+
34
+ Example:
35
+ >>> # String schema with constraints
36
+ >>> schema = Schema({
37
+ ... "type": "string",
38
+ ... "minLength": 1,
39
+ ... "maxLength": 100
40
+ ... })
41
+ >>> schema.type
42
+ 'string'
43
+
44
+ >>> # Object schema with properties
45
+ >>> schema = Schema({
46
+ ... "type": "object",
47
+ ... "required": ["name"],
48
+ ... "properties": {
49
+ ... "name": {"type": "string"},
50
+ ... "age": {"type": "integer"}
51
+ ... }
52
+ ... })
53
+ >>> schema.required
54
+ ['name']
55
+
56
+ >>> # Schema with discriminator
57
+ >>> schema = Schema({
58
+ ... "discriminator": {
59
+ ... "propertyName": "petType"
60
+ ... }
61
+ ... })
62
+ >>> schema.discriminator.property_name
63
+ 'petType'
64
+ """
65
+
66
+ _supports_extensions: bool = True
67
+ _fixed_fields: frozenset[str] = frozenset(
68
+ {
69
+ # JSON Schema Core
70
+ "title",
71
+ "multipleOf",
72
+ "maximum",
73
+ "exclusiveMaximum",
74
+ "minimum",
75
+ "exclusiveMinimum",
76
+ "maxLength",
77
+ "minLength",
78
+ "pattern",
79
+ "maxItems",
80
+ "minItems",
81
+ "uniqueItems",
82
+ "maxProperties",
83
+ "minProperties",
84
+ "required",
85
+ "enum",
86
+ # JSON Schema Type
87
+ "type",
88
+ "allOf",
89
+ "oneOf",
90
+ "anyOf",
91
+ "not",
92
+ "items",
93
+ "properties",
94
+ "additionalProperties",
95
+ # JSON Schema Metadata
96
+ "description",
97
+ "format",
98
+ "default",
99
+ # OpenAPI Extensions
100
+ "nullable",
101
+ "discriminator",
102
+ "readOnly",
103
+ "writeOnly",
104
+ "xml",
105
+ "externalDocs",
106
+ "example",
107
+ "deprecated",
108
+ }
109
+ )
110
+
111
+ def __init__(self, data: Mapping[str, Any] | None = None):
112
+ """
113
+ Initialize a Schema object.
114
+
115
+ Automatically marshals nested objects (discriminator, xml, externalDocs, and nested schemas).
116
+
117
+ Args:
118
+ data: Optional mapping to initialize the object with
119
+ """
120
+ super().__init__()
121
+ if data:
122
+ for key, value in data.items():
123
+ # Marshal specific nested objects
124
+ if (
125
+ key == "discriminator"
126
+ and isinstance(value, Mapping)
127
+ and not isinstance(value, Discriminator)
128
+ ):
129
+ self[key] = Discriminator(value)
130
+ elif key == "xml" and isinstance(value, Mapping) and not isinstance(value, XML):
131
+ self[key] = XML(value)
132
+ elif (
133
+ key == "externalDocs"
134
+ and isinstance(value, Mapping)
135
+ and not isinstance(value, ExternalDocumentation)
136
+ ):
137
+ self[key] = ExternalDocumentation(value)
138
+ # Unmarshal schema composition lists (allOf, oneOf, anyOf)
139
+ elif (
140
+ key in ("allOf", "oneOf", "anyOf")
141
+ and isinstance(value, Sequence)
142
+ and not isinstance(value, str)
143
+ ):
144
+ self[key] = [self._unmarshal_schema_or_reference(item) for item in value]
145
+ # Unmarshal single schema fields (not, items)
146
+ elif key in ("not", "items") and isinstance(value, Mapping):
147
+ self[key] = self._unmarshal_schema_or_reference(value)
148
+ # Unmarshal properties dict
149
+ elif key == "properties" and isinstance(value, Mapping):
150
+ self[key] = {
151
+ k: self._unmarshal_schema_or_reference(v) for k, v in value.items()
152
+ }
153
+ # Unmarshal additionalProperties (can be bool or schema)
154
+ elif key == "additionalProperties":
155
+ if isinstance(value, bool):
156
+ self[key] = value
157
+ elif isinstance(value, Mapping):
158
+ self[key] = self._unmarshal_schema_or_reference(value)
159
+ else:
160
+ self[key] = self._copy_value(value)
161
+ else:
162
+ # Store as-is (with defensive copy)
163
+ self[key] = self._copy_value(value)
164
+
165
+ # JSON Schema Core - Metadata
166
+ @property
167
+ def title(self) -> str | None:
168
+ """A title for the schema."""
169
+ return self.get("title")
170
+
171
+ @title.setter
172
+ def title(self, value: str | None) -> None:
173
+ if value is None:
174
+ self.pop("title", None)
175
+ else:
176
+ self["title"] = value
177
+
178
+ # JSON Schema Core - Numeric validation
179
+ @property
180
+ def multiple_of(self) -> float | int | None:
181
+ """Value must be multiple of this number."""
182
+ return self.get("multipleOf")
183
+
184
+ @multiple_of.setter
185
+ def multiple_of(self, value: float | int | None) -> None:
186
+ if value is None:
187
+ self.pop("multipleOf", None)
188
+ else:
189
+ self["multipleOf"] = value
190
+
191
+ @property
192
+ def maximum(self) -> float | int | None:
193
+ """Maximum value (inclusive)."""
194
+ return self.get("maximum")
195
+
196
+ @maximum.setter
197
+ def maximum(self, value: float | int | None) -> None:
198
+ if value is None:
199
+ self.pop("maximum", None)
200
+ else:
201
+ self["maximum"] = value
202
+
203
+ @property
204
+ def exclusive_maximum(self) -> float | int | None:
205
+ """Maximum value (exclusive)."""
206
+ return self.get("exclusiveMaximum")
207
+
208
+ @exclusive_maximum.setter
209
+ def exclusive_maximum(self, value: float | int | None) -> None:
210
+ if value is None:
211
+ self.pop("exclusiveMaximum", None)
212
+ else:
213
+ self["exclusiveMaximum"] = value
214
+
215
+ @property
216
+ def minimum(self) -> float | int | None:
217
+ """Minimum value (inclusive)."""
218
+ return self.get("minimum")
219
+
220
+ @minimum.setter
221
+ def minimum(self, value: float | int | None) -> None:
222
+ if value is None:
223
+ self.pop("minimum", None)
224
+ else:
225
+ self["minimum"] = value
226
+
227
+ @property
228
+ def exclusive_minimum(self) -> float | int | None:
229
+ """Minimum value (exclusive)."""
230
+ return self.get("exclusiveMinimum")
231
+
232
+ @exclusive_minimum.setter
233
+ def exclusive_minimum(self, value: float | int | None) -> None:
234
+ if value is None:
235
+ self.pop("exclusiveMinimum", None)
236
+ else:
237
+ self["exclusiveMinimum"] = value
238
+
239
+ # String validation properties
240
+ @property
241
+ def max_length(self) -> int | None:
242
+ """Maximum string length."""
243
+ return self.get("maxLength")
244
+
245
+ @max_length.setter
246
+ def max_length(self, value: int | None) -> None:
247
+ if value is None:
248
+ self.pop("maxLength", None)
249
+ else:
250
+ self["maxLength"] = value
251
+
252
+ @property
253
+ def min_length(self) -> int | None:
254
+ """Minimum string length."""
255
+ return self.get("minLength")
256
+
257
+ @min_length.setter
258
+ def min_length(self, value: int | None) -> None:
259
+ if value is None:
260
+ self.pop("minLength", None)
261
+ else:
262
+ self["minLength"] = value
263
+
264
+ @property
265
+ def pattern(self) -> str | None:
266
+ """Regular expression pattern for string validation."""
267
+ return self.get("pattern")
268
+
269
+ @pattern.setter
270
+ def pattern(self, value: str | None) -> None:
271
+ if value is None:
272
+ self.pop("pattern", None)
273
+ else:
274
+ self["pattern"] = value
275
+
276
+ # Array validation properties
277
+ @property
278
+ def max_items(self) -> int | None:
279
+ """Maximum number of array items."""
280
+ return self.get("maxItems")
281
+
282
+ @max_items.setter
283
+ def max_items(self, value: int | None) -> None:
284
+ if value is None:
285
+ self.pop("maxItems", None)
286
+ else:
287
+ self["maxItems"] = value
288
+
289
+ @property
290
+ def min_items(self) -> int | None:
291
+ """Minimum number of array items."""
292
+ return self.get("minItems")
293
+
294
+ @min_items.setter
295
+ def min_items(self, value: int | None) -> None:
296
+ if value is None:
297
+ self.pop("minItems", None)
298
+ else:
299
+ self["minItems"] = value
300
+
301
+ @property
302
+ def unique_items(self) -> bool | None:
303
+ """Whether array items must be unique."""
304
+ return self.get("uniqueItems")
305
+
306
+ @unique_items.setter
307
+ def unique_items(self, value: bool | None) -> None:
308
+ if value is None:
309
+ self.pop("uniqueItems", None)
310
+ else:
311
+ self["uniqueItems"] = value
312
+
313
+ # JSON Schema Core - Object validation
314
+ @property
315
+ def max_properties(self) -> int | None:
316
+ """Maximum number of object properties."""
317
+ return self.get("maxProperties")
318
+
319
+ @max_properties.setter
320
+ def max_properties(self, value: int | None) -> None:
321
+ if value is None:
322
+ self.pop("maxProperties", None)
323
+ else:
324
+ self["maxProperties"] = value
325
+
326
+ @property
327
+ def min_properties(self) -> int | None:
328
+ """Minimum number of object properties."""
329
+ return self.get("minProperties")
330
+
331
+ @min_properties.setter
332
+ def min_properties(self, value: int | None) -> None:
333
+ if value is None:
334
+ self.pop("minProperties", None)
335
+ else:
336
+ self["minProperties"] = value
337
+
338
+ @property
339
+ def required(self) -> list[str] | None:
340
+ """List of required property names (for object types)."""
341
+ return self.get("required")
342
+
343
+ @required.setter
344
+ def required(self, value: list[str] | None) -> None:
345
+ if value is None:
346
+ self.pop("required", None)
347
+ else:
348
+ self["required"] = value
349
+
350
+ @property
351
+ def enum(self) -> list[Any] | None:
352
+ """Allowed values."""
353
+ return self.get("enum")
354
+
355
+ @enum.setter
356
+ def enum(self, value: list[Any] | None) -> None:
357
+ if value is None:
358
+ self.pop("enum", None)
359
+ else:
360
+ self["enum"] = value
361
+
362
+ # JSON Schema Type
363
+ @property
364
+ def type(self) -> str | None:
365
+ """Type of the schema (string, number, integer, boolean, array, object, null)."""
366
+ return self.get("type")
367
+
368
+ @type.setter
369
+ def type(self, value: str | None) -> None:
370
+ if value is None:
371
+ self.pop("type", None)
372
+ else:
373
+ self["type"] = value
374
+
375
+ @property
376
+ def all_of(self) -> list[Schema | Reference] | None:
377
+ """Schemas that must all be valid (list of Schema or Reference objects)."""
378
+ return self.get("allOf")
379
+
380
+ @all_of.setter
381
+ def all_of(self, value: list[Schema | Reference] | None) -> None:
382
+ if value is None:
383
+ self.pop("allOf", None)
384
+ else:
385
+ self["allOf"] = value
386
+
387
+ @property
388
+ def one_of(self) -> list[Schema | Reference] | None:
389
+ """Schemas where exactly one must be valid (list of Schema or Reference objects)."""
390
+ return self.get("oneOf")
391
+
392
+ @one_of.setter
393
+ def one_of(self, value: list[Schema | Reference] | None) -> None:
394
+ if value is None:
395
+ self.pop("oneOf", None)
396
+ else:
397
+ self["oneOf"] = value
398
+
399
+ @property
400
+ def any_of(self) -> list[Schema | Reference] | None:
401
+ """Schemas where at least one must be valid (list of Schema or Reference objects)."""
402
+ return self.get("anyOf")
403
+
404
+ @any_of.setter
405
+ def any_of(self, value: list[Schema | Reference] | None) -> None:
406
+ if value is None:
407
+ self.pop("anyOf", None)
408
+ else:
409
+ self["anyOf"] = value
410
+
411
+ @property
412
+ def not_(self) -> Schema | Reference | None:
413
+ """Schema that must NOT be valid."""
414
+ return self.get("not")
415
+
416
+ @not_.setter
417
+ def not_(self, value: Schema | Reference | None) -> None:
418
+ if value is None:
419
+ self.pop("not", None)
420
+ else:
421
+ self["not"] = value
422
+
423
+ @property
424
+ def items_(self) -> Schema | Reference | None:
425
+ """
426
+ Schema for array items (Schema or Reference object).
427
+
428
+ Note: Property named 'items_' (with underscore) to avoid conflict with
429
+ MutableMapping.items() method. Dict access still uses the standard field name:
430
+ schema["items"].
431
+ """
432
+ return self.get("items")
433
+
434
+ @items_.setter
435
+ def items_(self, value: Schema | Reference | None) -> None:
436
+ if value is None:
437
+ self.pop("items", None)
438
+ else:
439
+ self["items"] = value
440
+
441
+ @property
442
+ def properties(self) -> dict[str, Schema | Reference] | None:
443
+ """Object properties (map of property name to Schema or Reference)."""
444
+ return self.get("properties")
445
+
446
+ @properties.setter
447
+ def properties(self, value: Mapping[str, Schema | Reference] | None) -> None:
448
+ if value is None:
449
+ self.pop("properties", None)
450
+ else:
451
+ self["properties"] = dict(value) if isinstance(value, Mapping) else value
452
+
453
+ @property
454
+ def additional_properties(self) -> bool | Schema | Reference | None:
455
+ """Schema for additional properties (bool or Schema or Reference object)."""
456
+ return self.get("additionalProperties")
457
+
458
+ @additional_properties.setter
459
+ def additional_properties(self, value: bool | Schema | Reference | None) -> None:
460
+ if value is None:
461
+ self.pop("additionalProperties", None)
462
+ else:
463
+ self["additionalProperties"] = value
464
+
465
+ # JSON Schema Metadata
466
+ @property
467
+ def description(self) -> str | None:
468
+ """A description of the schema."""
469
+ return self.get("description")
470
+
471
+ @description.setter
472
+ def description(self, value: str | None) -> None:
473
+ if value is None:
474
+ self.pop("description", None)
475
+ else:
476
+ self["description"] = value
477
+
478
+ @property
479
+ def format(self) -> str | None:
480
+ """Format hint for the type (e.g., date-time, email, uuid)."""
481
+ return self.get("format")
482
+
483
+ @format.setter
484
+ def format(self, value: str | None) -> None:
485
+ if value is None:
486
+ self.pop("format", None)
487
+ else:
488
+ self["format"] = value
489
+
490
+ @property
491
+ def default(self) -> Any:
492
+ """Default value."""
493
+ return self.get("default")
494
+
495
+ @default.setter
496
+ def default(self, value: Any) -> None:
497
+ if value is None:
498
+ self.pop("default", None)
499
+ else:
500
+ self["default"] = value
501
+
502
+ # OpenAPI Extensions
503
+ @property
504
+ def nullable(self) -> bool | None:
505
+ """Whether the value can be null (OpenAPI extension)."""
506
+ return self.get("nullable")
507
+
508
+ @nullable.setter
509
+ def nullable(self, value: bool | None) -> None:
510
+ if value is None:
511
+ self.pop("nullable", None)
512
+ else:
513
+ self["nullable"] = value
514
+
515
+ @property
516
+ def discriminator(self) -> Discriminator | None:
517
+ """Discriminator for polymorphism (OpenAPI extension)."""
518
+ return self.get("discriminator")
519
+
520
+ @discriminator.setter
521
+ def discriminator(self, value: Discriminator | None) -> None:
522
+ if value is None:
523
+ self.pop("discriminator", None)
524
+ else:
525
+ self["discriminator"] = value
526
+
527
+ @property
528
+ def read_only(self) -> bool | None:
529
+ """Whether the property is read-only (OpenAPI extension)."""
530
+ return self.get("readOnly")
531
+
532
+ @read_only.setter
533
+ def read_only(self, value: bool | None) -> None:
534
+ if value is None:
535
+ self.pop("readOnly", None)
536
+ else:
537
+ self["readOnly"] = value
538
+
539
+ @property
540
+ def write_only(self) -> bool | None:
541
+ """Whether the property is write-only (OpenAPI extension)."""
542
+ return self.get("writeOnly")
543
+
544
+ @write_only.setter
545
+ def write_only(self, value: bool | None) -> None:
546
+ if value is None:
547
+ self.pop("writeOnly", None)
548
+ else:
549
+ self["writeOnly"] = value
550
+
551
+ @property
552
+ def xml(self) -> XML | None:
553
+ """XML representation metadata (OpenAPI extension)."""
554
+ return self.get("xml")
555
+
556
+ @xml.setter
557
+ def xml(self, value: XML | None) -> None:
558
+ if value is None:
559
+ self.pop("xml", None)
560
+ else:
561
+ self["xml"] = value
562
+
563
+ @property
564
+ def external_docs(self) -> ExternalDocumentation | None:
565
+ """External documentation (OpenAPI extension)."""
566
+ return self.get("externalDocs")
567
+
568
+ @external_docs.setter
569
+ def external_docs(self, value: ExternalDocumentation | None) -> None:
570
+ if value is None:
571
+ self.pop("externalDocs", None)
572
+ else:
573
+ self["externalDocs"] = value
574
+
575
+ @property
576
+ def example(self) -> Any:
577
+ """Example value (OpenAPI extension)."""
578
+ return self.get("example")
579
+
580
+ @example.setter
581
+ def example(self, value: Any) -> None:
582
+ if value is None:
583
+ self.pop("example", None)
584
+ else:
585
+ self["example"] = value
586
+
587
+ @property
588
+ def deprecated(self) -> bool | None:
589
+ """Whether the schema is deprecated (OpenAPI extension)."""
590
+ return self.get("deprecated")
591
+
592
+ @deprecated.setter
593
+ def deprecated(self, value: bool | None) -> None:
594
+ if value is None:
595
+ self.pop("deprecated", None)
596
+ else:
597
+ self["deprecated"] = value
598
+
599
+ # Helper methods
600
+ @classmethod
601
+ def _unmarshal_schema_or_reference(cls, value: Any) -> Schema | Reference | Any:
602
+ """
603
+ Unmarshal a value into Schema or Reference if it's a Mapping.
604
+
605
+ Converts raw data (dict/Mapping) into Schema or Reference objects during construction.
606
+ Uses the actual class (or subclass) for creating Schema instances.
607
+
608
+ Args:
609
+ value: Value to unmarshal
610
+
611
+ Returns:
612
+ Schema or Reference instance, or value as-is
613
+ """
614
+ if not isinstance(value, Mapping):
615
+ return value
616
+
617
+ # Already unmarshaled
618
+ if isinstance(value, (Schema, Reference)):
619
+ return value
620
+
621
+ # Check if it's a reference (has $ref field)
622
+ if "$ref" in value:
623
+ return Reference(value)
624
+ else:
625
+ # Otherwise treat as Schema (or subclass)
626
+ return cls(value)