jentic-openapi-datamodels 1.0.0a12__tar.gz → 1.0.0a13__tar.gz
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.
- jentic_openapi_datamodels-1.0.0a13/PKG-INFO +211 -0
- jentic_openapi_datamodels-1.0.0a13/README.md +197 -0
- {jentic_openapi_datamodels-1.0.0a12 → jentic_openapi_datamodels-1.0.0a13}/pyproject.toml +4 -2
- jentic_openapi_datamodels-1.0.0a13/src/jentic/apitools/openapi/datamodels/low/context.py +21 -0
- jentic_openapi_datamodels-1.0.0a13/src/jentic/apitools/openapi/datamodels/low/extractors.py +126 -0
- jentic_openapi_datamodels-1.0.0a13/src/jentic/apitools/openapi/datamodels/low/fields.py +45 -0
- jentic_openapi_datamodels-1.0.0a13/src/jentic/apitools/openapi/datamodels/low/model_builder.py +113 -0
- jentic_openapi_datamodels-1.0.0a13/src/jentic/apitools/openapi/datamodels/low/py.typed +0 -0
- jentic_openapi_datamodels-1.0.0a13/src/jentic/apitools/openapi/datamodels/low/sources.py +89 -0
- jentic_openapi_datamodels-1.0.0a13/src/jentic/apitools/openapi/datamodels/low/v30/__init__.py +0 -0
- jentic_openapi_datamodels-1.0.0a13/src/jentic/apitools/openapi/datamodels/low/v30/discriminator.py +66 -0
- jentic_openapi_datamodels-1.0.0a13/src/jentic/apitools/openapi/datamodels/low/v30/external_documentation.py +65 -0
- jentic_openapi_datamodels-1.0.0a13/src/jentic/apitools/openapi/datamodels/low/v30/oauth_flow.py +71 -0
- jentic_openapi_datamodels-1.0.0a13/src/jentic/apitools/openapi/datamodels/low/v30/oauth_flows.py +116 -0
- jentic_openapi_datamodels-1.0.0a13/src/jentic/apitools/openapi/datamodels/low/v30/reference.py +63 -0
- jentic_openapi_datamodels-1.0.0a13/src/jentic/apitools/openapi/datamodels/low/v30/schema.py +335 -0
- jentic_openapi_datamodels-1.0.0a13/src/jentic/apitools/openapi/datamodels/low/v30/security_requirement.py +101 -0
- jentic_openapi_datamodels-1.0.0a13/src/jentic/apitools/openapi/datamodels/low/v30/security_scheme.py +109 -0
- jentic_openapi_datamodels-1.0.0a13/src/jentic/apitools/openapi/datamodels/low/v30/tag.py +101 -0
- jentic_openapi_datamodels-1.0.0a13/src/jentic/apitools/openapi/datamodels/low/v30/xml.py +60 -0
- jentic_openapi_datamodels-1.0.0a12/PKG-INFO +0 -52
- jentic_openapi_datamodels-1.0.0a12/README.md +0 -39
- jentic_openapi_datamodels-1.0.0a12/src/jentic/apitools/openapi/datamodels/low/v30/__init__.py +0 -28
- jentic_openapi_datamodels-1.0.0a12/src/jentic/apitools/openapi/datamodels/low/v30/discriminator.py +0 -91
- jentic_openapi_datamodels-1.0.0a12/src/jentic/apitools/openapi/datamodels/low/v30/external_documentation.py +0 -79
- jentic_openapi_datamodels-1.0.0a12/src/jentic/apitools/openapi/datamodels/low/v30/oauth_flow.py +0 -140
- jentic_openapi_datamodels-1.0.0a12/src/jentic/apitools/openapi/datamodels/low/v30/oauth_flows.py +0 -165
- jentic_openapi_datamodels-1.0.0a12/src/jentic/apitools/openapi/datamodels/low/v30/reference.py +0 -64
- jentic_openapi_datamodels-1.0.0a12/src/jentic/apitools/openapi/datamodels/low/v30/schema.py +0 -626
- jentic_openapi_datamodels-1.0.0a12/src/jentic/apitools/openapi/datamodels/low/v30/security_requirement.py +0 -91
- jentic_openapi_datamodels-1.0.0a12/src/jentic/apitools/openapi/datamodels/low/v30/security_scheme.py +0 -301
- jentic_openapi_datamodels-1.0.0a12/src/jentic/apitools/openapi/datamodels/low/v30/specification_object.py +0 -217
- jentic_openapi_datamodels-1.0.0a12/src/jentic/apitools/openapi/datamodels/low/v30/tag.py +0 -132
- jentic_openapi_datamodels-1.0.0a12/src/jentic/apitools/openapi/datamodels/low/v30/xml.py +0 -134
- {jentic_openapi_datamodels-1.0.0a12 → jentic_openapi_datamodels-1.0.0a13}/LICENSE +0 -0
- {jentic_openapi_datamodels-1.0.0a12 → jentic_openapi_datamodels-1.0.0a13}/NOTICE +0 -0
- /jentic_openapi_datamodels-1.0.0a12/src/jentic/apitools/openapi/datamodels/low/v30/py.typed → /jentic_openapi_datamodels-1.0.0a13/src/jentic/apitools/openapi/datamodels/low/__init__.py +0 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jentic-openapi-datamodels
|
|
3
|
+
Version: 1.0.0a13
|
|
4
|
+
Summary: Jentic OpenAPI Data Models
|
|
5
|
+
Author: Jentic
|
|
6
|
+
Author-email: Jentic <hello@jentic.com>
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
License-File: NOTICE
|
|
10
|
+
Requires-Dist: ruamel-yaml~=0.18.15
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Project-URL: Homepage, https://github.com/jentic/jentic-openapi-tools
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
from semantic_release.cli.commands.version import build_distributions
|
|
16
|
+
|
|
17
|
+
# jentic-openapi-datamodels
|
|
18
|
+
|
|
19
|
+
Low-level and high-level data models for OpenAPI specifications.
|
|
20
|
+
|
|
21
|
+
This package provides data model classes for representing OpenAPI specification objects in Python.
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
**Low-Level Architecture**
|
|
26
|
+
- **Preserve Everything**: All data from source documents preserved exactly as-is, including invalid values
|
|
27
|
+
- **Zero Validation**: No validation or coercion during parsing - deferred to higher layers
|
|
28
|
+
- **Separation of Concerns**: Low-level model focuses on faithful representation; validation belongs elsewhere
|
|
29
|
+
|
|
30
|
+
**Source Tracking**
|
|
31
|
+
- **Complete Source Fidelity**: Every field tracks its exact YAML node location
|
|
32
|
+
- **Precise Error Reporting**: Line and column numbers via `start_mark` and `end_mark`
|
|
33
|
+
- **Metadata Preservation**: Full position tracking for accurate diagnostics
|
|
34
|
+
|
|
35
|
+
**Python Integration**
|
|
36
|
+
- **Python-Idiomatic Naming**: snake_case field names (e.g., `bearer_format`, `property_name`)
|
|
37
|
+
- **Spec-Aligned Mapping**: Automatic YAML name mapping (e.g., `bearerFormat` ↔ `bearer_format`)
|
|
38
|
+
- **Type Safety**: Full type hints with Generic types (`FieldSource[T]`, `KeySource[T]`, `ValueSource[T]`)
|
|
39
|
+
|
|
40
|
+
**Extensibility**
|
|
41
|
+
- **Extension Support**: Automatic extraction of OpenAPI `x-*` specification extensions
|
|
42
|
+
- **Unknown Field Tracking**: Capture typos and invalid fields for validation tools
|
|
43
|
+
- **Generic Builder Pattern**: Core `build_model()` function with object-specific builders for complex cases
|
|
44
|
+
|
|
45
|
+
**Performance**
|
|
46
|
+
- **Memory Efficient**: Immutable frozen dataclasses with `__slots__` for optimal memory usage
|
|
47
|
+
- **Shared Context**: All instances share a single YAML constructor for efficiency
|
|
48
|
+
|
|
49
|
+
**Version Support**
|
|
50
|
+
- **OpenAPI 2.0**: Planned for future release
|
|
51
|
+
- **OpenAPI 3.0.x**: Currently implemented
|
|
52
|
+
- **OpenAPI 3.1.x**: Planned for future release
|
|
53
|
+
- **OpenAPI 3.2.x**: Planned for future release
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install jentic-openapi-datamodels
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Prerequisites:**
|
|
62
|
+
- Python 3.11+
|
|
63
|
+
|
|
64
|
+
## Quick Start
|
|
65
|
+
|
|
66
|
+
### Basic Usage
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from ruamel.yaml import YAML
|
|
70
|
+
from jentic.apitools.openapi.datamodels.low.v30.security_scheme import build
|
|
71
|
+
|
|
72
|
+
# Parse YAML
|
|
73
|
+
yaml = YAML()
|
|
74
|
+
root = yaml.compose("""
|
|
75
|
+
type: http
|
|
76
|
+
scheme: bearer
|
|
77
|
+
bearerFormat: JWT
|
|
78
|
+
""")
|
|
79
|
+
|
|
80
|
+
# Build low-level model
|
|
81
|
+
security_scheme = build(root)
|
|
82
|
+
|
|
83
|
+
# Access via Python field names (snake_case)
|
|
84
|
+
print(security_scheme.bearer_format.value) # "JWT"
|
|
85
|
+
|
|
86
|
+
# Access source location information
|
|
87
|
+
print(security_scheme.bearer_format.key_node.value) # "bearerFormat"
|
|
88
|
+
print(security_scheme.bearer_format.key_node.start_mark.line) # Line number
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Field Name Mapping
|
|
92
|
+
|
|
93
|
+
YAML `camelCase` fields automatically map to Python `snake_case`:
|
|
94
|
+
- `bearerFormat` → `bearer_format`
|
|
95
|
+
- `authorizationUrl` → `authorization_url`
|
|
96
|
+
- `openIdConnectUrl` → `openid_connect_url`
|
|
97
|
+
|
|
98
|
+
Special cases for Python reserved keywords/special characters:
|
|
99
|
+
- `$ref` → `ref`
|
|
100
|
+
- `in` → `in_`
|
|
101
|
+
|
|
102
|
+
### Source Tracking
|
|
103
|
+
|
|
104
|
+
The package provides three immutable wrapper types for preserving source information:
|
|
105
|
+
|
|
106
|
+
**FieldSource[T]** - For OpenAPI fields with key-value pairs
|
|
107
|
+
- Used for: Fixed fields (`name`, `bearer_format`) and patterned fields (status codes, path items, schema properties)
|
|
108
|
+
- Tracks: Both key and value nodes
|
|
109
|
+
- Example: `SecurityScheme.bearer_format` is `FieldSource[str]`, response status codes are `FieldSource[Response]`
|
|
110
|
+
|
|
111
|
+
**KeySource[T]** - For dictionary keys
|
|
112
|
+
- Used for: keys in OpenAPI fields, `x-*` extensions and mapping dictionaries
|
|
113
|
+
- Tracks: Only key node
|
|
114
|
+
- Example: Keys in `Discriminator.mapping` are `KeySource[str]`
|
|
115
|
+
|
|
116
|
+
**ValueSource[T]** - For dictionary values and array items
|
|
117
|
+
- Used for: values in OpenAPI fields, in `x-*` extensions, mapping dictionaries and array items
|
|
118
|
+
- Tracks: Only value node
|
|
119
|
+
- Example: Values in `Discriminator.mapping` are `ValueSource[str]`
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from ruamel.yaml import YAML
|
|
123
|
+
from jentic.apitools.openapi.datamodels.low.v30.security_scheme import build as build_security_scheme
|
|
124
|
+
from jentic.apitools.openapi.datamodels.low.v30.discriminator import build as build_discriminator
|
|
125
|
+
|
|
126
|
+
# FieldSource: Fixed specification fields
|
|
127
|
+
yaml = YAML()
|
|
128
|
+
root = yaml.compose("type: http\nscheme: bearer\nbearerFormat: JWT")
|
|
129
|
+
security_scheme = build_security_scheme(root)
|
|
130
|
+
|
|
131
|
+
field = security_scheme.bearer_format # FieldSource[str]
|
|
132
|
+
print(field.value) # "JWT" - The actual value
|
|
133
|
+
print(field.key_node) # YAML node for "bearerFormat"
|
|
134
|
+
print(field.value_node) # YAML node for "JWT"
|
|
135
|
+
|
|
136
|
+
# KeySource/ValueSource: Dictionary fields (mapping, extensions)
|
|
137
|
+
root = yaml.compose("propertyName: petType\nmapping:\n dog: Dog\n cat: Cat")
|
|
138
|
+
discriminator = build_discriminator(root)
|
|
139
|
+
|
|
140
|
+
for key, value in discriminator.mapping.value.items():
|
|
141
|
+
print(key.value) # KeySource[str]: "dog" or "cat"
|
|
142
|
+
print(key.key_node) # YAML node for the key
|
|
143
|
+
print(value.value) # ValueSource[str]: "Dog" or "Cat"
|
|
144
|
+
print(value.value_node) # YAML node for the value
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Location Ranges
|
|
148
|
+
|
|
149
|
+
Access precise location ranges within the source document using start_mark and end_mark:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from ruamel.yaml import YAML
|
|
153
|
+
from jentic.apitools.openapi.datamodels.low.v30.security_scheme import build as build_security_scheme
|
|
154
|
+
|
|
155
|
+
yaml_content = """
|
|
156
|
+
type: http
|
|
157
|
+
scheme: bearer
|
|
158
|
+
bearerFormat: JWT
|
|
159
|
+
description: Bearer token authentication
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
yaml = YAML()
|
|
163
|
+
root = yaml.compose(yaml_content)
|
|
164
|
+
security_scheme = build_security_scheme(root)
|
|
165
|
+
|
|
166
|
+
# Access location information for any field
|
|
167
|
+
field = security_scheme.bearer_format
|
|
168
|
+
|
|
169
|
+
# Key location (e.g., "bearerFormat")
|
|
170
|
+
print(f"Key start: line {field.key_node.start_mark.line}, col {field.key_node.start_mark.column}")
|
|
171
|
+
print(f"Key end: line {field.key_node.end_mark.line}, col {field.key_node.end_mark.column}")
|
|
172
|
+
|
|
173
|
+
# Value location (e.g., "JWT")
|
|
174
|
+
print(f"Value start: line {field.value_node.start_mark.line}, col {field.value_node.start_mark.column}")
|
|
175
|
+
print(f"Value end: line {field.value_node.end_mark.line}, col {field.value_node.end_mark.column}")
|
|
176
|
+
|
|
177
|
+
# Full field range (from key start to value end)
|
|
178
|
+
start = field.key_node.start_mark
|
|
179
|
+
end = field.value_node.end_mark
|
|
180
|
+
print(f"Field range: ({start.line}:{start.column}) to ({end.line}:{end.column})")
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Invalid Data Handling
|
|
184
|
+
|
|
185
|
+
Low-level models preserve invalid data:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from ruamel.yaml import YAML
|
|
189
|
+
from jentic.apitools.openapi.datamodels.low.v30.security_scheme import build as build_security_scheme
|
|
190
|
+
|
|
191
|
+
yaml = YAML()
|
|
192
|
+
root = yaml.compose("bearerFormat: 123") # Wrong type (should be string)
|
|
193
|
+
|
|
194
|
+
security_scheme = build_security_scheme(root)
|
|
195
|
+
print(security_scheme.bearer_format.value) # 123 (preserved as-is)
|
|
196
|
+
print(type(security_scheme.bearer_format.value)) # <class 'int'>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Error Reporting
|
|
200
|
+
|
|
201
|
+
This architecture—where the low-level model preserves data without validation and validation tools consume
|
|
202
|
+
that data—allows the low-level model to remain simple while enabling sophisticated validation tools to provide
|
|
203
|
+
user-friendly error messages with exact source locations.
|
|
204
|
+
|
|
205
|
+
## Testing
|
|
206
|
+
|
|
207
|
+
Run the test suite:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
uv run --package jentic-openapi-datamodels pytest packages/jentic-openapi-datamodels -v
|
|
211
|
+
```
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
from semantic_release.cli.commands.version import build_distributions
|
|
2
|
+
|
|
3
|
+
# jentic-openapi-datamodels
|
|
4
|
+
|
|
5
|
+
Low-level and high-level data models for OpenAPI specifications.
|
|
6
|
+
|
|
7
|
+
This package provides data model classes for representing OpenAPI specification objects in Python.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
**Low-Level Architecture**
|
|
12
|
+
- **Preserve Everything**: All data from source documents preserved exactly as-is, including invalid values
|
|
13
|
+
- **Zero Validation**: No validation or coercion during parsing - deferred to higher layers
|
|
14
|
+
- **Separation of Concerns**: Low-level model focuses on faithful representation; validation belongs elsewhere
|
|
15
|
+
|
|
16
|
+
**Source Tracking**
|
|
17
|
+
- **Complete Source Fidelity**: Every field tracks its exact YAML node location
|
|
18
|
+
- **Precise Error Reporting**: Line and column numbers via `start_mark` and `end_mark`
|
|
19
|
+
- **Metadata Preservation**: Full position tracking for accurate diagnostics
|
|
20
|
+
|
|
21
|
+
**Python Integration**
|
|
22
|
+
- **Python-Idiomatic Naming**: snake_case field names (e.g., `bearer_format`, `property_name`)
|
|
23
|
+
- **Spec-Aligned Mapping**: Automatic YAML name mapping (e.g., `bearerFormat` ↔ `bearer_format`)
|
|
24
|
+
- **Type Safety**: Full type hints with Generic types (`FieldSource[T]`, `KeySource[T]`, `ValueSource[T]`)
|
|
25
|
+
|
|
26
|
+
**Extensibility**
|
|
27
|
+
- **Extension Support**: Automatic extraction of OpenAPI `x-*` specification extensions
|
|
28
|
+
- **Unknown Field Tracking**: Capture typos and invalid fields for validation tools
|
|
29
|
+
- **Generic Builder Pattern**: Core `build_model()` function with object-specific builders for complex cases
|
|
30
|
+
|
|
31
|
+
**Performance**
|
|
32
|
+
- **Memory Efficient**: Immutable frozen dataclasses with `__slots__` for optimal memory usage
|
|
33
|
+
- **Shared Context**: All instances share a single YAML constructor for efficiency
|
|
34
|
+
|
|
35
|
+
**Version Support**
|
|
36
|
+
- **OpenAPI 2.0**: Planned for future release
|
|
37
|
+
- **OpenAPI 3.0.x**: Currently implemented
|
|
38
|
+
- **OpenAPI 3.1.x**: Planned for future release
|
|
39
|
+
- **OpenAPI 3.2.x**: Planned for future release
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install jentic-openapi-datamodels
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Prerequisites:**
|
|
48
|
+
- Python 3.11+
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
### Basic Usage
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from ruamel.yaml import YAML
|
|
56
|
+
from jentic.apitools.openapi.datamodels.low.v30.security_scheme import build
|
|
57
|
+
|
|
58
|
+
# Parse YAML
|
|
59
|
+
yaml = YAML()
|
|
60
|
+
root = yaml.compose("""
|
|
61
|
+
type: http
|
|
62
|
+
scheme: bearer
|
|
63
|
+
bearerFormat: JWT
|
|
64
|
+
""")
|
|
65
|
+
|
|
66
|
+
# Build low-level model
|
|
67
|
+
security_scheme = build(root)
|
|
68
|
+
|
|
69
|
+
# Access via Python field names (snake_case)
|
|
70
|
+
print(security_scheme.bearer_format.value) # "JWT"
|
|
71
|
+
|
|
72
|
+
# Access source location information
|
|
73
|
+
print(security_scheme.bearer_format.key_node.value) # "bearerFormat"
|
|
74
|
+
print(security_scheme.bearer_format.key_node.start_mark.line) # Line number
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Field Name Mapping
|
|
78
|
+
|
|
79
|
+
YAML `camelCase` fields automatically map to Python `snake_case`:
|
|
80
|
+
- `bearerFormat` → `bearer_format`
|
|
81
|
+
- `authorizationUrl` → `authorization_url`
|
|
82
|
+
- `openIdConnectUrl` → `openid_connect_url`
|
|
83
|
+
|
|
84
|
+
Special cases for Python reserved keywords/special characters:
|
|
85
|
+
- `$ref` → `ref`
|
|
86
|
+
- `in` → `in_`
|
|
87
|
+
|
|
88
|
+
### Source Tracking
|
|
89
|
+
|
|
90
|
+
The package provides three immutable wrapper types for preserving source information:
|
|
91
|
+
|
|
92
|
+
**FieldSource[T]** - For OpenAPI fields with key-value pairs
|
|
93
|
+
- Used for: Fixed fields (`name`, `bearer_format`) and patterned fields (status codes, path items, schema properties)
|
|
94
|
+
- Tracks: Both key and value nodes
|
|
95
|
+
- Example: `SecurityScheme.bearer_format` is `FieldSource[str]`, response status codes are `FieldSource[Response]`
|
|
96
|
+
|
|
97
|
+
**KeySource[T]** - For dictionary keys
|
|
98
|
+
- Used for: keys in OpenAPI fields, `x-*` extensions and mapping dictionaries
|
|
99
|
+
- Tracks: Only key node
|
|
100
|
+
- Example: Keys in `Discriminator.mapping` are `KeySource[str]`
|
|
101
|
+
|
|
102
|
+
**ValueSource[T]** - For dictionary values and array items
|
|
103
|
+
- Used for: values in OpenAPI fields, in `x-*` extensions, mapping dictionaries and array items
|
|
104
|
+
- Tracks: Only value node
|
|
105
|
+
- Example: Values in `Discriminator.mapping` are `ValueSource[str]`
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from ruamel.yaml import YAML
|
|
109
|
+
from jentic.apitools.openapi.datamodels.low.v30.security_scheme import build as build_security_scheme
|
|
110
|
+
from jentic.apitools.openapi.datamodels.low.v30.discriminator import build as build_discriminator
|
|
111
|
+
|
|
112
|
+
# FieldSource: Fixed specification fields
|
|
113
|
+
yaml = YAML()
|
|
114
|
+
root = yaml.compose("type: http\nscheme: bearer\nbearerFormat: JWT")
|
|
115
|
+
security_scheme = build_security_scheme(root)
|
|
116
|
+
|
|
117
|
+
field = security_scheme.bearer_format # FieldSource[str]
|
|
118
|
+
print(field.value) # "JWT" - The actual value
|
|
119
|
+
print(field.key_node) # YAML node for "bearerFormat"
|
|
120
|
+
print(field.value_node) # YAML node for "JWT"
|
|
121
|
+
|
|
122
|
+
# KeySource/ValueSource: Dictionary fields (mapping, extensions)
|
|
123
|
+
root = yaml.compose("propertyName: petType\nmapping:\n dog: Dog\n cat: Cat")
|
|
124
|
+
discriminator = build_discriminator(root)
|
|
125
|
+
|
|
126
|
+
for key, value in discriminator.mapping.value.items():
|
|
127
|
+
print(key.value) # KeySource[str]: "dog" or "cat"
|
|
128
|
+
print(key.key_node) # YAML node for the key
|
|
129
|
+
print(value.value) # ValueSource[str]: "Dog" or "Cat"
|
|
130
|
+
print(value.value_node) # YAML node for the value
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Location Ranges
|
|
134
|
+
|
|
135
|
+
Access precise location ranges within the source document using start_mark and end_mark:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from ruamel.yaml import YAML
|
|
139
|
+
from jentic.apitools.openapi.datamodels.low.v30.security_scheme import build as build_security_scheme
|
|
140
|
+
|
|
141
|
+
yaml_content = """
|
|
142
|
+
type: http
|
|
143
|
+
scheme: bearer
|
|
144
|
+
bearerFormat: JWT
|
|
145
|
+
description: Bearer token authentication
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
yaml = YAML()
|
|
149
|
+
root = yaml.compose(yaml_content)
|
|
150
|
+
security_scheme = build_security_scheme(root)
|
|
151
|
+
|
|
152
|
+
# Access location information for any field
|
|
153
|
+
field = security_scheme.bearer_format
|
|
154
|
+
|
|
155
|
+
# Key location (e.g., "bearerFormat")
|
|
156
|
+
print(f"Key start: line {field.key_node.start_mark.line}, col {field.key_node.start_mark.column}")
|
|
157
|
+
print(f"Key end: line {field.key_node.end_mark.line}, col {field.key_node.end_mark.column}")
|
|
158
|
+
|
|
159
|
+
# Value location (e.g., "JWT")
|
|
160
|
+
print(f"Value start: line {field.value_node.start_mark.line}, col {field.value_node.start_mark.column}")
|
|
161
|
+
print(f"Value end: line {field.value_node.end_mark.line}, col {field.value_node.end_mark.column}")
|
|
162
|
+
|
|
163
|
+
# Full field range (from key start to value end)
|
|
164
|
+
start = field.key_node.start_mark
|
|
165
|
+
end = field.value_node.end_mark
|
|
166
|
+
print(f"Field range: ({start.line}:{start.column}) to ({end.line}:{end.column})")
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Invalid Data Handling
|
|
170
|
+
|
|
171
|
+
Low-level models preserve invalid data:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from ruamel.yaml import YAML
|
|
175
|
+
from jentic.apitools.openapi.datamodels.low.v30.security_scheme import build as build_security_scheme
|
|
176
|
+
|
|
177
|
+
yaml = YAML()
|
|
178
|
+
root = yaml.compose("bearerFormat: 123") # Wrong type (should be string)
|
|
179
|
+
|
|
180
|
+
security_scheme = build_security_scheme(root)
|
|
181
|
+
print(security_scheme.bearer_format.value) # 123 (preserved as-is)
|
|
182
|
+
print(type(security_scheme.bearer_format.value)) # <class 'int'>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Error Reporting
|
|
186
|
+
|
|
187
|
+
This architecture—where the low-level model preserves data without validation and validation tools consume
|
|
188
|
+
that data—allows the low-level model to remain simple while enabling sophisticated validation tools to provide
|
|
189
|
+
user-friendly error messages with exact source locations.
|
|
190
|
+
|
|
191
|
+
## Testing
|
|
192
|
+
|
|
193
|
+
Run the test suite:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
uv run --package jentic-openapi-datamodels pytest packages/jentic-openapi-datamodels -v
|
|
197
|
+
```
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "jentic-openapi-datamodels"
|
|
3
|
-
version = "1.0.0-alpha.
|
|
3
|
+
version = "1.0.0-alpha.13"
|
|
4
4
|
description = "Jentic OpenAPI Data Models"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "Jentic", email = "hello@jentic.com" }]
|
|
7
7
|
license = "Apache-2.0"
|
|
8
8
|
license-files = ["LICENSE", "NOTICE"]
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
|
-
dependencies = [
|
|
10
|
+
dependencies = [
|
|
11
|
+
"ruamel-yaml~=0.18.15"
|
|
12
|
+
]
|
|
11
13
|
|
|
12
14
|
[project.urls]
|
|
13
15
|
Homepage = "https://github.com/jentic/jentic-openapi-tools"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
from ruamel.yaml import YAML
|
|
4
|
+
from ruamel.yaml.constructor import RoundTripConstructor
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
__all__ = ["Context"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class Context:
|
|
12
|
+
"""
|
|
13
|
+
Context for parsing OpenAPI documents.
|
|
14
|
+
|
|
15
|
+
Contains configuration and dependencies used during parsing operations.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
yaml_constructor: The YAML constructor used to deserialize YAML nodes into Python objects
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
yaml_constructor: RoundTripConstructor = field(default=YAML(typ="rt", pure=True).constructor)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from ruamel import yaml
|
|
2
|
+
|
|
3
|
+
from jentic.apitools.openapi.datamodels.low.context import Context
|
|
4
|
+
from jentic.apitools.openapi.datamodels.low.fields import fixed_fields
|
|
5
|
+
from jentic.apitools.openapi.datamodels.low.sources import KeySource, ValueSource, YAMLValue
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
__all__ = ["extract_extension_fields", "extract_unknown_fields"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def extract_extension_fields(
|
|
12
|
+
node: yaml.MappingNode, context: Context | None = None
|
|
13
|
+
) -> dict[KeySource[str], ValueSource[YAMLValue]]:
|
|
14
|
+
"""
|
|
15
|
+
Extract OpenAPI specification extension fields from a YAML mapping node.
|
|
16
|
+
|
|
17
|
+
Specification extension fields are any fields that start with "x-" and allow
|
|
18
|
+
users to add custom properties to OpenAPI definitions.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
node: The YAML mapping node to extract extension fields from
|
|
22
|
+
context: Optional parsing context. If None, a default context will be created.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
A dictionary mapping extension field names to their values, or empty dict if no extension fields found
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
Given YAML like:
|
|
29
|
+
name: id
|
|
30
|
+
x-custom: value
|
|
31
|
+
x-internal: true
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
{
|
|
35
|
+
KeySource(value="x-custom", key_node=...): ValueSource(value="value", value_node=...),
|
|
36
|
+
KeySource(value="x-internal", key_node=...): ValueSource(value=True, value_node=...)
|
|
37
|
+
}
|
|
38
|
+
"""
|
|
39
|
+
if not isinstance(node, yaml.MappingNode):
|
|
40
|
+
return {}
|
|
41
|
+
|
|
42
|
+
if context is None:
|
|
43
|
+
context = Context()
|
|
44
|
+
|
|
45
|
+
extensions: dict[KeySource[str], ValueSource[YAMLValue]] = {}
|
|
46
|
+
|
|
47
|
+
for key_node, value_node in node.value:
|
|
48
|
+
# Construct the key as a Python object (should be a string)
|
|
49
|
+
py_key = context.yaml_constructor.construct_yaml_str(key_node)
|
|
50
|
+
|
|
51
|
+
# Check if it's an extension (starts with "x-")
|
|
52
|
+
if isinstance(py_key, str) and py_key.startswith("x-"):
|
|
53
|
+
# Construct the actual Python value from the YAML node
|
|
54
|
+
py_value = context.yaml_constructor.construct_object(value_node, deep=True)
|
|
55
|
+
|
|
56
|
+
key_ref = KeySource[str](value=py_key, key_node=key_node)
|
|
57
|
+
value_ref = ValueSource[YAMLValue](value=py_value, value_node=value_node)
|
|
58
|
+
extensions[key_ref] = value_ref
|
|
59
|
+
|
|
60
|
+
return extensions
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def extract_unknown_fields(
|
|
64
|
+
node: yaml.MappingNode, dataclass_type: type, context: Context | None = None
|
|
65
|
+
) -> dict[KeySource[str], ValueSource[YAMLValue]]:
|
|
66
|
+
"""
|
|
67
|
+
Extract unknown fields from a YAML mapping node.
|
|
68
|
+
|
|
69
|
+
Unknown fields are fields that are not part of the OpenAPI specification
|
|
70
|
+
(not fixed fields of the dataclass) and are not extensions (don't start with "x-").
|
|
71
|
+
These are typically typos or fields from a different specification version.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
node: The YAML mapping node to extract unknown fields from
|
|
75
|
+
dataclass_type: The dataclass type to get valid field names from
|
|
76
|
+
context: Optional parsing context. If None, a default context will be created.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
A dictionary mapping unknown field names to their values, or empty dict if no unknown fields found
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
Given YAML like:
|
|
83
|
+
name: id
|
|
84
|
+
namspace: http://example.com # typo - should be "namespace"
|
|
85
|
+
customField: value # unknown field
|
|
86
|
+
x-custom: value # extension - not unknown
|
|
87
|
+
|
|
88
|
+
With dataclass_type=XML (which has fixed fields: name, namespace, prefix, attribute, wrapped)
|
|
89
|
+
Returns:
|
|
90
|
+
{
|
|
91
|
+
KeySource(value="namspace", key_node=...): ValueSource(value="http://example.com", value_node=...),
|
|
92
|
+
KeySource(value="customField", key_node=...): ValueSource(value="value", value_node=...)
|
|
93
|
+
}
|
|
94
|
+
"""
|
|
95
|
+
if not isinstance(node, yaml.MappingNode):
|
|
96
|
+
return {}
|
|
97
|
+
|
|
98
|
+
if context is None:
|
|
99
|
+
context = Context()
|
|
100
|
+
|
|
101
|
+
# Get valid YAML field names from the dataclass (considering yaml_name metadata)
|
|
102
|
+
_fixed_fields = fixed_fields(dataclass_type)
|
|
103
|
+
yaml_field_names = {
|
|
104
|
+
field.metadata.get("yaml_name", fname) for fname, field in _fixed_fields.items()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
unknown_fields: dict[KeySource[str], ValueSource[YAMLValue]] = {}
|
|
108
|
+
|
|
109
|
+
for key_node, value_node in node.value:
|
|
110
|
+
# Construct the key as a Python object (should be a string)
|
|
111
|
+
py_key = context.yaml_constructor.construct_yaml_str(key_node)
|
|
112
|
+
|
|
113
|
+
# Check if it's an unknown field (not in valid YAML field names and not an extension)
|
|
114
|
+
if (
|
|
115
|
+
isinstance(py_key, str)
|
|
116
|
+
and py_key not in yaml_field_names
|
|
117
|
+
and not py_key.startswith("x-")
|
|
118
|
+
):
|
|
119
|
+
# Construct the actual Python value from the YAML node
|
|
120
|
+
py_value = context.yaml_constructor.construct_object(value_node, deep=True)
|
|
121
|
+
|
|
122
|
+
key_ref = KeySource[str](value=py_key, key_node=key_node)
|
|
123
|
+
value_ref = ValueSource[YAMLValue](value=py_value, value_node=value_node)
|
|
124
|
+
unknown_fields[key_ref] = value_ref
|
|
125
|
+
|
|
126
|
+
return unknown_fields
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from dataclasses import field, fields
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
__all__ = ["fixed_field", "fixed_fields", "patterned_field", "patterned_fields"]
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def fixed_field(default=None, metadata=None):
|
|
8
|
+
"""Mark a field as a fixed OpenAPI specification field."""
|
|
9
|
+
return field(default=default, metadata={**(metadata or {}), "fixed_field": True})
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def fixed_fields(dataclass_type):
|
|
13
|
+
"""
|
|
14
|
+
Get all fixed specification fields from a dataclass.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
dataclass_type: The dataclass type to inspect
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
A dictionary mapping field names to field objects for all fields marked with fixed_field()
|
|
21
|
+
"""
|
|
22
|
+
return {f.name: f for f in fields(dataclass_type) if f.metadata.get("fixed_field")}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def patterned_field(default=None, metadata=None):
|
|
26
|
+
"""
|
|
27
|
+
Mark a field as containing OpenAPI patterned fields.
|
|
28
|
+
|
|
29
|
+
Patterned fields have dynamic names that follow a specific pattern (e.g., security scheme names,
|
|
30
|
+
path patterns, callback expressions, HTTP status codes).
|
|
31
|
+
"""
|
|
32
|
+
return field(default=default, metadata={**(metadata or {}), "patterned_field": True})
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def patterned_fields(dataclass_type):
|
|
36
|
+
"""
|
|
37
|
+
Get all patterned fields from a dataclass.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
dataclass_type: The dataclass type to inspect
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
A dictionary mapping field names to field objects for all fields marked with patterned_field()
|
|
44
|
+
"""
|
|
45
|
+
return {f.name: f for f in fields(dataclass_type) if f.metadata.get("patterned_field")}
|