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,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)}>"