scim2-models 0.3.5__py3-none-any.whl → 0.3.7__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.
- scim2_models/__init__.py +11 -11
- scim2_models/annotations.py +104 -0
- scim2_models/attributes.py +57 -0
- scim2_models/base.py +68 -525
- scim2_models/constants.py +3 -3
- scim2_models/context.py +168 -0
- scim2_models/reference.py +82 -0
- scim2_models/rfc7643/enterprise_user.py +4 -4
- scim2_models/rfc7643/group.py +5 -5
- scim2_models/rfc7643/resource.py +202 -94
- scim2_models/rfc7643/resource_type.py +13 -10
- scim2_models/rfc7643/schema.py +44 -38
- scim2_models/rfc7643/service_provider_config.py +7 -7
- scim2_models/rfc7643/user.py +9 -9
- scim2_models/rfc7644/bulk.py +2 -2
- scim2_models/rfc7644/error.py +11 -11
- scim2_models/rfc7644/list_response.py +5 -65
- scim2_models/rfc7644/message.py +103 -3
- scim2_models/rfc7644/patch_op.py +20 -6
- scim2_models/rfc7644/search_request.py +4 -4
- scim2_models/scim_object.py +143 -0
- scim2_models/utils.py +1 -1
- {scim2_models-0.3.5.dist-info → scim2_models-0.3.7.dist-info}/METADATA +1 -1
- scim2_models-0.3.7.dist-info/RECORD +29 -0
- scim2_models-0.3.5.dist-info/RECORD +0 -24
- {scim2_models-0.3.5.dist-info → scim2_models-0.3.7.dist-info}/WHEEL +0 -0
- {scim2_models-0.3.5.dist-info → scim2_models-0.3.7.dist-info}/licenses/LICENSE +0 -0
scim2_models/rfc7643/resource.py
CHANGED
|
@@ -1,31 +1,41 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
2
3
|
from typing import Annotated
|
|
3
4
|
from typing import Any
|
|
4
5
|
from typing import Generic
|
|
5
6
|
from typing import Optional
|
|
6
7
|
from typing import TypeVar
|
|
7
8
|
from typing import Union
|
|
9
|
+
from typing import cast
|
|
8
10
|
from typing import get_args
|
|
9
11
|
from typing import get_origin
|
|
10
12
|
|
|
11
13
|
from pydantic import Field
|
|
14
|
+
from pydantic import SerializationInfo
|
|
15
|
+
from pydantic import SerializerFunctionWrapHandler
|
|
12
16
|
from pydantic import WrapSerializer
|
|
13
17
|
from pydantic import field_serializer
|
|
14
18
|
|
|
19
|
+
from ..annotations import CaseExact
|
|
20
|
+
from ..annotations import Mutability
|
|
21
|
+
from ..annotations import Required
|
|
22
|
+
from ..annotations import Returned
|
|
23
|
+
from ..annotations import Uniqueness
|
|
24
|
+
from ..attributes import ComplexAttribute
|
|
25
|
+
from ..attributes import MultiValuedComplexAttribute
|
|
26
|
+
from ..attributes import is_complex_attribute
|
|
15
27
|
from ..base import BaseModel
|
|
16
|
-
from ..
|
|
17
|
-
from ..
|
|
18
|
-
from ..
|
|
19
|
-
from ..
|
|
20
|
-
from ..
|
|
21
|
-
from ..base import Mutability
|
|
22
|
-
from ..base import Required
|
|
23
|
-
from ..base import Returned
|
|
24
|
-
from ..base import Uniqueness
|
|
25
|
-
from ..base import URIReference
|
|
26
|
-
from ..base import is_complex_attribute
|
|
28
|
+
from ..context import Context
|
|
29
|
+
from ..reference import Reference
|
|
30
|
+
from ..scim_object import ScimObject
|
|
31
|
+
from ..scim_object import validate_attribute_urn
|
|
32
|
+
from ..utils import UNION_TYPES
|
|
27
33
|
from ..utils import normalize_attribute_name
|
|
28
34
|
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from .schema import Attribute
|
|
37
|
+
from .schema import Schema
|
|
38
|
+
|
|
29
39
|
|
|
30
40
|
class Meta(ComplexAttribute):
|
|
31
41
|
"""All "meta" sub-attributes are assigned by the service provider (have a "mutability" of "readOnly"), and all of these sub-attributes have a "returned" characteristic of "default".
|
|
@@ -78,14 +88,14 @@ class Meta(ComplexAttribute):
|
|
|
78
88
|
"""
|
|
79
89
|
|
|
80
90
|
|
|
81
|
-
class Extension(
|
|
91
|
+
class Extension(ScimObject):
|
|
82
92
|
@classmethod
|
|
83
|
-
def to_schema(cls):
|
|
93
|
+
def to_schema(cls) -> "Schema":
|
|
84
94
|
"""Build a :class:`~scim2_models.Schema` from the current extension class."""
|
|
85
95
|
return model_to_schema(cls)
|
|
86
96
|
|
|
87
97
|
@classmethod
|
|
88
|
-
def from_schema(cls, schema) -> "Extension":
|
|
98
|
+
def from_schema(cls, schema: "Schema") -> type["Extension"]:
|
|
89
99
|
"""Build a :class:`~scim2_models.Extension` subclass from the schema definition."""
|
|
90
100
|
from .schema import make_python_model
|
|
91
101
|
|
|
@@ -94,14 +104,18 @@ class Extension(BaseModel):
|
|
|
94
104
|
|
|
95
105
|
AnyExtension = TypeVar("AnyExtension", bound="Extension")
|
|
96
106
|
|
|
107
|
+
_PARAMETERIZED_CLASSES: dict[tuple[type, tuple[Any, ...]], type] = {}
|
|
108
|
+
|
|
97
109
|
|
|
98
|
-
def extension_serializer(
|
|
110
|
+
def extension_serializer(
|
|
111
|
+
value: Any, handler: SerializerFunctionWrapHandler, info: SerializationInfo
|
|
112
|
+
) -> Optional[dict[str, Any]]:
|
|
99
113
|
"""Exclude the Resource attributes from the extension dump.
|
|
100
114
|
|
|
101
115
|
For instance, attributes 'meta', 'id' or 'schemas' should not be
|
|
102
116
|
dumped when the model is used as an extension for another model.
|
|
103
117
|
"""
|
|
104
|
-
partial_result = handler(value
|
|
118
|
+
partial_result = handler(value)
|
|
105
119
|
result = {
|
|
106
120
|
attr_name: value
|
|
107
121
|
for attr_name, value in partial_result.items()
|
|
@@ -110,39 +124,7 @@ def extension_serializer(value: Any, handler, info) -> Optional[dict[str, Any]]:
|
|
|
110
124
|
return result or None
|
|
111
125
|
|
|
112
126
|
|
|
113
|
-
class
|
|
114
|
-
def __new__(cls, name, bases, attrs, **kwargs):
|
|
115
|
-
"""Dynamically add a field for each extension."""
|
|
116
|
-
if "__pydantic_generic_metadata__" in kwargs:
|
|
117
|
-
extensions = kwargs["__pydantic_generic_metadata__"]["args"][0]
|
|
118
|
-
extensions = (
|
|
119
|
-
get_args(extensions)
|
|
120
|
-
if get_origin(extensions) == Union
|
|
121
|
-
else [extensions]
|
|
122
|
-
)
|
|
123
|
-
for extension in extensions:
|
|
124
|
-
schema = extension.model_fields["schemas"].default[0]
|
|
125
|
-
attrs.setdefault("__annotations__", {})[extension.__name__] = Annotated[
|
|
126
|
-
Optional[extension],
|
|
127
|
-
WrapSerializer(extension_serializer),
|
|
128
|
-
]
|
|
129
|
-
attrs[extension.__name__] = Field(
|
|
130
|
-
None,
|
|
131
|
-
serialization_alias=schema,
|
|
132
|
-
validation_alias=normalize_attribute_name(schema),
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
klass = super().__new__(cls, name, bases, attrs, **kwargs)
|
|
136
|
-
return klass
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
class Resource(BaseModel, Generic[AnyExtension], metaclass=ResourceMetaclass):
|
|
140
|
-
schemas: Annotated[list[str], Required.true]
|
|
141
|
-
"""The "schemas" attribute is a REQUIRED attribute and is an array of
|
|
142
|
-
Strings containing URIs that are used to indicate the namespaces of the
|
|
143
|
-
SCIM schemas that define the attributes present in the current JSON
|
|
144
|
-
structure."""
|
|
145
|
-
|
|
127
|
+
class Resource(ScimObject, Generic[AnyExtension]):
|
|
146
128
|
# Common attributes as defined by
|
|
147
129
|
# https://www.rfc-editor.org/rfc/rfc7643#section-3.1
|
|
148
130
|
|
|
@@ -165,13 +147,74 @@ class Resource(BaseModel, Generic[AnyExtension], metaclass=ResourceMetaclass):
|
|
|
165
147
|
meta: Annotated[Optional[Meta], Mutability.read_only, Returned.default] = None
|
|
166
148
|
"""A complex attribute containing resource metadata."""
|
|
167
149
|
|
|
168
|
-
|
|
150
|
+
@classmethod
|
|
151
|
+
def __class_getitem__(cls, item: Any) -> type["Resource"]:
|
|
152
|
+
"""Create a Resource class with extension fields dynamically added."""
|
|
153
|
+
if hasattr(cls, "__scim_extension_metadata__"):
|
|
154
|
+
return cls
|
|
155
|
+
|
|
156
|
+
extensions = get_args(item) if get_origin(item) in UNION_TYPES else [item]
|
|
157
|
+
|
|
158
|
+
# Skip TypeVar parameters and Any (used for generic class definitions)
|
|
159
|
+
valid_extensions = [
|
|
160
|
+
extension
|
|
161
|
+
for extension in extensions
|
|
162
|
+
if not isinstance(extension, TypeVar) and extension is not Any
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
if not valid_extensions:
|
|
166
|
+
return cls
|
|
167
|
+
|
|
168
|
+
cache_key = (cls, tuple(valid_extensions))
|
|
169
|
+
if cache_key in _PARAMETERIZED_CLASSES:
|
|
170
|
+
return _PARAMETERIZED_CLASSES[cache_key]
|
|
171
|
+
|
|
172
|
+
for extension in valid_extensions:
|
|
173
|
+
if not (isinstance(extension, type) and issubclass(extension, Extension)):
|
|
174
|
+
raise TypeError(f"{extension} is not a valid Extension type")
|
|
175
|
+
|
|
176
|
+
class_name = (
|
|
177
|
+
f"{cls.__name__}[{', '.join(ext.__name__ for ext in valid_extensions)}]"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
class_attrs = {"__scim_extension_metadata__": valid_extensions}
|
|
181
|
+
|
|
182
|
+
for extension in valid_extensions:
|
|
183
|
+
schema = extension.model_fields["schemas"].default[0]
|
|
184
|
+
class_attrs[extension.__name__] = Field(
|
|
185
|
+
default=None, # type: ignore[arg-type]
|
|
186
|
+
serialization_alias=schema,
|
|
187
|
+
validation_alias=normalize_attribute_name(schema),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
new_annotations = {
|
|
191
|
+
extension.__name__: Annotated[
|
|
192
|
+
Optional[extension],
|
|
193
|
+
WrapSerializer(extension_serializer),
|
|
194
|
+
]
|
|
195
|
+
for extension in valid_extensions
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
new_class = type(
|
|
199
|
+
class_name,
|
|
200
|
+
(cls,),
|
|
201
|
+
{
|
|
202
|
+
"__annotations__": new_annotations,
|
|
203
|
+
**class_attrs,
|
|
204
|
+
},
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
_PARAMETERIZED_CLASSES[cache_key] = new_class
|
|
208
|
+
|
|
209
|
+
return new_class
|
|
210
|
+
|
|
211
|
+
def __getitem__(self, item: Any) -> Optional[Extension]:
|
|
169
212
|
if not isinstance(item, type) or not issubclass(item, Extension):
|
|
170
213
|
raise KeyError(f"{item} is not a valid extension type")
|
|
171
214
|
|
|
172
|
-
return getattr(self, item.__name__)
|
|
215
|
+
return cast(Optional[Extension], getattr(self, item.__name__))
|
|
173
216
|
|
|
174
|
-
def __setitem__(self, item: Any, value: "
|
|
217
|
+
def __setitem__(self, item: Any, value: "Extension") -> None:
|
|
175
218
|
if not isinstance(item, type) or not issubclass(item, Extension):
|
|
176
219
|
raise KeyError(f"{item} is not a valid extension type")
|
|
177
220
|
|
|
@@ -180,20 +223,16 @@ class Resource(BaseModel, Generic[AnyExtension], metaclass=ResourceMetaclass):
|
|
|
180
223
|
@classmethod
|
|
181
224
|
def get_extension_models(cls) -> dict[str, type[Extension]]:
|
|
182
225
|
"""Return extension a dict associating extension models with their schemas."""
|
|
183
|
-
extension_models = cls
|
|
184
|
-
extension_models = (
|
|
185
|
-
get_args(extension_models[0])
|
|
186
|
-
if len(extension_models) == 1 and get_origin(extension_models[0]) == Union
|
|
187
|
-
else extension_models
|
|
188
|
-
)
|
|
189
|
-
|
|
226
|
+
extension_models = getattr(cls, "__scim_extension_metadata__", [])
|
|
190
227
|
by_schema = {
|
|
191
228
|
ext.model_fields["schemas"].default[0]: ext for ext in extension_models
|
|
192
229
|
}
|
|
193
230
|
return by_schema
|
|
194
231
|
|
|
195
232
|
@classmethod
|
|
196
|
-
def get_extension_model(
|
|
233
|
+
def get_extension_model(
|
|
234
|
+
cls, name_or_schema: Union[str, "Schema"]
|
|
235
|
+
) -> Optional[type[Extension]]:
|
|
197
236
|
"""Return an extension by its name or schema."""
|
|
198
237
|
for schema, extension in cls.get_extension_models().items():
|
|
199
238
|
if schema == name_or_schema or extension.__name__ == name_or_schema:
|
|
@@ -202,15 +241,17 @@ class Resource(BaseModel, Generic[AnyExtension], metaclass=ResourceMetaclass):
|
|
|
202
241
|
|
|
203
242
|
@staticmethod
|
|
204
243
|
def get_by_schema(
|
|
205
|
-
resource_types: list[type[
|
|
206
|
-
|
|
244
|
+
resource_types: list[type["Resource"]],
|
|
245
|
+
schema: str,
|
|
246
|
+
with_extensions: bool = True,
|
|
247
|
+
) -> Optional[Union[type["Resource"], type["Extension"]]]:
|
|
207
248
|
"""Given a resource type list and a schema, find the matching resource type."""
|
|
208
|
-
by_schema = {
|
|
249
|
+
by_schema: dict[str, Union[type[Resource], type[Extension]]] = {
|
|
209
250
|
resource_type.model_fields["schemas"].default[0].lower(): resource_type
|
|
210
251
|
for resource_type in (resource_types or [])
|
|
211
252
|
}
|
|
212
253
|
if with_extensions:
|
|
213
|
-
for resource_type in
|
|
254
|
+
for resource_type in resource_types:
|
|
214
255
|
by_schema.update(
|
|
215
256
|
{
|
|
216
257
|
schema.lower(): extension
|
|
@@ -221,7 +262,11 @@ class Resource(BaseModel, Generic[AnyExtension], metaclass=ResourceMetaclass):
|
|
|
221
262
|
return by_schema.get(schema.lower())
|
|
222
263
|
|
|
223
264
|
@staticmethod
|
|
224
|
-
def get_by_payload(
|
|
265
|
+
def get_by_payload(
|
|
266
|
+
resource_types: list[type["Resource"]],
|
|
267
|
+
payload: dict[str, Any],
|
|
268
|
+
**kwargs: Any,
|
|
269
|
+
) -> Optional[type]:
|
|
225
270
|
"""Given a resource type list and a payload, find the matching resource type."""
|
|
226
271
|
if not payload or not payload.get("schemas"):
|
|
227
272
|
return None
|
|
@@ -230,7 +275,9 @@ class Resource(BaseModel, Generic[AnyExtension], metaclass=ResourceMetaclass):
|
|
|
230
275
|
return Resource.get_by_schema(resource_types, schema, **kwargs)
|
|
231
276
|
|
|
232
277
|
@field_serializer("schemas")
|
|
233
|
-
def set_extension_schemas(
|
|
278
|
+
def set_extension_schemas(
|
|
279
|
+
self, schemas: Annotated[list[str], Required.true]
|
|
280
|
+
) -> list[str]:
|
|
234
281
|
"""Add model extension ids to the 'schemas' attribute."""
|
|
235
282
|
extension_schemas = self.get_extension_models().keys()
|
|
236
283
|
schemas = self.schemas + [
|
|
@@ -239,25 +286,95 @@ class Resource(BaseModel, Generic[AnyExtension], metaclass=ResourceMetaclass):
|
|
|
239
286
|
return schemas
|
|
240
287
|
|
|
241
288
|
@classmethod
|
|
242
|
-
def to_schema(cls):
|
|
289
|
+
def to_schema(cls) -> "Schema":
|
|
243
290
|
"""Build a :class:`~scim2_models.Schema` from the current resource class."""
|
|
244
291
|
return model_to_schema(cls)
|
|
245
292
|
|
|
246
293
|
@classmethod
|
|
247
|
-
def from_schema(cls, schema) -> "Resource":
|
|
294
|
+
def from_schema(cls, schema: "Schema") -> type["Resource"]:
|
|
248
295
|
"""Build a :class:`scim2_models.Resource` subclass from the schema definition."""
|
|
249
296
|
from .schema import make_python_model
|
|
250
297
|
|
|
251
298
|
return make_python_model(schema, cls)
|
|
252
299
|
|
|
300
|
+
def _prepare_model_dump(
|
|
301
|
+
self,
|
|
302
|
+
scim_ctx: Optional[Context] = Context.DEFAULT,
|
|
303
|
+
attributes: Optional[list[str]] = None,
|
|
304
|
+
excluded_attributes: Optional[list[str]] = None,
|
|
305
|
+
**kwargs: Any,
|
|
306
|
+
) -> dict[str, Any]:
|
|
307
|
+
kwargs = super()._prepare_model_dump(scim_ctx, **kwargs)
|
|
308
|
+
kwargs["context"]["scim_attributes"] = [
|
|
309
|
+
validate_attribute_urn(attribute, self.__class__)
|
|
310
|
+
for attribute in (attributes or [])
|
|
311
|
+
]
|
|
312
|
+
kwargs["context"]["scim_excluded_attributes"] = [
|
|
313
|
+
validate_attribute_urn(attribute, self.__class__)
|
|
314
|
+
for attribute in (excluded_attributes or [])
|
|
315
|
+
]
|
|
316
|
+
return kwargs
|
|
317
|
+
|
|
318
|
+
def model_dump(
|
|
319
|
+
self,
|
|
320
|
+
*args: Any,
|
|
321
|
+
scim_ctx: Optional[Context] = Context.DEFAULT,
|
|
322
|
+
attributes: Optional[list[str]] = None,
|
|
323
|
+
excluded_attributes: Optional[list[str]] = None,
|
|
324
|
+
**kwargs: Any,
|
|
325
|
+
) -> dict[str, Any]:
|
|
326
|
+
"""Create a model representation that can be included in SCIM messages by using Pydantic :code:`BaseModel.model_dump`.
|
|
327
|
+
|
|
328
|
+
:param scim_ctx: If a SCIM context is passed, some default values of
|
|
329
|
+
Pydantic :code:`BaseModel.model_dump` are tuned to generate valid SCIM
|
|
330
|
+
messages. Pass :data:`None` to get the default Pydantic behavior.
|
|
331
|
+
:param attributes: A multi-valued list of strings indicating the names of resource
|
|
332
|
+
attributes to return in the response, overriding the set of attributes that
|
|
333
|
+
would be returned by default.
|
|
334
|
+
:param excluded_attributes: A multi-valued list of strings indicating the names of resource
|
|
335
|
+
attributes to be removed from the default set of attributes to return.
|
|
336
|
+
"""
|
|
337
|
+
dump_kwargs = self._prepare_model_dump(
|
|
338
|
+
scim_ctx, attributes, excluded_attributes, **kwargs
|
|
339
|
+
)
|
|
340
|
+
if scim_ctx:
|
|
341
|
+
dump_kwargs.setdefault("mode", "json")
|
|
342
|
+
return super(ScimObject, self).model_dump(*args, **dump_kwargs)
|
|
343
|
+
|
|
344
|
+
def model_dump_json(
|
|
345
|
+
self,
|
|
346
|
+
*args: Any,
|
|
347
|
+
scim_ctx: Optional[Context] = Context.DEFAULT,
|
|
348
|
+
attributes: Optional[list[str]] = None,
|
|
349
|
+
excluded_attributes: Optional[list[str]] = None,
|
|
350
|
+
**kwargs: Any,
|
|
351
|
+
) -> str:
|
|
352
|
+
"""Create a JSON model representation that can be included in SCIM messages by using Pydantic :code:`BaseModel.model_dump_json`.
|
|
353
|
+
|
|
354
|
+
:param scim_ctx: If a SCIM context is passed, some default values of
|
|
355
|
+
Pydantic :code:`BaseModel.model_dump` are tuned to generate valid SCIM
|
|
356
|
+
messages. Pass :data:`None` to get the default Pydantic behavior.
|
|
357
|
+
:param attributes: A multi-valued list of strings indicating the names of resource
|
|
358
|
+
attributes to return in the response, overriding the set of attributes that
|
|
359
|
+
would be returned by default.
|
|
360
|
+
:param excluded_attributes: A multi-valued list of strings indicating the names of resource
|
|
361
|
+
attributes to be removed from the default set of attributes to return.
|
|
362
|
+
"""
|
|
363
|
+
dump_kwargs = self._prepare_model_dump(
|
|
364
|
+
scim_ctx, attributes, excluded_attributes, **kwargs
|
|
365
|
+
)
|
|
366
|
+
return super(ScimObject, self).model_dump_json(*args, **dump_kwargs)
|
|
367
|
+
|
|
253
368
|
|
|
254
369
|
AnyResource = TypeVar("AnyResource", bound="Resource")
|
|
255
370
|
|
|
256
371
|
|
|
257
|
-
def dedicated_attributes(
|
|
372
|
+
def dedicated_attributes(
|
|
373
|
+
model: type[BaseModel], excluded_models: list[type[BaseModel]]
|
|
374
|
+
) -> dict[str, Any]:
|
|
258
375
|
"""Return attributes that are not members the parent 'excluded_models'."""
|
|
259
376
|
|
|
260
|
-
def compare_field_infos(fi1, fi2):
|
|
377
|
+
def compare_field_infos(fi1: Any, fi2: Any) -> bool:
|
|
261
378
|
return (
|
|
262
379
|
fi1
|
|
263
380
|
and fi2
|
|
@@ -280,13 +397,13 @@ def dedicated_attributes(model, excluded_models):
|
|
|
280
397
|
return field_infos
|
|
281
398
|
|
|
282
399
|
|
|
283
|
-
def model_to_schema(model: type[BaseModel]):
|
|
400
|
+
def model_to_schema(model: type[BaseModel]) -> "Schema":
|
|
284
401
|
from scim2_models.rfc7643.schema import Schema
|
|
285
402
|
|
|
286
403
|
schema_urn = model.model_fields["schemas"].default[0]
|
|
287
404
|
field_infos = dedicated_attributes(model, [Resource])
|
|
288
405
|
attributes = [
|
|
289
|
-
|
|
406
|
+
model_attribute_to_scim_attribute(model, attribute_name)
|
|
290
407
|
for attribute_name in field_infos
|
|
291
408
|
if attribute_name != "schemas"
|
|
292
409
|
]
|
|
@@ -299,46 +416,37 @@ def model_to_schema(model: type[BaseModel]):
|
|
|
299
416
|
return schema
|
|
300
417
|
|
|
301
418
|
|
|
302
|
-
def
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
def serialize_ref_type(ref_type):
|
|
307
|
-
if ref_type == URIReference:
|
|
308
|
-
return "uri"
|
|
309
|
-
|
|
310
|
-
elif ref_type == ExternalReference:
|
|
311
|
-
return "external"
|
|
312
|
-
|
|
313
|
-
return get_args(ref_type)[0]
|
|
314
|
-
|
|
315
|
-
return list(map(serialize_ref_type, types))
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
def model_attribute_to_attribute(model, attribute_name):
|
|
419
|
+
def model_attribute_to_scim_attribute(
|
|
420
|
+
model: type[BaseModel], attribute_name: str
|
|
421
|
+
) -> "Attribute":
|
|
319
422
|
from scim2_models.rfc7643.schema import Attribute
|
|
320
423
|
|
|
321
424
|
field_info = model.model_fields[attribute_name]
|
|
322
425
|
root_type = model.get_field_root_type(attribute_name)
|
|
426
|
+
if root_type is None:
|
|
427
|
+
raise ValueError(
|
|
428
|
+
f"Could not determine root type for attribute {attribute_name}"
|
|
429
|
+
)
|
|
323
430
|
attribute_type = Attribute.Type.from_python(root_type)
|
|
324
431
|
sub_attributes = (
|
|
325
432
|
[
|
|
326
|
-
|
|
433
|
+
model_attribute_to_scim_attribute(root_type, sub_attribute_name)
|
|
327
434
|
for sub_attribute_name in dedicated_attributes(
|
|
328
|
-
root_type,
|
|
435
|
+
root_type,
|
|
436
|
+
[MultiValuedComplexAttribute],
|
|
329
437
|
)
|
|
330
438
|
if (
|
|
331
439
|
attribute_name != "sub_attributes"
|
|
332
440
|
or sub_attribute_name != "sub_attributes"
|
|
333
441
|
)
|
|
334
442
|
]
|
|
335
|
-
if is_complex_attribute(root_type)
|
|
443
|
+
if root_type and is_complex_attribute(root_type)
|
|
336
444
|
else None
|
|
337
445
|
)
|
|
338
446
|
|
|
339
447
|
return Attribute(
|
|
340
448
|
name=field_info.serialization_alias or attribute_name,
|
|
341
|
-
type=attribute_type,
|
|
449
|
+
type=Attribute.Type(attribute_type),
|
|
342
450
|
multi_valued=model.get_field_multiplicity(attribute_name),
|
|
343
451
|
description=field_info.description,
|
|
344
452
|
canonical_values=field_info.examples,
|
|
@@ -348,7 +456,7 @@ def model_attribute_to_attribute(model, attribute_name):
|
|
|
348
456
|
returned=model.get_field_annotation(attribute_name, Returned),
|
|
349
457
|
uniqueness=model.get_field_annotation(attribute_name, Uniqueness),
|
|
350
458
|
sub_attributes=sub_attributes,
|
|
351
|
-
reference_types=
|
|
459
|
+
reference_types=Reference.get_types(root_type)
|
|
352
460
|
if attribute_type == Attribute.Type.reference
|
|
353
461
|
else None,
|
|
354
462
|
)
|
|
@@ -4,13 +4,13 @@ from typing import Optional
|
|
|
4
4
|
from pydantic import Field
|
|
5
5
|
from typing_extensions import Self
|
|
6
6
|
|
|
7
|
-
from ..
|
|
8
|
-
from ..
|
|
9
|
-
from ..
|
|
10
|
-
from ..
|
|
11
|
-
from ..
|
|
12
|
-
from ..
|
|
13
|
-
from ..
|
|
7
|
+
from ..annotations import CaseExact
|
|
8
|
+
from ..annotations import Mutability
|
|
9
|
+
from ..annotations import Required
|
|
10
|
+
from ..annotations import Returned
|
|
11
|
+
from ..attributes import ComplexAttribute
|
|
12
|
+
from ..reference import Reference
|
|
13
|
+
from ..reference import URIReference
|
|
14
14
|
from .resource import Resource
|
|
15
15
|
|
|
16
16
|
|
|
@@ -82,12 +82,15 @@ class ResourceType(Resource):
|
|
|
82
82
|
"""Build a naive ResourceType from a resource model."""
|
|
83
83
|
schema = resource_model.model_fields["schemas"].default[0]
|
|
84
84
|
name = schema.split(":")[-1]
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
|
|
86
|
+
# Get extensions from the metadata system
|
|
87
|
+
extensions = getattr(resource_model, "__scim_extension_metadata__", [])
|
|
88
|
+
|
|
89
|
+
return cls(
|
|
87
90
|
id=name,
|
|
88
91
|
name=name,
|
|
89
92
|
description=name,
|
|
90
|
-
endpoint=f"/{name}s",
|
|
93
|
+
endpoint=Reference[URIReference](f"/{name}s"),
|
|
91
94
|
schema_=schema,
|
|
92
95
|
schema_extensions=[
|
|
93
96
|
SchemaExtension(
|
scim2_models/rfc7643/schema.py
CHANGED
|
@@ -6,6 +6,7 @@ from typing import Any
|
|
|
6
6
|
from typing import List # noqa : UP005,UP035
|
|
7
7
|
from typing import Literal
|
|
8
8
|
from typing import Optional
|
|
9
|
+
from typing import TypeVar
|
|
9
10
|
from typing import Union
|
|
10
11
|
from typing import get_origin
|
|
11
12
|
|
|
@@ -16,24 +17,25 @@ from pydantic.alias_generators import to_pascal
|
|
|
16
17
|
from pydantic.alias_generators import to_snake
|
|
17
18
|
from pydantic_core import Url
|
|
18
19
|
|
|
20
|
+
from ..annotations import CaseExact
|
|
21
|
+
from ..annotations import Mutability
|
|
22
|
+
from ..annotations import Required
|
|
23
|
+
from ..annotations import Returned
|
|
24
|
+
from ..annotations import Uniqueness
|
|
25
|
+
from ..attributes import ComplexAttribute
|
|
26
|
+
from ..attributes import MultiValuedComplexAttribute
|
|
27
|
+
from ..attributes import is_complex_attribute
|
|
19
28
|
from ..base import BaseModel
|
|
20
|
-
from ..base import CaseExact
|
|
21
|
-
from ..base import ComplexAttribute
|
|
22
|
-
from ..base import ExternalReference
|
|
23
|
-
from ..base import MultiValuedComplexAttribute
|
|
24
|
-
from ..base import Mutability
|
|
25
|
-
from ..base import Reference
|
|
26
|
-
from ..base import Required
|
|
27
|
-
from ..base import Returned
|
|
28
|
-
from ..base import Uniqueness
|
|
29
|
-
from ..base import URIReference
|
|
30
|
-
from ..base import is_complex_attribute
|
|
31
29
|
from ..constants import RESERVED_WORDS
|
|
30
|
+
from ..reference import ExternalReference
|
|
31
|
+
from ..reference import Reference
|
|
32
|
+
from ..reference import URIReference
|
|
32
33
|
from ..utils import Base64Bytes
|
|
33
34
|
from ..utils import normalize_attribute_name
|
|
34
|
-
from .resource import Extension
|
|
35
35
|
from .resource import Resource
|
|
36
36
|
|
|
37
|
+
T = TypeVar("T", bound=BaseModel)
|
|
38
|
+
|
|
37
39
|
|
|
38
40
|
def make_python_identifier(identifier: str) -> str:
|
|
39
41
|
"""Sanitize string to be a suitable Python/Pydantic class attribute name."""
|
|
@@ -46,9 +48,9 @@ def make_python_identifier(identifier: str) -> str:
|
|
|
46
48
|
|
|
47
49
|
def make_python_model(
|
|
48
50
|
obj: Union["Schema", "Attribute"],
|
|
49
|
-
base:
|
|
50
|
-
multiple=False,
|
|
51
|
-
) ->
|
|
51
|
+
base: type[T],
|
|
52
|
+
multiple: bool = False,
|
|
53
|
+
) -> type[T]:
|
|
52
54
|
"""Build a Python model from a Schema or an Attribute object."""
|
|
53
55
|
if isinstance(obj, Attribute):
|
|
54
56
|
pydantic_attributes = {
|
|
@@ -56,7 +58,6 @@ def make_python_model(
|
|
|
56
58
|
for attr in (obj.sub_attributes or [])
|
|
57
59
|
if attr.name
|
|
58
60
|
}
|
|
59
|
-
base = MultiValuedComplexAttribute if multiple else ComplexAttribute
|
|
60
61
|
|
|
61
62
|
else:
|
|
62
63
|
pydantic_attributes = {
|
|
@@ -69,14 +70,17 @@ def make_python_model(
|
|
|
69
70
|
Field(default=[obj.id]),
|
|
70
71
|
)
|
|
71
72
|
|
|
73
|
+
if not obj.name:
|
|
74
|
+
raise ValueError("Schema or Attribute 'name' must be defined")
|
|
75
|
+
|
|
72
76
|
model_name = to_pascal(to_snake(obj.name))
|
|
73
|
-
model = create_model(model_name, __base__=base, **pydantic_attributes)
|
|
77
|
+
model: type[T] = create_model(model_name, __base__=base, **pydantic_attributes) # type: ignore[call-overload]
|
|
74
78
|
|
|
75
79
|
# Set the ComplexType class as a member of the model
|
|
76
80
|
# e.g. make Member an attribute of Group
|
|
77
81
|
for attr_name in model.model_fields:
|
|
78
82
|
attr_type = model.get_field_root_type(attr_name)
|
|
79
|
-
if is_complex_attribute(attr_type):
|
|
83
|
+
if attr_type and is_complex_attribute(attr_type):
|
|
80
84
|
setattr(model, attr_type.__name__, attr_type)
|
|
81
85
|
|
|
82
86
|
return model
|
|
@@ -95,7 +99,7 @@ class Attribute(ComplexAttribute):
|
|
|
95
99
|
|
|
96
100
|
def to_python(
|
|
97
101
|
self,
|
|
98
|
-
multiple=False,
|
|
102
|
+
multiple: bool = False,
|
|
99
103
|
reference_types: Optional[list[str]] = None,
|
|
100
104
|
) -> type:
|
|
101
105
|
if self.value == self.reference and reference_types is not None:
|
|
@@ -122,32 +126,32 @@ class Attribute(ComplexAttribute):
|
|
|
122
126
|
return attr_types[self.value]
|
|
123
127
|
|
|
124
128
|
@classmethod
|
|
125
|
-
def from_python(cls, pytype) ->
|
|
129
|
+
def from_python(cls, pytype: type) -> "Attribute.Type":
|
|
126
130
|
if get_origin(pytype) == Reference:
|
|
127
|
-
return cls.reference
|
|
131
|
+
return cls.reference
|
|
128
132
|
|
|
129
|
-
if is_complex_attribute(pytype):
|
|
130
|
-
return cls.complex
|
|
133
|
+
if pytype and is_complex_attribute(pytype):
|
|
134
|
+
return cls.complex
|
|
131
135
|
|
|
132
136
|
if pytype in (Required, CaseExact):
|
|
133
|
-
return cls.boolean
|
|
137
|
+
return cls.boolean
|
|
134
138
|
|
|
135
139
|
attr_types = {
|
|
136
|
-
str: cls.string
|
|
137
|
-
bool: cls.boolean
|
|
138
|
-
float: cls.decimal
|
|
139
|
-
int: cls.integer
|
|
140
|
-
datetime: cls.date_time
|
|
141
|
-
Base64Bytes: cls.binary
|
|
140
|
+
str: cls.string,
|
|
141
|
+
bool: cls.boolean,
|
|
142
|
+
float: cls.decimal,
|
|
143
|
+
int: cls.integer,
|
|
144
|
+
datetime: cls.date_time,
|
|
145
|
+
Base64Bytes: cls.binary,
|
|
142
146
|
}
|
|
143
|
-
return attr_types.get(pytype, cls.string
|
|
147
|
+
return attr_types.get(pytype, cls.string)
|
|
144
148
|
|
|
145
149
|
name: Annotated[
|
|
146
150
|
Optional[str], Mutability.read_only, Required.true, CaseExact.true
|
|
147
151
|
] = None
|
|
148
152
|
"""The attribute's name."""
|
|
149
153
|
|
|
150
|
-
type: Annotated[Type, Mutability.read_only, Required.true] = Field(
|
|
154
|
+
type: Annotated[Optional[Type], Mutability.read_only, Required.true] = Field(
|
|
151
155
|
None, examples=[item.value for item in Type]
|
|
152
156
|
)
|
|
153
157
|
"""The attribute's data type."""
|
|
@@ -206,15 +210,17 @@ class Attribute(ComplexAttribute):
|
|
|
206
210
|
"""When an attribute is of type "complex", "subAttributes" defines a set of
|
|
207
211
|
sub-attributes."""
|
|
208
212
|
|
|
209
|
-
def to_python(self) -> Optional[tuple[Any,
|
|
213
|
+
def to_python(self) -> Optional[tuple[Any, Any]]:
|
|
210
214
|
"""Build tuple suited to be passed to pydantic 'create_model'."""
|
|
211
|
-
if not self.name:
|
|
215
|
+
if not self.name or not self.type:
|
|
212
216
|
return None
|
|
213
217
|
|
|
214
|
-
attr_type = self.type.to_python(self.multi_valued, self.reference_types)
|
|
218
|
+
attr_type = self.type.to_python(bool(self.multi_valued), self.reference_types)
|
|
215
219
|
|
|
216
220
|
if attr_type in (ComplexAttribute, MultiValuedComplexAttribute):
|
|
217
|
-
attr_type = make_python_model(
|
|
221
|
+
attr_type = make_python_model(
|
|
222
|
+
obj=self, base=attr_type, multiple=bool(self.multi_valued)
|
|
223
|
+
)
|
|
218
224
|
|
|
219
225
|
if self.multi_valued:
|
|
220
226
|
attr_type = list[attr_type] # type: ignore
|
|
@@ -245,7 +251,7 @@ class Attribute(ComplexAttribute):
|
|
|
245
251
|
return sub_attribute
|
|
246
252
|
return None
|
|
247
253
|
|
|
248
|
-
def __getitem__(self, name):
|
|
254
|
+
def __getitem__(self, name: str) -> "Attribute":
|
|
249
255
|
"""Find an attribute by its name."""
|
|
250
256
|
if attribute := self.get_attribute(name):
|
|
251
257
|
return attribute
|
|
@@ -287,7 +293,7 @@ class Schema(Resource):
|
|
|
287
293
|
return attribute
|
|
288
294
|
return None
|
|
289
295
|
|
|
290
|
-
def __getitem__(self, name):
|
|
296
|
+
def __getitem__(self, name: str) -> "Attribute": # type: ignore[override]
|
|
291
297
|
"""Find an attribute by its name."""
|
|
292
298
|
if attribute := self.get_attribute(name):
|
|
293
299
|
return attribute
|