scim2-models 0.5.2__py3-none-any.whl → 0.6.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 +32 -0
- scim2_models/attributes.py +5 -1
- scim2_models/base.py +52 -8
- scim2_models/exceptions.py +265 -0
- scim2_models/messages/bulk.py +3 -7
- scim2_models/messages/error.py +184 -15
- scim2_models/messages/list_response.py +2 -5
- scim2_models/messages/message.py +4 -4
- scim2_models/messages/patch_op.py +127 -261
- scim2_models/messages/search_request.py +9 -57
- scim2_models/path.py +731 -0
- scim2_models/reference.py +139 -48
- scim2_models/resources/enterprise_user.py +10 -8
- scim2_models/resources/group.py +9 -7
- scim2_models/resources/resource.py +110 -23
- scim2_models/resources/resource_type.py +15 -14
- scim2_models/resources/schema.py +15 -20
- scim2_models/resources/service_provider_config.py +10 -13
- scim2_models/resources/user.py +13 -10
- scim2_models/scim_object.py +84 -1
- scim2_models/utils.py +0 -140
- {scim2_models-0.5.2.dist-info → scim2_models-0.6.0.dist-info}/METADATA +2 -2
- scim2_models-0.6.0.dist-info/RECORD +30 -0
- scim2_models/urn.py +0 -126
- scim2_models-0.5.2.dist-info/RECORD +0 -29
- {scim2_models-0.5.2.dist-info → scim2_models-0.6.0.dist-info}/WHEEL +0 -0
scim2_models/reference.py
CHANGED
|
@@ -1,80 +1,171 @@
|
|
|
1
|
-
|
|
1
|
+
import warnings
|
|
2
2
|
from typing import Any
|
|
3
3
|
from typing import Generic
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Literal
|
|
5
5
|
from typing import TypeVar
|
|
6
6
|
from typing import get_args
|
|
7
7
|
from typing import get_origin
|
|
8
8
|
|
|
9
9
|
from pydantic import GetCoreSchemaHandler
|
|
10
|
+
from pydantic_core import Url
|
|
11
|
+
from pydantic_core import ValidationError
|
|
10
12
|
from pydantic_core import core_schema
|
|
11
13
|
|
|
12
14
|
from .utils import UNION_TYPES
|
|
13
15
|
|
|
14
16
|
ReferenceTypes = TypeVar("ReferenceTypes")
|
|
15
17
|
|
|
16
|
-
URIReference = NewType("URIReference", str)
|
|
17
|
-
ExternalReference = NewType("ExternalReference", str)
|
|
18
18
|
|
|
19
|
+
class External:
|
|
20
|
+
"""Marker for external references per :rfc:`RFC7643 §7 <7643#section-7>`.
|
|
19
21
|
|
|
20
|
-
class
|
|
22
|
+
Use with :class:`Reference` to type external resource URLs (photos, websites)::
|
|
23
|
+
|
|
24
|
+
profile_url: Reference[External] | None = None
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class URI:
|
|
29
|
+
"""Marker for URI references per :rfc:`RFC7643 §7 <7643#section-7>`.
|
|
30
|
+
|
|
31
|
+
Use with :class:`Reference` to type URI identifiers (schema URNs, endpoints)::
|
|
32
|
+
|
|
33
|
+
endpoint: Reference[URI] | None = None
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ExternalReference:
|
|
38
|
+
"""Deprecated. Use :class:`External` instead."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class URIReference:
|
|
42
|
+
"""Deprecated. Use :class:`URI` instead."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Reference(str, Generic[ReferenceTypes]):
|
|
21
46
|
"""Reference type as defined in :rfc:`RFC7643 §2.3.7 <7643#section-2.3.7>`.
|
|
22
47
|
|
|
23
48
|
References can take different type parameters:
|
|
24
49
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
Examples
|
|
30
|
-
--------
|
|
50
|
+
- :class:`~scim2_models.External` for external resources (photos, websites)
|
|
51
|
+
- :class:`~scim2_models.URI` for URI identifiers (schema URNs, endpoints)
|
|
52
|
+
- String forward references for SCIM resource types (``"User"``, ``"Group"``)
|
|
53
|
+
- Resource classes directly if imports allow
|
|
31
54
|
|
|
32
|
-
|
|
55
|
+
Examples::
|
|
33
56
|
|
|
34
57
|
class Foobar(Resource):
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
58
|
+
photo: Reference[External] | None = None
|
|
59
|
+
website: Reference[URI] | None = None
|
|
60
|
+
manager: Reference["User"] | None = None
|
|
61
|
+
members: Reference[Union["User", "Group"]] | None = None
|
|
62
|
+
|
|
63
|
+
.. versionchanged:: 0.6.0
|
|
64
|
+
|
|
65
|
+
- ``Reference[ExternalReference]`` becomes ``Reference[External]``
|
|
66
|
+
- ``Reference[URIReference]`` becomes ``Reference[URI]``
|
|
67
|
+
- ``Reference[Literal["User"]]`` becomes ``Reference["User"]``
|
|
68
|
+
- ``Reference[Literal["User"] | Literal["Group"]]`` becomes
|
|
69
|
+
``Reference[Union["User", "Group"]]``
|
|
40
70
|
"""
|
|
41
71
|
|
|
72
|
+
__slots__ = ()
|
|
73
|
+
__reference_types__: tuple[str, ...] = ()
|
|
74
|
+
_cache: dict[tuple[str, ...], type["Reference[Any]"]] = {}
|
|
75
|
+
|
|
76
|
+
def __class_getitem__(cls, item: Any) -> type["Reference[Any]"]:
|
|
77
|
+
if get_origin(item) in UNION_TYPES:
|
|
78
|
+
items = get_args(item)
|
|
79
|
+
else:
|
|
80
|
+
items = (item,)
|
|
81
|
+
|
|
82
|
+
type_strings = tuple(_to_type_string(i) for i in items)
|
|
83
|
+
|
|
84
|
+
if type_strings in cls._cache:
|
|
85
|
+
return cls._cache[type_strings]
|
|
86
|
+
|
|
87
|
+
class TypedReference(cls): # type: ignore[valid-type,misc]
|
|
88
|
+
__reference_types__ = type_strings
|
|
89
|
+
|
|
90
|
+
TypedReference.__name__ = f"Reference[{' | '.join(type_strings)}]"
|
|
91
|
+
TypedReference.__qualname__ = TypedReference.__name__
|
|
92
|
+
cls._cache[type_strings] = TypedReference
|
|
93
|
+
return TypedReference
|
|
94
|
+
|
|
42
95
|
@classmethod
|
|
43
96
|
def __get_pydantic_core_schema__(
|
|
44
97
|
cls,
|
|
45
|
-
|
|
46
|
-
|
|
98
|
+
source_type: type[Any],
|
|
99
|
+
handler: GetCoreSchemaHandler,
|
|
47
100
|
) -> core_schema.CoreSchema:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
101
|
+
ref_types = getattr(source_type, "__reference_types__", ())
|
|
102
|
+
|
|
103
|
+
def validate(value: Any) -> "Reference[Any]":
|
|
104
|
+
if not isinstance(value, str):
|
|
105
|
+
raise ValueError(f"Expected string, got {type(value).__name__}")
|
|
106
|
+
if "external" in ref_types or "uri" in ref_types:
|
|
107
|
+
_validate_uri(value)
|
|
108
|
+
return source_type(value) # type: ignore[no-any-return]
|
|
109
|
+
|
|
110
|
+
return core_schema.no_info_plain_validator_function(
|
|
111
|
+
validate,
|
|
112
|
+
serialization=core_schema.plain_serializer_function_ser_schema(str),
|
|
53
113
|
)
|
|
54
114
|
|
|
55
115
|
@classmethod
|
|
56
|
-
def
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
116
|
+
def get_scim_reference_types(cls) -> list[str]:
|
|
117
|
+
"""Return referenceTypes for SCIM schema generation."""
|
|
118
|
+
return list(cls.__reference_types__)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _to_type_string(item: Any) -> str:
|
|
122
|
+
"""Convert any type parameter to its SCIM referenceType string."""
|
|
123
|
+
if item is Any:
|
|
124
|
+
return "uri"
|
|
125
|
+
if item is External:
|
|
126
|
+
return "external"
|
|
127
|
+
if item is ExternalReference:
|
|
128
|
+
warnings.warn(
|
|
129
|
+
"Reference[ExternalReference] is deprecated, "
|
|
130
|
+
"use Reference[External] instead. Will be removed in 0.7.0.",
|
|
131
|
+
DeprecationWarning,
|
|
132
|
+
stacklevel=4,
|
|
69
133
|
)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return
|
|
134
|
+
return "external"
|
|
135
|
+
if item is URI:
|
|
136
|
+
return "uri"
|
|
137
|
+
if item is URIReference:
|
|
138
|
+
warnings.warn(
|
|
139
|
+
"Reference[URIReference] is deprecated, "
|
|
140
|
+
"use Reference[URI] instead. Will be removed in 0.7.0.",
|
|
141
|
+
DeprecationWarning,
|
|
142
|
+
stacklevel=4,
|
|
143
|
+
)
|
|
144
|
+
return "uri"
|
|
145
|
+
if isinstance(item, str):
|
|
146
|
+
return item
|
|
147
|
+
if isinstance(item, type):
|
|
148
|
+
return item.__name__
|
|
149
|
+
if hasattr(item, "__forward_arg__"):
|
|
150
|
+
return item.__forward_arg__ # type: ignore[no-any-return]
|
|
151
|
+
# Support Literal["User"] for backwards compatibility
|
|
152
|
+
if get_origin(item) is Literal:
|
|
153
|
+
value = get_args(item)[0]
|
|
154
|
+
warnings.warn(
|
|
155
|
+
f'Reference[Literal["{value}"]] is deprecated, '
|
|
156
|
+
f'use Reference["{value}"] instead. Will be removed in 0.7.0.',
|
|
157
|
+
DeprecationWarning,
|
|
158
|
+
stacklevel=4,
|
|
159
|
+
)
|
|
160
|
+
return value # type: ignore[no-any-return]
|
|
161
|
+
raise TypeError(f"Invalid reference type: {item!r}")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _validate_uri(value: str) -> None:
|
|
165
|
+
"""Validate URI format, allowing relative URIs per RFC 7643."""
|
|
166
|
+
if value.startswith("/"):
|
|
167
|
+
return
|
|
168
|
+
try:
|
|
169
|
+
Url(value)
|
|
170
|
+
except ValidationError as e:
|
|
171
|
+
raise ValueError(f"Invalid URI: {value}") from e
|
|
@@ -1,23 +1,27 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
1
2
|
from typing import Annotated
|
|
2
|
-
from typing import Literal
|
|
3
3
|
|
|
4
4
|
from pydantic import Field
|
|
5
5
|
|
|
6
6
|
from ..annotations import Mutability
|
|
7
7
|
from ..annotations import Required
|
|
8
8
|
from ..attributes import ComplexAttribute
|
|
9
|
+
from ..path import URN
|
|
9
10
|
from ..reference import Reference
|
|
10
11
|
from .resource import Extension
|
|
11
12
|
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .user import User
|
|
15
|
+
|
|
12
16
|
|
|
13
17
|
class Manager(ComplexAttribute):
|
|
14
18
|
value: Annotated[str | None, Required.true] = None
|
|
15
19
|
"""The id of the SCIM resource representing the User's manager."""
|
|
16
20
|
|
|
17
|
-
ref: Annotated[
|
|
18
|
-
None,
|
|
19
|
-
|
|
20
|
-
)
|
|
21
|
+
ref: Annotated[ # type: ignore[type-arg]
|
|
22
|
+
Reference["User"] | None,
|
|
23
|
+
Required.true,
|
|
24
|
+
] = Field(None, serialization_alias="$ref")
|
|
21
25
|
"""The URI of the SCIM resource representing the User's manager."""
|
|
22
26
|
|
|
23
27
|
display_name: Annotated[str | None, Mutability.read_only] = None
|
|
@@ -25,9 +29,7 @@ class Manager(ComplexAttribute):
|
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
class EnterpriseUser(Extension):
|
|
28
|
-
schemas:
|
|
29
|
-
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
|
|
30
|
-
]
|
|
32
|
+
__schema__ = URN("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User")
|
|
31
33
|
|
|
32
34
|
employee_number: str | None = None
|
|
33
35
|
"""Numeric or alphanumeric identifier assigned to a person, typically based
|
scim2_models/resources/group.py
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
1
2
|
from typing import Annotated
|
|
2
3
|
from typing import Any
|
|
3
4
|
from typing import ClassVar
|
|
4
|
-
from typing import
|
|
5
|
+
from typing import Union
|
|
5
6
|
|
|
6
7
|
from pydantic import Field
|
|
7
8
|
|
|
8
9
|
from ..annotations import Mutability
|
|
9
|
-
from ..annotations import Required
|
|
10
10
|
from ..attributes import ComplexAttribute
|
|
11
|
+
from ..path import URN
|
|
11
12
|
from ..reference import Reference
|
|
12
13
|
from .resource import Resource
|
|
13
14
|
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from .user import User
|
|
17
|
+
|
|
14
18
|
|
|
15
19
|
class GroupMember(ComplexAttribute):
|
|
16
20
|
value: Annotated[str | None, Mutability.immutable] = None
|
|
17
21
|
"""Identifier of the member of this Group."""
|
|
18
22
|
|
|
19
|
-
ref: Annotated[
|
|
20
|
-
Reference[
|
|
23
|
+
ref: Annotated[ # type: ignore[type-arg]
|
|
24
|
+
Reference[Union["User", "Group"]] | None,
|
|
21
25
|
Mutability.immutable,
|
|
22
26
|
] = Field(None, serialization_alias="$ref")
|
|
23
27
|
"""The reference URI of a target resource, if the attribute is a
|
|
@@ -32,9 +36,7 @@ class GroupMember(ComplexAttribute):
|
|
|
32
36
|
|
|
33
37
|
|
|
34
38
|
class Group(Resource[Any]):
|
|
35
|
-
schemas:
|
|
36
|
-
"urn:ietf:params:scim:schemas:core:2.0:Group"
|
|
37
|
-
]
|
|
39
|
+
__schema__ = URN("urn:ietf:params:scim:schemas:core:2.0:Group")
|
|
38
40
|
|
|
39
41
|
display_name: str | None = None
|
|
40
42
|
"""A human-readable name for the Group."""
|
|
@@ -5,15 +5,19 @@ from typing import Any
|
|
|
5
5
|
from typing import Generic
|
|
6
6
|
from typing import TypeVar
|
|
7
7
|
from typing import Union
|
|
8
|
-
from typing import cast
|
|
9
8
|
from typing import get_args
|
|
10
9
|
from typing import get_origin
|
|
11
10
|
|
|
12
11
|
from pydantic import Field
|
|
13
12
|
from pydantic import SerializationInfo
|
|
14
13
|
from pydantic import SerializerFunctionWrapHandler
|
|
14
|
+
from pydantic import ValidationInfo
|
|
15
|
+
from pydantic import ValidatorFunctionWrapHandler
|
|
15
16
|
from pydantic import WrapSerializer
|
|
16
17
|
from pydantic import field_serializer
|
|
18
|
+
from pydantic import model_validator
|
|
19
|
+
from pydantic_core import PydanticCustomError
|
|
20
|
+
from typing_extensions import Self
|
|
17
21
|
|
|
18
22
|
from ..annotations import CaseExact
|
|
19
23
|
from ..annotations import Mutability
|
|
@@ -24,9 +28,9 @@ from ..attributes import ComplexAttribute
|
|
|
24
28
|
from ..attributes import is_complex_attribute
|
|
25
29
|
from ..base import BaseModel
|
|
26
30
|
from ..context import Context
|
|
27
|
-
from ..
|
|
31
|
+
from ..exceptions import InvalidPathException
|
|
32
|
+
from ..path import Path
|
|
28
33
|
from ..scim_object import ScimObject
|
|
29
|
-
from ..urn import _validate_attribute_urn
|
|
30
34
|
from ..utils import UNION_TYPES
|
|
31
35
|
from ..utils import _normalize_attribute_name
|
|
32
36
|
|
|
@@ -178,7 +182,7 @@ class Resource(ScimObject, Generic[AnyExtension]):
|
|
|
178
182
|
class_attrs = {"__scim_extension_metadata__": valid_extensions}
|
|
179
183
|
|
|
180
184
|
for extension in valid_extensions:
|
|
181
|
-
schema = extension.
|
|
185
|
+
schema = extension.__schema__
|
|
182
186
|
class_attrs[extension.__name__] = Field(
|
|
183
187
|
default=None, # type: ignore[arg-type]
|
|
184
188
|
serialization_alias=schema,
|
|
@@ -206,24 +210,79 @@ class Resource(ScimObject, Generic[AnyExtension]):
|
|
|
206
210
|
|
|
207
211
|
return new_class
|
|
208
212
|
|
|
209
|
-
def __getitem__(self, item: Any) ->
|
|
210
|
-
|
|
211
|
-
raise KeyError(f"{item} is not a valid extension type")
|
|
213
|
+
def __getitem__(self, item: Any) -> Any:
|
|
214
|
+
"""Get a value by extension type or path.
|
|
212
215
|
|
|
213
|
-
|
|
216
|
+
:param item: An Extension subclass or a path (string or Path).
|
|
217
|
+
:returns: The extension instance or the value at the path.
|
|
218
|
+
:raises KeyError: If the path references a non-existent field.
|
|
214
219
|
|
|
215
|
-
|
|
216
|
-
if not isinstance(item, type) or not issubclass(item, Extension):
|
|
217
|
-
raise KeyError(f"{item} is not a valid extension type")
|
|
220
|
+
Examples::
|
|
218
221
|
|
|
219
|
-
|
|
222
|
+
user[EnterpriseUser] # Get extension
|
|
223
|
+
user["userName"] # Get attribute
|
|
224
|
+
user["name.familyName"] # Get nested attribute
|
|
225
|
+
"""
|
|
226
|
+
if isinstance(item, type) and issubclass(item, Extension):
|
|
227
|
+
item = item.__schema__
|
|
228
|
+
|
|
229
|
+
bound_path = Path.__class_getitem__(type(self))
|
|
230
|
+
path = item if isinstance(item, Path) else bound_path(str(item))
|
|
231
|
+
try:
|
|
232
|
+
return path.get(self)
|
|
233
|
+
except InvalidPathException as exc:
|
|
234
|
+
raise KeyError(str(item)) from exc
|
|
235
|
+
|
|
236
|
+
def __setitem__(self, item: Any, value: Any) -> None:
|
|
237
|
+
"""Set a value by extension type or path.
|
|
238
|
+
|
|
239
|
+
:param item: An Extension subclass or a path (string or Path).
|
|
240
|
+
:param value: The value to set.
|
|
241
|
+
:raises KeyError: If the path references a non-existent field.
|
|
242
|
+
|
|
243
|
+
Examples::
|
|
244
|
+
|
|
245
|
+
user[EnterpriseUser] = EnterpriseUser(employee_number="123")
|
|
246
|
+
user["displayName"] = "John Doe"
|
|
247
|
+
user["name.familyName"] = "Doe"
|
|
248
|
+
"""
|
|
249
|
+
if isinstance(item, type) and issubclass(item, Extension):
|
|
250
|
+
item = item.__schema__
|
|
251
|
+
|
|
252
|
+
bound_path = Path.__class_getitem__(type(self))
|
|
253
|
+
path = item if isinstance(item, Path) else bound_path(str(item))
|
|
254
|
+
try:
|
|
255
|
+
path.set(self, value)
|
|
256
|
+
except InvalidPathException as exc:
|
|
257
|
+
raise KeyError(str(item)) from exc
|
|
258
|
+
|
|
259
|
+
def __delitem__(self, item: Any) -> None:
|
|
260
|
+
"""Delete a value by extension type or path.
|
|
261
|
+
|
|
262
|
+
:param item: An Extension subclass or a path (string or Path).
|
|
263
|
+
:raises KeyError: If the path references a non-existent field.
|
|
264
|
+
|
|
265
|
+
Examples::
|
|
266
|
+
|
|
267
|
+
del user[EnterpriseUser] # Remove extension
|
|
268
|
+
del user["displayName"] # Remove attribute
|
|
269
|
+
"""
|
|
270
|
+
if isinstance(item, type) and issubclass(item, Extension):
|
|
271
|
+
item = item.__schema__
|
|
272
|
+
|
|
273
|
+
bound_path = Path.__class_getitem__(type(self))
|
|
274
|
+
path = item if isinstance(item, Path) else bound_path(str(item))
|
|
275
|
+
try:
|
|
276
|
+
path.delete(self)
|
|
277
|
+
except InvalidPathException as exc:
|
|
278
|
+
raise KeyError(str(item)) from exc
|
|
220
279
|
|
|
221
280
|
@classmethod
|
|
222
281
|
def get_extension_models(cls) -> dict[str, type[Extension]]:
|
|
223
282
|
"""Return extension a dict associating extension models with their schemas."""
|
|
224
283
|
extension_models = getattr(cls, "__scim_extension_metadata__", [])
|
|
225
|
-
by_schema = {
|
|
226
|
-
ext.
|
|
284
|
+
by_schema: dict[str, type[Extension]] = {
|
|
285
|
+
ext.__schema__: ext for ext in extension_models
|
|
227
286
|
}
|
|
228
287
|
return by_schema
|
|
229
288
|
|
|
@@ -245,7 +304,7 @@ class Resource(ScimObject, Generic[AnyExtension]):
|
|
|
245
304
|
) -> type["Resource[Any]"] | type["Extension"] | None:
|
|
246
305
|
"""Given a resource type list and a schema, find the matching resource type."""
|
|
247
306
|
by_schema: dict[str, type[Resource[Any]] | type[Extension]] = {
|
|
248
|
-
resource_type
|
|
307
|
+
getattr(resource_type, "__schema__", "").lower(): resource_type
|
|
249
308
|
for resource_type in (resource_types or [])
|
|
250
309
|
}
|
|
251
310
|
if with_extensions:
|
|
@@ -283,6 +342,35 @@ class Resource(ScimObject, Generic[AnyExtension]):
|
|
|
283
342
|
]
|
|
284
343
|
return schemas
|
|
285
344
|
|
|
345
|
+
@model_validator(mode="wrap")
|
|
346
|
+
@classmethod
|
|
347
|
+
def _validate_extension_schemas(
|
|
348
|
+
cls, value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
|
|
349
|
+
) -> Self:
|
|
350
|
+
"""Validate that extension schemas are known."""
|
|
351
|
+
obj: Self = handler(value)
|
|
352
|
+
|
|
353
|
+
scim_ctx = info.context.get("scim") if info.context else None
|
|
354
|
+
if scim_ctx is None or scim_ctx == Context.DEFAULT:
|
|
355
|
+
return obj
|
|
356
|
+
|
|
357
|
+
base_schema = getattr(cls, "__schema__", None)
|
|
358
|
+
if not base_schema:
|
|
359
|
+
return obj
|
|
360
|
+
|
|
361
|
+
allowed_extensions = set(cls.get_extension_models().keys())
|
|
362
|
+
provided_schemas = set(obj.schemas) - {base_schema}
|
|
363
|
+
|
|
364
|
+
unknown = provided_schemas - allowed_extensions
|
|
365
|
+
if unknown:
|
|
366
|
+
raise PydanticCustomError(
|
|
367
|
+
"unknown_extension_schema",
|
|
368
|
+
"Unknown extension schemas: {schemas}",
|
|
369
|
+
{"schemas": ", ".join(sorted(unknown))},
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
return obj
|
|
373
|
+
|
|
286
374
|
@classmethod
|
|
287
375
|
def to_schema(cls) -> "Schema":
|
|
288
376
|
"""Build a :class:`~scim2_models.Schema` from the current resource class."""
|
|
@@ -305,17 +393,16 @@ class Resource(ScimObject, Generic[AnyExtension]):
|
|
|
305
393
|
kwargs = super()._prepare_model_dump(scim_ctx, **kwargs)
|
|
306
394
|
|
|
307
395
|
# RFC 7644: "SHOULD ignore any query parameters they do not recognize"
|
|
396
|
+
bound_path = Path.__class_getitem__(type(self))
|
|
308
397
|
kwargs["context"]["scim_attributes"] = [
|
|
309
|
-
|
|
398
|
+
urn
|
|
310
399
|
for attribute in (attributes or [])
|
|
311
|
-
if (
|
|
312
|
-
is not None
|
|
400
|
+
if (urn := bound_path(attribute).urn) is not None
|
|
313
401
|
]
|
|
314
402
|
kwargs["context"]["scim_excluded_attributes"] = [
|
|
315
|
-
|
|
403
|
+
urn
|
|
316
404
|
for attribute in (excluded_attributes or [])
|
|
317
|
-
if (
|
|
318
|
-
is not None
|
|
405
|
+
if (urn := bound_path(attribute).urn) is not None
|
|
319
406
|
]
|
|
320
407
|
return kwargs
|
|
321
408
|
|
|
@@ -404,7 +491,7 @@ def _dedicated_attributes(
|
|
|
404
491
|
def _model_to_schema(model: type[BaseModel]) -> "Schema":
|
|
405
492
|
from scim2_models.resources.schema import Schema
|
|
406
493
|
|
|
407
|
-
schema_urn = model
|
|
494
|
+
schema_urn = getattr(model, "__schema__", "") or ""
|
|
408
495
|
field_infos = _dedicated_attributes(model, [Resource])
|
|
409
496
|
attributes = [
|
|
410
497
|
_model_attribute_to_scim_attribute(model, attribute_name)
|
|
@@ -457,7 +544,7 @@ def _model_attribute_to_scim_attribute(
|
|
|
457
544
|
returned=model.get_field_annotation(attribute_name, Returned),
|
|
458
545
|
uniqueness=model.get_field_annotation(attribute_name, Uniqueness),
|
|
459
546
|
sub_attributes=sub_attributes,
|
|
460
|
-
reference_types=
|
|
547
|
+
reference_types=root_type.get_scim_reference_types() # type: ignore[attr-defined]
|
|
461
548
|
if attribute_type == Attribute.Type.reference
|
|
462
549
|
else None,
|
|
463
550
|
)
|
|
@@ -9,14 +9,15 @@ from ..annotations import Mutability
|
|
|
9
9
|
from ..annotations import Required
|
|
10
10
|
from ..annotations import Returned
|
|
11
11
|
from ..attributes import ComplexAttribute
|
|
12
|
+
from ..path import URN
|
|
13
|
+
from ..reference import URI
|
|
12
14
|
from ..reference import Reference
|
|
13
|
-
from ..reference import URIReference
|
|
14
15
|
from .resource import Resource
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class SchemaExtension(ComplexAttribute):
|
|
18
19
|
schema_: Annotated[
|
|
19
|
-
Reference[
|
|
20
|
+
Reference[URI] | None,
|
|
20
21
|
Mutability.read_only,
|
|
21
22
|
Required.true,
|
|
22
23
|
CaseExact.true,
|
|
@@ -35,9 +36,7 @@ class SchemaExtension(ComplexAttribute):
|
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
class ResourceType(Resource[Any]):
|
|
38
|
-
schemas:
|
|
39
|
-
"urn:ietf:params:scim:schemas:core:2.0:ResourceType"
|
|
40
|
-
]
|
|
39
|
+
__schema__ = URN("urn:ietf:params:scim:schemas:core:2.0:ResourceType")
|
|
41
40
|
|
|
42
41
|
name: Annotated[str | None, Mutability.read_only, Required.true] = None
|
|
43
42
|
"""The resource type name.
|
|
@@ -58,14 +57,14 @@ class ResourceType(Resource[Any]):
|
|
|
58
57
|
This is often the same value as the "name" attribute.
|
|
59
58
|
"""
|
|
60
59
|
|
|
61
|
-
endpoint: Annotated[
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
endpoint: Annotated[Reference[URI] | None, Mutability.read_only, Required.true] = (
|
|
61
|
+
None
|
|
62
|
+
)
|
|
64
63
|
"""The resource type's HTTP-addressable endpoint relative to the Base URL,
|
|
65
64
|
e.g., '/Users'."""
|
|
66
65
|
|
|
67
66
|
schema_: Annotated[
|
|
68
|
-
Reference[
|
|
67
|
+
Reference[URI] | None,
|
|
69
68
|
Mutability.read_only,
|
|
70
69
|
Required.true,
|
|
71
70
|
CaseExact.true,
|
|
@@ -80,21 +79,23 @@ class ResourceType(Resource[Any]):
|
|
|
80
79
|
@classmethod
|
|
81
80
|
def from_resource(cls, resource_model: type[Resource[Any]]) -> Self:
|
|
82
81
|
"""Build a naive ResourceType from a resource model."""
|
|
83
|
-
schema = resource_model.
|
|
82
|
+
schema = resource_model.__schema__
|
|
83
|
+
if schema is None:
|
|
84
|
+
raise ValueError(f"{resource_model.__name__} has no __schema__ defined")
|
|
84
85
|
name = schema.split(":")[-1]
|
|
85
86
|
|
|
86
|
-
# Get extensions from the metadata system
|
|
87
87
|
extensions = getattr(resource_model, "__scim_extension_metadata__", [])
|
|
88
88
|
|
|
89
89
|
return cls(
|
|
90
90
|
id=name,
|
|
91
91
|
name=name,
|
|
92
92
|
description=name,
|
|
93
|
-
endpoint=Reference[
|
|
94
|
-
schema_=schema,
|
|
93
|
+
endpoint=Reference[URI](f"/{name}s"),
|
|
94
|
+
schema_=Reference[URI](schema),
|
|
95
95
|
schema_extensions=[
|
|
96
96
|
SchemaExtension(
|
|
97
|
-
schema_=
|
|
97
|
+
schema_=Reference[URI](extension.__schema__),
|
|
98
|
+
required=False,
|
|
98
99
|
)
|
|
99
100
|
for extension in extensions
|
|
100
101
|
],
|