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/constants.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
PYTHON_RESERVED_WORDS = [
|
|
1
|
+
PYTHON_RESERVED_WORDS: list[str] = [
|
|
2
2
|
"False",
|
|
3
3
|
"def",
|
|
4
4
|
"if",
|
|
@@ -34,5 +34,5 @@ PYTHON_RESERVED_WORDS = [
|
|
|
34
34
|
"pass",
|
|
35
35
|
]
|
|
36
36
|
|
|
37
|
-
PYDANTIC_RESERVED_WORDS = ["schema"]
|
|
38
|
-
RESERVED_WORDS = PYTHON_RESERVED_WORDS + PYDANTIC_RESERVED_WORDS
|
|
37
|
+
PYDANTIC_RESERVED_WORDS: list[str] = ["schema"]
|
|
38
|
+
RESERVED_WORDS: list[str] = PYTHON_RESERVED_WORDS + PYDANTIC_RESERVED_WORDS
|
scim2_models/context.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from enum import auto
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Context(Enum):
|
|
6
|
+
"""Represent the different HTTP contexts detailed in :rfc:`RFC7644 §3.2 <7644#section-3.2>`.
|
|
7
|
+
|
|
8
|
+
Contexts are intended to be used during model validation and serialization.
|
|
9
|
+
For instance a client preparing a resource creation POST request can use
|
|
10
|
+
:code:`resource.model_dump(Context.RESOURCE_CREATION_REQUEST)` and
|
|
11
|
+
the server can then validate it with
|
|
12
|
+
:code:`resource.model_validate(Context.RESOURCE_CREATION_REQUEST)`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
DEFAULT = auto()
|
|
16
|
+
"""The default context.
|
|
17
|
+
|
|
18
|
+
All fields are accepted during validation, and all fields are
|
|
19
|
+
serialized during a dump.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
RESOURCE_CREATION_REQUEST = auto()
|
|
23
|
+
"""The resource creation request context.
|
|
24
|
+
|
|
25
|
+
Should be used for clients building a payload for a resource creation request,
|
|
26
|
+
and servers validating resource creation request payloads.
|
|
27
|
+
|
|
28
|
+
- When used for serialization, it will not dump attributes annotated with :attr:`~scim2_models.Mutability.read_only`.
|
|
29
|
+
- When used for validation, it will raise a :class:`~pydantic.ValidationError`:
|
|
30
|
+
- when finding attributes annotated with :attr:`~scim2_models.Mutability.read_only`,
|
|
31
|
+
- when attributes annotated with :attr:`Required.true <scim2_models.Required.true>` are missing on null.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
RESOURCE_CREATION_RESPONSE = auto()
|
|
35
|
+
"""The resource creation response context.
|
|
36
|
+
|
|
37
|
+
Should be used for servers building a payload for a resource
|
|
38
|
+
creation response, and clients validating resource creation response
|
|
39
|
+
payloads.
|
|
40
|
+
|
|
41
|
+
- 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`;
|
|
42
|
+
- When used for serialization, it will:
|
|
43
|
+
- always dump attributes annotated with :attr:`~scim2_models.Returned.always`;
|
|
44
|
+
- never dump attributes annotated with :attr:`~scim2_models.Returned.never`;
|
|
45
|
+
- dump attributes annotated with :attr:`~scim2_models.Returned.default` unless they are explicitly excluded;
|
|
46
|
+
- not dump attributes annotated with :attr:`~scim2_models.Returned.request` unless they are explicitly included.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
RESOURCE_QUERY_REQUEST = auto()
|
|
50
|
+
"""The resource query request context.
|
|
51
|
+
|
|
52
|
+
Should be used for clients building a payload for a resource query request,
|
|
53
|
+
and servers validating resource query request payloads.
|
|
54
|
+
|
|
55
|
+
- When used for serialization, it will not dump attributes annotated with :attr:`~scim2_models.Mutability.write_only`.
|
|
56
|
+
- When used for validation, it will raise a :class:`~pydantic.ValidationError` when finding attributes annotated with :attr:`~scim2_models.Mutability.write_only`.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
RESOURCE_QUERY_RESPONSE = auto()
|
|
60
|
+
"""The resource query response context.
|
|
61
|
+
|
|
62
|
+
Should be used for servers building a payload for a resource query
|
|
63
|
+
response, and clients validating resource query response payloads.
|
|
64
|
+
|
|
65
|
+
- 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`;
|
|
66
|
+
- When used for serialization, it will:
|
|
67
|
+
- always dump attributes annotated with :attr:`~scim2_models.Returned.always`;
|
|
68
|
+
- never dump attributes annotated with :attr:`~scim2_models.Returned.never`;
|
|
69
|
+
- dump attributes annotated with :attr:`~scim2_models.Returned.default` unless they are explicitly excluded;
|
|
70
|
+
- not dump attributes annotated with :attr:`~scim2_models.Returned.request` unless they are explicitly included.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
RESOURCE_REPLACEMENT_REQUEST = auto()
|
|
74
|
+
"""The resource replacement request context.
|
|
75
|
+
|
|
76
|
+
Should be used for clients building a payload for a resource replacement request,
|
|
77
|
+
and servers validating resource replacement request payloads.
|
|
78
|
+
|
|
79
|
+
- When used for serialization, it will not dump attributes annotated with :attr:`~scim2_models.Mutability.read_only`.
|
|
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 the ``original`` parameter passed to :meth:`~scim2_models.BaseModel.model_validate`;
|
|
82
|
+
- when attributes annotated with :attr:`Required.true <scim2_models.Required.true>` are missing on null.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
RESOURCE_REPLACEMENT_RESPONSE = auto()
|
|
86
|
+
"""The resource replacement response context.
|
|
87
|
+
|
|
88
|
+
Should be used for servers building a payload for a resource
|
|
89
|
+
replacement response, and clients validating resource query
|
|
90
|
+
replacement payloads.
|
|
91
|
+
|
|
92
|
+
- 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`;
|
|
93
|
+
- When used for serialization, it will:
|
|
94
|
+
- always dump attributes annotated with :attr:`~scim2_models.Returned.always`;
|
|
95
|
+
- never dump attributes annotated with :attr:`~scim2_models.Returned.never`;
|
|
96
|
+
- dump attributes annotated with :attr:`~scim2_models.Returned.default` unless they are explicitly excluded;
|
|
97
|
+
- not dump attributes annotated with :attr:`~scim2_models.Returned.request` unless they are explicitly included.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
SEARCH_REQUEST = auto()
|
|
101
|
+
"""The search request context.
|
|
102
|
+
|
|
103
|
+
Should be used for clients building a payload for a search request,
|
|
104
|
+
and servers validating search request payloads.
|
|
105
|
+
|
|
106
|
+
- When used for serialization, it will not dump attributes annotated with :attr:`~scim2_models.Mutability.write_only`.
|
|
107
|
+
- When used for validation, it will raise a :class:`~pydantic.ValidationError` when finding attributes annotated with :attr:`~scim2_models.Mutability.write_only`.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
SEARCH_RESPONSE = auto()
|
|
111
|
+
"""The resource query response context.
|
|
112
|
+
|
|
113
|
+
Should be used for servers building a payload for a search response,
|
|
114
|
+
and clients validating resource search payloads.
|
|
115
|
+
|
|
116
|
+
- 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`;
|
|
117
|
+
- When used for serialization, it will:
|
|
118
|
+
- always dump attributes annotated with :attr:`~scim2_models.Returned.always`;
|
|
119
|
+
- never dump attributes annotated with :attr:`~scim2_models.Returned.never`;
|
|
120
|
+
- dump attributes annotated with :attr:`~scim2_models.Returned.default` unless they are explicitly excluded;
|
|
121
|
+
- not dump attributes annotated with :attr:`~scim2_models.Returned.request` unless they are explicitly included.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
RESOURCE_PATCH_REQUEST = auto()
|
|
125
|
+
"""The resource patch request context.
|
|
126
|
+
|
|
127
|
+
Should be used for clients building a payload for a PATCH request,
|
|
128
|
+
and servers validating PATCH request payloads.
|
|
129
|
+
|
|
130
|
+
- When used for serialization, it will not dump attributes annotated with :attr:`~scim2_models.Mutability.read_only`.
|
|
131
|
+
- When used for validation, it will raise a :class:`~pydantic.ValidationError`:
|
|
132
|
+
- when finding attributes annotated with :attr:`~scim2_models.Mutability.read_only`,
|
|
133
|
+
- when attributes annotated with :attr:`Required.true <scim2_models.Required.true>` are missing or null.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
RESOURCE_PATCH_RESPONSE = auto()
|
|
137
|
+
"""The resource patch response context.
|
|
138
|
+
|
|
139
|
+
Should be used for servers building a payload for a PATCH response,
|
|
140
|
+
and clients validating patch response payloads.
|
|
141
|
+
|
|
142
|
+
- 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`;
|
|
143
|
+
- When used for serialization, it will:
|
|
144
|
+
- always dump attributes annotated with :attr:`~scim2_models.Returned.always`;
|
|
145
|
+
- never dump attributes annotated with :attr:`~scim2_models.Returned.never`;
|
|
146
|
+
- dump attributes annotated with :attr:`~scim2_models.Returned.default` unless they are explicitly excluded;
|
|
147
|
+
- not dump attributes annotated with :attr:`~scim2_models.Returned.request` unless they are explicitly included.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def is_request(cls, ctx: "Context") -> bool:
|
|
152
|
+
return ctx in (
|
|
153
|
+
cls.RESOURCE_CREATION_REQUEST,
|
|
154
|
+
cls.RESOURCE_QUERY_REQUEST,
|
|
155
|
+
cls.RESOURCE_REPLACEMENT_REQUEST,
|
|
156
|
+
cls.SEARCH_REQUEST,
|
|
157
|
+
cls.RESOURCE_PATCH_REQUEST,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
@classmethod
|
|
161
|
+
def is_response(cls, ctx: "Context") -> bool:
|
|
162
|
+
return ctx in (
|
|
163
|
+
cls.RESOURCE_CREATION_RESPONSE,
|
|
164
|
+
cls.RESOURCE_QUERY_RESPONSE,
|
|
165
|
+
cls.RESOURCE_REPLACEMENT_RESPONSE,
|
|
166
|
+
cls.SEARCH_RESPONSE,
|
|
167
|
+
cls.RESOURCE_PATCH_RESPONSE,
|
|
168
|
+
)
|
|
@@ -6,9 +6,9 @@ from typing import Optional
|
|
|
6
6
|
from pydantic import Field
|
|
7
7
|
from pydantic import PlainSerializer
|
|
8
8
|
|
|
9
|
-
from ..
|
|
10
|
-
from ..
|
|
11
|
-
from ..utils import
|
|
9
|
+
from ..annotations import Required
|
|
10
|
+
from ..attributes import ComplexAttribute
|
|
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
|
|
|
@@ -3,8 +3,8 @@ from typing import Optional
|
|
|
3
3
|
|
|
4
4
|
from pydantic import PlainSerializer
|
|
5
5
|
|
|
6
|
-
from ..
|
|
7
|
-
from ..utils import
|
|
6
|
+
from ..annotations import Required
|
|
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
|
|
|
@@ -26,7 +26,7 @@ class Error(Message):
|
|
|
26
26
|
"""A detailed human-readable message."""
|
|
27
27
|
|
|
28
28
|
@classmethod
|
|
29
|
-
def make_invalid_filter_error(cls):
|
|
29
|
+
def make_invalid_filter_error(cls) -> "Error":
|
|
30
30
|
"""Pre-defined error intended to be raised when the specified filter syntax was invalid (does not comply with :rfc:`Figure 1 of RFC7644 <7644#section-3.4.2.2>`), or the specified attribute and filter comparison combination is not supported."""
|
|
31
31
|
return Error(
|
|
32
32
|
status=400,
|
|
@@ -35,7 +35,7 @@ class Error(Message):
|
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
@classmethod
|
|
38
|
-
def make_too_many_error(cls):
|
|
38
|
+
def make_too_many_error(cls) -> "Error":
|
|
39
39
|
"""Pre-defined error intended to be raised when the specified filter yields many more results than the server is willing to calculate or process. For example, a filter such as ``(userName pr)`` by itself would return all entries with a ``userName`` and MAY not be acceptable to the service provider."""
|
|
40
40
|
return Error(
|
|
41
41
|
status=400,
|
|
@@ -44,7 +44,7 @@ class Error(Message):
|
|
|
44
44
|
)
|
|
45
45
|
|
|
46
46
|
@classmethod
|
|
47
|
-
def make_uniqueness_error(cls):
|
|
47
|
+
def make_uniqueness_error(cls) -> "Error":
|
|
48
48
|
"""Pre-defined error intended to be raised when One or more of the attribute values are already in use or are reserved."""
|
|
49
49
|
return Error(
|
|
50
50
|
status=409,
|
|
@@ -53,7 +53,7 @@ class Error(Message):
|
|
|
53
53
|
)
|
|
54
54
|
|
|
55
55
|
@classmethod
|
|
56
|
-
def make_mutability_error(cls):
|
|
56
|
+
def make_mutability_error(cls) -> "Error":
|
|
57
57
|
"""Pre-defined error intended to be raised when the attempted modification is not compatible with the target attribute's mutability or current state (e.g., modification of an "immutable" attribute with an existing value)."""
|
|
58
58
|
return Error(
|
|
59
59
|
status=400,
|
|
@@ -62,7 +62,7 @@ class Error(Message):
|
|
|
62
62
|
)
|
|
63
63
|
|
|
64
64
|
@classmethod
|
|
65
|
-
def make_invalid_syntax_error(cls):
|
|
65
|
+
def make_invalid_syntax_error(cls) -> "Error":
|
|
66
66
|
"""Pre-defined error intended to be raised when the request body message structure was invalid or did not conform to the request schema."""
|
|
67
67
|
return Error(
|
|
68
68
|
status=400,
|
|
@@ -71,7 +71,7 @@ class Error(Message):
|
|
|
71
71
|
)
|
|
72
72
|
|
|
73
73
|
@classmethod
|
|
74
|
-
def make_invalid_path_error(cls):
|
|
74
|
+
def make_invalid_path_error(cls) -> "Error":
|
|
75
75
|
"""Pre-defined error intended to be raised when the "path" attribute was invalid or malformed (see :rfc:`Figure 7 of RFC7644 <7644#section-3.5.2>`)."""
|
|
76
76
|
return Error(
|
|
77
77
|
status=400,
|
|
@@ -80,7 +80,7 @@ class Error(Message):
|
|
|
80
80
|
)
|
|
81
81
|
|
|
82
82
|
@classmethod
|
|
83
|
-
def make_no_target_error(cls):
|
|
83
|
+
def make_no_target_error(cls) -> "Error":
|
|
84
84
|
"""Pre-defined error intended to be raised when the specified "path" did not yield an attribute or attribute value that could be operated on. This occurs when the specified "path" value contains a filter that yields no match."""
|
|
85
85
|
return Error(
|
|
86
86
|
status=400,
|
|
@@ -89,7 +89,7 @@ class Error(Message):
|
|
|
89
89
|
)
|
|
90
90
|
|
|
91
91
|
@classmethod
|
|
92
|
-
def make_invalid_value_error(cls):
|
|
92
|
+
def make_invalid_value_error(cls) -> "Error":
|
|
93
93
|
"""Pre-defined error intended to be raised when a required value was missing, or the value specified was not compatible with the operation or attribute type (see :rfc:`Section 2.2 of RFC7643 <7643#section-2.2>`), or resource schema (see :rfc:`Section 4 of RFC7643 <7643#section-4>`)."""
|
|
94
94
|
return Error(
|
|
95
95
|
status=400,
|
|
@@ -98,7 +98,7 @@ class Error(Message):
|
|
|
98
98
|
)
|
|
99
99
|
|
|
100
100
|
@classmethod
|
|
101
|
-
def make_invalid_version_error(cls):
|
|
101
|
+
def make_invalid_version_error(cls) -> "Error":
|
|
102
102
|
"""Pre-defined error intended to be raised when the specified SCIM protocol version is not supported (see :rfc:`Section 3.13 of RFC7644 <7644#section-3.13>`)."""
|
|
103
103
|
return Error(
|
|
104
104
|
status=400,
|
|
@@ -107,7 +107,7 @@ class Error(Message):
|
|
|
107
107
|
)
|
|
108
108
|
|
|
109
109
|
@classmethod
|
|
110
|
-
def make_sensitive_error(cls):
|
|
110
|
+
def make_sensitive_error(cls) -> "Error":
|
|
111
111
|
"""Pre-defined error intended to be raised when the specified request cannot be completed, due to the passing of sensitive (e.g., personal) information in a request URI. For example, personal information SHALL NOT be transmitted over request URIs. See :rfc:`Section 7.5.2 of RFC7644 <7644#section-7.5.2>`."""
|
|
112
112
|
return Error(
|
|
113
113
|
status=400,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
from typing import Any
|
|
3
|
+
from typing import Generic
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
from pydantic import ValidationInfo
|
|
8
|
+
from pydantic import ValidatorFunctionWrapHandler
|
|
9
|
+
from pydantic import model_validator
|
|
10
|
+
from pydantic_core import PydanticCustomError
|
|
11
|
+
from typing_extensions import Self
|
|
12
|
+
|
|
13
|
+
from ..annotations import Required
|
|
14
|
+
from ..context import Context
|
|
15
|
+
from ..resources.resource import AnyResource
|
|
16
|
+
from .message import Message
|
|
17
|
+
from .message import _GenericMessageMetaclass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ListResponse(Message, Generic[AnyResource], metaclass=_GenericMessageMetaclass):
|
|
21
|
+
schemas: Annotated[list[str], Required.true] = [
|
|
22
|
+
"urn:ietf:params:scim:api:messages:2.0:ListResponse"
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
total_results: Optional[int] = None
|
|
26
|
+
"""The total number of results returned by the list or query operation."""
|
|
27
|
+
|
|
28
|
+
start_index: Optional[int] = None
|
|
29
|
+
"""The 1-based index of the first result in the current set of list
|
|
30
|
+
results."""
|
|
31
|
+
|
|
32
|
+
items_per_page: Optional[int] = None
|
|
33
|
+
"""The number of resources returned in a list response page."""
|
|
34
|
+
|
|
35
|
+
resources: Optional[list[AnyResource]] = Field(
|
|
36
|
+
None, serialization_alias="Resources"
|
|
37
|
+
)
|
|
38
|
+
"""A multi-valued list of complex objects containing the requested
|
|
39
|
+
resources."""
|
|
40
|
+
|
|
41
|
+
@model_validator(mode="wrap")
|
|
42
|
+
@classmethod
|
|
43
|
+
def check_results_number(
|
|
44
|
+
cls, value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
|
|
45
|
+
) -> Self:
|
|
46
|
+
"""Validate result numbers.
|
|
47
|
+
|
|
48
|
+
:rfc:`RFC7644 §3.4.2 <7644#section-3.4.2.4>` indicates that:
|
|
49
|
+
|
|
50
|
+
- 'totalResults' is required
|
|
51
|
+
- 'resources' must be set if 'totalResults' is non-zero.
|
|
52
|
+
"""
|
|
53
|
+
obj = handler(value)
|
|
54
|
+
assert isinstance(obj, cls)
|
|
55
|
+
|
|
56
|
+
if (
|
|
57
|
+
not info.context
|
|
58
|
+
or not info.context.get("scim")
|
|
59
|
+
or not Context.is_response(info.context["scim"])
|
|
60
|
+
):
|
|
61
|
+
return obj
|
|
62
|
+
|
|
63
|
+
if obj.total_results is None:
|
|
64
|
+
raise PydanticCustomError(
|
|
65
|
+
"required_error",
|
|
66
|
+
"Field 'total_results' is required but value is missing or null",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if obj.total_results > 0 and not obj.resources:
|
|
70
|
+
raise PydanticCustomError(
|
|
71
|
+
"no_resource_error",
|
|
72
|
+
"Field 'resources' is missing or null but 'total_results' is non-zero.",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return obj
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
from typing import Any
|
|
3
|
+
from typing import Callable
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from typing import Union
|
|
6
|
+
from typing import get_args
|
|
7
|
+
from typing import get_origin
|
|
8
|
+
|
|
9
|
+
from pydantic import Discriminator
|
|
10
|
+
from pydantic import Tag
|
|
11
|
+
from pydantic._internal._model_construction import ModelMetaclass
|
|
12
|
+
|
|
13
|
+
from scim2_models.resources.resource import Resource
|
|
14
|
+
|
|
15
|
+
from ..base import BaseModel
|
|
16
|
+
from ..scim_object import ScimObject
|
|
17
|
+
from ..utils import UNION_TYPES
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Message(ScimObject):
|
|
21
|
+
"""SCIM protocol messages as defined by :rfc:`RFC7644 §3.1 <7644#section-3.1>`."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _create_schema_discriminator(
|
|
25
|
+
resource_types_schemas: list[str],
|
|
26
|
+
) -> Callable[[Any], Optional[str]]:
|
|
27
|
+
"""Create a schema discriminator function for the given resource schemas.
|
|
28
|
+
|
|
29
|
+
:param resource_types_schemas: List of valid resource schemas
|
|
30
|
+
:return: Discriminator function for Pydantic
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def get_schema_from_payload(payload: Any) -> Optional[str]:
|
|
34
|
+
"""Extract schema from SCIM payload for discrimination.
|
|
35
|
+
|
|
36
|
+
:param payload: SCIM payload dict or object
|
|
37
|
+
:return: First matching schema or None
|
|
38
|
+
"""
|
|
39
|
+
if not payload:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
payload_schemas = (
|
|
43
|
+
payload.get("schemas", []) if isinstance(payload, dict) else payload.schemas
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
common_schemas = [
|
|
47
|
+
schema for schema in payload_schemas if schema in resource_types_schemas
|
|
48
|
+
]
|
|
49
|
+
return common_schemas[0] if common_schemas else None
|
|
50
|
+
|
|
51
|
+
return get_schema_from_payload
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_tag(resource_type: type[BaseModel]) -> Tag:
|
|
55
|
+
"""Create Pydantic tag from resource type schema.
|
|
56
|
+
|
|
57
|
+
:param resource_type: SCIM resource type
|
|
58
|
+
:return: Pydantic Tag for discrimination
|
|
59
|
+
"""
|
|
60
|
+
return Tag(resource_type.model_fields["schemas"].default[0])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _create_tagged_resource_union(resource_union: Any) -> Any:
|
|
64
|
+
"""Build Discriminated Unions for SCIM resources.
|
|
65
|
+
|
|
66
|
+
Creates discriminated unions so Pydantic can determine which class to instantiate
|
|
67
|
+
by inspecting the payload's schemas field.
|
|
68
|
+
|
|
69
|
+
:param resource_union: Union type of SCIM resources
|
|
70
|
+
:return: Annotated discriminated union or original type
|
|
71
|
+
"""
|
|
72
|
+
if get_origin(resource_union) not in UNION_TYPES:
|
|
73
|
+
return resource_union
|
|
74
|
+
|
|
75
|
+
resource_types = get_args(resource_union)
|
|
76
|
+
|
|
77
|
+
# Set up schemas for the discriminator function
|
|
78
|
+
resource_types_schemas = [
|
|
79
|
+
resource_type.model_fields["schemas"].default[0]
|
|
80
|
+
for resource_type in resource_types
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
# Create discriminator function with schemas captured in closure
|
|
84
|
+
schema_discriminator = _create_schema_discriminator(resource_types_schemas)
|
|
85
|
+
discriminator = Discriminator(schema_discriminator)
|
|
86
|
+
|
|
87
|
+
tagged_resources = [
|
|
88
|
+
Annotated[resource_type, _get_tag(resource_type)]
|
|
89
|
+
for resource_type in resource_types
|
|
90
|
+
]
|
|
91
|
+
# Dynamic union construction from tuple - MyPy can't validate this at compile time
|
|
92
|
+
union = Union[tuple(tagged_resources)] # type: ignore
|
|
93
|
+
return Annotated[union, discriminator]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class _GenericMessageMetaclass(ModelMetaclass):
|
|
97
|
+
"""Metaclass for SCIM generic types with discriminated unions."""
|
|
98
|
+
|
|
99
|
+
def __new__(
|
|
100
|
+
cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any], **kwargs: Any
|
|
101
|
+
) -> type:
|
|
102
|
+
"""Create class with tagged resource unions for generic parameters."""
|
|
103
|
+
if kwargs.get("__pydantic_generic_metadata__") and kwargs[
|
|
104
|
+
"__pydantic_generic_metadata__"
|
|
105
|
+
].get("args"):
|
|
106
|
+
tagged_union = _create_tagged_resource_union(
|
|
107
|
+
kwargs["__pydantic_generic_metadata__"]["args"][0]
|
|
108
|
+
)
|
|
109
|
+
kwargs["__pydantic_generic_metadata__"]["args"] = (tagged_union,)
|
|
110
|
+
|
|
111
|
+
klass = super().__new__(cls, name, bases, attrs, **kwargs)
|
|
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
|