scim2-models 0.3.6__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 -95
- scim2_models/rfc7643/resource_type.py +13 -22
- 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 -66
- 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.6.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.6.dist-info/RECORD +0 -24
- {scim2_models-0.3.6.dist-info → scim2_models-0.3.7.dist-info}/WHEEL +0 -0
- {scim2_models-0.3.6.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
|
|
8
|
+
from typing import Union
|
|
9
|
+
from typing import cast
|
|
7
10
|
from typing import get_args
|
|
8
11
|
from typing import get_origin
|
|
9
12
|
|
|
10
13
|
from pydantic import Field
|
|
14
|
+
from pydantic import SerializationInfo
|
|
15
|
+
from pydantic import SerializerFunctionWrapHandler
|
|
11
16
|
from pydantic import WrapSerializer
|
|
12
17
|
from pydantic import field_serializer
|
|
13
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
|
|
14
27
|
from ..base import BaseModel
|
|
15
|
-
from ..
|
|
16
|
-
from ..
|
|
17
|
-
from ..
|
|
18
|
-
from ..
|
|
19
|
-
from ..base import MultiValuedComplexAttribute
|
|
20
|
-
from ..base import Mutability
|
|
21
|
-
from ..base import Required
|
|
22
|
-
from ..base import Returned
|
|
23
|
-
from ..base import Uniqueness
|
|
24
|
-
from ..base import URIReference
|
|
25
|
-
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
|
|
26
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) in UNION_TYPES
|
|
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,21 +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
|
|
187
|
-
and get_origin(extension_models[0]) in UNION_TYPES
|
|
188
|
-
else extension_models
|
|
189
|
-
)
|
|
190
|
-
|
|
226
|
+
extension_models = getattr(cls, "__scim_extension_metadata__", [])
|
|
191
227
|
by_schema = {
|
|
192
228
|
ext.model_fields["schemas"].default[0]: ext for ext in extension_models
|
|
193
229
|
}
|
|
194
230
|
return by_schema
|
|
195
231
|
|
|
196
232
|
@classmethod
|
|
197
|
-
def get_extension_model(
|
|
233
|
+
def get_extension_model(
|
|
234
|
+
cls, name_or_schema: Union[str, "Schema"]
|
|
235
|
+
) -> Optional[type[Extension]]:
|
|
198
236
|
"""Return an extension by its name or schema."""
|
|
199
237
|
for schema, extension in cls.get_extension_models().items():
|
|
200
238
|
if schema == name_or_schema or extension.__name__ == name_or_schema:
|
|
@@ -203,15 +241,17 @@ class Resource(BaseModel, Generic[AnyExtension], metaclass=ResourceMetaclass):
|
|
|
203
241
|
|
|
204
242
|
@staticmethod
|
|
205
243
|
def get_by_schema(
|
|
206
|
-
resource_types: list[type[
|
|
207
|
-
|
|
244
|
+
resource_types: list[type["Resource"]],
|
|
245
|
+
schema: str,
|
|
246
|
+
with_extensions: bool = True,
|
|
247
|
+
) -> Optional[Union[type["Resource"], type["Extension"]]]:
|
|
208
248
|
"""Given a resource type list and a schema, find the matching resource type."""
|
|
209
|
-
by_schema = {
|
|
249
|
+
by_schema: dict[str, Union[type[Resource], type[Extension]]] = {
|
|
210
250
|
resource_type.model_fields["schemas"].default[0].lower(): resource_type
|
|
211
251
|
for resource_type in (resource_types or [])
|
|
212
252
|
}
|
|
213
253
|
if with_extensions:
|
|
214
|
-
for resource_type in
|
|
254
|
+
for resource_type in resource_types:
|
|
215
255
|
by_schema.update(
|
|
216
256
|
{
|
|
217
257
|
schema.lower(): extension
|
|
@@ -222,7 +262,11 @@ class Resource(BaseModel, Generic[AnyExtension], metaclass=ResourceMetaclass):
|
|
|
222
262
|
return by_schema.get(schema.lower())
|
|
223
263
|
|
|
224
264
|
@staticmethod
|
|
225
|
-
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]:
|
|
226
270
|
"""Given a resource type list and a payload, find the matching resource type."""
|
|
227
271
|
if not payload or not payload.get("schemas"):
|
|
228
272
|
return None
|
|
@@ -231,7 +275,9 @@ class Resource(BaseModel, Generic[AnyExtension], metaclass=ResourceMetaclass):
|
|
|
231
275
|
return Resource.get_by_schema(resource_types, schema, **kwargs)
|
|
232
276
|
|
|
233
277
|
@field_serializer("schemas")
|
|
234
|
-
def set_extension_schemas(
|
|
278
|
+
def set_extension_schemas(
|
|
279
|
+
self, schemas: Annotated[list[str], Required.true]
|
|
280
|
+
) -> list[str]:
|
|
235
281
|
"""Add model extension ids to the 'schemas' attribute."""
|
|
236
282
|
extension_schemas = self.get_extension_models().keys()
|
|
237
283
|
schemas = self.schemas + [
|
|
@@ -240,25 +286,95 @@ class Resource(BaseModel, Generic[AnyExtension], metaclass=ResourceMetaclass):
|
|
|
240
286
|
return schemas
|
|
241
287
|
|
|
242
288
|
@classmethod
|
|
243
|
-
def to_schema(cls):
|
|
289
|
+
def to_schema(cls) -> "Schema":
|
|
244
290
|
"""Build a :class:`~scim2_models.Schema` from the current resource class."""
|
|
245
291
|
return model_to_schema(cls)
|
|
246
292
|
|
|
247
293
|
@classmethod
|
|
248
|
-
def from_schema(cls, schema) -> "Resource":
|
|
294
|
+
def from_schema(cls, schema: "Schema") -> type["Resource"]:
|
|
249
295
|
"""Build a :class:`scim2_models.Resource` subclass from the schema definition."""
|
|
250
296
|
from .schema import make_python_model
|
|
251
297
|
|
|
252
298
|
return make_python_model(schema, cls)
|
|
253
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
|
+
|
|
254
368
|
|
|
255
369
|
AnyResource = TypeVar("AnyResource", bound="Resource")
|
|
256
370
|
|
|
257
371
|
|
|
258
|
-
def dedicated_attributes(
|
|
372
|
+
def dedicated_attributes(
|
|
373
|
+
model: type[BaseModel], excluded_models: list[type[BaseModel]]
|
|
374
|
+
) -> dict[str, Any]:
|
|
259
375
|
"""Return attributes that are not members the parent 'excluded_models'."""
|
|
260
376
|
|
|
261
|
-
def compare_field_infos(fi1, fi2):
|
|
377
|
+
def compare_field_infos(fi1: Any, fi2: Any) -> bool:
|
|
262
378
|
return (
|
|
263
379
|
fi1
|
|
264
380
|
and fi2
|
|
@@ -281,13 +397,13 @@ def dedicated_attributes(model, excluded_models):
|
|
|
281
397
|
return field_infos
|
|
282
398
|
|
|
283
399
|
|
|
284
|
-
def model_to_schema(model: type[BaseModel]):
|
|
400
|
+
def model_to_schema(model: type[BaseModel]) -> "Schema":
|
|
285
401
|
from scim2_models.rfc7643.schema import Schema
|
|
286
402
|
|
|
287
403
|
schema_urn = model.model_fields["schemas"].default[0]
|
|
288
404
|
field_infos = dedicated_attributes(model, [Resource])
|
|
289
405
|
attributes = [
|
|
290
|
-
|
|
406
|
+
model_attribute_to_scim_attribute(model, attribute_name)
|
|
291
407
|
for attribute_name in field_infos
|
|
292
408
|
if attribute_name != "schemas"
|
|
293
409
|
]
|
|
@@ -300,46 +416,37 @@ def model_to_schema(model: type[BaseModel]):
|
|
|
300
416
|
return schema
|
|
301
417
|
|
|
302
418
|
|
|
303
|
-
def
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
def serialize_ref_type(ref_type):
|
|
308
|
-
if ref_type == URIReference:
|
|
309
|
-
return "uri"
|
|
310
|
-
|
|
311
|
-
elif ref_type == ExternalReference:
|
|
312
|
-
return "external"
|
|
313
|
-
|
|
314
|
-
return get_args(ref_type)[0]
|
|
315
|
-
|
|
316
|
-
return list(map(serialize_ref_type, types))
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
def model_attribute_to_attribute(model, attribute_name):
|
|
419
|
+
def model_attribute_to_scim_attribute(
|
|
420
|
+
model: type[BaseModel], attribute_name: str
|
|
421
|
+
) -> "Attribute":
|
|
320
422
|
from scim2_models.rfc7643.schema import Attribute
|
|
321
423
|
|
|
322
424
|
field_info = model.model_fields[attribute_name]
|
|
323
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
|
+
)
|
|
324
430
|
attribute_type = Attribute.Type.from_python(root_type)
|
|
325
431
|
sub_attributes = (
|
|
326
432
|
[
|
|
327
|
-
|
|
433
|
+
model_attribute_to_scim_attribute(root_type, sub_attribute_name)
|
|
328
434
|
for sub_attribute_name in dedicated_attributes(
|
|
329
|
-
root_type,
|
|
435
|
+
root_type,
|
|
436
|
+
[MultiValuedComplexAttribute],
|
|
330
437
|
)
|
|
331
438
|
if (
|
|
332
439
|
attribute_name != "sub_attributes"
|
|
333
440
|
or sub_attribute_name != "sub_attributes"
|
|
334
441
|
)
|
|
335
442
|
]
|
|
336
|
-
if is_complex_attribute(root_type)
|
|
443
|
+
if root_type and is_complex_attribute(root_type)
|
|
337
444
|
else None
|
|
338
445
|
)
|
|
339
446
|
|
|
340
447
|
return Attribute(
|
|
341
448
|
name=field_info.serialization_alias or attribute_name,
|
|
342
|
-
type=attribute_type,
|
|
449
|
+
type=Attribute.Type(attribute_type),
|
|
343
450
|
multi_valued=model.get_field_multiplicity(attribute_name),
|
|
344
451
|
description=field_info.description,
|
|
345
452
|
canonical_values=field_info.examples,
|
|
@@ -349,7 +456,7 @@ def model_attribute_to_attribute(model, attribute_name):
|
|
|
349
456
|
returned=model.get_field_annotation(attribute_name, Returned),
|
|
350
457
|
uniqueness=model.get_field_annotation(attribute_name, Uniqueness),
|
|
351
458
|
sub_attributes=sub_attributes,
|
|
352
|
-
reference_types=
|
|
459
|
+
reference_types=Reference.get_types(root_type)
|
|
353
460
|
if attribute_type == Attribute.Type.reference
|
|
354
461
|
else None,
|
|
355
462
|
)
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
from typing import Annotated
|
|
2
2
|
from typing import Optional
|
|
3
|
-
from typing import get_args
|
|
4
|
-
from typing import get_origin
|
|
5
3
|
|
|
6
4
|
from pydantic import Field
|
|
7
5
|
from typing_extensions import Self
|
|
8
6
|
|
|
9
|
-
from ..
|
|
10
|
-
from ..
|
|
11
|
-
from ..
|
|
12
|
-
from ..
|
|
13
|
-
from ..
|
|
14
|
-
from ..
|
|
15
|
-
from ..
|
|
16
|
-
from ..utils import UNION_TYPES
|
|
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
|
|
17
14
|
from .resource import Resource
|
|
18
15
|
|
|
19
16
|
|
|
@@ -85,21 +82,15 @@ class ResourceType(Resource):
|
|
|
85
82
|
"""Build a naive ResourceType from a resource model."""
|
|
86
83
|
schema = resource_model.model_fields["schemas"].default[0]
|
|
87
84
|
name = schema.split(":")[-1]
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
else [extensions]
|
|
94
|
-
)
|
|
95
|
-
else:
|
|
96
|
-
extensions = []
|
|
97
|
-
|
|
98
|
-
return ResourceType(
|
|
85
|
+
|
|
86
|
+
# Get extensions from the metadata system
|
|
87
|
+
extensions = getattr(resource_model, "__scim_extension_metadata__", [])
|
|
88
|
+
|
|
89
|
+
return cls(
|
|
99
90
|
id=name,
|
|
100
91
|
name=name,
|
|
101
92
|
description=name,
|
|
102
|
-
endpoint=f"/{name}s",
|
|
93
|
+
endpoint=Reference[URIReference](f"/{name}s"),
|
|
103
94
|
schema_=schema,
|
|
104
95
|
schema_extensions=[
|
|
105
96
|
SchemaExtension(
|