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
scim2_models/base.py
CHANGED
|
@@ -1,20 +1,13 @@
|
|
|
1
|
-
from collections import UserString
|
|
2
|
-
from enum import Enum
|
|
3
|
-
from enum import auto
|
|
4
1
|
from inspect import isclass
|
|
5
|
-
from typing import Annotated
|
|
6
2
|
from typing import Any
|
|
7
|
-
from typing import Generic
|
|
8
3
|
from typing import Optional
|
|
9
|
-
from typing import TypeVar
|
|
10
4
|
from typing import get_args
|
|
11
5
|
from typing import get_origin
|
|
12
6
|
|
|
13
7
|
from pydantic import AliasGenerator
|
|
14
8
|
from pydantic import BaseModel as PydanticBaseModel
|
|
15
9
|
from pydantic import ConfigDict
|
|
16
|
-
from pydantic import
|
|
17
|
-
from pydantic import GetCoreSchemaHandler
|
|
10
|
+
from pydantic import FieldSerializationInfo
|
|
18
11
|
from pydantic import SerializationInfo
|
|
19
12
|
from pydantic import SerializerFunctionWrapHandler
|
|
20
13
|
from pydantic import ValidationInfo
|
|
@@ -24,389 +17,34 @@ from pydantic import field_validator
|
|
|
24
17
|
from pydantic import model_serializer
|
|
25
18
|
from pydantic import model_validator
|
|
26
19
|
from pydantic_core import PydanticCustomError
|
|
27
|
-
from pydantic_core import core_schema
|
|
28
|
-
from typing_extensions import NewType
|
|
29
20
|
from typing_extensions import Self
|
|
30
21
|
|
|
31
|
-
from scim2_models.
|
|
32
|
-
from scim2_models.
|
|
22
|
+
from scim2_models.annotations import Mutability
|
|
23
|
+
from scim2_models.annotations import Required
|
|
24
|
+
from scim2_models.annotations import Returned
|
|
25
|
+
from scim2_models.context import Context
|
|
26
|
+
from scim2_models.utils import UNION_TYPES
|
|
27
|
+
from scim2_models.utils import _find_field_name
|
|
28
|
+
from scim2_models.utils import _normalize_attribute_name
|
|
29
|
+
from scim2_models.utils import _to_camel
|
|
33
30
|
|
|
34
|
-
from .utils import UNION_TYPES
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def validate_model_attribute(model: type["BaseModel"], attribute_base: str) -> None:
|
|
42
|
-
"""Validate that an attribute name or a sub-attribute path exist for a given model."""
|
|
43
|
-
from scim2_models.base import BaseModel
|
|
44
|
-
|
|
45
|
-
attribute_name, *sub_attribute_blocks = attribute_base.split(".")
|
|
46
|
-
sub_attribute_base = ".".join(sub_attribute_blocks)
|
|
47
|
-
|
|
48
|
-
aliases = {field.validation_alias for field in model.model_fields.values()}
|
|
49
|
-
|
|
50
|
-
if normalize_attribute_name(attribute_name) not in aliases:
|
|
51
|
-
raise ValueError(
|
|
52
|
-
f"Model '{model.__name__}' has no attribute named '{attribute_name}'"
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
if sub_attribute_base:
|
|
56
|
-
attribute_type = model.get_field_root_type(attribute_name)
|
|
57
|
-
|
|
58
|
-
if not attribute_type or not issubclass(attribute_type, BaseModel):
|
|
59
|
-
raise ValueError(
|
|
60
|
-
f"Attribute '{attribute_name}' is not a complex attribute, and cannot have a '{sub_attribute_base}' sub-attribute"
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
validate_model_attribute(attribute_type, sub_attribute_base)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def extract_schema_and_attribute_base(attribute_urn: str) -> tuple[str, str]:
|
|
67
|
-
# Extract the schema urn part and the attribute name part from attribute
|
|
68
|
-
# name, as defined in :rfc:`RFC7644 §3.10 <7644#section-3.10>`.
|
|
69
|
-
|
|
70
|
-
*urn_blocks, attribute_base = attribute_urn.split(":")
|
|
71
|
-
schema = ":".join(urn_blocks)
|
|
72
|
-
return schema, attribute_base
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def validate_attribute_urn(
|
|
76
|
-
attribute_name: str,
|
|
77
|
-
default_resource: Optional[type["BaseModel"]] = None,
|
|
78
|
-
resource_types: Optional[list[type["BaseModel"]]] = None,
|
|
79
|
-
) -> str:
|
|
80
|
-
"""Validate that an attribute urn is valid or not.
|
|
81
|
-
|
|
82
|
-
:param attribute_name: The attribute urn to check.
|
|
83
|
-
:default_resource: The default resource if `attribute_name` is not an absolute urn.
|
|
84
|
-
:resource_types: The available resources in which to look for the attribute.
|
|
85
|
-
:return: The normalized attribute URN.
|
|
86
|
-
"""
|
|
87
|
-
from scim2_models.rfc7643.resource import Resource
|
|
88
|
-
|
|
89
|
-
if not resource_types:
|
|
90
|
-
resource_types = []
|
|
91
|
-
|
|
92
|
-
if default_resource and default_resource not in resource_types:
|
|
93
|
-
resource_types.append(default_resource)
|
|
94
|
-
|
|
95
|
-
default_schema = (
|
|
96
|
-
default_resource.model_fields["schemas"].default[0]
|
|
97
|
-
if default_resource
|
|
98
|
-
else None
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
schema: Optional[Any]
|
|
102
|
-
schema, attribute_base = extract_schema_and_attribute_base(attribute_name)
|
|
103
|
-
if not schema:
|
|
104
|
-
schema = default_schema
|
|
105
|
-
|
|
106
|
-
if not schema:
|
|
107
|
-
raise ValueError("No default schema and relative URN")
|
|
108
|
-
|
|
109
|
-
resource = Resource.get_by_schema(resource_types, schema)
|
|
110
|
-
if not resource:
|
|
111
|
-
raise ValueError(f"No resource matching schema '{schema}'")
|
|
112
|
-
|
|
113
|
-
validate_model_attribute(resource, attribute_base)
|
|
114
|
-
|
|
115
|
-
return f"{schema}:{attribute_base}"
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def contains_attribute_or_subattributes(attribute_urns: list[str], attribute_urn: str):
|
|
32
|
+
def _contains_attribute_or_subattributes(
|
|
33
|
+
attribute_urns: list[str], attribute_urn: str
|
|
34
|
+
) -> bool:
|
|
119
35
|
return attribute_urn in attribute_urns or any(
|
|
120
36
|
item.startswith(f"{attribute_urn}.") or item.startswith(f"{attribute_urn}:")
|
|
121
37
|
for item in attribute_urns
|
|
122
38
|
)
|
|
123
39
|
|
|
124
40
|
|
|
125
|
-
class Reference(UserString, Generic[ReferenceTypes]):
|
|
126
|
-
"""Reference type as defined in :rfc:`RFC7643 §2.3.7 <7643#section-2.3.7>`.
|
|
127
|
-
|
|
128
|
-
References can take different type parameters:
|
|
129
|
-
|
|
130
|
-
- Any :class:`~scim2_models.Resource` subtype, or :class:`~typing.ForwardRef` of a Resource subtype, or :data:`~typing.Union` of those,
|
|
131
|
-
- :data:`~scim2_models.ExternalReference`
|
|
132
|
-
- :data:`~scim2_models.URIReference`
|
|
133
|
-
|
|
134
|
-
Examples
|
|
135
|
-
--------
|
|
136
|
-
|
|
137
|
-
.. code-block:: python
|
|
138
|
-
|
|
139
|
-
class Foobar(Resource):
|
|
140
|
-
bff: Reference[User]
|
|
141
|
-
managers: Reference[Union["User", "Group"]]
|
|
142
|
-
photo: Reference[ExternalReference]
|
|
143
|
-
website: Reference[URIReference]
|
|
144
|
-
|
|
145
|
-
"""
|
|
146
|
-
|
|
147
|
-
@classmethod
|
|
148
|
-
def __get_pydantic_core_schema__(
|
|
149
|
-
cls,
|
|
150
|
-
_source: type[Any],
|
|
151
|
-
_handler: GetCoreSchemaHandler,
|
|
152
|
-
) -> core_schema.CoreSchema:
|
|
153
|
-
return core_schema.no_info_after_validator_function(
|
|
154
|
-
cls._validate, core_schema.str_schema()
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
@classmethod
|
|
158
|
-
def _validate(cls, input_value: str, /) -> str:
|
|
159
|
-
return input_value
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
class Context(Enum):
|
|
163
|
-
"""Represent the different HTTP contexts detailed in :rfc:`RFC7644 §3.2 <7644#section-3.2>`.
|
|
164
|
-
|
|
165
|
-
Contexts are intended to be used during model validation and serialization.
|
|
166
|
-
For instance a client preparing a resource creation POST request can use
|
|
167
|
-
:code:`resource.model_dump(Context.RESOURCE_CREATION_REQUEST)` and
|
|
168
|
-
the server can then validate it with
|
|
169
|
-
:code:`resource.model_validate(Context.RESOURCE_CREATION_REQUEST)`.
|
|
170
|
-
"""
|
|
171
|
-
|
|
172
|
-
DEFAULT = auto()
|
|
173
|
-
"""The default context.
|
|
174
|
-
|
|
175
|
-
All fields are accepted during validation, and all fields are
|
|
176
|
-
serialized during a dump.
|
|
177
|
-
"""
|
|
178
|
-
|
|
179
|
-
RESOURCE_CREATION_REQUEST = auto()
|
|
180
|
-
"""The resource creation request context.
|
|
181
|
-
|
|
182
|
-
Should be used for clients building a payload for a resource creation request,
|
|
183
|
-
and servers validating resource creation request payloads.
|
|
184
|
-
|
|
185
|
-
- When used for serialization, it will not dump attributes annotated with :attr:`~scim2_models.Mutability.read_only`.
|
|
186
|
-
- When used for validation, it will raise a :class:`~pydantic.ValidationError`:
|
|
187
|
-
- when finding attributes annotated with :attr:`~scim2_models.Mutability.read_only`,
|
|
188
|
-
- when attributes annotated with :attr:`Required.true <scim2_models.Required.true>` are missing on null.
|
|
189
|
-
"""
|
|
190
|
-
|
|
191
|
-
RESOURCE_CREATION_RESPONSE = auto()
|
|
192
|
-
"""The resource creation response context.
|
|
193
|
-
|
|
194
|
-
Should be used for servers building a payload for a resource
|
|
195
|
-
creation response, and clients validating resource creation response
|
|
196
|
-
payloads.
|
|
197
|
-
|
|
198
|
-
- When used for validation, it will raise a :class:`~pydantic.ValidationError` when finding attributes annotated with :attr:`~scim2_models.Returned.never` or when attributes annotated with :attr:`~scim2_models.Returned.always` are missing or :data:`None`;
|
|
199
|
-
- When used for serialization, it will:
|
|
200
|
-
- always dump attributes annotated with :attr:`~scim2_models.Returned.always`;
|
|
201
|
-
- never dump attributes annotated with :attr:`~scim2_models.Returned.never`;
|
|
202
|
-
- dump attributes annotated with :attr:`~scim2_models.Returned.default` unless they are explicitly excluded;
|
|
203
|
-
- not dump attributes annotated with :attr:`~scim2_models.Returned.request` unless they are explicitly included.
|
|
204
|
-
"""
|
|
205
|
-
|
|
206
|
-
RESOURCE_QUERY_REQUEST = auto()
|
|
207
|
-
"""The resource query request context.
|
|
208
|
-
|
|
209
|
-
Should be used for clients building a payload for a resource query request,
|
|
210
|
-
and servers validating resource query request payloads.
|
|
211
|
-
|
|
212
|
-
- When used for serialization, it will not dump attributes annotated with :attr:`~scim2_models.Mutability.write_only`.
|
|
213
|
-
- When used for validation, it will raise a :class:`~pydantic.ValidationError` when finding attributes annotated with :attr:`~scim2_models.Mutability.write_only`.
|
|
214
|
-
"""
|
|
215
|
-
|
|
216
|
-
RESOURCE_QUERY_RESPONSE = auto()
|
|
217
|
-
"""The resource query response context.
|
|
218
|
-
|
|
219
|
-
Should be used for servers building a payload for a resource query
|
|
220
|
-
response, and clients validating resource query response payloads.
|
|
221
|
-
|
|
222
|
-
- When used for validation, it will raise a :class:`~pydantic.ValidationError` when finding attributes annotated with :attr:`~scim2_models.Returned.never` or when attributes annotated with :attr:`~scim2_models.Returned.always` are missing or :data:`None`;
|
|
223
|
-
- When used for serialization, it will:
|
|
224
|
-
- always dump attributes annotated with :attr:`~scim2_models.Returned.always`;
|
|
225
|
-
- never dump attributes annotated with :attr:`~scim2_models.Returned.never`;
|
|
226
|
-
- dump attributes annotated with :attr:`~scim2_models.Returned.default` unless they are explicitly excluded;
|
|
227
|
-
- not dump attributes annotated with :attr:`~scim2_models.Returned.request` unless they are explicitly included.
|
|
228
|
-
"""
|
|
229
|
-
|
|
230
|
-
RESOURCE_REPLACEMENT_REQUEST = auto()
|
|
231
|
-
"""The resource replacement request context.
|
|
232
|
-
|
|
233
|
-
Should be used for clients building a payload for a resource replacement request,
|
|
234
|
-
and servers validating resource replacement request payloads.
|
|
235
|
-
|
|
236
|
-
- When used for serialization, it will not dump attributes annotated with :attr:`~scim2_models.Mutability.read_only`.
|
|
237
|
-
- When used for validation, it will ignore attributes annotated with :attr:`scim2_models.Mutability.read_only` and raise a :class:`~pydantic.ValidationError`:
|
|
238
|
-
- when finding attributes annotated with :attr:`~scim2_models.Mutability.immutable` different than :paramref:`~scim2_models.BaseModel.model_validate.original`:
|
|
239
|
-
- when attributes annotated with :attr:`Required.true <scim2_models.Required.true>` are missing on null.
|
|
240
|
-
"""
|
|
241
|
-
|
|
242
|
-
RESOURCE_REPLACEMENT_RESPONSE = auto()
|
|
243
|
-
"""The resource replacement response context.
|
|
244
|
-
|
|
245
|
-
Should be used for servers building a payload for a resource
|
|
246
|
-
replacement response, and clients validating resource query
|
|
247
|
-
replacement payloads.
|
|
248
|
-
|
|
249
|
-
- When used for validation, it will raise a :class:`~pydantic.ValidationError` when finding attributes annotated with :attr:`~scim2_models.Returned.never` or when attributes annotated with :attr:`~scim2_models.Returned.always` are missing or :data:`None`;
|
|
250
|
-
- When used for serialization, it will:
|
|
251
|
-
- always dump attributes annotated with :attr:`~scim2_models.Returned.always`;
|
|
252
|
-
- never dump attributes annotated with :attr:`~scim2_models.Returned.never`;
|
|
253
|
-
- dump attributes annotated with :attr:`~scim2_models.Returned.default` unless they are explicitly excluded;
|
|
254
|
-
- not dump attributes annotated with :attr:`~scim2_models.Returned.request` unless they are explicitly included.
|
|
255
|
-
"""
|
|
256
|
-
|
|
257
|
-
SEARCH_REQUEST = auto()
|
|
258
|
-
"""The search request context.
|
|
259
|
-
|
|
260
|
-
Should be used for clients building a payload for a search request,
|
|
261
|
-
and servers validating search request payloads.
|
|
262
|
-
|
|
263
|
-
- When used for serialization, it will not dump attributes annotated with :attr:`~scim2_models.Mutability.write_only`.
|
|
264
|
-
- When used for validation, it will raise a :class:`~pydantic.ValidationError` when finding attributes annotated with :attr:`~scim2_models.Mutability.write_only`.
|
|
265
|
-
"""
|
|
266
|
-
|
|
267
|
-
SEARCH_RESPONSE = auto()
|
|
268
|
-
"""The resource query response context.
|
|
269
|
-
|
|
270
|
-
Should be used for servers building a payload for a search response,
|
|
271
|
-
and clients validating resource search payloads.
|
|
272
|
-
|
|
273
|
-
- When used for validation, it will raise a :class:`~pydantic.ValidationError` when finding attributes annotated with :attr:`~scim2_models.Returned.never` or when attributes annotated with :attr:`~scim2_models.Returned.always` are missing or :data:`None`;
|
|
274
|
-
- When used for serialization, it will:
|
|
275
|
-
- always dump attributes annotated with :attr:`~scim2_models.Returned.always`;
|
|
276
|
-
- never dump attributes annotated with :attr:`~scim2_models.Returned.never`;
|
|
277
|
-
- dump attributes annotated with :attr:`~scim2_models.Returned.default` unless they are explicitly excluded;
|
|
278
|
-
- not dump attributes annotated with :attr:`~scim2_models.Returned.request` unless they are explicitly included.
|
|
279
|
-
"""
|
|
280
|
-
|
|
281
|
-
@classmethod
|
|
282
|
-
def is_request(cls, ctx: "Context") -> bool:
|
|
283
|
-
return ctx in (
|
|
284
|
-
cls.RESOURCE_CREATION_REQUEST,
|
|
285
|
-
cls.RESOURCE_QUERY_REQUEST,
|
|
286
|
-
cls.RESOURCE_REPLACEMENT_REQUEST,
|
|
287
|
-
cls.SEARCH_REQUEST,
|
|
288
|
-
)
|
|
289
|
-
|
|
290
|
-
@classmethod
|
|
291
|
-
def is_response(cls, ctx: "Context") -> bool:
|
|
292
|
-
return ctx in (
|
|
293
|
-
cls.RESOURCE_CREATION_RESPONSE,
|
|
294
|
-
cls.RESOURCE_QUERY_RESPONSE,
|
|
295
|
-
cls.RESOURCE_REPLACEMENT_RESPONSE,
|
|
296
|
-
cls.SEARCH_RESPONSE,
|
|
297
|
-
)
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
class Mutability(str, Enum):
|
|
301
|
-
"""A single keyword indicating the circumstances under which the value of the attribute can be (re)defined."""
|
|
302
|
-
|
|
303
|
-
read_only = "readOnly"
|
|
304
|
-
"""The attribute SHALL NOT be modified."""
|
|
305
|
-
|
|
306
|
-
read_write = "readWrite"
|
|
307
|
-
"""The attribute MAY be updated and read at any time."""
|
|
308
|
-
|
|
309
|
-
immutable = "immutable"
|
|
310
|
-
"""The attribute MAY be defined at resource creation (e.g., POST) or at
|
|
311
|
-
record replacement via a request (e.g., a PUT).
|
|
312
|
-
|
|
313
|
-
The attribute SHALL NOT be updated.
|
|
314
|
-
"""
|
|
315
|
-
|
|
316
|
-
write_only = "writeOnly"
|
|
317
|
-
"""The attribute MAY be updated at any time.
|
|
318
|
-
|
|
319
|
-
Attribute values SHALL NOT be returned (e.g., because the value is a
|
|
320
|
-
stored hash). Note: An attribute with a mutability of "writeOnly"
|
|
321
|
-
usually also has a returned setting of "never".
|
|
322
|
-
"""
|
|
323
|
-
|
|
324
|
-
_default = read_write
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
class Returned(str, Enum):
|
|
328
|
-
"""A single keyword that indicates when an attribute and associated values are returned in response to a GET request or in response to a PUT, POST, or PATCH request."""
|
|
329
|
-
|
|
330
|
-
always = "always" # cannot be excluded
|
|
331
|
-
"""The attribute is always returned, regardless of the contents of the
|
|
332
|
-
"attributes" parameter.
|
|
333
|
-
|
|
334
|
-
For example, "id" is always returned to identify a SCIM resource.
|
|
335
|
-
"""
|
|
336
|
-
|
|
337
|
-
never = "never" # always excluded
|
|
338
|
-
"""The attribute is never returned, regardless of the contents of the
|
|
339
|
-
"attributes" parameter."""
|
|
340
|
-
|
|
341
|
-
default = "default" # included by default but can be excluded
|
|
342
|
-
"""The attribute is returned by default in all SCIM operation responses
|
|
343
|
-
where attribute values are returned, unless it is explicitly excluded."""
|
|
344
|
-
|
|
345
|
-
request = "request" # excluded by default but can be included
|
|
346
|
-
"""The attribute is returned in response to any PUT, POST, or PATCH
|
|
347
|
-
operations if specified in the "attributes" parameter."""
|
|
348
|
-
|
|
349
|
-
_default = default
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
class Uniqueness(str, Enum):
|
|
353
|
-
"""A single keyword value that specifies how the service provider enforces uniqueness of attribute values."""
|
|
354
|
-
|
|
355
|
-
none = "none"
|
|
356
|
-
"""The values are not intended to be unique in any way."""
|
|
357
|
-
|
|
358
|
-
server = "server"
|
|
359
|
-
"""The value SHOULD be unique within the context of the current SCIM
|
|
360
|
-
endpoint (or tenancy) and MAY be globally unique (e.g., a "username", email
|
|
361
|
-
address, or other server-generated key or counter).
|
|
362
|
-
|
|
363
|
-
No two resources on the same server SHOULD possess the same value.
|
|
364
|
-
"""
|
|
365
|
-
|
|
366
|
-
global_ = "global"
|
|
367
|
-
"""The value SHOULD be globally unique (e.g., an email address, a GUID, or
|
|
368
|
-
other value).
|
|
369
|
-
|
|
370
|
-
No two resources on any server SHOULD possess the same value.
|
|
371
|
-
"""
|
|
372
|
-
|
|
373
|
-
_default = none
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
class Required(Enum):
|
|
377
|
-
"""A Boolean value that specifies whether the attribute is required or not.
|
|
378
|
-
|
|
379
|
-
Missing required attributes raise a :class:`~pydantic.ValidationError` on :attr:`~scim2_models.Context.RESOURCE_CREATION_REQUEST` and :attr:`~scim2_models.Context.RESOURCE_REPLACEMENT_REQUEST` validations.
|
|
380
|
-
"""
|
|
381
|
-
|
|
382
|
-
true = True
|
|
383
|
-
false = False
|
|
384
|
-
|
|
385
|
-
_default = false
|
|
386
|
-
|
|
387
|
-
def __bool__(self):
|
|
388
|
-
return self.value
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
class CaseExact(Enum):
|
|
392
|
-
"""A Boolean value that specifies whether a string attribute is case- sensitive or not."""
|
|
393
|
-
|
|
394
|
-
true = True
|
|
395
|
-
false = False
|
|
396
|
-
|
|
397
|
-
_default = false
|
|
398
|
-
|
|
399
|
-
def __bool__(self):
|
|
400
|
-
return self.value
|
|
401
|
-
|
|
402
|
-
|
|
403
41
|
class BaseModel(PydanticBaseModel):
|
|
404
42
|
"""Base Model for everything."""
|
|
405
43
|
|
|
406
44
|
model_config = ConfigDict(
|
|
407
45
|
alias_generator=AliasGenerator(
|
|
408
|
-
validation_alias=
|
|
409
|
-
serialization_alias=
|
|
46
|
+
validation_alias=_normalize_attribute_name,
|
|
47
|
+
serialization_alias=_to_camel,
|
|
410
48
|
),
|
|
411
49
|
validate_assignment=True,
|
|
412
50
|
populate_by_name=True,
|
|
@@ -416,12 +54,40 @@ class BaseModel(PydanticBaseModel):
|
|
|
416
54
|
|
|
417
55
|
@classmethod
|
|
418
56
|
def get_field_annotation(cls, field_name: str, annotation_type: type) -> Any:
|
|
419
|
-
"""Return the annotation of type 'annotation_type' of the field 'field_name'.
|
|
57
|
+
"""Return the annotation of type 'annotation_type' of the field 'field_name'.
|
|
58
|
+
|
|
59
|
+
This method extracts SCIM-specific annotations from a field's metadata,
|
|
60
|
+
such as :class:`~scim2_models.Mutability`, :class:`~scim2_models.Required`,
|
|
61
|
+
or :class:`~scim2_models.Returned` annotations.
|
|
62
|
+
|
|
63
|
+
:return: The annotation instance if found, otherwise the annotation type's default value
|
|
64
|
+
|
|
65
|
+
>>> from scim2_models.resources.user import User
|
|
66
|
+
>>> from scim2_models.annotations import Mutability, Required
|
|
67
|
+
|
|
68
|
+
Get the mutability annotation of the 'id' field:
|
|
69
|
+
|
|
70
|
+
>>> mutability = User.get_field_annotation("id", Mutability)
|
|
71
|
+
>>> mutability
|
|
72
|
+
<Mutability.read_only: 'readOnly'>
|
|
73
|
+
|
|
74
|
+
Get the required annotation of the 'user_name' field:
|
|
75
|
+
|
|
76
|
+
>>> required = User.get_field_annotation("user_name", Required)
|
|
77
|
+
>>> required
|
|
78
|
+
<Required.true: True>
|
|
79
|
+
|
|
80
|
+
If no annotation is found, returns the default value:
|
|
81
|
+
|
|
82
|
+
>>> missing = User.get_field_annotation("display_name", Required)
|
|
83
|
+
>>> missing
|
|
84
|
+
<Required.false: False>
|
|
85
|
+
"""
|
|
420
86
|
field_metadata = cls.model_fields[field_name].metadata
|
|
421
87
|
|
|
422
88
|
default_value = getattr(annotation_type, "_default", None)
|
|
423
89
|
|
|
424
|
-
def annotation_type_filter(item):
|
|
90
|
+
def annotation_type_filter(item: Any) -> bool:
|
|
425
91
|
return isinstance(item, annotation_type)
|
|
426
92
|
|
|
427
93
|
field_annotation = next(
|
|
@@ -433,8 +99,34 @@ class BaseModel(PydanticBaseModel):
|
|
|
433
99
|
def get_field_root_type(cls, attribute_name: str) -> Optional[type]:
|
|
434
100
|
"""Extract the root type from a model field.
|
|
435
101
|
|
|
436
|
-
|
|
437
|
-
|
|
102
|
+
This method unwraps complex type annotations to find the underlying
|
|
103
|
+
type, removing Optional and List wrappers to get to the actual type
|
|
104
|
+
of the field's content.
|
|
105
|
+
|
|
106
|
+
:return: The root type of the field, or None if not found
|
|
107
|
+
|
|
108
|
+
>>> from scim2_models.resources.user import User
|
|
109
|
+
>>> from scim2_models.resources.group import Group
|
|
110
|
+
|
|
111
|
+
Simple type:
|
|
112
|
+
|
|
113
|
+
>>> User.get_field_root_type("user_name")
|
|
114
|
+
<class 'str'>
|
|
115
|
+
|
|
116
|
+
``Optional`` type unwraps to the underlying type:
|
|
117
|
+
|
|
118
|
+
>>> User.get_field_root_type("display_name")
|
|
119
|
+
<class 'str'>
|
|
120
|
+
|
|
121
|
+
``List`` type unwraps to the element type:
|
|
122
|
+
|
|
123
|
+
>>> User.get_field_root_type("emails") # doctest: +ELLIPSIS
|
|
124
|
+
<class 'scim2_models.resources.user.Email'>
|
|
125
|
+
|
|
126
|
+
``Optional[List[T]]`` unwraps to ``T``:
|
|
127
|
+
|
|
128
|
+
>>> Group.get_field_root_type("members") # doctest: +ELLIPSIS
|
|
129
|
+
<class 'scim2_models.resources.group.GroupMember'>
|
|
438
130
|
"""
|
|
439
131
|
attribute_type = cls.model_fields[attribute_name].annotation
|
|
440
132
|
|
|
@@ -451,7 +143,20 @@ class BaseModel(PydanticBaseModel):
|
|
|
451
143
|
|
|
452
144
|
@classmethod
|
|
453
145
|
def get_field_multiplicity(cls, attribute_name: str) -> bool:
|
|
454
|
-
"""Indicate whether a field holds multiple values.
|
|
146
|
+
"""Indicate whether a field holds multiple values.
|
|
147
|
+
|
|
148
|
+
This method determines if a field is defined as a list type,
|
|
149
|
+
which indicates it can contain multiple values. It handles
|
|
150
|
+
Optional wrappers correctly.
|
|
151
|
+
|
|
152
|
+
:return: True if the field holds multiple values (is a list), False otherwise
|
|
153
|
+
|
|
154
|
+
>>> from scim2_models.resources.user import User
|
|
155
|
+
>>> User.get_field_multiplicity("user_name")
|
|
156
|
+
False
|
|
157
|
+
>>> User.get_field_multiplicity("emails")
|
|
158
|
+
True
|
|
159
|
+
"""
|
|
455
160
|
attribute_type = cls.model_fields[attribute_name].annotation
|
|
456
161
|
|
|
457
162
|
# extract 'x' from 'Optional[x]'
|
|
@@ -466,9 +171,10 @@ class BaseModel(PydanticBaseModel):
|
|
|
466
171
|
def check_request_attributes_mutability(
|
|
467
172
|
cls, value: Any, info: ValidationInfo
|
|
468
173
|
) -> Any:
|
|
469
|
-
"""Check and fix that the field mutability is expected according to the requests validation context, as defined in :rfc:`RFC7643 §7 <
|
|
174
|
+
"""Check and fix that the field mutability is expected according to the requests validation context, as defined in :rfc:`RFC7643 §7 <7643#section-7>`."""
|
|
470
175
|
if (
|
|
471
176
|
not info.context
|
|
177
|
+
or not info.field_name
|
|
472
178
|
or not info.context.get("scim")
|
|
473
179
|
or not Context.is_request(info.context["scim"])
|
|
474
180
|
):
|
|
@@ -508,41 +214,73 @@ class BaseModel(PydanticBaseModel):
|
|
|
508
214
|
) -> Self:
|
|
509
215
|
"""Normalize payload attribute names.
|
|
510
216
|
|
|
511
|
-
:rfc:`RFC7643 §2.1 <
|
|
217
|
+
:rfc:`RFC7643 §2.1 <7643#section-2.1>` indicate that attribute
|
|
512
218
|
names should be case-insensitive. Any attribute name is
|
|
513
219
|
transformed in lowercase so any case is handled the same way.
|
|
514
220
|
"""
|
|
515
221
|
|
|
516
|
-
def
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
522
|
-
return value
|
|
222
|
+
def normalize_dict_keys(
|
|
223
|
+
input_dict: dict, model_class: type["BaseModel"]
|
|
224
|
+
) -> dict:
|
|
225
|
+
"""Normalize dictionary keys, preserving case for Any fields."""
|
|
226
|
+
result = {}
|
|
523
227
|
|
|
524
|
-
|
|
525
|
-
|
|
228
|
+
for key, val in input_dict.items():
|
|
229
|
+
field_name = _find_field_name(model_class, key)
|
|
230
|
+
field_type = (
|
|
231
|
+
model_class.get_field_root_type(field_name) if field_name else None
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Don't normalize keys for attributes typed with Any
|
|
235
|
+
# This way, agnostic dicts such as PatchOp.operations.value
|
|
236
|
+
# are preserved
|
|
237
|
+
if field_name and field_type == Any:
|
|
238
|
+
result[key] = normalize_value(val)
|
|
239
|
+
else:
|
|
240
|
+
result[_normalize_attribute_name(key)] = normalize_value(
|
|
241
|
+
val, field_type
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return result
|
|
245
|
+
|
|
246
|
+
def normalize_value(
|
|
247
|
+
val: Any, model_class: Optional[type["BaseModel"]] = None
|
|
248
|
+
) -> Any:
|
|
249
|
+
"""Normalize input value based on model class."""
|
|
250
|
+
if not isinstance(val, dict):
|
|
251
|
+
return val
|
|
252
|
+
|
|
253
|
+
# If no model_class, preserve original keys
|
|
254
|
+
if not model_class:
|
|
255
|
+
return {k: normalize_value(v) for k, v in val.items()}
|
|
256
|
+
|
|
257
|
+
return normalize_dict_keys(val, model_class)
|
|
258
|
+
|
|
259
|
+
normalized_value = normalize_value(value, cls)
|
|
260
|
+
obj = handler(normalized_value)
|
|
261
|
+
assert isinstance(obj, cls)
|
|
262
|
+
return obj
|
|
526
263
|
|
|
527
264
|
@model_validator(mode="wrap")
|
|
528
265
|
@classmethod
|
|
529
266
|
def check_response_attributes_returnability(
|
|
530
267
|
cls, value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
|
|
531
268
|
) -> Self:
|
|
532
|
-
"""Check that the fields returnability is expected according to the responses validation context, as defined in :rfc:`RFC7643 §7 <
|
|
533
|
-
|
|
269
|
+
"""Check that the fields returnability is expected according to the responses validation context, as defined in :rfc:`RFC7643 §7 <7643#section-7>`."""
|
|
270
|
+
obj = handler(value)
|
|
271
|
+
assert isinstance(obj, cls)
|
|
534
272
|
|
|
535
273
|
if (
|
|
536
274
|
not info.context
|
|
537
275
|
or not info.context.get("scim")
|
|
538
276
|
or not Context.is_response(info.context["scim"])
|
|
539
277
|
):
|
|
540
|
-
return
|
|
278
|
+
return obj
|
|
541
279
|
|
|
542
280
|
for field_name in cls.model_fields:
|
|
543
281
|
returnability = cls.get_field_annotation(field_name, Returned)
|
|
544
282
|
|
|
545
|
-
if returnability == Returned.always and getattr(
|
|
283
|
+
if returnability == Returned.always and getattr(obj, field_name) is None:
|
|
546
284
|
raise PydanticCustomError(
|
|
547
285
|
"returned_error",
|
|
548
286
|
"Field '{field_name}' has returnability 'always' but value is missing or null",
|
|
@@ -551,10 +289,7 @@ class BaseModel(PydanticBaseModel):
|
|
|
551
289
|
},
|
|
552
290
|
)
|
|
553
291
|
|
|
554
|
-
if (
|
|
555
|
-
returnability == Returned.never
|
|
556
|
-
and getattr(value, field_name) is not None
|
|
557
|
-
):
|
|
292
|
+
if returnability == Returned.never and getattr(obj, field_name) is not None:
|
|
558
293
|
raise PydanticCustomError(
|
|
559
294
|
"returned_error",
|
|
560
295
|
"Field '{field_name}' has returnability 'never' but value is set",
|
|
@@ -563,7 +298,7 @@ class BaseModel(PydanticBaseModel):
|
|
|
563
298
|
},
|
|
564
299
|
)
|
|
565
300
|
|
|
566
|
-
return
|
|
301
|
+
return obj
|
|
567
302
|
|
|
568
303
|
@model_validator(mode="wrap")
|
|
569
304
|
@classmethod
|
|
@@ -571,7 +306,8 @@ class BaseModel(PydanticBaseModel):
|
|
|
571
306
|
cls, value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
|
|
572
307
|
) -> Self:
|
|
573
308
|
"""Check that the required attributes are present in creations and replacement requests."""
|
|
574
|
-
|
|
309
|
+
obj = handler(value)
|
|
310
|
+
assert isinstance(obj, cls)
|
|
575
311
|
|
|
576
312
|
if (
|
|
577
313
|
not info.context
|
|
@@ -582,12 +318,12 @@ class BaseModel(PydanticBaseModel):
|
|
|
582
318
|
Context.RESOURCE_REPLACEMENT_REQUEST,
|
|
583
319
|
)
|
|
584
320
|
):
|
|
585
|
-
return
|
|
321
|
+
return obj
|
|
586
322
|
|
|
587
323
|
for field_name in cls.model_fields:
|
|
588
324
|
necessity = cls.get_field_annotation(field_name, Required)
|
|
589
325
|
|
|
590
|
-
if necessity == Required.true and getattr(
|
|
326
|
+
if necessity == Required.true and getattr(obj, field_name) is None:
|
|
591
327
|
raise PydanticCustomError(
|
|
592
328
|
"required_error",
|
|
593
329
|
"Field '{field_name}' is required but value is missing or null",
|
|
@@ -596,7 +332,7 @@ class BaseModel(PydanticBaseModel):
|
|
|
596
332
|
},
|
|
597
333
|
)
|
|
598
334
|
|
|
599
|
-
return
|
|
335
|
+
return obj
|
|
600
336
|
|
|
601
337
|
@model_validator(mode="wrap")
|
|
602
338
|
@classmethod
|
|
@@ -604,9 +340,10 @@ class BaseModel(PydanticBaseModel):
|
|
|
604
340
|
cls, value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
|
|
605
341
|
) -> Self:
|
|
606
342
|
"""Check if 'immutable' attributes have been mutated in replacement requests."""
|
|
607
|
-
from scim2_models.
|
|
343
|
+
from scim2_models.resources.resource import Resource
|
|
608
344
|
|
|
609
|
-
|
|
345
|
+
obj = handler(value)
|
|
346
|
+
assert isinstance(obj, cls)
|
|
610
347
|
|
|
611
348
|
context = info.context.get("scim") if info.context else None
|
|
612
349
|
original = info.context.get("original") if info.context else None
|
|
@@ -615,12 +352,16 @@ class BaseModel(PydanticBaseModel):
|
|
|
615
352
|
and issubclass(cls, Resource)
|
|
616
353
|
and original is not None
|
|
617
354
|
):
|
|
618
|
-
cls.
|
|
619
|
-
return
|
|
355
|
+
cls._check_mutability_issues(original, obj)
|
|
356
|
+
return obj
|
|
620
357
|
|
|
621
358
|
@classmethod
|
|
622
|
-
def
|
|
359
|
+
def _check_mutability_issues(
|
|
360
|
+
cls, original: "BaseModel", replacement: "BaseModel"
|
|
361
|
+
) -> None:
|
|
623
362
|
"""Compare two instances, and check for differences of values on the fields marked as immutable."""
|
|
363
|
+
from .attributes import is_complex_attribute
|
|
364
|
+
|
|
624
365
|
model = replacement.__class__
|
|
625
366
|
for field_name in model.model_fields:
|
|
626
367
|
mutability = model.get_field_annotation(field_name, Mutability)
|
|
@@ -634,61 +375,65 @@ class BaseModel(PydanticBaseModel):
|
|
|
634
375
|
)
|
|
635
376
|
|
|
636
377
|
attr_type = model.get_field_root_type(field_name)
|
|
637
|
-
if
|
|
638
|
-
|
|
378
|
+
if (
|
|
379
|
+
attr_type
|
|
380
|
+
and is_complex_attribute(attr_type)
|
|
381
|
+
and not model.get_field_multiplicity(field_name)
|
|
639
382
|
):
|
|
640
383
|
original_val = getattr(original, field_name)
|
|
641
384
|
replacement_value = getattr(replacement, field_name)
|
|
642
385
|
if original_val is not None and replacement_value is not None:
|
|
643
|
-
cls.
|
|
386
|
+
cls._check_mutability_issues(original_val, replacement_value)
|
|
644
387
|
|
|
645
|
-
def
|
|
646
|
-
"""Navigate through attributes and sub-attributes of type ComplexAttribute, and mark them with a '
|
|
388
|
+
def _set_complex_attribute_urns(self) -> None:
|
|
389
|
+
"""Navigate through attributes and sub-attributes of type ComplexAttribute, and mark them with a '_attribute_urn' attribute.
|
|
647
390
|
|
|
648
|
-
'
|
|
391
|
+
'_attribute_urn' will later be used by 'get_attribute_urn'.
|
|
649
392
|
"""
|
|
650
|
-
from
|
|
393
|
+
from .attributes import ComplexAttribute
|
|
394
|
+
from .attributes import is_complex_attribute
|
|
395
|
+
|
|
396
|
+
if isinstance(self, ComplexAttribute):
|
|
397
|
+
main_schema = self._attribute_urn
|
|
398
|
+
separator = "."
|
|
399
|
+
else:
|
|
400
|
+
main_schema = self.__class__.model_fields["schemas"].default[0]
|
|
401
|
+
separator = ":"
|
|
651
402
|
|
|
652
403
|
for field_name in self.__class__.model_fields:
|
|
653
404
|
attr_type = self.get_field_root_type(field_name)
|
|
654
|
-
if not is_complex_attribute(attr_type):
|
|
405
|
+
if not attr_type or not is_complex_attribute(attr_type):
|
|
655
406
|
continue
|
|
656
407
|
|
|
657
|
-
main_schema = (
|
|
658
|
-
getattr(self, "_schema", None)
|
|
659
|
-
or self.__class__.model_fields["schemas"].default[0]
|
|
660
|
-
)
|
|
661
|
-
|
|
662
|
-
separator = ":" if isinstance(self, Resource) else "."
|
|
663
408
|
schema = f"{main_schema}{separator}{field_name}"
|
|
664
409
|
|
|
665
410
|
if attr_value := getattr(self, field_name):
|
|
666
411
|
if isinstance(attr_value, list):
|
|
667
412
|
for item in attr_value:
|
|
668
|
-
item.
|
|
413
|
+
item._attribute_urn = schema
|
|
669
414
|
else:
|
|
670
|
-
attr_value.
|
|
415
|
+
attr_value._attribute_urn = schema
|
|
671
416
|
|
|
672
417
|
@field_serializer("*", mode="wrap")
|
|
673
418
|
def scim_serializer(
|
|
674
419
|
self,
|
|
675
420
|
value: Any,
|
|
676
421
|
handler: SerializerFunctionWrapHandler,
|
|
677
|
-
info:
|
|
422
|
+
info: FieldSerializationInfo,
|
|
678
423
|
) -> Any:
|
|
679
424
|
"""Serialize the fields according to mutability indications passed in the serialization context."""
|
|
680
425
|
value = handler(value)
|
|
681
426
|
scim_ctx = info.context.get("scim") if info.context else None
|
|
682
427
|
|
|
683
428
|
if scim_ctx and Context.is_request(scim_ctx):
|
|
684
|
-
value = self.
|
|
429
|
+
value = self._scim_request_serializer(value, info)
|
|
685
430
|
|
|
686
431
|
if scim_ctx and Context.is_response(scim_ctx):
|
|
687
|
-
value = self.
|
|
432
|
+
value = self._scim_response_serializer(value, info)
|
|
688
433
|
|
|
689
434
|
return value
|
|
690
435
|
|
|
691
|
-
def
|
|
436
|
+
def _scim_request_serializer(self, value: Any, info: FieldSerializationInfo) -> Any:
|
|
692
437
|
"""Serialize the fields according to mutability indications passed in the serialization context."""
|
|
693
438
|
mutability = self.get_field_annotation(info.field_name, Mutability)
|
|
694
439
|
scim_ctx = info.context.get("scim") if info.context else None
|
|
@@ -712,7 +457,9 @@ class BaseModel(PydanticBaseModel):
|
|
|
712
457
|
|
|
713
458
|
return value
|
|
714
459
|
|
|
715
|
-
def
|
|
460
|
+
def _scim_response_serializer(
|
|
461
|
+
self, value: Any, info: FieldSerializationInfo
|
|
462
|
+
) -> Any:
|
|
716
463
|
"""Serialize the fields according to returnability indications passed in the serialization context."""
|
|
717
464
|
returnability = self.get_field_annotation(info.field_name, Returned)
|
|
718
465
|
attribute_urn = self.get_attribute_urn(info.field_name)
|
|
@@ -721,9 +468,9 @@ class BaseModel(PydanticBaseModel):
|
|
|
721
468
|
info.context.get("scim_excluded_attributes", []) if info.context else []
|
|
722
469
|
)
|
|
723
470
|
|
|
724
|
-
attribute_urn =
|
|
725
|
-
included_urns = [
|
|
726
|
-
excluded_urns = [
|
|
471
|
+
attribute_urn = _normalize_attribute_name(attribute_urn)
|
|
472
|
+
included_urns = [_normalize_attribute_name(urn) for urn in included_urns]
|
|
473
|
+
excluded_urns = [_normalize_attribute_name(urn) for urn in excluded_urns]
|
|
727
474
|
|
|
728
475
|
if returnability == Returned.never:
|
|
729
476
|
return None
|
|
@@ -731,7 +478,7 @@ class BaseModel(PydanticBaseModel):
|
|
|
731
478
|
if returnability == Returned.default and (
|
|
732
479
|
(
|
|
733
480
|
included_urns
|
|
734
|
-
and not
|
|
481
|
+
and not _contains_attribute_or_subattributes(
|
|
735
482
|
included_urns, attribute_urn
|
|
736
483
|
)
|
|
737
484
|
)
|
|
@@ -746,20 +493,20 @@ class BaseModel(PydanticBaseModel):
|
|
|
746
493
|
|
|
747
494
|
@model_serializer(mode="wrap")
|
|
748
495
|
def model_serializer_exclude_none(
|
|
749
|
-
self, handler, info: SerializationInfo
|
|
496
|
+
self, handler: SerializerFunctionWrapHandler, info: SerializationInfo
|
|
750
497
|
) -> dict[str, Any]:
|
|
751
498
|
"""Remove `None` values inserted by the :meth:`~scim2_models.base.BaseModel.scim_serializer`."""
|
|
752
|
-
self.
|
|
499
|
+
self._set_complex_attribute_urns()
|
|
753
500
|
result = handler(self)
|
|
754
501
|
return {key: value for key, value in result.items() if value is not None}
|
|
755
502
|
|
|
756
503
|
@classmethod
|
|
757
504
|
def model_validate(
|
|
758
505
|
cls,
|
|
759
|
-
*args,
|
|
506
|
+
*args: Any,
|
|
760
507
|
scim_ctx: Optional[Context] = Context.DEFAULT,
|
|
761
508
|
original: Optional["BaseModel"] = None,
|
|
762
|
-
**kwargs,
|
|
509
|
+
**kwargs: Any,
|
|
763
510
|
) -> Self:
|
|
764
511
|
"""Validate SCIM payloads and generate model representation by using Pydantic :code:`BaseModel.model_validate`.
|
|
765
512
|
|
|
@@ -779,126 +526,20 @@ class BaseModel(PydanticBaseModel):
|
|
|
779
526
|
|
|
780
527
|
return super().model_validate(*args, **kwargs)
|
|
781
528
|
|
|
782
|
-
def _prepare_model_dump(
|
|
783
|
-
self,
|
|
784
|
-
scim_ctx: Optional[Context] = Context.DEFAULT,
|
|
785
|
-
attributes: Optional[list[str]] = None,
|
|
786
|
-
excluded_attributes: Optional[list[str]] = None,
|
|
787
|
-
**kwargs,
|
|
788
|
-
):
|
|
789
|
-
kwargs.setdefault("context", {}).setdefault("scim", scim_ctx)
|
|
790
|
-
kwargs["context"]["scim_attributes"] = [
|
|
791
|
-
validate_attribute_urn(attribute, self.__class__)
|
|
792
|
-
for attribute in (attributes or [])
|
|
793
|
-
]
|
|
794
|
-
kwargs["context"]["scim_excluded_attributes"] = [
|
|
795
|
-
validate_attribute_urn(attribute, self.__class__)
|
|
796
|
-
for attribute in (excluded_attributes or [])
|
|
797
|
-
]
|
|
798
|
-
|
|
799
|
-
if scim_ctx:
|
|
800
|
-
kwargs.setdefault("exclude_none", True)
|
|
801
|
-
kwargs.setdefault("by_alias", True)
|
|
802
|
-
|
|
803
|
-
return kwargs
|
|
804
|
-
|
|
805
|
-
def model_dump(
|
|
806
|
-
self,
|
|
807
|
-
*args,
|
|
808
|
-
scim_ctx: Optional[Context] = Context.DEFAULT,
|
|
809
|
-
attributes: Optional[list[str]] = None,
|
|
810
|
-
excluded_attributes: Optional[list[str]] = None,
|
|
811
|
-
**kwargs,
|
|
812
|
-
) -> dict:
|
|
813
|
-
"""Create a model representation that can be included in SCIM messages by using Pydantic :code:`BaseModel.model_dump`.
|
|
814
|
-
|
|
815
|
-
:param scim_ctx: If a SCIM context is passed, some default values of
|
|
816
|
-
Pydantic :code:`BaseModel.model_dump` are tuned to generate valid SCIM
|
|
817
|
-
messages. Pass :data:`None` to get the default Pydantic behavior.
|
|
818
|
-
"""
|
|
819
|
-
dump_kwargs = self._prepare_model_dump(
|
|
820
|
-
scim_ctx, attributes, excluded_attributes, **kwargs
|
|
821
|
-
)
|
|
822
|
-
if scim_ctx:
|
|
823
|
-
dump_kwargs.setdefault("mode", "json")
|
|
824
|
-
return super().model_dump(*args, **dump_kwargs)
|
|
825
|
-
|
|
826
|
-
def model_dump_json(
|
|
827
|
-
self,
|
|
828
|
-
*args,
|
|
829
|
-
scim_ctx: Optional[Context] = Context.DEFAULT,
|
|
830
|
-
attributes: Optional[list[str]] = None,
|
|
831
|
-
excluded_attributes: Optional[list[str]] = None,
|
|
832
|
-
**kwargs,
|
|
833
|
-
) -> dict:
|
|
834
|
-
"""Create a JSON model representation that can be included in SCIM messages by using Pydantic :code:`BaseModel.model_dump_json`.
|
|
835
|
-
|
|
836
|
-
:param scim_ctx: If a SCIM context is passed, some default values of
|
|
837
|
-
Pydantic :code:`BaseModel.model_dump` are tuned to generate valid SCIM
|
|
838
|
-
messages. Pass :data:`None` to get the default Pydantic behavior.
|
|
839
|
-
"""
|
|
840
|
-
dump_kwargs = self._prepare_model_dump(
|
|
841
|
-
scim_ctx, attributes, excluded_attributes, **kwargs
|
|
842
|
-
)
|
|
843
|
-
return super().model_dump_json(*args, **dump_kwargs)
|
|
844
|
-
|
|
845
529
|
def get_attribute_urn(self, field_name: str) -> str:
|
|
846
530
|
"""Build the full URN of the attribute.
|
|
847
531
|
|
|
848
532
|
See :rfc:`RFC7644 §3.10 <7644#section-3.10>`.
|
|
849
533
|
"""
|
|
534
|
+
from scim2_models.resources.resource import Extension
|
|
535
|
+
|
|
850
536
|
main_schema = self.__class__.model_fields["schemas"].default[0]
|
|
851
|
-
|
|
852
|
-
|
|
537
|
+
field = self.__class__.model_fields[field_name]
|
|
538
|
+
alias = field.serialization_alias or field_name
|
|
539
|
+
field_type = self.get_field_root_type(field_name)
|
|
540
|
+
full_urn = (
|
|
541
|
+
alias
|
|
542
|
+
if isclass(field_type) and issubclass(field_type, Extension)
|
|
543
|
+
else f"{main_schema}:{alias}"
|
|
853
544
|
)
|
|
854
|
-
|
|
855
|
-
# if alias contains a ':' this is an extension urn
|
|
856
|
-
full_urn = alias if ":" in alias else f"{main_schema}:{alias}"
|
|
857
545
|
return full_urn
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
class ComplexAttribute(BaseModel):
|
|
861
|
-
"""A complex attribute as defined in :rfc:`RFC7643 §2.3.8 <7643#section-2.3.8>`."""
|
|
862
|
-
|
|
863
|
-
_schema: Optional[str] = None
|
|
864
|
-
|
|
865
|
-
def get_attribute_urn(self, field_name: str) -> str:
|
|
866
|
-
"""Build the full URN of the attribute.
|
|
867
|
-
|
|
868
|
-
See :rfc:`RFC7644 §3.10 <7644#section-3.10>`.
|
|
869
|
-
"""
|
|
870
|
-
alias = (
|
|
871
|
-
self.__class__.model_fields[field_name].serialization_alias or field_name
|
|
872
|
-
)
|
|
873
|
-
return f"{self._schema}.{alias}"
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
class MultiValuedComplexAttribute(ComplexAttribute):
|
|
877
|
-
type: Optional[str] = None
|
|
878
|
-
"""A label indicating the attribute's function."""
|
|
879
|
-
|
|
880
|
-
primary: Optional[bool] = None
|
|
881
|
-
"""A Boolean value indicating the 'primary' or preferred attribute value
|
|
882
|
-
for this attribute."""
|
|
883
|
-
|
|
884
|
-
display: Annotated[Optional[str], Mutability.immutable] = None
|
|
885
|
-
"""A human-readable name, primarily used for display purposes."""
|
|
886
|
-
|
|
887
|
-
value: Optional[Any] = None
|
|
888
|
-
"""The value of an entitlement."""
|
|
889
|
-
|
|
890
|
-
ref: Optional[Reference] = Field(None, serialization_alias="$ref")
|
|
891
|
-
"""The reference URI of a target resource, if the attribute is a
|
|
892
|
-
reference."""
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
def is_complex_attribute(type) -> bool:
|
|
896
|
-
# issubclass raise a TypeError with 'Reference' on python < 3.11
|
|
897
|
-
return (
|
|
898
|
-
get_origin(type) != Reference
|
|
899
|
-
and isclass(type)
|
|
900
|
-
and issubclass(type, (ComplexAttribute, MultiValuedComplexAttribute))
|
|
901
|
-
)
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
BaseModelType: type = type(BaseModel)
|