scim2-models 0.3.7__py3-none-any.whl → 0.4.1__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 +41 -41
- scim2_models/attributes.py +2 -2
- scim2_models/base.py +140 -42
- scim2_models/context.py +1 -1
- scim2_models/{rfc7644 → messages}/bulk.py +2 -2
- scim2_models/{rfc7644 → messages}/error.py +2 -2
- scim2_models/{rfc7644 → messages}/list_response.py +3 -3
- scim2_models/{rfc7644 → messages}/message.py +16 -7
- scim2_models/messages/patch_op.py +492 -0
- scim2_models/{rfc7644 → messages}/search_request.py +51 -2
- scim2_models/{rfc7643 → resources}/resource.py +32 -26
- scim2_models/{rfc7643 → resources}/schema.py +10 -10
- scim2_models/scim_object.py +1 -78
- scim2_models/urn.py +109 -0
- scim2_models/utils.py +107 -5
- {scim2_models-0.3.7.dist-info → scim2_models-0.4.1.dist-info}/METADATA +1 -1
- scim2_models-0.4.1.dist-info/RECORD +30 -0
- scim2_models/rfc7644/patch_op.py +0 -84
- scim2_models-0.3.7.dist-info/RECORD +0 -29
- /scim2_models/{rfc7643 → messages}/__init__.py +0 -0
- /scim2_models/{rfc7644 → resources}/__init__.py +0 -0
- /scim2_models/{rfc7643 → resources}/enterprise_user.py +0 -0
- /scim2_models/{rfc7643 → resources}/group.py +0 -0
- /scim2_models/{rfc7643 → resources}/resource_type.py +0 -0
- /scim2_models/{rfc7643 → resources}/service_provider_config.py +0 -0
- /scim2_models/{rfc7643 → resources}/user.py +0 -0
- {scim2_models-0.3.7.dist-info → scim2_models-0.4.1.dist-info}/WHEEL +0 -0
- {scim2_models-0.3.7.dist-info → scim2_models-0.4.1.dist-info}/licenses/LICENSE +0 -0
scim2_models/__init__.py
CHANGED
|
@@ -7,50 +7,50 @@ from .attributes import ComplexAttribute
|
|
|
7
7
|
from .attributes import MultiValuedComplexAttribute
|
|
8
8
|
from .base import BaseModel
|
|
9
9
|
from .context import Context
|
|
10
|
+
from .messages.bulk import BulkOperation
|
|
11
|
+
from .messages.bulk import BulkRequest
|
|
12
|
+
from .messages.bulk import BulkResponse
|
|
13
|
+
from .messages.error import Error
|
|
14
|
+
from .messages.list_response import ListResponse
|
|
15
|
+
from .messages.message import Message
|
|
16
|
+
from .messages.patch_op import PatchOp
|
|
17
|
+
from .messages.patch_op import PatchOperation
|
|
18
|
+
from .messages.search_request import SearchRequest
|
|
10
19
|
from .reference import ExternalReference
|
|
11
20
|
from .reference import Reference
|
|
12
21
|
from .reference import URIReference
|
|
13
|
-
from .
|
|
14
|
-
from .
|
|
15
|
-
from .
|
|
16
|
-
from .
|
|
17
|
-
from .
|
|
18
|
-
from .
|
|
19
|
-
from .
|
|
20
|
-
from .
|
|
21
|
-
from .
|
|
22
|
-
from .
|
|
23
|
-
from .
|
|
24
|
-
from .
|
|
25
|
-
from .
|
|
26
|
-
from .
|
|
27
|
-
from .
|
|
28
|
-
from .
|
|
29
|
-
from .
|
|
30
|
-
from .
|
|
31
|
-
from .
|
|
32
|
-
from .
|
|
33
|
-
from .
|
|
34
|
-
from .
|
|
35
|
-
from .
|
|
36
|
-
from .
|
|
37
|
-
from .
|
|
38
|
-
from .
|
|
39
|
-
from .
|
|
40
|
-
from .
|
|
41
|
-
from .
|
|
42
|
-
from .
|
|
43
|
-
from .
|
|
44
|
-
from .
|
|
45
|
-
from .rfc7644.bulk import BulkOperation
|
|
46
|
-
from .rfc7644.bulk import BulkRequest
|
|
47
|
-
from .rfc7644.bulk import BulkResponse
|
|
48
|
-
from .rfc7644.error import Error
|
|
49
|
-
from .rfc7644.list_response import ListResponse
|
|
50
|
-
from .rfc7644.message import Message
|
|
51
|
-
from .rfc7644.patch_op import PatchOp
|
|
52
|
-
from .rfc7644.patch_op import PatchOperation
|
|
53
|
-
from .rfc7644.search_request import SearchRequest
|
|
22
|
+
from .resources.enterprise_user import EnterpriseUser
|
|
23
|
+
from .resources.enterprise_user import Manager
|
|
24
|
+
from .resources.group import Group
|
|
25
|
+
from .resources.group import GroupMember
|
|
26
|
+
from .resources.resource import AnyExtension
|
|
27
|
+
from .resources.resource import AnyResource
|
|
28
|
+
from .resources.resource import Extension
|
|
29
|
+
from .resources.resource import Meta
|
|
30
|
+
from .resources.resource import Resource
|
|
31
|
+
from .resources.resource_type import ResourceType
|
|
32
|
+
from .resources.resource_type import SchemaExtension
|
|
33
|
+
from .resources.schema import Attribute
|
|
34
|
+
from .resources.schema import Schema
|
|
35
|
+
from .resources.service_provider_config import AuthenticationScheme
|
|
36
|
+
from .resources.service_provider_config import Bulk
|
|
37
|
+
from .resources.service_provider_config import ChangePassword
|
|
38
|
+
from .resources.service_provider_config import ETag
|
|
39
|
+
from .resources.service_provider_config import Filter
|
|
40
|
+
from .resources.service_provider_config import Patch
|
|
41
|
+
from .resources.service_provider_config import ServiceProviderConfig
|
|
42
|
+
from .resources.service_provider_config import Sort
|
|
43
|
+
from .resources.user import Address
|
|
44
|
+
from .resources.user import Email
|
|
45
|
+
from .resources.user import Entitlement
|
|
46
|
+
from .resources.user import GroupMembership
|
|
47
|
+
from .resources.user import Im
|
|
48
|
+
from .resources.user import Name
|
|
49
|
+
from .resources.user import PhoneNumber
|
|
50
|
+
from .resources.user import Photo
|
|
51
|
+
from .resources.user import Role
|
|
52
|
+
from .resources.user import User
|
|
53
|
+
from .resources.user import X509Certificate
|
|
54
54
|
|
|
55
55
|
__all__ = [
|
|
56
56
|
"Address",
|
scim2_models/attributes.py
CHANGED
|
@@ -16,7 +16,7 @@ from .reference import Reference
|
|
|
16
16
|
class ComplexAttribute(BaseModel):
|
|
17
17
|
"""A complex attribute as defined in :rfc:`RFC7643 §2.3.8 <7643#section-2.3.8>`."""
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
_attribute_urn: Optional[str] = None
|
|
20
20
|
|
|
21
21
|
def get_attribute_urn(self, field_name: str) -> str:
|
|
22
22
|
"""Build the full URN of the attribute.
|
|
@@ -26,7 +26,7 @@ class ComplexAttribute(BaseModel):
|
|
|
26
26
|
alias = (
|
|
27
27
|
self.__class__.model_fields[field_name].serialization_alias or field_name
|
|
28
28
|
)
|
|
29
|
-
return f"{self.
|
|
29
|
+
return f"{self._attribute_urn}.{alias}"
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class MultiValuedComplexAttribute(ComplexAttribute):
|
scim2_models/base.py
CHANGED
|
@@ -23,13 +23,13 @@ from scim2_models.annotations import Mutability
|
|
|
23
23
|
from scim2_models.annotations import Required
|
|
24
24
|
from scim2_models.annotations import Returned
|
|
25
25
|
from scim2_models.context import Context
|
|
26
|
-
from scim2_models.utils import
|
|
27
|
-
from scim2_models.utils import
|
|
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
|
|
28
30
|
|
|
29
|
-
from .utils import UNION_TYPES
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
def contains_attribute_or_subattributes(
|
|
32
|
+
def _contains_attribute_or_subattributes(
|
|
33
33
|
attribute_urns: list[str], attribute_urn: str
|
|
34
34
|
) -> bool:
|
|
35
35
|
return attribute_urn in attribute_urns or any(
|
|
@@ -43,8 +43,8 @@ class BaseModel(PydanticBaseModel):
|
|
|
43
43
|
|
|
44
44
|
model_config = ConfigDict(
|
|
45
45
|
alias_generator=AliasGenerator(
|
|
46
|
-
validation_alias=
|
|
47
|
-
serialization_alias=
|
|
46
|
+
validation_alias=_normalize_attribute_name,
|
|
47
|
+
serialization_alias=_to_camel,
|
|
48
48
|
),
|
|
49
49
|
validate_assignment=True,
|
|
50
50
|
populate_by_name=True,
|
|
@@ -54,7 +54,35 @@ class BaseModel(PydanticBaseModel):
|
|
|
54
54
|
|
|
55
55
|
@classmethod
|
|
56
56
|
def get_field_annotation(cls, field_name: str, annotation_type: type) -> Any:
|
|
57
|
-
"""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
|
+
"""
|
|
58
86
|
field_metadata = cls.model_fields[field_name].metadata
|
|
59
87
|
|
|
60
88
|
default_value = getattr(annotation_type, "_default", None)
|
|
@@ -71,8 +99,34 @@ class BaseModel(PydanticBaseModel):
|
|
|
71
99
|
def get_field_root_type(cls, attribute_name: str) -> Optional[type]:
|
|
72
100
|
"""Extract the root type from a model field.
|
|
73
101
|
|
|
74
|
-
|
|
75
|
-
|
|
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'>
|
|
76
130
|
"""
|
|
77
131
|
attribute_type = cls.model_fields[attribute_name].annotation
|
|
78
132
|
|
|
@@ -89,7 +143,20 @@ class BaseModel(PydanticBaseModel):
|
|
|
89
143
|
|
|
90
144
|
@classmethod
|
|
91
145
|
def get_field_multiplicity(cls, attribute_name: str) -> bool:
|
|
92
|
-
"""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
|
+
"""
|
|
93
160
|
attribute_type = cls.model_fields[attribute_name].annotation
|
|
94
161
|
|
|
95
162
|
# extract 'x' from 'Optional[x]'
|
|
@@ -104,7 +171,7 @@ class BaseModel(PydanticBaseModel):
|
|
|
104
171
|
def check_request_attributes_mutability(
|
|
105
172
|
cls, value: Any, info: ValidationInfo
|
|
106
173
|
) -> Any:
|
|
107
|
-
"""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>`."""
|
|
108
175
|
if (
|
|
109
176
|
not info.context
|
|
110
177
|
or not info.field_name
|
|
@@ -147,20 +214,49 @@ class BaseModel(PydanticBaseModel):
|
|
|
147
214
|
) -> Self:
|
|
148
215
|
"""Normalize payload attribute names.
|
|
149
216
|
|
|
150
|
-
:rfc:`RFC7643 §2.1 <
|
|
217
|
+
:rfc:`RFC7643 §2.1 <7643#section-2.1>` indicate that attribute
|
|
151
218
|
names should be case-insensitive. Any attribute name is
|
|
152
219
|
transformed in lowercase so any case is handled the same way.
|
|
153
220
|
"""
|
|
154
221
|
|
|
155
|
-
def
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
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 = {}
|
|
162
227
|
|
|
163
|
-
|
|
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)
|
|
164
260
|
obj = handler(normalized_value)
|
|
165
261
|
assert isinstance(obj, cls)
|
|
166
262
|
return obj
|
|
@@ -170,7 +266,7 @@ class BaseModel(PydanticBaseModel):
|
|
|
170
266
|
def check_response_attributes_returnability(
|
|
171
267
|
cls, value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
|
|
172
268
|
) -> Self:
|
|
173
|
-
"""Check that the fields returnability is expected according to the responses validation context, as defined in :rfc:`RFC7643 §7 <
|
|
269
|
+
"""Check that the fields returnability is expected according to the responses validation context, as defined in :rfc:`RFC7643 §7 <7643#section-7>`."""
|
|
174
270
|
obj = handler(value)
|
|
175
271
|
assert isinstance(obj, cls)
|
|
176
272
|
|
|
@@ -244,7 +340,7 @@ class BaseModel(PydanticBaseModel):
|
|
|
244
340
|
cls, value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
|
|
245
341
|
) -> Self:
|
|
246
342
|
"""Check if 'immutable' attributes have been mutated in replacement requests."""
|
|
247
|
-
from scim2_models.
|
|
343
|
+
from scim2_models.resources.resource import Resource
|
|
248
344
|
|
|
249
345
|
obj = handler(value)
|
|
250
346
|
assert isinstance(obj, cls)
|
|
@@ -256,11 +352,11 @@ class BaseModel(PydanticBaseModel):
|
|
|
256
352
|
and issubclass(cls, Resource)
|
|
257
353
|
and original is not None
|
|
258
354
|
):
|
|
259
|
-
cls.
|
|
355
|
+
cls._check_mutability_issues(original, obj)
|
|
260
356
|
return obj
|
|
261
357
|
|
|
262
358
|
@classmethod
|
|
263
|
-
def
|
|
359
|
+
def _check_mutability_issues(
|
|
264
360
|
cls, original: "BaseModel", replacement: "BaseModel"
|
|
265
361
|
) -> None:
|
|
266
362
|
"""Compare two instances, and check for differences of values on the fields marked as immutable."""
|
|
@@ -287,18 +383,18 @@ class BaseModel(PydanticBaseModel):
|
|
|
287
383
|
original_val = getattr(original, field_name)
|
|
288
384
|
replacement_value = getattr(replacement, field_name)
|
|
289
385
|
if original_val is not None and replacement_value is not None:
|
|
290
|
-
cls.
|
|
386
|
+
cls._check_mutability_issues(original_val, replacement_value)
|
|
291
387
|
|
|
292
|
-
def
|
|
293
|
-
"""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.
|
|
294
390
|
|
|
295
|
-
'
|
|
391
|
+
'_attribute_urn' will later be used by 'get_attribute_urn'.
|
|
296
392
|
"""
|
|
297
393
|
from .attributes import ComplexAttribute
|
|
298
394
|
from .attributes import is_complex_attribute
|
|
299
395
|
|
|
300
396
|
if isinstance(self, ComplexAttribute):
|
|
301
|
-
main_schema = self.
|
|
397
|
+
main_schema = self._attribute_urn
|
|
302
398
|
separator = "."
|
|
303
399
|
else:
|
|
304
400
|
main_schema = self.__class__.model_fields["schemas"].default[0]
|
|
@@ -314,9 +410,9 @@ class BaseModel(PydanticBaseModel):
|
|
|
314
410
|
if attr_value := getattr(self, field_name):
|
|
315
411
|
if isinstance(attr_value, list):
|
|
316
412
|
for item in attr_value:
|
|
317
|
-
item.
|
|
413
|
+
item._attribute_urn = schema
|
|
318
414
|
else:
|
|
319
|
-
attr_value.
|
|
415
|
+
attr_value._attribute_urn = schema
|
|
320
416
|
|
|
321
417
|
@field_serializer("*", mode="wrap")
|
|
322
418
|
def scim_serializer(
|
|
@@ -330,14 +426,14 @@ class BaseModel(PydanticBaseModel):
|
|
|
330
426
|
scim_ctx = info.context.get("scim") if info.context else None
|
|
331
427
|
|
|
332
428
|
if scim_ctx and Context.is_request(scim_ctx):
|
|
333
|
-
value = self.
|
|
429
|
+
value = self._scim_request_serializer(value, info)
|
|
334
430
|
|
|
335
431
|
if scim_ctx and Context.is_response(scim_ctx):
|
|
336
|
-
value = self.
|
|
432
|
+
value = self._scim_response_serializer(value, info)
|
|
337
433
|
|
|
338
434
|
return value
|
|
339
435
|
|
|
340
|
-
def
|
|
436
|
+
def _scim_request_serializer(self, value: Any, info: FieldSerializationInfo) -> Any:
|
|
341
437
|
"""Serialize the fields according to mutability indications passed in the serialization context."""
|
|
342
438
|
mutability = self.get_field_annotation(info.field_name, Mutability)
|
|
343
439
|
scim_ctx = info.context.get("scim") if info.context else None
|
|
@@ -361,7 +457,9 @@ class BaseModel(PydanticBaseModel):
|
|
|
361
457
|
|
|
362
458
|
return value
|
|
363
459
|
|
|
364
|
-
def
|
|
460
|
+
def _scim_response_serializer(
|
|
461
|
+
self, value: Any, info: FieldSerializationInfo
|
|
462
|
+
) -> Any:
|
|
365
463
|
"""Serialize the fields according to returnability indications passed in the serialization context."""
|
|
366
464
|
returnability = self.get_field_annotation(info.field_name, Returned)
|
|
367
465
|
attribute_urn = self.get_attribute_urn(info.field_name)
|
|
@@ -370,9 +468,9 @@ class BaseModel(PydanticBaseModel):
|
|
|
370
468
|
info.context.get("scim_excluded_attributes", []) if info.context else []
|
|
371
469
|
)
|
|
372
470
|
|
|
373
|
-
attribute_urn =
|
|
374
|
-
included_urns = [
|
|
375
|
-
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]
|
|
376
474
|
|
|
377
475
|
if returnability == Returned.never:
|
|
378
476
|
return None
|
|
@@ -380,7 +478,7 @@ class BaseModel(PydanticBaseModel):
|
|
|
380
478
|
if returnability == Returned.default and (
|
|
381
479
|
(
|
|
382
480
|
included_urns
|
|
383
|
-
and not
|
|
481
|
+
and not _contains_attribute_or_subattributes(
|
|
384
482
|
included_urns, attribute_urn
|
|
385
483
|
)
|
|
386
484
|
)
|
|
@@ -398,7 +496,7 @@ class BaseModel(PydanticBaseModel):
|
|
|
398
496
|
self, handler: SerializerFunctionWrapHandler, info: SerializationInfo
|
|
399
497
|
) -> dict[str, Any]:
|
|
400
498
|
"""Remove `None` values inserted by the :meth:`~scim2_models.base.BaseModel.scim_serializer`."""
|
|
401
|
-
self.
|
|
499
|
+
self._set_complex_attribute_urns()
|
|
402
500
|
result = handler(self)
|
|
403
501
|
return {key: value for key, value in result.items() if value is not None}
|
|
404
502
|
|
|
@@ -433,7 +531,7 @@ class BaseModel(PydanticBaseModel):
|
|
|
433
531
|
|
|
434
532
|
See :rfc:`RFC7644 §3.10 <7644#section-3.10>`.
|
|
435
533
|
"""
|
|
436
|
-
from scim2_models.
|
|
534
|
+
from scim2_models.resources.resource import Extension
|
|
437
535
|
|
|
438
536
|
main_schema = self.__class__.model_fields["schemas"].default[0]
|
|
439
537
|
field = self.__class__.model_fields[field_name]
|
scim2_models/context.py
CHANGED
|
@@ -78,7 +78,7 @@ class Context(Enum):
|
|
|
78
78
|
|
|
79
79
|
- When used for serialization, it will not dump attributes annotated with :attr:`~scim2_models.Mutability.read_only`.
|
|
80
80
|
- When used for validation, it will ignore attributes annotated with :attr:`scim2_models.Mutability.read_only` and raise a :class:`~pydantic.ValidationError`:
|
|
81
|
-
- when finding attributes annotated with :attr:`~scim2_models.Mutability.immutable` different than :
|
|
81
|
+
- when finding attributes annotated with :attr:`~scim2_models.Mutability.immutable` different than the ``original`` parameter passed to :meth:`~scim2_models.BaseModel.model_validate`;
|
|
82
82
|
- when attributes annotated with :attr:`Required.true <scim2_models.Required.true>` are missing on null.
|
|
83
83
|
"""
|
|
84
84
|
|
|
@@ -8,7 +8,7 @@ from pydantic import PlainSerializer
|
|
|
8
8
|
|
|
9
9
|
from ..annotations import Required
|
|
10
10
|
from ..attributes import ComplexAttribute
|
|
11
|
-
from ..utils import
|
|
11
|
+
from ..utils import _int_to_str
|
|
12
12
|
from .message import Message
|
|
13
13
|
|
|
14
14
|
|
|
@@ -42,7 +42,7 @@ class BulkOperation(ComplexAttribute):
|
|
|
42
42
|
response: Optional[Any] = None
|
|
43
43
|
"""The HTTP response body for the specified request operation."""
|
|
44
44
|
|
|
45
|
-
status: Annotated[Optional[int], PlainSerializer(
|
|
45
|
+
status: Annotated[Optional[int], PlainSerializer(_int_to_str)] = None
|
|
46
46
|
"""The HTTP response status code for the requested operation."""
|
|
47
47
|
|
|
48
48
|
|
|
@@ -4,7 +4,7 @@ from typing import Optional
|
|
|
4
4
|
from pydantic import PlainSerializer
|
|
5
5
|
|
|
6
6
|
from ..annotations import Required
|
|
7
|
-
from ..utils import
|
|
7
|
+
from ..utils import _int_to_str
|
|
8
8
|
from .message import Message
|
|
9
9
|
|
|
10
10
|
|
|
@@ -15,7 +15,7 @@ class Error(Message):
|
|
|
15
15
|
"urn:ietf:params:scim:api:messages:2.0:Error"
|
|
16
16
|
]
|
|
17
17
|
|
|
18
|
-
status: Annotated[Optional[int], PlainSerializer(
|
|
18
|
+
status: Annotated[Optional[int], PlainSerializer(_int_to_str)] = None
|
|
19
19
|
"""The HTTP status code (see Section 6 of [RFC7231]) expressed as a JSON
|
|
20
20
|
string."""
|
|
21
21
|
|
|
@@ -12,12 +12,12 @@ from typing_extensions import Self
|
|
|
12
12
|
|
|
13
13
|
from ..annotations import Required
|
|
14
14
|
from ..context import Context
|
|
15
|
-
from ..
|
|
16
|
-
from .message import GenericMessageMetaclass
|
|
15
|
+
from ..resources.resource import AnyResource
|
|
17
16
|
from .message import Message
|
|
17
|
+
from .message import _GenericMessageMetaclass
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
class ListResponse(Message, Generic[AnyResource], metaclass=
|
|
20
|
+
class ListResponse(Message, Generic[AnyResource], metaclass=_GenericMessageMetaclass):
|
|
21
21
|
schemas: Annotated[list[str], Required.true] = [
|
|
22
22
|
"urn:ietf:params:scim:api:messages:2.0:ListResponse"
|
|
23
23
|
]
|
|
@@ -10,6 +10,8 @@ from pydantic import Discriminator
|
|
|
10
10
|
from pydantic import Tag
|
|
11
11
|
from pydantic._internal._model_construction import ModelMetaclass
|
|
12
12
|
|
|
13
|
+
from scim2_models.resources.resource import Resource
|
|
14
|
+
|
|
13
15
|
from ..base import BaseModel
|
|
14
16
|
from ..scim_object import ScimObject
|
|
15
17
|
from ..utils import UNION_TYPES
|
|
@@ -19,7 +21,7 @@ class Message(ScimObject):
|
|
|
19
21
|
"""SCIM protocol messages as defined by :rfc:`RFC7644 §3.1 <7644#section-3.1>`."""
|
|
20
22
|
|
|
21
23
|
|
|
22
|
-
def
|
|
24
|
+
def _create_schema_discriminator(
|
|
23
25
|
resource_types_schemas: list[str],
|
|
24
26
|
) -> Callable[[Any], Optional[str]]:
|
|
25
27
|
"""Create a schema discriminator function for the given resource schemas.
|
|
@@ -49,7 +51,7 @@ def create_schema_discriminator(
|
|
|
49
51
|
return get_schema_from_payload
|
|
50
52
|
|
|
51
53
|
|
|
52
|
-
def
|
|
54
|
+
def _get_tag(resource_type: type[BaseModel]) -> Tag:
|
|
53
55
|
"""Create Pydantic tag from resource type schema.
|
|
54
56
|
|
|
55
57
|
:param resource_type: SCIM resource type
|
|
@@ -58,7 +60,7 @@ def get_tag(resource_type: type[BaseModel]) -> Tag:
|
|
|
58
60
|
return Tag(resource_type.model_fields["schemas"].default[0])
|
|
59
61
|
|
|
60
62
|
|
|
61
|
-
def
|
|
63
|
+
def _create_tagged_resource_union(resource_union: Any) -> Any:
|
|
62
64
|
"""Build Discriminated Unions for SCIM resources.
|
|
63
65
|
|
|
64
66
|
Creates discriminated unions so Pydantic can determine which class to instantiate
|
|
@@ -79,11 +81,11 @@ def create_tagged_resource_union(resource_union: Any) -> Any:
|
|
|
79
81
|
]
|
|
80
82
|
|
|
81
83
|
# Create discriminator function with schemas captured in closure
|
|
82
|
-
schema_discriminator =
|
|
84
|
+
schema_discriminator = _create_schema_discriminator(resource_types_schemas)
|
|
83
85
|
discriminator = Discriminator(schema_discriminator)
|
|
84
86
|
|
|
85
87
|
tagged_resources = [
|
|
86
|
-
Annotated[resource_type,
|
|
88
|
+
Annotated[resource_type, _get_tag(resource_type)]
|
|
87
89
|
for resource_type in resource_types
|
|
88
90
|
]
|
|
89
91
|
# Dynamic union construction from tuple - MyPy can't validate this at compile time
|
|
@@ -91,7 +93,7 @@ def create_tagged_resource_union(resource_union: Any) -> Any:
|
|
|
91
93
|
return Annotated[union, discriminator]
|
|
92
94
|
|
|
93
95
|
|
|
94
|
-
class
|
|
96
|
+
class _GenericMessageMetaclass(ModelMetaclass):
|
|
95
97
|
"""Metaclass for SCIM generic types with discriminated unions."""
|
|
96
98
|
|
|
97
99
|
def __new__(
|
|
@@ -101,10 +103,17 @@ class GenericMessageMetaclass(ModelMetaclass):
|
|
|
101
103
|
if kwargs.get("__pydantic_generic_metadata__") and kwargs[
|
|
102
104
|
"__pydantic_generic_metadata__"
|
|
103
105
|
].get("args"):
|
|
104
|
-
tagged_union =
|
|
106
|
+
tagged_union = _create_tagged_resource_union(
|
|
105
107
|
kwargs["__pydantic_generic_metadata__"]["args"][0]
|
|
106
108
|
)
|
|
107
109
|
kwargs["__pydantic_generic_metadata__"]["args"] = (tagged_union,)
|
|
108
110
|
|
|
109
111
|
klass = super().__new__(cls, name, bases, attrs, **kwargs)
|
|
110
112
|
return klass
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _get_resource_class(obj) -> Optional[type[Resource]]:
|
|
116
|
+
"""Extract the resource class from generic type parameter."""
|
|
117
|
+
metadata = getattr(obj.__class__, "__pydantic_generic_metadata__", {"args": [None]})
|
|
118
|
+
resource_class = metadata["args"][0]
|
|
119
|
+
return resource_class
|