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.
- jentic/apitools/openapi/datamodels/low/v30/__init__.py +28 -0
- jentic/apitools/openapi/datamodels/low/v30/discriminator.py +91 -0
- jentic/apitools/openapi/datamodels/low/v30/external_documentation.py +79 -0
- jentic/apitools/openapi/datamodels/low/v30/oauth_flow.py +140 -0
- jentic/apitools/openapi/datamodels/low/v30/oauth_flows.py +165 -0
- jentic/apitools/openapi/datamodels/low/v30/py.typed +0 -0
- jentic/apitools/openapi/datamodels/low/v30/reference.py +64 -0
- jentic/apitools/openapi/datamodels/low/v30/schema.py +626 -0
- jentic/apitools/openapi/datamodels/low/v30/security_requirement.py +91 -0
- jentic/apitools/openapi/datamodels/low/v30/security_scheme.py +301 -0
- jentic/apitools/openapi/datamodels/low/v30/specification_object.py +217 -0
- jentic/apitools/openapi/datamodels/low/v30/tag.py +132 -0
- jentic/apitools/openapi/datamodels/low/v30/xml.py +134 -0
- jentic_openapi_datamodels-1.0.0a2.dist-info/METADATA +52 -0
- jentic_openapi_datamodels-1.0.0a2.dist-info/RECORD +18 -0
- jentic_openapi_datamodels-1.0.0a2.dist-info/WHEEL +4 -0
- jentic_openapi_datamodels-1.0.0a2.dist-info/licenses/LICENSE +202 -0
- jentic_openapi_datamodels-1.0.0a2.dist-info/licenses/NOTICE +4 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAPI 3.0.4 Security Requirement Object model.
|
|
3
|
+
|
|
4
|
+
The Security Requirement Object defines which security mechanisms can be used for a
|
|
5
|
+
particular operation. Each named security scheme is mapped to a list of scope names
|
|
6
|
+
required for execution (for OAuth2/OIDC) or an empty list (for other schemes).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from jentic.apitools.openapi.datamodels.low.v30.specification_object import SpecificationObject
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
__all__ = ["SecurityRequirement"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SecurityRequirement(SpecificationObject):
|
|
16
|
+
"""
|
|
17
|
+
Represents a Security Requirement Object from OpenAPI 3.0.4.
|
|
18
|
+
|
|
19
|
+
This IS a mapping. Keys are security scheme names, values are lists of scope strings.
|
|
20
|
+
|
|
21
|
+
Lists the required security schemes to execute an operation. For each security
|
|
22
|
+
scheme, a list of scope names is provided. When multiple Security Requirement
|
|
23
|
+
Objects are defined, only ONE needs to be satisfied to authorize the request.
|
|
24
|
+
|
|
25
|
+
IMPORTANT: Security Requirement Objects do NOT support specification extensions.
|
|
26
|
+
Any key starting with "x-" is treated as a security scheme name, not an extension.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
>>> # Non-OAuth2 requirement
|
|
30
|
+
>>> req = SecurityRequirement({"api_key": []})
|
|
31
|
+
>>> req["api_key"]
|
|
32
|
+
[]
|
|
33
|
+
|
|
34
|
+
>>> # OAuth2 requirement with scopes
|
|
35
|
+
>>> req = SecurityRequirement({"petstore_auth": ["write:pets", "read:pets"]})
|
|
36
|
+
>>> req["petstore_auth"]
|
|
37
|
+
['write:pets', 'read:pets']
|
|
38
|
+
|
|
39
|
+
>>> # Empty requirement (makes security optional)
|
|
40
|
+
>>> req = SecurityRequirement({})
|
|
41
|
+
|
|
42
|
+
>>> # Security scheme named "x-custom" (NOT an extension)
|
|
43
|
+
>>> req = SecurityRequirement({"x-custom": []})
|
|
44
|
+
>>> "x-custom" in req
|
|
45
|
+
True
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
_supports_extensions: bool = False
|
|
49
|
+
|
|
50
|
+
def __getitem__(self, key: str) -> list[str]:
|
|
51
|
+
"""Get scopes for a security scheme (dict-style access)."""
|
|
52
|
+
return super().__getitem__(key) # type: ignore
|
|
53
|
+
|
|
54
|
+
def __getattr__(self, name: str) -> list[str]:
|
|
55
|
+
"""Get scopes for a security scheme (attribute-style access)."""
|
|
56
|
+
return super().__getattr__(name) # type: ignore
|
|
57
|
+
|
|
58
|
+
def get_schemes(self) -> list[str]:
|
|
59
|
+
"""
|
|
60
|
+
Get the list of security scheme names referenced.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
List of security scheme names
|
|
64
|
+
"""
|
|
65
|
+
return list(self.keys())
|
|
66
|
+
|
|
67
|
+
def get_scopes(self, scheme_name: str) -> list[str]:
|
|
68
|
+
"""
|
|
69
|
+
Get the scopes required for a specific security scheme.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
scheme_name: Name of the security scheme
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of scope names (empty for non-OAuth2/OIDC schemes)
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
KeyError: If scheme_name is not in this requirement
|
|
79
|
+
"""
|
|
80
|
+
return self[scheme_name]
|
|
81
|
+
|
|
82
|
+
def is_empty(self) -> bool:
|
|
83
|
+
"""
|
|
84
|
+
Check if this is an empty security requirement.
|
|
85
|
+
|
|
86
|
+
Empty requirements ({}) make security optional for an operation.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
True if requirements mapping is empty
|
|
90
|
+
"""
|
|
91
|
+
return len(self) == 0
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAPI 3.0.4 Security Scheme Object model.
|
|
3
|
+
|
|
4
|
+
Defines a security scheme that can be used by the operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from jentic.apitools.openapi.datamodels.low.v30.oauth_flows import OAuthFlows
|
|
11
|
+
from jentic.apitools.openapi.datamodels.low.v30.specification_object import SpecificationObject
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__all__ = ["SecurityScheme"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SecurityScheme(SpecificationObject):
|
|
18
|
+
"""
|
|
19
|
+
Represents a Security Scheme Object from OpenAPI 3.0.4.
|
|
20
|
+
|
|
21
|
+
Defines a security scheme that can be used by the operations. Different
|
|
22
|
+
scheme types require different combinations of fields.
|
|
23
|
+
|
|
24
|
+
Supports specification extensions (x-* fields).
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
>>> # API Key scheme
|
|
28
|
+
>>> scheme = SecurityScheme({
|
|
29
|
+
... "type": "apiKey",
|
|
30
|
+
... "name": "api_key",
|
|
31
|
+
... "in": "header"
|
|
32
|
+
... })
|
|
33
|
+
>>> scheme.type
|
|
34
|
+
'apiKey'
|
|
35
|
+
>>> scheme.in_
|
|
36
|
+
'header'
|
|
37
|
+
|
|
38
|
+
>>> # HTTP Bearer scheme
|
|
39
|
+
>>> scheme = SecurityScheme({
|
|
40
|
+
... "type": "http",
|
|
41
|
+
... "scheme": "bearer",
|
|
42
|
+
... "bearerFormat": "JWT"
|
|
43
|
+
... })
|
|
44
|
+
>>> scheme.is_http()
|
|
45
|
+
True
|
|
46
|
+
>>> scheme.bearer_format
|
|
47
|
+
'JWT'
|
|
48
|
+
|
|
49
|
+
>>> # OAuth2 scheme with flows
|
|
50
|
+
>>> scheme = SecurityScheme({
|
|
51
|
+
... "type": "oauth2",
|
|
52
|
+
... "flows": {
|
|
53
|
+
... "implicit": {
|
|
54
|
+
... "authorizationUrl": "https://example.com/oauth/authorize",
|
|
55
|
+
... "scopes": {"read": "Read access"}
|
|
56
|
+
... }
|
|
57
|
+
... }
|
|
58
|
+
... })
|
|
59
|
+
>>> scheme.is_oauth2()
|
|
60
|
+
True
|
|
61
|
+
>>> scheme.flows.implicit.authorization_url
|
|
62
|
+
'https://example.com/oauth/authorize'
|
|
63
|
+
|
|
64
|
+
>>> # OpenID Connect scheme
|
|
65
|
+
>>> scheme = SecurityScheme({
|
|
66
|
+
... "type": "openIdConnect",
|
|
67
|
+
... "openIdConnectUrl": "https://example.com/.well-known/openid-configuration"
|
|
68
|
+
... })
|
|
69
|
+
>>> scheme.is_openid_connect()
|
|
70
|
+
True
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
_supports_extensions: bool = True
|
|
74
|
+
_fixed_fields: frozenset[str] = frozenset(
|
|
75
|
+
{"type", "description", "name", "in", "scheme", "bearerFormat", "flows", "openIdConnectUrl"}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def __init__(self, data: Mapping[str, Any] | None = None):
|
|
79
|
+
"""
|
|
80
|
+
Initialize a SecurityScheme object.
|
|
81
|
+
|
|
82
|
+
Automatically marshals nested flows data (Mapping) into OAuthFlows instance.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
data: Optional mapping to initialize the object with
|
|
86
|
+
"""
|
|
87
|
+
super().__init__()
|
|
88
|
+
if data:
|
|
89
|
+
for key, value in data.items():
|
|
90
|
+
# Marshal flows field specifically if it's a raw Mapping (not already OAuthFlows)
|
|
91
|
+
if (
|
|
92
|
+
key == "flows"
|
|
93
|
+
and isinstance(value, Mapping)
|
|
94
|
+
and not isinstance(value, OAuthFlows)
|
|
95
|
+
):
|
|
96
|
+
self[key] = OAuthFlows(value)
|
|
97
|
+
else:
|
|
98
|
+
# Store as-is (already OAuthFlows, extension, or other)
|
|
99
|
+
self[key] = self._copy_value(value)
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def type(self) -> str | None:
|
|
103
|
+
"""
|
|
104
|
+
The type of the security scheme.
|
|
105
|
+
|
|
106
|
+
Valid values: "apiKey", "http", "oauth2", "openIdConnect", "mutualTLS"
|
|
107
|
+
|
|
108
|
+
REQUIRED field.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Security scheme type or None if not present
|
|
112
|
+
"""
|
|
113
|
+
return self.get("type")
|
|
114
|
+
|
|
115
|
+
@type.setter
|
|
116
|
+
def type(self, value: str | None) -> None:
|
|
117
|
+
"""Set the security scheme type."""
|
|
118
|
+
if value is None:
|
|
119
|
+
self.pop("type", None)
|
|
120
|
+
else:
|
|
121
|
+
self["type"] = value
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def description(self) -> str | None:
|
|
125
|
+
"""
|
|
126
|
+
A description for security scheme.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Description or None if not present
|
|
130
|
+
"""
|
|
131
|
+
return self.get("description")
|
|
132
|
+
|
|
133
|
+
@description.setter
|
|
134
|
+
def description(self, value: str | None) -> None:
|
|
135
|
+
"""Set the description."""
|
|
136
|
+
if value is None:
|
|
137
|
+
self.pop("description", None)
|
|
138
|
+
else:
|
|
139
|
+
self["description"] = value
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def name(self) -> str | None:
|
|
143
|
+
"""
|
|
144
|
+
The name of the header, query or cookie parameter.
|
|
145
|
+
|
|
146
|
+
REQUIRED for apiKey type.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Parameter name or None if not present
|
|
150
|
+
"""
|
|
151
|
+
return self.get("name")
|
|
152
|
+
|
|
153
|
+
@name.setter
|
|
154
|
+
def name(self, value: str | None) -> None:
|
|
155
|
+
"""Set the parameter name."""
|
|
156
|
+
if value is None:
|
|
157
|
+
self.pop("name", None)
|
|
158
|
+
else:
|
|
159
|
+
self["name"] = value
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def in_(self) -> str | None:
|
|
163
|
+
"""
|
|
164
|
+
The location of the API key.
|
|
165
|
+
|
|
166
|
+
Valid values: "query", "header", "cookie"
|
|
167
|
+
|
|
168
|
+
REQUIRED for apiKey type.
|
|
169
|
+
|
|
170
|
+
Note: Uses 'in_' to avoid Python keyword collision.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Location or None if not present
|
|
174
|
+
"""
|
|
175
|
+
return self.get("in")
|
|
176
|
+
|
|
177
|
+
@in_.setter
|
|
178
|
+
def in_(self, value: str | None) -> None:
|
|
179
|
+
"""Set the API key location."""
|
|
180
|
+
if value is None:
|
|
181
|
+
self.pop("in", None)
|
|
182
|
+
else:
|
|
183
|
+
self["in"] = value
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def scheme(self) -> str | None:
|
|
187
|
+
"""
|
|
188
|
+
The name of the HTTP Authorization scheme.
|
|
189
|
+
|
|
190
|
+
Examples: "bearer", "basic", "digest"
|
|
191
|
+
|
|
192
|
+
REQUIRED for http type.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Scheme name or None if not present
|
|
196
|
+
"""
|
|
197
|
+
return self.get("scheme")
|
|
198
|
+
|
|
199
|
+
@scheme.setter
|
|
200
|
+
def scheme(self, value: str | None) -> None:
|
|
201
|
+
"""Set the HTTP scheme."""
|
|
202
|
+
if value is None:
|
|
203
|
+
self.pop("scheme", None)
|
|
204
|
+
else:
|
|
205
|
+
self["scheme"] = value
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def bearer_format(self) -> str | None:
|
|
209
|
+
"""
|
|
210
|
+
A hint to the client to identify how the bearer token is formatted.
|
|
211
|
+
|
|
212
|
+
Examples: "JWT", "opaque"
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Bearer format or None if not present
|
|
216
|
+
"""
|
|
217
|
+
return self.get("bearerFormat")
|
|
218
|
+
|
|
219
|
+
@bearer_format.setter
|
|
220
|
+
def bearer_format(self, value: str | None) -> None:
|
|
221
|
+
"""Set the bearer format."""
|
|
222
|
+
if value is None:
|
|
223
|
+
self.pop("bearerFormat", None)
|
|
224
|
+
else:
|
|
225
|
+
self["bearerFormat"] = value
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def flows(self) -> OAuthFlows | None:
|
|
229
|
+
"""
|
|
230
|
+
Configuration information for the OAuth flows.
|
|
231
|
+
|
|
232
|
+
REQUIRED for oauth2 type.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
OAuthFlows instance or None if not present
|
|
236
|
+
"""
|
|
237
|
+
return self.get("flows")
|
|
238
|
+
|
|
239
|
+
@flows.setter
|
|
240
|
+
def flows(self, value: OAuthFlows | None) -> None:
|
|
241
|
+
"""Set the OAuth flows configuration."""
|
|
242
|
+
if value is None:
|
|
243
|
+
self.pop("flows", None)
|
|
244
|
+
else:
|
|
245
|
+
self["flows"] = value
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def open_id_connect_url(self) -> str | None:
|
|
249
|
+
"""
|
|
250
|
+
OpenID Connect URL to discover OAuth2 configuration values.
|
|
251
|
+
|
|
252
|
+
REQUIRED for openIdConnect type.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
OpenID Connect URL or None if not present
|
|
256
|
+
"""
|
|
257
|
+
return self.get("openIdConnectUrl")
|
|
258
|
+
|
|
259
|
+
@open_id_connect_url.setter
|
|
260
|
+
def open_id_connect_url(self, value: str | None) -> None:
|
|
261
|
+
"""Set the OpenID Connect URL."""
|
|
262
|
+
if value is None:
|
|
263
|
+
self.pop("openIdConnectUrl", None)
|
|
264
|
+
else:
|
|
265
|
+
self["openIdConnectUrl"] = value
|
|
266
|
+
|
|
267
|
+
def is_api_key(self) -> bool:
|
|
268
|
+
"""
|
|
269
|
+
Check if this is an API Key security scheme.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
True if type is "apiKey"
|
|
273
|
+
"""
|
|
274
|
+
return self.type == "apiKey"
|
|
275
|
+
|
|
276
|
+
def is_http(self) -> bool:
|
|
277
|
+
"""
|
|
278
|
+
Check if this is an HTTP security scheme.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
True if type is "http"
|
|
282
|
+
"""
|
|
283
|
+
return self.type == "http"
|
|
284
|
+
|
|
285
|
+
def is_oauth2(self) -> bool:
|
|
286
|
+
"""
|
|
287
|
+
Check if this is an OAuth2 security scheme.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
True if type is "oauth2"
|
|
291
|
+
"""
|
|
292
|
+
return self.type == "oauth2"
|
|
293
|
+
|
|
294
|
+
def is_openid_connect(self) -> bool:
|
|
295
|
+
"""
|
|
296
|
+
Check if this is an OpenID Connect security scheme.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
True if type is "openIdConnect"
|
|
300
|
+
"""
|
|
301
|
+
return self.type == "openIdConnect"
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Base class for OpenAPI specification objects."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC
|
|
4
|
+
from collections.abc import Mapping, MutableMapping, Sequence
|
|
5
|
+
from copy import copy
|
|
6
|
+
from typing import Any, Iterator, TypeVar
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
__all__ = ["SpecificationObject"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T", bound="SpecificationObject")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SpecificationObject(ABC, MutableMapping[str, Any]):
|
|
16
|
+
"""
|
|
17
|
+
Base class for OpenAPI specification objects.
|
|
18
|
+
|
|
19
|
+
Implements a MutableMapping interface with data stored in __dict__.
|
|
20
|
+
Subclasses become dict-like objects where all attributes are accessible
|
|
21
|
+
via both attribute access (obj.foo) and item access (obj["foo"]).
|
|
22
|
+
|
|
23
|
+
Class Attributes:
|
|
24
|
+
_supports_extensions: Whether this object type supports x-* specification extensions.
|
|
25
|
+
Default is True. Set to False for objects like Security Requirement
|
|
26
|
+
that are pure maps where x-* are regular keys.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
_supports_extensions: bool = False
|
|
30
|
+
|
|
31
|
+
def __init__(self, data: Mapping[str, Any] | None = None):
|
|
32
|
+
"""
|
|
33
|
+
Initialize a SpecificationObject.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
data: Optional mapping to initialize the object with
|
|
37
|
+
"""
|
|
38
|
+
if data:
|
|
39
|
+
for key, value in data.items():
|
|
40
|
+
self[key] = self._copy_value(value)
|
|
41
|
+
|
|
42
|
+
# MutableMapping abstract methods
|
|
43
|
+
def __getitem__(self, key: str) -> Any:
|
|
44
|
+
"""Get an item."""
|
|
45
|
+
return self.__dict__[key]
|
|
46
|
+
|
|
47
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
48
|
+
"""Set an item."""
|
|
49
|
+
self.__dict__[key] = value
|
|
50
|
+
|
|
51
|
+
def __delitem__(self, key: str) -> None:
|
|
52
|
+
"""Delete an item."""
|
|
53
|
+
del self.__dict__[key]
|
|
54
|
+
|
|
55
|
+
def __iter__(self) -> Iterator[str]:
|
|
56
|
+
"""Iterate over keys."""
|
|
57
|
+
return iter(self.__dict__)
|
|
58
|
+
|
|
59
|
+
def __len__(self) -> int:
|
|
60
|
+
"""Return the number of items."""
|
|
61
|
+
return len(self.__dict__)
|
|
62
|
+
|
|
63
|
+
def __getattr__(self, name: str) -> Any:
|
|
64
|
+
"""
|
|
65
|
+
Get an attribute via attribute access.
|
|
66
|
+
|
|
67
|
+
This allows both dict-style (obj["key"]) and attribute-style (obj.key) access.
|
|
68
|
+
Called only when the attribute is not found through normal lookup.
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
return self[name]
|
|
72
|
+
except KeyError:
|
|
73
|
+
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
|
|
74
|
+
|
|
75
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Set an attribute via attribute access.
|
|
78
|
+
|
|
79
|
+
This allows both dict-style (obj["key"] = val) and attribute-style (obj.key = val).
|
|
80
|
+
For properties and other descriptors, delegates to the descriptor.
|
|
81
|
+
"""
|
|
82
|
+
# Check if this is a data descriptor (property, etc.) on the class
|
|
83
|
+
cls = type(self)
|
|
84
|
+
if hasattr(cls, name):
|
|
85
|
+
attr = getattr(cls, name)
|
|
86
|
+
# If it's a data descriptor (has __set__), use normal attribute setting
|
|
87
|
+
if hasattr(attr, "__set__"):
|
|
88
|
+
object.__setattr__(self, name, value)
|
|
89
|
+
return
|
|
90
|
+
# Otherwise, store in the dict
|
|
91
|
+
self[name] = value
|
|
92
|
+
|
|
93
|
+
def get_extensions(self) -> Mapping[str, Any]:
|
|
94
|
+
"""
|
|
95
|
+
Get specification extensions (x-* fields).
|
|
96
|
+
|
|
97
|
+
Returns a filtered view of fields starting with 'x-'.
|
|
98
|
+
If this object type doesn't support extensions (_supports_extensions=False),
|
|
99
|
+
returns an empty mapping.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Mapping of extension fields (keys starting with 'x-')
|
|
103
|
+
"""
|
|
104
|
+
if not type(self)._supports_extensions:
|
|
105
|
+
return {}
|
|
106
|
+
return {k: v for k, v in self.items() if isinstance(k, str) and k.startswith("x-")}
|
|
107
|
+
|
|
108
|
+
def get_fields(self) -> Mapping[str, Any]:
|
|
109
|
+
"""
|
|
110
|
+
Get regular fields (non-extension fields).
|
|
111
|
+
|
|
112
|
+
Returns a filtered view excluding fields starting with 'x-'.
|
|
113
|
+
If this object type doesn't support extensions (_supports_extensions=False),
|
|
114
|
+
returns all fields (x-* are treated as regular fields).
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Mapping of regular fields (excluding x-* if extensions are supported)
|
|
118
|
+
"""
|
|
119
|
+
if not type(self)._supports_extensions:
|
|
120
|
+
return dict(self)
|
|
121
|
+
return {k: v for k, v in self.items() if isinstance(k, str) and not k.startswith("x-")}
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def from_mapping(cls: type[T], data: Mapping[str, Any]) -> T:
|
|
125
|
+
"""
|
|
126
|
+
Create an instance from a mapping.
|
|
127
|
+
|
|
128
|
+
This method does not validate the structure. Use a separate
|
|
129
|
+
validator to ensure the data conforms to the OpenAPI specification.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
data: Mapping to create the object from
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Instance of the class
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
TypeError: If data is not a Mapping
|
|
139
|
+
"""
|
|
140
|
+
if not isinstance(data, Mapping):
|
|
141
|
+
raise TypeError(f"Expected Mapping, got {type(data).__name__}")
|
|
142
|
+
return cls(data=data)
|
|
143
|
+
|
|
144
|
+
def to_mapping(self) -> dict[str, Any]:
|
|
145
|
+
"""
|
|
146
|
+
Convert to a plain dictionary representation.
|
|
147
|
+
|
|
148
|
+
Recursively converts nested SpecificationObject instances to plain dicts.
|
|
149
|
+
Useful for serialization to JSON/YAML.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Plain dictionary with all nested objects converted
|
|
153
|
+
"""
|
|
154
|
+
return {key: self._marshal_value(value) for key, value in MutableMapping.items(self)}
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
def _marshal_value(cls, value: Any) -> Any:
|
|
158
|
+
"""
|
|
159
|
+
Helper to recursively marshal values to plain types.
|
|
160
|
+
|
|
161
|
+
Uses the actual class (or subclass) to support custom marshaling behavior.
|
|
162
|
+
"""
|
|
163
|
+
if isinstance(value, SpecificationObject):
|
|
164
|
+
return value.to_mapping()
|
|
165
|
+
elif isinstance(value, (list, tuple)):
|
|
166
|
+
return type(value)(cls._marshal_value(item) for item in value)
|
|
167
|
+
elif isinstance(value, dict):
|
|
168
|
+
return {k: cls._marshal_value(v) for k, v in value.items()}
|
|
169
|
+
else:
|
|
170
|
+
return value
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def _copy_value(value: Any) -> Any:
|
|
174
|
+
"""
|
|
175
|
+
Defensive shallow copy for mutable collections.
|
|
176
|
+
|
|
177
|
+
Copies mutable types (list, dict, etc.) to prevent unintended mutation
|
|
178
|
+
of input data. Does not copy SpecificationObjects (already defensive).
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
value: Value to potentially copy
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Copy of value if mutable collection, otherwise value itself
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
# Don't copy SpecificationObjects (already create new instances)
|
|
188
|
+
if isinstance(value, SpecificationObject):
|
|
189
|
+
return value
|
|
190
|
+
|
|
191
|
+
# Copy mutable collections (dict, list, etc.)
|
|
192
|
+
# Exclude strings (they're Sequence but immutable)
|
|
193
|
+
if isinstance(value, (Mapping, Sequence)) and not isinstance(value, str):
|
|
194
|
+
return copy(value)
|
|
195
|
+
|
|
196
|
+
# Primitives and immutables - no copy needed
|
|
197
|
+
return value
|
|
198
|
+
|
|
199
|
+
def __repr__(self) -> str:
|
|
200
|
+
"""Return a developer-friendly string representation."""
|
|
201
|
+
class_name = self.__class__.__name__
|
|
202
|
+
|
|
203
|
+
# Count regular fields and extensions separately
|
|
204
|
+
extensions = self.get_extensions()
|
|
205
|
+
ext_count = len(extensions)
|
|
206
|
+
field_count = len(self) - ext_count
|
|
207
|
+
|
|
208
|
+
# Build field part
|
|
209
|
+
field_word = "field" if field_count == 1 else "fields"
|
|
210
|
+
parts = [f"{field_count} {field_word}"]
|
|
211
|
+
|
|
212
|
+
# Add extensions part if any
|
|
213
|
+
if ext_count > 0:
|
|
214
|
+
ext_word = "specification extension" if ext_count == 1 else "specification extensions"
|
|
215
|
+
parts.append(f"{ext_count} {ext_word}")
|
|
216
|
+
|
|
217
|
+
return f"<{class_name} {', '.join(parts)}>"
|