jentic-openapi-datamodels 1.0.0a18__py3-none-any.whl → 1.0.0a20__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 (73) hide show
  1. jentic/apitools/openapi/datamodels/low/extractors.py +3 -3
  2. jentic/apitools/openapi/datamodels/low/v30/__init__.py +76 -0
  3. jentic/apitools/openapi/datamodels/low/v30/builders/__init__.py +312 -0
  4. jentic/apitools/openapi/datamodels/low/v30/callback.py +131 -0
  5. jentic/apitools/openapi/datamodels/low/v30/components.py +236 -0
  6. jentic/apitools/openapi/datamodels/low/v30/contact.py +4 -10
  7. jentic/apitools/openapi/datamodels/low/v30/discriminator.py +4 -9
  8. jentic/apitools/openapi/datamodels/low/v30/encoding.py +81 -0
  9. jentic/apitools/openapi/datamodels/low/v30/example.py +91 -0
  10. jentic/apitools/openapi/datamodels/low/v30/external_documentation.py +4 -10
  11. jentic/apitools/openapi/datamodels/low/v30/header.py +120 -0
  12. jentic/apitools/openapi/datamodels/low/v30/info.py +14 -23
  13. jentic/apitools/openapi/datamodels/low/v30/license.py +4 -10
  14. jentic/apitools/openapi/datamodels/low/v30/link.py +141 -0
  15. jentic/apitools/openapi/datamodels/low/v30/media_type.py +110 -0
  16. jentic/apitools/openapi/datamodels/low/v30/oauth_flow.py +4 -10
  17. jentic/apitools/openapi/datamodels/low/v30/oauth_flows.py +7 -15
  18. jentic/apitools/openapi/datamodels/low/v30/openapi.py +149 -0
  19. jentic/apitools/openapi/datamodels/low/v30/operation.py +134 -0
  20. jentic/apitools/openapi/datamodels/low/v30/parameter.py +123 -0
  21. jentic/apitools/openapi/datamodels/low/v30/path_item.py +125 -0
  22. jentic/apitools/openapi/datamodels/low/v30/paths.py +108 -0
  23. jentic/apitools/openapi/datamodels/low/v30/reference.py +5 -9
  24. jentic/apitools/openapi/datamodels/low/v30/request_body.py +108 -0
  25. jentic/apitools/openapi/datamodels/low/v30/response.py +104 -0
  26. jentic/apitools/openapi/datamodels/low/v30/responses.py +109 -0
  27. jentic/apitools/openapi/datamodels/low/v30/schema.py +81 -97
  28. jentic/apitools/openapi/datamodels/low/v30/security_requirement.py +14 -9
  29. jentic/apitools/openapi/datamodels/low/v30/security_scheme.py +42 -22
  30. jentic/apitools/openapi/datamodels/low/v30/server.py +111 -0
  31. jentic/apitools/openapi/datamodels/low/v30/server_variable.py +4 -10
  32. jentic/apitools/openapi/datamodels/low/v30/tag.py +8 -46
  33. jentic/apitools/openapi/datamodels/low/v30/xml.py +4 -10
  34. jentic/apitools/openapi/datamodels/low/v31/__init__.py +77 -0
  35. jentic/apitools/openapi/datamodels/low/v31/builders/__init__.py +347 -0
  36. jentic/apitools/openapi/datamodels/low/v31/callback.py +131 -0
  37. jentic/apitools/openapi/datamodels/low/v31/components.py +240 -0
  38. jentic/apitools/openapi/datamodels/low/v31/contact.py +61 -0
  39. jentic/apitools/openapi/datamodels/low/v31/discriminator.py +62 -0
  40. jentic/apitools/openapi/datamodels/low/v31/encoding.py +81 -0
  41. jentic/apitools/openapi/datamodels/low/v31/example.py +91 -0
  42. jentic/apitools/openapi/datamodels/low/v31/external_documentation.py +59 -0
  43. jentic/apitools/openapi/datamodels/low/v31/header.py +120 -0
  44. jentic/apitools/openapi/datamodels/low/v31/info.py +116 -0
  45. jentic/apitools/openapi/datamodels/low/v31/license.py +61 -0
  46. jentic/apitools/openapi/datamodels/low/v31/link.py +141 -0
  47. jentic/apitools/openapi/datamodels/low/v31/media_type.py +110 -0
  48. jentic/apitools/openapi/datamodels/low/v31/oauth_flow.py +65 -0
  49. jentic/apitools/openapi/datamodels/low/v31/oauth_flows.py +108 -0
  50. jentic/apitools/openapi/datamodels/low/v31/openapi.py +168 -0
  51. jentic/apitools/openapi/datamodels/low/v31/operation.py +133 -0
  52. jentic/apitools/openapi/datamodels/low/v31/parameter.py +123 -0
  53. jentic/apitools/openapi/datamodels/low/v31/path_item.py +125 -0
  54. jentic/apitools/openapi/datamodels/low/v31/paths.py +108 -0
  55. jentic/apitools/openapi/datamodels/low/v31/reference.py +65 -0
  56. jentic/apitools/openapi/datamodels/low/v31/request_body.py +108 -0
  57. jentic/apitools/openapi/datamodels/low/v31/response.py +104 -0
  58. jentic/apitools/openapi/datamodels/low/v31/responses.py +109 -0
  59. jentic/apitools/openapi/datamodels/low/v31/schema.py +498 -0
  60. jentic/apitools/openapi/datamodels/low/v31/security_requirement.py +106 -0
  61. jentic/apitools/openapi/datamodels/low/v31/security_scheme.py +129 -0
  62. jentic/apitools/openapi/datamodels/low/v31/server.py +111 -0
  63. jentic/apitools/openapi/datamodels/low/v31/server_variable.py +70 -0
  64. jentic/apitools/openapi/datamodels/low/v31/tag.py +63 -0
  65. jentic/apitools/openapi/datamodels/low/v31/xml.py +54 -0
  66. jentic_openapi_datamodels-1.0.0a20.dist-info/METADATA +379 -0
  67. jentic_openapi_datamodels-1.0.0a20.dist-info/RECORD +75 -0
  68. jentic/apitools/openapi/datamodels/low/model_builder.py +0 -129
  69. jentic_openapi_datamodels-1.0.0a18.dist-info/METADATA +0 -211
  70. jentic_openapi_datamodels-1.0.0a18.dist-info/RECORD +0 -27
  71. {jentic_openapi_datamodels-1.0.0a18.dist-info → jentic_openapi_datamodels-1.0.0a20.dist-info}/WHEEL +0 -0
  72. {jentic_openapi_datamodels-1.0.0a18.dist-info → jentic_openapi_datamodels-1.0.0a20.dist-info}/licenses/LICENSE +0 -0
  73. {jentic_openapi_datamodels-1.0.0a18.dist-info → jentic_openapi_datamodels-1.0.0a20.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,498 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, TypeAlias, get_args
3
+
4
+ from ruamel import yaml
5
+ from ruamel.yaml.comments import CommentedSeq
6
+
7
+ from ..context import Context
8
+ from ..extractors import extract_extension_fields
9
+ from ..fields import fixed_field, fixed_fields
10
+ from ..sources import FieldSource, KeySource, ValueSource, YAMLValue
11
+ from .builders import build_field_source
12
+ from .discriminator import Discriminator
13
+ from .discriminator import build as build_discriminator
14
+ from .external_documentation import ExternalDocumentation
15
+ from .external_documentation import build as build_external_documentation
16
+ from .xml import XML
17
+ from .xml import build as build_xml
18
+
19
+
20
+ __all__ = ["Schema", "BooleanJSONSchema", "NestedSchema", "build"]
21
+
22
+
23
+ BooleanJSONSchema: TypeAlias = ValueSource[bool]
24
+
25
+
26
+ # Type alias for nested schema references
27
+ # A schema node that can be nested within another schema, representing:
28
+ # - Schema: A valid schema object
29
+ # - Boolean JSON Schema: True or False
30
+ # - ValueSource[str | int | float | None | CommentedSeq]: Invalid/malformed data preserved for validation
31
+
32
+ NestedSchema: TypeAlias = (
33
+ "Schema | BooleanJSONSchema | ValueSource[str | int | float | None | CommentedSeq]"
34
+ )
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class Schema:
39
+ """
40
+ Schema Object representation for OpenAPI 3.1.
41
+
42
+ In OpenAPI 3.1, the Schema Object is a full JSON Schema 2020-12 vocabulary with OpenAPI extensions.
43
+ This represents a complete JSON Schema with additional OpenAPI-specific fields.
44
+
45
+ Attributes:
46
+ root_node: The top-level node representing the entire Schema object in the original source file
47
+
48
+ # JSON Schema Core Keywords (2020-12)
49
+ schema_: The $schema keyword - URI of the meta-schema
50
+ id_: The $id keyword - URI that identifies the schema resource
51
+ ref_: The $ref keyword - URI reference to another schema
52
+ anchor: The $anchor keyword - Plain name fragment for identification
53
+ dynamic_ref: The $dynamicRef keyword - Dynamic reference to another schema
54
+ dynamic_anchor: The $dynamicAnchor keyword - Dynamic anchor for identification
55
+ vocabulary: The $vocabulary keyword - Available vocabularies and their usage
56
+ comment: The $comment keyword - Comments for schema authors
57
+ defs: The $defs keyword - Schema definitions for reuse
58
+
59
+ # JSON Schema Validation Keywords (2020-12)
60
+ # Validation - Any Type
61
+ type: The type keyword - Value type(s): string, number, integer, boolean, array, object, null
62
+ enum: The enum keyword - Fixed set of allowed values
63
+ const: The const keyword - Single allowed value
64
+
65
+ # Validation - Numeric
66
+ multiple_of: A numeric instance is valid only if division by this value results in an integer
67
+ maximum: Upper limit for a numeric instance (inclusive)
68
+ exclusive_maximum: Upper limit for a numeric instance (exclusive)
69
+ minimum: Lower limit for a numeric instance (inclusive)
70
+ exclusive_minimum: Lower limit for a numeric instance (exclusive)
71
+
72
+ # Validation - String
73
+ max_length: Maximum length of a string instance
74
+ min_length: Minimum length of a string instance
75
+ pattern: A string instance is valid if the regular expression matches the instance successfully
76
+
77
+ # Validation - Array
78
+ max_items: Maximum number of items in an array instance
79
+ min_items: Minimum number of items in an array instance
80
+ unique_items: If true, array items must be unique
81
+ max_contains: Maximum number of items that must match the contains schema
82
+ min_contains: Minimum number of items that must match the contains schema
83
+
84
+ # Validation - Object
85
+ max_properties: Maximum number of properties in an object instance
86
+ min_properties: Minimum number of properties in an object instance
87
+ required: List of required property names
88
+ dependent_required: Dependencies between properties (property-based requirements)
89
+
90
+ # JSON Schema Applicator Keywords (2020-12)
91
+ # Schema Composition
92
+ all_of: Must be valid against all of the subschemas
93
+ any_of: Must be valid against any of the subschemas
94
+ one_of: Must be valid against exactly one of the subschemas
95
+ not_: Must not be valid against the given schema
96
+
97
+ # Conditional Application
98
+ if_: Conditional schema - if this validates, then/else is applied
99
+ then_: Schema to apply if 'if' validates
100
+ else_: Schema to apply if 'if' does not validate
101
+
102
+ # Array Applicators
103
+ prefix_items: Array of schemas for validating tuple-like arrays (positional items)
104
+ items: Schema for array items (all items or items after prefix_items)
105
+ contains: Schema that at least one array item must validate against
106
+
107
+ # Object Applicators
108
+ properties: Property name to schema mappings
109
+ pattern_properties: Property patterns to schema mappings
110
+ additional_properties: Schema for properties not defined in properties/pattern_properties, or boolean
111
+ property_names: Schema that all property names must validate against
112
+
113
+ # Dependent Schemas
114
+ dependent_schemas: Schemas that must validate when specific properties are present
115
+
116
+ # Unevaluated Locations
117
+ unevaluated_items: Schema for array items not evaluated by other keywords
118
+ unevaluated_properties: Schema for object properties not evaluated by other keywords
119
+
120
+ # JSON Schema Meta-Data Keywords (2020-12)
121
+ title: A title for the schema
122
+ description: A description of the schema. CommonMark syntax MAY be used for rich text representation.
123
+ default: Default value for the schema
124
+ deprecated: Specifies that the schema is deprecated
125
+ read_only: Indicates the value should not be modified (sent in response but not in request)
126
+ write_only: Indicates the value should only be sent in requests (not in responses)
127
+ examples: Array of example values
128
+
129
+ # JSON Schema Format Keywords (2020-12)
130
+ format: Additional format hint for the type (e.g., "email", "uuid", "uri", "date-time")
131
+
132
+ # JSON Schema Content Keywords (2020-12)
133
+ content_encoding: Content encoding for string instances (e.g., "base64")
134
+ content_media_type: Media type of string instance contents (e.g., "application/json")
135
+ content_schema: Schema for validating the decoded content
136
+
137
+ # OpenAPI-specific extensions (not in JSON Schema 2020-12)
138
+ discriminator: Adds support for polymorphism
139
+ xml: Additional metadata for XML representations
140
+ external_docs: Additional external documentation
141
+ example: A single example value (OpenAPI extension, use examples for standard JSON Schema)
142
+
143
+ extensions: Specification extensions (x-* fields)
144
+ """
145
+
146
+ root_node: yaml.Node
147
+
148
+ # Core Keywords
149
+ id_: FieldSource[str] | None = fixed_field(metadata={"yaml_name": "$id"})
150
+ schema_: FieldSource[str] | None = fixed_field(metadata={"yaml_name": "$schema"})
151
+ ref_: FieldSource[str] | None = fixed_field(metadata={"yaml_name": "$ref"})
152
+ comment: FieldSource[str] | None = fixed_field(metadata={"yaml_name": "$comment"})
153
+ defs: FieldSource[dict[KeySource[str], NestedSchema]] | None = fixed_field(
154
+ metadata={"yaml_name": "$defs"}
155
+ )
156
+ anchor: FieldSource[str] | None = fixed_field(metadata={"yaml_name": "$anchor"})
157
+ dynamic_anchor: FieldSource[str] | None = fixed_field(metadata={"yaml_name": "$dynamicAnchor"})
158
+ dynamic_ref: FieldSource[str] | None = fixed_field(metadata={"yaml_name": "$dynamicRef"})
159
+ vocabulary: FieldSource[dict[KeySource[str], ValueSource[bool]]] | None = fixed_field(
160
+ metadata={"yaml_name": "$vocabulary"}
161
+ )
162
+
163
+ # JSON Schema Applicator Keywords
164
+ all_of: FieldSource[list[NestedSchema]] | None = fixed_field(metadata={"yaml_name": "allOf"})
165
+ any_of: FieldSource[list[NestedSchema]] | None = fixed_field(metadata={"yaml_name": "anyOf"})
166
+ one_of: FieldSource[list[NestedSchema]] | None = fixed_field(metadata={"yaml_name": "oneOf"})
167
+ if_: FieldSource[NestedSchema] | None = fixed_field(metadata={"yaml_name": "if"})
168
+ then_: FieldSource[NestedSchema] | None = fixed_field(metadata={"yaml_name": "then"})
169
+ else_: FieldSource[NestedSchema] | None = fixed_field(metadata={"yaml_name": "else"})
170
+ not_: FieldSource[NestedSchema] | None = fixed_field(metadata={"yaml_name": "not"})
171
+ properties: FieldSource[dict[KeySource[str], NestedSchema]] | None = fixed_field()
172
+ additional_properties: FieldSource[NestedSchema] | None = fixed_field(
173
+ metadata={"yaml_name": "additionalProperties"}
174
+ )
175
+ pattern_properties: FieldSource[dict[KeySource[str], NestedSchema]] | None = fixed_field(
176
+ metadata={"yaml_name": "patternProperties"}
177
+ )
178
+ dependent_schemas: FieldSource[dict[KeySource[str], NestedSchema]] | None = fixed_field(
179
+ metadata={"yaml_name": "dependentSchemas"}
180
+ )
181
+ property_names: FieldSource[NestedSchema] | None = fixed_field(
182
+ metadata={"yaml_name": "propertyNames"}
183
+ )
184
+ contains: FieldSource[NestedSchema] | None = fixed_field()
185
+ items: FieldSource[NestedSchema] | None = fixed_field()
186
+ prefix_items: FieldSource[list[NestedSchema]] | None = fixed_field(
187
+ metadata={"yaml_name": "prefixItems"}
188
+ )
189
+
190
+ # Validation Keywords
191
+ type: FieldSource[str | list[ValueSource[str]]] | None = fixed_field()
192
+ enum: FieldSource[list[ValueSource[YAMLValue]]] | None = fixed_field()
193
+ const: FieldSource[YAMLValue] | None = fixed_field()
194
+ max_length: FieldSource[int] | None = fixed_field(metadata={"yaml_name": "maxLength"})
195
+ min_length: FieldSource[int] | None = fixed_field(metadata={"yaml_name": "minLength"})
196
+ pattern: FieldSource[str] | None = fixed_field()
197
+ exclusive_maximum: FieldSource[int | float] | None = fixed_field(
198
+ metadata={"yaml_name": "exclusiveMaximum"}
199
+ )
200
+ exclusive_minimum: FieldSource[int | float] | None = fixed_field(
201
+ metadata={"yaml_name": "exclusiveMinimum"}
202
+ )
203
+ minimum: FieldSource[int | float] | None = fixed_field()
204
+ maximum: FieldSource[int | float] | None = fixed_field()
205
+ multiple_of: FieldSource[int | float] | None = fixed_field(metadata={"yaml_name": "multipleOf"})
206
+ dependent_required: FieldSource[dict[KeySource[str], list[ValueSource[str]]]] | None = (
207
+ fixed_field(metadata={"yaml_name": "dependentRequired"})
208
+ )
209
+ max_properties: FieldSource[int] | None = fixed_field(metadata={"yaml_name": "maxProperties"})
210
+ min_properties: FieldSource[int] | None = fixed_field(metadata={"yaml_name": "minProperties"})
211
+ required: FieldSource[list[ValueSource[str]]] | None = fixed_field()
212
+ max_items: FieldSource[int] | None = fixed_field(metadata={"yaml_name": "maxItems"})
213
+ min_items: FieldSource[int] | None = fixed_field(metadata={"yaml_name": "minItems"})
214
+ max_contains: FieldSource[int] | None = fixed_field(metadata={"yaml_name": "maxContains"})
215
+ min_contains: FieldSource[int] | None = fixed_field(metadata={"yaml_name": "minContains"})
216
+ unique_items: FieldSource[bool] | None = fixed_field(metadata={"yaml_name": "uniqueItems"})
217
+
218
+ # Meta Data Keywords
219
+ title: FieldSource[str] | None = fixed_field()
220
+ description: FieldSource[str] | None = fixed_field()
221
+ default: FieldSource[YAMLValue] | None = fixed_field()
222
+ deprecated: FieldSource[bool] | None = fixed_field()
223
+ examples: FieldSource[list[ValueSource[YAMLValue]]] | None = fixed_field()
224
+ read_only: FieldSource[bool] | None = fixed_field(metadata={"yaml_name": "readOnly"})
225
+ write_only: FieldSource[bool] | None = fixed_field(metadata={"yaml_name": "writeOnly"})
226
+
227
+ # Format Annotation Keywords
228
+ format: FieldSource[str] | None = fixed_field()
229
+
230
+ # Content Keywords
231
+ content_encoding: FieldSource[str] | None = fixed_field(
232
+ metadata={"yaml_name": "contentEncoding"}
233
+ )
234
+ content_media_type: FieldSource[str] | None = fixed_field(
235
+ metadata={"yaml_name": "contentMediaType"}
236
+ )
237
+ content_schema: FieldSource[NestedSchema] | None = fixed_field(
238
+ metadata={"yaml_name": "contentSchema"}
239
+ )
240
+
241
+ # Unevaluated Keywords
242
+ unevaluated_items: FieldSource[NestedSchema] | None = fixed_field(
243
+ metadata={"yaml_name": "unevaluatedItems"}
244
+ )
245
+ unevaluated_properties: FieldSource[NestedSchema] | None = fixed_field(
246
+ metadata={"yaml_name": "unevaluatedProperties"}
247
+ )
248
+
249
+ # OpenAPI base vocabulary Keywords (not in JSON Schema 2020-12)
250
+ discriminator: FieldSource[Discriminator] | None = fixed_field()
251
+ xml: FieldSource[XML] | None = fixed_field()
252
+ external_docs: FieldSource[ExternalDocumentation] | None = fixed_field(
253
+ metadata={"yaml_name": "externalDocs"}
254
+ )
255
+ example: FieldSource[YAMLValue] | None = fixed_field()
256
+
257
+ extensions: dict[KeySource[str], ValueSource[YAMLValue]] = field(default_factory=dict)
258
+
259
+
260
+ def build(
261
+ root: yaml.Node, context: Context | None = None
262
+ ) -> "Schema | BooleanJSONSchema | ValueSource[str | int | float | None | CommentedSeq]":
263
+ """
264
+ Build a Schema object from a YAML node.
265
+
266
+ Preserves all source data as-is, regardless of type. This is a low-level/plumbing
267
+ model that provides complete source fidelity for inspection and validation.
268
+
269
+ Note: Schema is self-referential (can contain other Schema objects in all_of, one_of, any_of, not_,
270
+ items, properties, additional_properties). The builder handles nested Schema objects by preserving
271
+ them as raw YAML values, letting validation layers interpret them.
272
+
273
+ Args:
274
+ root: The YAML node to parse (should be a MappingNode)
275
+ context: Optional parsing context. If None, a default context will be created.
276
+
277
+ Returns:
278
+ A Schema object if the node is valid, or a ValueSource containing
279
+ the invalid data if the root is not a MappingNode (preserving the invalid data
280
+ and its source location for validation).
281
+
282
+ Example:
283
+ from ruamel.yaml import YAML
284
+ yaml = YAML()
285
+ root = yaml.compose("type: string\\nminLength: 1\\nmaxLength: 100")
286
+ schema = build(root)
287
+ assert schema.type.value == 'string'
288
+ assert schema.min_length.value == 1
289
+ """
290
+ context = context or Context()
291
+
292
+ if not isinstance(root, yaml.MappingNode):
293
+ # Preserve invalid root data instead of returning None
294
+ value = context.yaml_constructor.construct_object(root, deep=True)
295
+ return ValueSource(value=value, value_node=root)
296
+
297
+ # Build YAML name to Python field name mapping
298
+ _fixed_fields = fixed_fields(Schema)
299
+ yaml_to_field = {
300
+ f.metadata.get("yaml_name", fname): fname for fname, f in _fixed_fields.items()
301
+ }
302
+
303
+ # Accumulate all field values in a single pass
304
+ field_values: dict[str, Any] = {}
305
+
306
+ for key_node, value_node in root.value:
307
+ key = context.yaml_constructor.construct_yaml_str(key_node)
308
+
309
+ # Skip extension fields - handled separately at the end
310
+ if key.startswith("x-"):
311
+ continue
312
+
313
+ # Map YAML key to Python field name
314
+ field_name = yaml_to_field.get(key)
315
+ if not field_name:
316
+ continue
317
+
318
+ # Get field metadata
319
+ field_info = _fixed_fields[field_name]
320
+ field_type_args = set(get_args(field_info.type))
321
+
322
+ # Simple scalar fields (handled like build_model does)
323
+ if field_type_args & {
324
+ FieldSource[str],
325
+ FieldSource[bool],
326
+ FieldSource[int],
327
+ FieldSource[int | float],
328
+ FieldSource[YAMLValue],
329
+ }:
330
+ field_values[field_name] = build_field_source(key_node, value_node, context)
331
+
332
+ # Handle list with ValueSource wrapping for each item (e.g., required, enum fields)
333
+ elif field_type_args & {
334
+ FieldSource[list[ValueSource[str]]],
335
+ FieldSource[list[ValueSource[YAMLValue]]],
336
+ }:
337
+ if isinstance(value_node, yaml.SequenceNode):
338
+ value_list: list[ValueSource[Any]] = []
339
+ for item_node in value_node.value:
340
+ item_value = context.yaml_constructor.construct_object(item_node, deep=True)
341
+ value_list.append(ValueSource(value=item_value, value_node=item_node))
342
+ field_values[field_name] = FieldSource(
343
+ value=value_list, key_node=key_node, value_node=value_node
344
+ )
345
+ else:
346
+ # Not a sequence - preserve as-is for validation
347
+ field_values[field_name] = build_field_source(key_node, value_node, context)
348
+
349
+ # Recursive schema list fields (allOf, oneOf, anyOf, prefixItems)
350
+ elif key in ("allOf", "oneOf", "anyOf", "prefixItems"):
351
+ if isinstance(value_node, yaml.SequenceNode):
352
+ schemas = []
353
+ for item_node in value_node.value:
354
+ schema = build(item_node, context)
355
+ schemas.append(schema)
356
+ field_values[field_name] = FieldSource(
357
+ value=schemas, key_node=key_node, value_node=value_node
358
+ )
359
+ else:
360
+ # Not a sequence - preserve as-is for validation
361
+ field_values[field_name] = build_field_source(key_node, value_node, context)
362
+ # Recursive schema single fields (not, if, then, else, contains, propertyNames, contentSchema)
363
+ elif key in ("not", "if", "then", "else", "contains", "propertyNames", "contentSchema"):
364
+ schema = build(value_node, context)
365
+ field_values[field_name] = FieldSource(
366
+ value=schema, key_node=key_node, value_node=value_node
367
+ )
368
+ # items (boolean | schema in JSON Schema 2020-12)
369
+ elif key == "items":
370
+ # Check if it's a boolean or a schema
371
+ if (
372
+ isinstance(value_node, yaml.ScalarNode)
373
+ and value_node.tag == "tag:yaml.org,2002:bool"
374
+ ):
375
+ field_values[field_name] = build_field_source(key_node, value_node, context)
376
+ else:
377
+ # It's a schema
378
+ schema = build(value_node, context)
379
+ field_values[field_name] = FieldSource(
380
+ value=schema, key_node=key_node, value_node=value_node
381
+ )
382
+ # Boolean | schema fields (additionalProperties, unevaluatedItems, unevaluatedProperties)
383
+ elif key in ("additionalProperties", "unevaluatedItems", "unevaluatedProperties"):
384
+ # Check if it's a boolean or a schema
385
+ if (
386
+ isinstance(value_node, yaml.ScalarNode)
387
+ and value_node.tag == "tag:yaml.org,2002:bool"
388
+ ):
389
+ field_values[field_name] = build_field_source(key_node, value_node, context)
390
+ else:
391
+ # It's a schema
392
+ schema = build(value_node, context)
393
+ field_values[field_name] = FieldSource(
394
+ value=schema, key_node=key_node, value_node=value_node
395
+ )
396
+ # Dict of schemas (properties, $defs, patternProperties, dependentSchemas)
397
+ elif key in ("properties", "$defs", "patternProperties", "dependentSchemas"):
398
+ if isinstance(value_node, yaml.MappingNode):
399
+ schemas_dict: dict[KeySource[str], NestedSchema] = {}
400
+ for map_key_node, map_value_node in value_node.value:
401
+ map_key = context.yaml_constructor.construct_yaml_str(map_key_node)
402
+ # Recursively build each schema
403
+ nested_schema: NestedSchema = build(map_value_node, context)
404
+ schemas_dict[KeySource(value=map_key, key_node=map_key_node)] = nested_schema
405
+ field_values[field_name] = FieldSource(
406
+ value=schemas_dict, key_node=key_node, value_node=value_node
407
+ )
408
+ else:
409
+ # Not a mapping - preserve as-is for validation
410
+ field_values[field_name] = build_field_source(key_node, value_node, context)
411
+ # $vocabulary (dict[KeySource[str], ValueSource[bool]])
412
+ elif key == "$vocabulary":
413
+ if isinstance(value_node, yaml.MappingNode):
414
+ vocabulary_dict: dict[KeySource[str], ValueSource[bool]] = {}
415
+ for vocab_key_node, vocab_value_node in value_node.value:
416
+ vocab_key = context.yaml_constructor.construct_yaml_str(vocab_key_node)
417
+ vocab_value = context.yaml_constructor.construct_object(
418
+ vocab_value_node, deep=True
419
+ )
420
+ vocabulary_dict[KeySource(value=vocab_key, key_node=vocab_key_node)] = (
421
+ ValueSource(value=vocab_value, value_node=vocab_value_node)
422
+ )
423
+ field_values[field_name] = FieldSource(
424
+ value=vocabulary_dict, key_node=key_node, value_node=value_node
425
+ )
426
+ else:
427
+ # Not a mapping - preserve as-is for validation
428
+ field_values[field_name] = build_field_source(key_node, value_node, context)
429
+ # dependentRequired (dict[KeySource[str], list[ValueSource[str]]])
430
+ elif key == "dependentRequired":
431
+ if isinstance(value_node, yaml.MappingNode):
432
+ dependent_required_dict: dict[KeySource[str], list[ValueSource[str]]] = {}
433
+ for dep_key_node, dep_value_node in value_node.value:
434
+ dep_key = context.yaml_constructor.construct_yaml_str(dep_key_node)
435
+ if isinstance(dep_value_node, yaml.SequenceNode):
436
+ dep_list: list[ValueSource[str]] = []
437
+ for item_node in dep_value_node.value:
438
+ item_value = context.yaml_constructor.construct_object(
439
+ item_node, deep=True
440
+ )
441
+ dep_list.append(ValueSource(value=item_value, value_node=item_node))
442
+ dependent_required_dict[KeySource(value=dep_key, key_node=dep_key_node)] = (
443
+ dep_list
444
+ )
445
+ else:
446
+ # Not a sequence - preserve as invalid for validation
447
+ invalid_value = context.yaml_constructor.construct_object(
448
+ dep_value_node, deep=True
449
+ )
450
+ dependent_required_dict[KeySource(value=dep_key, key_node=dep_key_node)] = (
451
+ invalid_value # type: ignore
452
+ )
453
+ field_values[field_name] = FieldSource(
454
+ value=dependent_required_dict, key_node=key_node, value_node=value_node
455
+ )
456
+ else:
457
+ # Not a mapping - preserve as-is for validation
458
+ field_values[field_name] = build_field_source(key_node, value_node, context)
459
+ # type field (can be string or list of strings in JSON Schema 2020-12)
460
+ elif key == "type":
461
+ if isinstance(value_node, yaml.SequenceNode):
462
+ # It's a list of types
463
+ type_list: list[ValueSource[str]] = []
464
+ for item_node in value_node.value:
465
+ item_value = context.yaml_constructor.construct_object(item_node, deep=True)
466
+ type_list.append(ValueSource(value=item_value, value_node=item_node))
467
+ field_values[field_name] = FieldSource(
468
+ value=type_list, key_node=key_node, value_node=value_node
469
+ )
470
+ else:
471
+ # It's a single type string
472
+ field_values[field_name] = build_field_source(key_node, value_node, context)
473
+ # Nested objects (discriminator, xml, externalDocs)
474
+ elif key == "discriminator":
475
+ field_values[field_name] = FieldSource(
476
+ value=build_discriminator(value_node, context=context),
477
+ key_node=key_node,
478
+ value_node=value_node,
479
+ )
480
+ elif key == "xml":
481
+ field_values[field_name] = FieldSource(
482
+ value=build_xml(value_node, context=context),
483
+ key_node=key_node,
484
+ value_node=value_node,
485
+ )
486
+ elif key == "externalDocs":
487
+ field_values[field_name] = FieldSource(
488
+ value=build_external_documentation(value_node, context=context),
489
+ key_node=key_node,
490
+ value_node=value_node,
491
+ )
492
+
493
+ # Build and return the Schema instance (single constructor call)
494
+ return Schema(
495
+ root_node=root,
496
+ **field_values,
497
+ extensions=extract_extension_fields(root, context),
498
+ )
@@ -0,0 +1,106 @@
1
+ from dataclasses import dataclass
2
+
3
+ from ruamel import yaml
4
+ from ruamel.yaml import MappingNode, SequenceNode
5
+
6
+ from ..context import Context
7
+ from ..fields import patterned_field
8
+ from ..sources import KeySource, ValueSource, YAMLInvalidValue
9
+
10
+
11
+ __all__ = ["SecurityRequirement", "build"]
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class SecurityRequirement:
16
+ """
17
+ Security Requirement Object representation for OpenAPI 3.1.
18
+
19
+ Lists the required security schemes to execute an operation. Each named security scheme
20
+ must correspond to a security scheme declared in the Security Schemes under the Components Object.
21
+
22
+ When multiple Security Requirement Objects are specified, only ONE needs to be satisfied
23
+ to authorize a request. Within a single Security Requirement Object, ALL schemes must be satisfied.
24
+
25
+ Note: An empty Security Requirement object ({}) makes security optional for the operation.
26
+ Note: Specification extensions (x-* fields) are NOT supported for Security Requirement objects.
27
+
28
+ Attributes:
29
+ root_node: The top-level node representing the entire Security Requirement object in the original source file
30
+ requirements: Dictionary mapping security scheme names to arrays of scope strings.
31
+ For OAuth2 schemes, the array contains required scopes.
32
+ For other schemes (API key, HTTP), the array is empty.
33
+ """
34
+
35
+ root_node: yaml.Node
36
+ requirements: ValueSource[dict[KeySource[str], ValueSource[list[ValueSource[str]]]]] | None = (
37
+ patterned_field()
38
+ )
39
+
40
+
41
+ def build(
42
+ root: yaml.Node, context: Context | None = None
43
+ ) -> SecurityRequirement | ValueSource[YAMLInvalidValue]:
44
+ """
45
+ Build a SecurityRequirement object from a YAML node.
46
+
47
+ Preserves all source data as-is, regardless of type. This is a low-level/plumbing
48
+ model that provides complete source fidelity for inspection and validation.
49
+
50
+ Args:
51
+ root: The YAML node to parse (should be a MappingNode)
52
+ context: Optional parsing context. If None, a default context will be created.
53
+
54
+ Returns:
55
+ A SecurityRequirement object if the node is valid, or a ValueSource containing
56
+ the invalid data if the root is not a MappingNode (preserving the invalid data
57
+ and its source location for validation).
58
+
59
+ Example:
60
+ from ruamel.yaml import YAML
61
+ yaml = YAML()
62
+ root = yaml.compose("api_key: []")
63
+ security_req = build(root)
64
+ assert security_req.requirements is not None
65
+ """
66
+ context = context or Context()
67
+
68
+ if not isinstance(root, MappingNode):
69
+ # Preserve invalid root data instead of returning None
70
+ value = context.yaml_constructor.construct_object(root, deep=True)
71
+ return ValueSource(value=value, value_node=root)
72
+
73
+ requirements_dict: dict[KeySource[str], ValueSource[list[ValueSource[str]]]] = {}
74
+
75
+ for key_node, value_node in root.value:
76
+ key = context.yaml_constructor.construct_yaml_str(key_node)
77
+
78
+ # Skip non-string keys
79
+ if not isinstance(key, str):
80
+ continue
81
+
82
+ # Security scheme requirement field
83
+ # For requirements, we need to wrap each scope string in ValueSource
84
+ if isinstance(value_node, SequenceNode):
85
+ # Wrap each scope string in the array with its source node
86
+ scope_list: list[ValueSource[str]] = []
87
+ for item_node in value_node.value:
88
+ item_value = context.yaml_constructor.construct_object(item_node, deep=True)
89
+ scope_list.append(ValueSource(value=item_value, value_node=item_node))
90
+
91
+ requirements_dict[KeySource(value=key, key_node=key_node)] = ValueSource(
92
+ value=scope_list, value_node=value_node
93
+ )
94
+ else:
95
+ # Not a sequence - preserve as-is for validation to catch
96
+ value = context.yaml_constructor.construct_object(value_node, deep=True)
97
+ requirements_dict[KeySource(value=key, key_node=key_node)] = ValueSource(
98
+ value=value, value_node=value_node
99
+ )
100
+
101
+ return SecurityRequirement(
102
+ root_node=root,
103
+ requirements=(
104
+ ValueSource(value=requirements_dict, value_node=root) if requirements_dict else None
105
+ ),
106
+ )