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/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,382 +17,27 @@ 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
|
|
|
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
|
|
31
26
|
from scim2_models.utils import normalize_attribute_name
|
|
32
27
|
from scim2_models.utils import to_camel
|
|
33
28
|
|
|
34
29
|
from .utils import UNION_TYPES
|
|
35
30
|
|
|
36
|
-
ReferenceTypes = TypeVar("ReferenceTypes")
|
|
37
|
-
URIReference = NewType("URIReference", str)
|
|
38
|
-
ExternalReference = NewType("ExternalReference", str)
|
|
39
31
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
|
@@ -421,7 +59,7 @@ class BaseModel(PydanticBaseModel):
|
|
|
421
59
|
|
|
422
60
|
default_value = getattr(annotation_type, "_default", None)
|
|
423
61
|
|
|
424
|
-
def annotation_type_filter(item):
|
|
62
|
+
def annotation_type_filter(item: Any) -> bool:
|
|
425
63
|
return isinstance(item, annotation_type)
|
|
426
64
|
|
|
427
65
|
field_annotation = next(
|
|
@@ -469,6 +107,7 @@ class BaseModel(PydanticBaseModel):
|
|
|
469
107
|
"""Check and fix that the field mutability is expected according to the requests validation context, as defined in :rfc:`RFC7643 §7 <7653#section-7>`."""
|
|
470
108
|
if (
|
|
471
109
|
not info.context
|
|
110
|
+
or not info.field_name
|
|
472
111
|
or not info.context.get("scim")
|
|
473
112
|
or not Context.is_request(info.context["scim"])
|
|
474
113
|
):
|
|
@@ -522,7 +161,9 @@ class BaseModel(PydanticBaseModel):
|
|
|
522
161
|
return value
|
|
523
162
|
|
|
524
163
|
normalized_value = normalize_value(value)
|
|
525
|
-
|
|
164
|
+
obj = handler(normalized_value)
|
|
165
|
+
assert isinstance(obj, cls)
|
|
166
|
+
return obj
|
|
526
167
|
|
|
527
168
|
@model_validator(mode="wrap")
|
|
528
169
|
@classmethod
|
|
@@ -530,19 +171,20 @@ class BaseModel(PydanticBaseModel):
|
|
|
530
171
|
cls, value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
|
|
531
172
|
) -> Self:
|
|
532
173
|
"""Check that the fields returnability is expected according to the responses validation context, as defined in :rfc:`RFC7643 §7 <7653#section-7>`."""
|
|
533
|
-
|
|
174
|
+
obj = handler(value)
|
|
175
|
+
assert isinstance(obj, cls)
|
|
534
176
|
|
|
535
177
|
if (
|
|
536
178
|
not info.context
|
|
537
179
|
or not info.context.get("scim")
|
|
538
180
|
or not Context.is_response(info.context["scim"])
|
|
539
181
|
):
|
|
540
|
-
return
|
|
182
|
+
return obj
|
|
541
183
|
|
|
542
184
|
for field_name in cls.model_fields:
|
|
543
185
|
returnability = cls.get_field_annotation(field_name, Returned)
|
|
544
186
|
|
|
545
|
-
if returnability == Returned.always and getattr(
|
|
187
|
+
if returnability == Returned.always and getattr(obj, field_name) is None:
|
|
546
188
|
raise PydanticCustomError(
|
|
547
189
|
"returned_error",
|
|
548
190
|
"Field '{field_name}' has returnability 'always' but value is missing or null",
|
|
@@ -551,10 +193,7 @@ class BaseModel(PydanticBaseModel):
|
|
|
551
193
|
},
|
|
552
194
|
)
|
|
553
195
|
|
|
554
|
-
if (
|
|
555
|
-
returnability == Returned.never
|
|
556
|
-
and getattr(value, field_name) is not None
|
|
557
|
-
):
|
|
196
|
+
if returnability == Returned.never and getattr(obj, field_name) is not None:
|
|
558
197
|
raise PydanticCustomError(
|
|
559
198
|
"returned_error",
|
|
560
199
|
"Field '{field_name}' has returnability 'never' but value is set",
|
|
@@ -563,7 +202,7 @@ class BaseModel(PydanticBaseModel):
|
|
|
563
202
|
},
|
|
564
203
|
)
|
|
565
204
|
|
|
566
|
-
return
|
|
205
|
+
return obj
|
|
567
206
|
|
|
568
207
|
@model_validator(mode="wrap")
|
|
569
208
|
@classmethod
|
|
@@ -571,7 +210,8 @@ class BaseModel(PydanticBaseModel):
|
|
|
571
210
|
cls, value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
|
|
572
211
|
) -> Self:
|
|
573
212
|
"""Check that the required attributes are present in creations and replacement requests."""
|
|
574
|
-
|
|
213
|
+
obj = handler(value)
|
|
214
|
+
assert isinstance(obj, cls)
|
|
575
215
|
|
|
576
216
|
if (
|
|
577
217
|
not info.context
|
|
@@ -582,12 +222,12 @@ class BaseModel(PydanticBaseModel):
|
|
|
582
222
|
Context.RESOURCE_REPLACEMENT_REQUEST,
|
|
583
223
|
)
|
|
584
224
|
):
|
|
585
|
-
return
|
|
225
|
+
return obj
|
|
586
226
|
|
|
587
227
|
for field_name in cls.model_fields:
|
|
588
228
|
necessity = cls.get_field_annotation(field_name, Required)
|
|
589
229
|
|
|
590
|
-
if necessity == Required.true and getattr(
|
|
230
|
+
if necessity == Required.true and getattr(obj, field_name) is None:
|
|
591
231
|
raise PydanticCustomError(
|
|
592
232
|
"required_error",
|
|
593
233
|
"Field '{field_name}' is required but value is missing or null",
|
|
@@ -596,7 +236,7 @@ class BaseModel(PydanticBaseModel):
|
|
|
596
236
|
},
|
|
597
237
|
)
|
|
598
238
|
|
|
599
|
-
return
|
|
239
|
+
return obj
|
|
600
240
|
|
|
601
241
|
@model_validator(mode="wrap")
|
|
602
242
|
@classmethod
|
|
@@ -606,7 +246,8 @@ class BaseModel(PydanticBaseModel):
|
|
|
606
246
|
"""Check if 'immutable' attributes have been mutated in replacement requests."""
|
|
607
247
|
from scim2_models.rfc7643.resource import Resource
|
|
608
248
|
|
|
609
|
-
|
|
249
|
+
obj = handler(value)
|
|
250
|
+
assert isinstance(obj, cls)
|
|
610
251
|
|
|
611
252
|
context = info.context.get("scim") if info.context else None
|
|
612
253
|
original = info.context.get("original") if info.context else None
|
|
@@ -615,12 +256,16 @@ class BaseModel(PydanticBaseModel):
|
|
|
615
256
|
and issubclass(cls, Resource)
|
|
616
257
|
and original is not None
|
|
617
258
|
):
|
|
618
|
-
cls.check_mutability_issues(original,
|
|
619
|
-
return
|
|
259
|
+
cls.check_mutability_issues(original, obj)
|
|
260
|
+
return obj
|
|
620
261
|
|
|
621
262
|
@classmethod
|
|
622
|
-
def check_mutability_issues(
|
|
263
|
+
def check_mutability_issues(
|
|
264
|
+
cls, original: "BaseModel", replacement: "BaseModel"
|
|
265
|
+
) -> None:
|
|
623
266
|
"""Compare two instances, and check for differences of values on the fields marked as immutable."""
|
|
267
|
+
from .attributes import is_complex_attribute
|
|
268
|
+
|
|
624
269
|
model = replacement.__class__
|
|
625
270
|
for field_name in model.model_fields:
|
|
626
271
|
mutability = model.get_field_annotation(field_name, Mutability)
|
|
@@ -634,47 +279,51 @@ class BaseModel(PydanticBaseModel):
|
|
|
634
279
|
)
|
|
635
280
|
|
|
636
281
|
attr_type = model.get_field_root_type(field_name)
|
|
637
|
-
if
|
|
638
|
-
|
|
282
|
+
if (
|
|
283
|
+
attr_type
|
|
284
|
+
and is_complex_attribute(attr_type)
|
|
285
|
+
and not model.get_field_multiplicity(field_name)
|
|
639
286
|
):
|
|
640
287
|
original_val = getattr(original, field_name)
|
|
641
288
|
replacement_value = getattr(replacement, field_name)
|
|
642
289
|
if original_val is not None and replacement_value is not None:
|
|
643
290
|
cls.check_mutability_issues(original_val, replacement_value)
|
|
644
291
|
|
|
645
|
-
def
|
|
646
|
-
"""Navigate through attributes and sub-attributes of type ComplexAttribute, and mark them with a '
|
|
292
|
+
def set_complex_attribute_urns(self) -> None:
|
|
293
|
+
"""Navigate through attributes and sub-attributes of type ComplexAttribute, and mark them with a 'attribute_urn' attribute.
|
|
647
294
|
|
|
648
|
-
'
|
|
295
|
+
'attribute_urn' will later be used by 'get_attribute_urn'.
|
|
649
296
|
"""
|
|
650
|
-
from
|
|
297
|
+
from .attributes import ComplexAttribute
|
|
298
|
+
from .attributes import is_complex_attribute
|
|
299
|
+
|
|
300
|
+
if isinstance(self, ComplexAttribute):
|
|
301
|
+
main_schema = self.attribute_urn
|
|
302
|
+
separator = "."
|
|
303
|
+
else:
|
|
304
|
+
main_schema = self.__class__.model_fields["schemas"].default[0]
|
|
305
|
+
separator = ":"
|
|
651
306
|
|
|
652
307
|
for field_name in self.__class__.model_fields:
|
|
653
308
|
attr_type = self.get_field_root_type(field_name)
|
|
654
|
-
if not is_complex_attribute(attr_type):
|
|
309
|
+
if not attr_type or not is_complex_attribute(attr_type):
|
|
655
310
|
continue
|
|
656
311
|
|
|
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
312
|
schema = f"{main_schema}{separator}{field_name}"
|
|
664
313
|
|
|
665
314
|
if attr_value := getattr(self, field_name):
|
|
666
315
|
if isinstance(attr_value, list):
|
|
667
316
|
for item in attr_value:
|
|
668
|
-
item.
|
|
317
|
+
item.attribute_urn = schema
|
|
669
318
|
else:
|
|
670
|
-
attr_value.
|
|
319
|
+
attr_value.attribute_urn = schema
|
|
671
320
|
|
|
672
321
|
@field_serializer("*", mode="wrap")
|
|
673
322
|
def scim_serializer(
|
|
674
323
|
self,
|
|
675
324
|
value: Any,
|
|
676
325
|
handler: SerializerFunctionWrapHandler,
|
|
677
|
-
info:
|
|
326
|
+
info: FieldSerializationInfo,
|
|
678
327
|
) -> Any:
|
|
679
328
|
"""Serialize the fields according to mutability indications passed in the serialization context."""
|
|
680
329
|
value = handler(value)
|
|
@@ -688,7 +337,7 @@ class BaseModel(PydanticBaseModel):
|
|
|
688
337
|
|
|
689
338
|
return value
|
|
690
339
|
|
|
691
|
-
def scim_request_serializer(self, value: Any, info:
|
|
340
|
+
def scim_request_serializer(self, value: Any, info: FieldSerializationInfo) -> Any:
|
|
692
341
|
"""Serialize the fields according to mutability indications passed in the serialization context."""
|
|
693
342
|
mutability = self.get_field_annotation(info.field_name, Mutability)
|
|
694
343
|
scim_ctx = info.context.get("scim") if info.context else None
|
|
@@ -712,7 +361,7 @@ class BaseModel(PydanticBaseModel):
|
|
|
712
361
|
|
|
713
362
|
return value
|
|
714
363
|
|
|
715
|
-
def scim_response_serializer(self, value: Any, info:
|
|
364
|
+
def scim_response_serializer(self, value: Any, info: FieldSerializationInfo) -> Any:
|
|
716
365
|
"""Serialize the fields according to returnability indications passed in the serialization context."""
|
|
717
366
|
returnability = self.get_field_annotation(info.field_name, Returned)
|
|
718
367
|
attribute_urn = self.get_attribute_urn(info.field_name)
|
|
@@ -746,20 +395,20 @@ class BaseModel(PydanticBaseModel):
|
|
|
746
395
|
|
|
747
396
|
@model_serializer(mode="wrap")
|
|
748
397
|
def model_serializer_exclude_none(
|
|
749
|
-
self, handler, info: SerializationInfo
|
|
398
|
+
self, handler: SerializerFunctionWrapHandler, info: SerializationInfo
|
|
750
399
|
) -> dict[str, Any]:
|
|
751
400
|
"""Remove `None` values inserted by the :meth:`~scim2_models.base.BaseModel.scim_serializer`."""
|
|
752
|
-
self.
|
|
401
|
+
self.set_complex_attribute_urns()
|
|
753
402
|
result = handler(self)
|
|
754
403
|
return {key: value for key, value in result.items() if value is not None}
|
|
755
404
|
|
|
756
405
|
@classmethod
|
|
757
406
|
def model_validate(
|
|
758
407
|
cls,
|
|
759
|
-
*args,
|
|
408
|
+
*args: Any,
|
|
760
409
|
scim_ctx: Optional[Context] = Context.DEFAULT,
|
|
761
410
|
original: Optional["BaseModel"] = None,
|
|
762
|
-
**kwargs,
|
|
411
|
+
**kwargs: Any,
|
|
763
412
|
) -> Self:
|
|
764
413
|
"""Validate SCIM payloads and generate model representation by using Pydantic :code:`BaseModel.model_validate`.
|
|
765
414
|
|
|
@@ -779,126 +428,20 @@ class BaseModel(PydanticBaseModel):
|
|
|
779
428
|
|
|
780
429
|
return super().model_validate(*args, **kwargs)
|
|
781
430
|
|
|
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
431
|
def get_attribute_urn(self, field_name: str) -> str:
|
|
846
432
|
"""Build the full URN of the attribute.
|
|
847
433
|
|
|
848
434
|
See :rfc:`RFC7644 §3.10 <7644#section-3.10>`.
|
|
849
435
|
"""
|
|
436
|
+
from scim2_models.rfc7643.resource import Extension
|
|
437
|
+
|
|
850
438
|
main_schema = self.__class__.model_fields["schemas"].default[0]
|
|
851
|
-
|
|
852
|
-
|
|
439
|
+
field = self.__class__.model_fields[field_name]
|
|
440
|
+
alias = field.serialization_alias or field_name
|
|
441
|
+
field_type = self.get_field_root_type(field_name)
|
|
442
|
+
full_urn = (
|
|
443
|
+
alias
|
|
444
|
+
if isclass(field_type) and issubclass(field_type, Extension)
|
|
445
|
+
else f"{main_schema}:{alias}"
|
|
853
446
|
)
|
|
854
|
-
|
|
855
|
-
# if alias contains a ':' this is an extension urn
|
|
856
|
-
full_urn = alias if ":" in alias else f"{main_schema}:{alias}"
|
|
857
447
|
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)
|