scim2-models 0.3.6__py3-none-any.whl → 0.4.0__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 +52 -52
- scim2_models/annotations.py +104 -0
- scim2_models/attributes.py +57 -0
- scim2_models/base.py +195 -554
- scim2_models/constants.py +3 -3
- scim2_models/context.py +168 -0
- scim2_models/{rfc7644 → messages}/bulk.py +4 -4
- scim2_models/{rfc7644 → messages}/error.py +13 -13
- scim2_models/messages/list_response.py +75 -0
- scim2_models/messages/message.py +119 -0
- scim2_models/messages/patch_op.py +478 -0
- scim2_models/{rfc7644 → messages}/search_request.py +55 -6
- scim2_models/reference.py +82 -0
- scim2_models/{rfc7643 → resources}/enterprise_user.py +4 -4
- scim2_models/{rfc7643 → resources}/group.py +5 -5
- scim2_models/resources/resource.py +468 -0
- scim2_models/{rfc7643 → resources}/resource_type.py +13 -22
- scim2_models/{rfc7643 → resources}/schema.py +51 -45
- scim2_models/{rfc7643 → resources}/service_provider_config.py +7 -7
- scim2_models/{rfc7643 → resources}/user.py +9 -9
- scim2_models/scim_object.py +66 -0
- scim2_models/urn.py +109 -0
- scim2_models/utils.py +108 -6
- {scim2_models-0.3.6.dist-info → scim2_models-0.4.0.dist-info}/METADATA +1 -1
- scim2_models-0.4.0.dist-info/RECORD +30 -0
- scim2_models/rfc7643/resource.py +0 -355
- scim2_models/rfc7644/list_response.py +0 -136
- scim2_models/rfc7644/message.py +0 -10
- scim2_models/rfc7644/patch_op.py +0 -70
- scim2_models-0.3.6.dist-info/RECORD +0 -24
- /scim2_models/{rfc7643 → messages}/__init__.py +0 -0
- /scim2_models/{rfc7644 → resources}/__init__.py +0 -0
- {scim2_models-0.3.6.dist-info → scim2_models-0.4.0.dist-info}/WHEEL +0 -0
- {scim2_models-0.3.6.dist-info → scim2_models-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,11 +6,11 @@ from typing import Union
|
|
|
6
6
|
|
|
7
7
|
from pydantic import Field
|
|
8
8
|
|
|
9
|
-
from ..
|
|
10
|
-
from ..
|
|
11
|
-
from ..
|
|
12
|
-
from ..
|
|
13
|
-
from ..
|
|
9
|
+
from ..annotations import Mutability
|
|
10
|
+
from ..annotations import Required
|
|
11
|
+
from ..attributes import ComplexAttribute
|
|
12
|
+
from ..attributes import MultiValuedComplexAttribute
|
|
13
|
+
from ..reference import Reference
|
|
14
14
|
from .resource import Resource
|
|
15
15
|
|
|
16
16
|
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
from typing import Any
|
|
5
|
+
from typing import Generic
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from typing import TypeVar
|
|
8
|
+
from typing import Union
|
|
9
|
+
from typing import cast
|
|
10
|
+
from typing import get_args
|
|
11
|
+
from typing import get_origin
|
|
12
|
+
|
|
13
|
+
from pydantic import Field
|
|
14
|
+
from pydantic import SerializationInfo
|
|
15
|
+
from pydantic import SerializerFunctionWrapHandler
|
|
16
|
+
from pydantic import WrapSerializer
|
|
17
|
+
from pydantic import field_serializer
|
|
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
|
|
27
|
+
from ..base import BaseModel
|
|
28
|
+
from ..context import Context
|
|
29
|
+
from ..reference import Reference
|
|
30
|
+
from ..scim_object import ScimObject
|
|
31
|
+
from ..urn import _validate_attribute_urn
|
|
32
|
+
from ..utils import UNION_TYPES
|
|
33
|
+
from ..utils import _normalize_attribute_name
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from .schema import Attribute
|
|
37
|
+
from .schema import Schema
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Meta(ComplexAttribute):
|
|
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".
|
|
42
|
+
|
|
43
|
+
This attribute SHALL be ignored when provided by clients. "meta" contains the following sub-attributes:
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
resource_type: Optional[str] = None
|
|
47
|
+
"""The name of the resource type of the resource.
|
|
48
|
+
|
|
49
|
+
This attribute has a mutability of "readOnly" and "caseExact" as
|
|
50
|
+
"true".
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
created: Optional[datetime] = None
|
|
54
|
+
"""The "DateTime" that the resource was added to the service provider.
|
|
55
|
+
|
|
56
|
+
This attribute MUST be a DateTime.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
last_modified: Optional[datetime] = None
|
|
60
|
+
"""The most recent DateTime that the details of this resource were updated
|
|
61
|
+
at the service provider.
|
|
62
|
+
|
|
63
|
+
If this resource has never been modified since its initial creation,
|
|
64
|
+
the value MUST be the same as the value of "created".
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
location: Optional[str] = None
|
|
68
|
+
"""The URI of the resource being returned.
|
|
69
|
+
|
|
70
|
+
This value MUST be the same as the "Content-Location" HTTP response
|
|
71
|
+
header (see Section 3.1.4.2 of [RFC7231]).
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
version: Optional[str] = None
|
|
75
|
+
"""The version of the resource being returned.
|
|
76
|
+
|
|
77
|
+
This value must be the same as the entity-tag (ETag) HTTP response
|
|
78
|
+
header (see Sections 2.1 and 2.3 of [RFC7232]). This attribute has
|
|
79
|
+
"caseExact" as "true". Service provider support for this attribute
|
|
80
|
+
is optional and subject to the service provider's support for
|
|
81
|
+
versioning (see Section 3.14 of [RFC7644]). If a service provider
|
|
82
|
+
provides "version" (entity-tag) for a representation and the
|
|
83
|
+
generation of that entity-tag does not satisfy all of the
|
|
84
|
+
characteristics of a strong validator (see Section 2.1 of
|
|
85
|
+
[RFC7232]), then the origin server MUST mark the "version" (entity-
|
|
86
|
+
tag) as weak by prefixing its opaque value with "W/" (case
|
|
87
|
+
sensitive).
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Extension(ScimObject):
|
|
92
|
+
@classmethod
|
|
93
|
+
def to_schema(cls) -> "Schema":
|
|
94
|
+
"""Build a :class:`~scim2_models.Schema` from the current extension class."""
|
|
95
|
+
return _model_to_schema(cls)
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def from_schema(cls, schema: "Schema") -> type["Extension"]:
|
|
99
|
+
"""Build a :class:`~scim2_models.Extension` subclass from the schema definition."""
|
|
100
|
+
from .schema import _make_python_model
|
|
101
|
+
|
|
102
|
+
return _make_python_model(schema, cls)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
AnyExtension = TypeVar("AnyExtension", bound="Extension")
|
|
106
|
+
|
|
107
|
+
_PARAMETERIZED_CLASSES: dict[tuple[type, tuple[Any, ...]], type] = {}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _extension_serializer(
|
|
111
|
+
value: Any, handler: SerializerFunctionWrapHandler, info: SerializationInfo
|
|
112
|
+
) -> Optional[dict[str, Any]]:
|
|
113
|
+
"""Exclude the Resource attributes from the extension dump.
|
|
114
|
+
|
|
115
|
+
For instance, attributes 'meta', 'id' or 'schemas' should not be
|
|
116
|
+
dumped when the model is used as an extension for another model.
|
|
117
|
+
"""
|
|
118
|
+
partial_result = handler(value)
|
|
119
|
+
result = {
|
|
120
|
+
attr_name: value
|
|
121
|
+
for attr_name, value in partial_result.items()
|
|
122
|
+
if attr_name not in Resource.model_fields
|
|
123
|
+
}
|
|
124
|
+
return result or None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Resource(ScimObject, Generic[AnyExtension]):
|
|
128
|
+
# Common attributes as defined by
|
|
129
|
+
# https://www.rfc-editor.org/rfc/rfc7643#section-3.1
|
|
130
|
+
|
|
131
|
+
id: Annotated[
|
|
132
|
+
Optional[str], Mutability.read_only, Returned.always, Uniqueness.global_
|
|
133
|
+
] = None
|
|
134
|
+
"""A unique identifier for a SCIM resource as defined by the service
|
|
135
|
+
provider.
|
|
136
|
+
|
|
137
|
+
id is mandatory is the resource representation, but is forbidden in
|
|
138
|
+
resource creation or replacement requests.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
external_id: Annotated[
|
|
142
|
+
Optional[str], Mutability.read_write, Returned.default, CaseExact.true
|
|
143
|
+
] = None
|
|
144
|
+
"""A String that is an identifier for the resource as defined by the
|
|
145
|
+
provisioning client."""
|
|
146
|
+
|
|
147
|
+
meta: Annotated[Optional[Meta], Mutability.read_only, Returned.default] = None
|
|
148
|
+
"""A complex attribute containing resource metadata."""
|
|
149
|
+
|
|
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]:
|
|
212
|
+
if not isinstance(item, type) or not issubclass(item, Extension):
|
|
213
|
+
raise KeyError(f"{item} is not a valid extension type")
|
|
214
|
+
|
|
215
|
+
return cast(Optional[Extension], getattr(self, item.__name__))
|
|
216
|
+
|
|
217
|
+
def __setitem__(self, item: Any, value: "Extension") -> None:
|
|
218
|
+
if not isinstance(item, type) or not issubclass(item, Extension):
|
|
219
|
+
raise KeyError(f"{item} is not a valid extension type")
|
|
220
|
+
|
|
221
|
+
setattr(self, item.__name__, value)
|
|
222
|
+
|
|
223
|
+
@classmethod
|
|
224
|
+
def get_extension_models(cls) -> dict[str, type[Extension]]:
|
|
225
|
+
"""Return extension a dict associating extension models with their schemas."""
|
|
226
|
+
extension_models = getattr(cls, "__scim_extension_metadata__", [])
|
|
227
|
+
by_schema = {
|
|
228
|
+
ext.model_fields["schemas"].default[0]: ext for ext in extension_models
|
|
229
|
+
}
|
|
230
|
+
return by_schema
|
|
231
|
+
|
|
232
|
+
@classmethod
|
|
233
|
+
def get_extension_model(
|
|
234
|
+
cls, name_or_schema: Union[str, "Schema"]
|
|
235
|
+
) -> Optional[type[Extension]]:
|
|
236
|
+
"""Return an extension by its name or schema."""
|
|
237
|
+
for schema, extension in cls.get_extension_models().items():
|
|
238
|
+
if schema == name_or_schema or extension.__name__ == name_or_schema:
|
|
239
|
+
return extension
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
@staticmethod
|
|
243
|
+
def get_by_schema(
|
|
244
|
+
resource_types: list[type["Resource"]],
|
|
245
|
+
schema: str,
|
|
246
|
+
with_extensions: bool = True,
|
|
247
|
+
) -> Optional[Union[type["Resource"], type["Extension"]]]:
|
|
248
|
+
"""Given a resource type list and a schema, find the matching resource type."""
|
|
249
|
+
by_schema: dict[str, Union[type[Resource], type[Extension]]] = {
|
|
250
|
+
resource_type.model_fields["schemas"].default[0].lower(): resource_type
|
|
251
|
+
for resource_type in (resource_types or [])
|
|
252
|
+
}
|
|
253
|
+
if with_extensions:
|
|
254
|
+
for resource_type in resource_types:
|
|
255
|
+
by_schema.update(
|
|
256
|
+
{
|
|
257
|
+
schema.lower(): extension
|
|
258
|
+
for schema, extension in resource_type.get_extension_models().items()
|
|
259
|
+
}
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
return by_schema.get(schema.lower())
|
|
263
|
+
|
|
264
|
+
@staticmethod
|
|
265
|
+
def get_by_payload(
|
|
266
|
+
resource_types: list[type["Resource"]],
|
|
267
|
+
payload: dict[str, Any],
|
|
268
|
+
**kwargs: Any,
|
|
269
|
+
) -> Optional[type]:
|
|
270
|
+
"""Given a resource type list and a payload, find the matching resource type."""
|
|
271
|
+
if not payload or not payload.get("schemas"):
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
schema = payload["schemas"][0]
|
|
275
|
+
return Resource.get_by_schema(resource_types, schema, **kwargs)
|
|
276
|
+
|
|
277
|
+
@field_serializer("schemas")
|
|
278
|
+
def set_extension_schemas(
|
|
279
|
+
self, schemas: Annotated[list[str], Required.true]
|
|
280
|
+
) -> list[str]:
|
|
281
|
+
"""Add model extension ids to the 'schemas' attribute."""
|
|
282
|
+
extension_schemas = self.get_extension_models().keys()
|
|
283
|
+
schemas = self.schemas + [
|
|
284
|
+
schema for schema in extension_schemas if schema not in self.schemas
|
|
285
|
+
]
|
|
286
|
+
return schemas
|
|
287
|
+
|
|
288
|
+
@classmethod
|
|
289
|
+
def to_schema(cls) -> "Schema":
|
|
290
|
+
"""Build a :class:`~scim2_models.Schema` from the current resource class."""
|
|
291
|
+
return _model_to_schema(cls)
|
|
292
|
+
|
|
293
|
+
@classmethod
|
|
294
|
+
def from_schema(cls, schema: "Schema") -> type["Resource"]:
|
|
295
|
+
"""Build a :class:`scim2_models.Resource` subclass from the schema definition."""
|
|
296
|
+
from .schema import _make_python_model
|
|
297
|
+
|
|
298
|
+
return _make_python_model(schema, cls)
|
|
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
|
+
|
|
309
|
+
# RFC 7644: "SHOULD ignore any query parameters they do not recognize"
|
|
310
|
+
kwargs["context"]["scim_attributes"] = [
|
|
311
|
+
valid_attr
|
|
312
|
+
for attribute in (attributes or [])
|
|
313
|
+
if (valid_attr := _validate_attribute_urn(attribute, self.__class__))
|
|
314
|
+
is not None
|
|
315
|
+
]
|
|
316
|
+
kwargs["context"]["scim_excluded_attributes"] = [
|
|
317
|
+
valid_attr
|
|
318
|
+
for attribute in (excluded_attributes or [])
|
|
319
|
+
if (valid_attr := _validate_attribute_urn(attribute, self.__class__))
|
|
320
|
+
is not None
|
|
321
|
+
]
|
|
322
|
+
return kwargs
|
|
323
|
+
|
|
324
|
+
def model_dump(
|
|
325
|
+
self,
|
|
326
|
+
*args: Any,
|
|
327
|
+
scim_ctx: Optional[Context] = Context.DEFAULT,
|
|
328
|
+
attributes: Optional[list[str]] = None,
|
|
329
|
+
excluded_attributes: Optional[list[str]] = None,
|
|
330
|
+
**kwargs: Any,
|
|
331
|
+
) -> dict[str, Any]:
|
|
332
|
+
"""Create a model representation that can be included in SCIM messages by using Pydantic :code:`BaseModel.model_dump`.
|
|
333
|
+
|
|
334
|
+
:param scim_ctx: If a SCIM context is passed, some default values of
|
|
335
|
+
Pydantic :code:`BaseModel.model_dump` are tuned to generate valid SCIM
|
|
336
|
+
messages. Pass :data:`None` to get the default Pydantic behavior.
|
|
337
|
+
:param attributes: A multi-valued list of strings indicating the names of resource
|
|
338
|
+
attributes to return in the response, overriding the set of attributes that
|
|
339
|
+
would be returned by default. Invalid values are ignored.
|
|
340
|
+
:param excluded_attributes: A multi-valued list of strings indicating the names of resource
|
|
341
|
+
attributes to be removed from the default set of attributes to return. Invalid values are ignored.
|
|
342
|
+
"""
|
|
343
|
+
dump_kwargs = self._prepare_model_dump(
|
|
344
|
+
scim_ctx, attributes, excluded_attributes, **kwargs
|
|
345
|
+
)
|
|
346
|
+
if scim_ctx:
|
|
347
|
+
dump_kwargs.setdefault("mode", "json")
|
|
348
|
+
return super(ScimObject, self).model_dump(*args, **dump_kwargs)
|
|
349
|
+
|
|
350
|
+
def model_dump_json(
|
|
351
|
+
self,
|
|
352
|
+
*args: Any,
|
|
353
|
+
scim_ctx: Optional[Context] = Context.DEFAULT,
|
|
354
|
+
attributes: Optional[list[str]] = None,
|
|
355
|
+
excluded_attributes: Optional[list[str]] = None,
|
|
356
|
+
**kwargs: Any,
|
|
357
|
+
) -> str:
|
|
358
|
+
"""Create a JSON model representation that can be included in SCIM messages by using Pydantic :code:`BaseModel.model_dump_json`.
|
|
359
|
+
|
|
360
|
+
:param scim_ctx: If a SCIM context is passed, some default values of
|
|
361
|
+
Pydantic :code:`BaseModel.model_dump` are tuned to generate valid SCIM
|
|
362
|
+
messages. Pass :data:`None` to get the default Pydantic behavior.
|
|
363
|
+
:param attributes: A multi-valued list of strings indicating the names of resource
|
|
364
|
+
attributes to return in the response, overriding the set of attributes that
|
|
365
|
+
would be returned by default. Invalid values are ignored.
|
|
366
|
+
:param excluded_attributes: A multi-valued list of strings indicating the names of resource
|
|
367
|
+
attributes to be removed from the default set of attributes to return. Invalid values are ignored.
|
|
368
|
+
"""
|
|
369
|
+
dump_kwargs = self._prepare_model_dump(
|
|
370
|
+
scim_ctx, attributes, excluded_attributes, **kwargs
|
|
371
|
+
)
|
|
372
|
+
return super(ScimObject, self).model_dump_json(*args, **dump_kwargs)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
AnyResource = TypeVar("AnyResource", bound="Resource")
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _dedicated_attributes(
|
|
379
|
+
model: type[BaseModel], excluded_models: list[type[BaseModel]]
|
|
380
|
+
) -> dict[str, Any]:
|
|
381
|
+
"""Return attributes that are not members the parent 'excluded_models'."""
|
|
382
|
+
|
|
383
|
+
def compare_field_infos(fi1: Any, fi2: Any) -> bool:
|
|
384
|
+
return (
|
|
385
|
+
fi1
|
|
386
|
+
and fi2
|
|
387
|
+
and fi1.__slotnames__ == fi2.__slotnames__
|
|
388
|
+
and all(
|
|
389
|
+
getattr(fi1, attr) == getattr(fi2, attr) for attr in fi1.__slotnames__
|
|
390
|
+
)
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
parent_field_infos = {
|
|
394
|
+
field_name: field_info
|
|
395
|
+
for excluded_model in excluded_models
|
|
396
|
+
for field_name, field_info in excluded_model.model_fields.items()
|
|
397
|
+
}
|
|
398
|
+
field_infos = {
|
|
399
|
+
field_name: field_info
|
|
400
|
+
for field_name, field_info in model.model_fields.items()
|
|
401
|
+
if not compare_field_infos(field_info, parent_field_infos.get(field_name))
|
|
402
|
+
}
|
|
403
|
+
return field_infos
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _model_to_schema(model: type[BaseModel]) -> "Schema":
|
|
407
|
+
from scim2_models.resources.schema import Schema
|
|
408
|
+
|
|
409
|
+
schema_urn = model.model_fields["schemas"].default[0]
|
|
410
|
+
field_infos = _dedicated_attributes(model, [Resource])
|
|
411
|
+
attributes = [
|
|
412
|
+
_model_attribute_to_scim_attribute(model, attribute_name)
|
|
413
|
+
for attribute_name in field_infos
|
|
414
|
+
if attribute_name != "schemas"
|
|
415
|
+
]
|
|
416
|
+
schema = Schema(
|
|
417
|
+
name=model.__name__,
|
|
418
|
+
id=schema_urn,
|
|
419
|
+
description=model.__doc__ or model.__name__,
|
|
420
|
+
attributes=attributes,
|
|
421
|
+
)
|
|
422
|
+
return schema
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _model_attribute_to_scim_attribute(
|
|
426
|
+
model: type[BaseModel], attribute_name: str
|
|
427
|
+
) -> "Attribute":
|
|
428
|
+
from scim2_models.resources.schema import Attribute
|
|
429
|
+
|
|
430
|
+
field_info = model.model_fields[attribute_name]
|
|
431
|
+
root_type = model.get_field_root_type(attribute_name)
|
|
432
|
+
if root_type is None:
|
|
433
|
+
raise ValueError(
|
|
434
|
+
f"Could not determine root type for attribute {attribute_name}"
|
|
435
|
+
)
|
|
436
|
+
attribute_type = Attribute.Type.from_python(root_type)
|
|
437
|
+
sub_attributes = (
|
|
438
|
+
[
|
|
439
|
+
_model_attribute_to_scim_attribute(root_type, sub_attribute_name)
|
|
440
|
+
for sub_attribute_name in _dedicated_attributes(
|
|
441
|
+
root_type,
|
|
442
|
+
[MultiValuedComplexAttribute],
|
|
443
|
+
)
|
|
444
|
+
if (
|
|
445
|
+
attribute_name != "sub_attributes"
|
|
446
|
+
or sub_attribute_name != "sub_attributes"
|
|
447
|
+
)
|
|
448
|
+
]
|
|
449
|
+
if root_type and is_complex_attribute(root_type)
|
|
450
|
+
else None
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
return Attribute(
|
|
454
|
+
name=field_info.serialization_alias or attribute_name,
|
|
455
|
+
type=Attribute.Type(attribute_type),
|
|
456
|
+
multi_valued=model.get_field_multiplicity(attribute_name),
|
|
457
|
+
description=field_info.description,
|
|
458
|
+
canonical_values=field_info.examples,
|
|
459
|
+
required=model.get_field_annotation(attribute_name, Required),
|
|
460
|
+
case_exact=model.get_field_annotation(attribute_name, CaseExact),
|
|
461
|
+
mutability=model.get_field_annotation(attribute_name, Mutability),
|
|
462
|
+
returned=model.get_field_annotation(attribute_name, Returned),
|
|
463
|
+
uniqueness=model.get_field_annotation(attribute_name, Uniqueness),
|
|
464
|
+
sub_attributes=sub_attributes,
|
|
465
|
+
reference_types=Reference.get_types(root_type)
|
|
466
|
+
if attribute_type == Attribute.Type.reference
|
|
467
|
+
else None,
|
|
468
|
+
)
|
|
@@ -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(
|