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/rfc7643/resource.py
DELETED
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
from datetime import datetime
|
|
2
|
-
from typing import Annotated
|
|
3
|
-
from typing import Any
|
|
4
|
-
from typing import Generic
|
|
5
|
-
from typing import Optional
|
|
6
|
-
from typing import TypeVar
|
|
7
|
-
from typing import get_args
|
|
8
|
-
from typing import get_origin
|
|
9
|
-
|
|
10
|
-
from pydantic import Field
|
|
11
|
-
from pydantic import WrapSerializer
|
|
12
|
-
from pydantic import field_serializer
|
|
13
|
-
|
|
14
|
-
from ..base import BaseModel
|
|
15
|
-
from ..base import BaseModelType
|
|
16
|
-
from ..base import CaseExact
|
|
17
|
-
from ..base import ComplexAttribute
|
|
18
|
-
from ..base import ExternalReference
|
|
19
|
-
from ..base import MultiValuedComplexAttribute
|
|
20
|
-
from ..base import Mutability
|
|
21
|
-
from ..base import Required
|
|
22
|
-
from ..base import Returned
|
|
23
|
-
from ..base import Uniqueness
|
|
24
|
-
from ..base import URIReference
|
|
25
|
-
from ..base import is_complex_attribute
|
|
26
|
-
from ..utils import UNION_TYPES
|
|
27
|
-
from ..utils import normalize_attribute_name
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class Meta(ComplexAttribute):
|
|
31
|
-
"""All "meta" sub-attributes are assigned by the service provider (have a "mutability" of "readOnly"), and all of these sub-attributes have a "returned" characteristic of "default".
|
|
32
|
-
|
|
33
|
-
This attribute SHALL be ignored when provided by clients. "meta" contains the following sub-attributes:
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
resource_type: Optional[str] = None
|
|
37
|
-
"""The name of the resource type of the resource.
|
|
38
|
-
|
|
39
|
-
This attribute has a mutability of "readOnly" and "caseExact" as
|
|
40
|
-
"true".
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
created: Optional[datetime] = None
|
|
44
|
-
"""The "DateTime" that the resource was added to the service provider.
|
|
45
|
-
|
|
46
|
-
This attribute MUST be a DateTime.
|
|
47
|
-
"""
|
|
48
|
-
|
|
49
|
-
last_modified: Optional[datetime] = None
|
|
50
|
-
"""The most recent DateTime that the details of this resource were updated
|
|
51
|
-
at the service provider.
|
|
52
|
-
|
|
53
|
-
If this resource has never been modified since its initial creation,
|
|
54
|
-
the value MUST be the same as the value of "created".
|
|
55
|
-
"""
|
|
56
|
-
|
|
57
|
-
location: Optional[str] = None
|
|
58
|
-
"""The URI of the resource being returned.
|
|
59
|
-
|
|
60
|
-
This value MUST be the same as the "Content-Location" HTTP response
|
|
61
|
-
header (see Section 3.1.4.2 of [RFC7231]).
|
|
62
|
-
"""
|
|
63
|
-
|
|
64
|
-
version: Optional[str] = None
|
|
65
|
-
"""The version of the resource being returned.
|
|
66
|
-
|
|
67
|
-
This value must be the same as the entity-tag (ETag) HTTP response
|
|
68
|
-
header (see Sections 2.1 and 2.3 of [RFC7232]). This attribute has
|
|
69
|
-
"caseExact" as "true". Service provider support for this attribute
|
|
70
|
-
is optional and subject to the service provider's support for
|
|
71
|
-
versioning (see Section 3.14 of [RFC7644]). If a service provider
|
|
72
|
-
provides "version" (entity-tag) for a representation and the
|
|
73
|
-
generation of that entity-tag does not satisfy all of the
|
|
74
|
-
characteristics of a strong validator (see Section 2.1 of
|
|
75
|
-
[RFC7232]), then the origin server MUST mark the "version" (entity-
|
|
76
|
-
tag) as weak by prefixing its opaque value with "W/" (case
|
|
77
|
-
sensitive).
|
|
78
|
-
"""
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
class Extension(BaseModel):
|
|
82
|
-
@classmethod
|
|
83
|
-
def to_schema(cls):
|
|
84
|
-
"""Build a :class:`~scim2_models.Schema` from the current extension class."""
|
|
85
|
-
return model_to_schema(cls)
|
|
86
|
-
|
|
87
|
-
@classmethod
|
|
88
|
-
def from_schema(cls, schema) -> "Extension":
|
|
89
|
-
"""Build a :class:`~scim2_models.Extension` subclass from the schema definition."""
|
|
90
|
-
from .schema import make_python_model
|
|
91
|
-
|
|
92
|
-
return make_python_model(schema, cls)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
AnyExtension = TypeVar("AnyExtension", bound="Extension")
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def extension_serializer(value: Any, handler, info) -> Optional[dict[str, Any]]:
|
|
99
|
-
"""Exclude the Resource attributes from the extension dump.
|
|
100
|
-
|
|
101
|
-
For instance, attributes 'meta', 'id' or 'schemas' should not be
|
|
102
|
-
dumped when the model is used as an extension for another model.
|
|
103
|
-
"""
|
|
104
|
-
partial_result = handler(value, info)
|
|
105
|
-
result = {
|
|
106
|
-
attr_name: value
|
|
107
|
-
for attr_name, value in partial_result.items()
|
|
108
|
-
if attr_name not in Resource.model_fields
|
|
109
|
-
}
|
|
110
|
-
return result or None
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
class ResourceMetaclass(BaseModelType):
|
|
114
|
-
def __new__(cls, name, bases, attrs, **kwargs):
|
|
115
|
-
"""Dynamically add a field for each extension."""
|
|
116
|
-
if "__pydantic_generic_metadata__" in kwargs:
|
|
117
|
-
extensions = kwargs["__pydantic_generic_metadata__"]["args"][0]
|
|
118
|
-
extensions = (
|
|
119
|
-
get_args(extensions)
|
|
120
|
-
if get_origin(extensions) in UNION_TYPES
|
|
121
|
-
else [extensions]
|
|
122
|
-
)
|
|
123
|
-
for extension in extensions:
|
|
124
|
-
schema = extension.model_fields["schemas"].default[0]
|
|
125
|
-
attrs.setdefault("__annotations__", {})[extension.__name__] = Annotated[
|
|
126
|
-
Optional[extension],
|
|
127
|
-
WrapSerializer(extension_serializer),
|
|
128
|
-
]
|
|
129
|
-
attrs[extension.__name__] = Field(
|
|
130
|
-
None,
|
|
131
|
-
serialization_alias=schema,
|
|
132
|
-
validation_alias=normalize_attribute_name(schema),
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
klass = super().__new__(cls, name, bases, attrs, **kwargs)
|
|
136
|
-
return klass
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
class Resource(BaseModel, Generic[AnyExtension], metaclass=ResourceMetaclass):
|
|
140
|
-
schemas: Annotated[list[str], Required.true]
|
|
141
|
-
"""The "schemas" attribute is a REQUIRED attribute and is an array of
|
|
142
|
-
Strings containing URIs that are used to indicate the namespaces of the
|
|
143
|
-
SCIM schemas that define the attributes present in the current JSON
|
|
144
|
-
structure."""
|
|
145
|
-
|
|
146
|
-
# Common attributes as defined by
|
|
147
|
-
# https://www.rfc-editor.org/rfc/rfc7643#section-3.1
|
|
148
|
-
|
|
149
|
-
id: Annotated[
|
|
150
|
-
Optional[str], Mutability.read_only, Returned.always, Uniqueness.global_
|
|
151
|
-
] = None
|
|
152
|
-
"""A unique identifier for a SCIM resource as defined by the service
|
|
153
|
-
provider.
|
|
154
|
-
|
|
155
|
-
id is mandatory is the resource representation, but is forbidden in
|
|
156
|
-
resource creation or replacement requests.
|
|
157
|
-
"""
|
|
158
|
-
|
|
159
|
-
external_id: Annotated[
|
|
160
|
-
Optional[str], Mutability.read_write, Returned.default, CaseExact.true
|
|
161
|
-
] = None
|
|
162
|
-
"""A String that is an identifier for the resource as defined by the
|
|
163
|
-
provisioning client."""
|
|
164
|
-
|
|
165
|
-
meta: Annotated[Optional[Meta], Mutability.read_only, Returned.default] = None
|
|
166
|
-
"""A complex attribute containing resource metadata."""
|
|
167
|
-
|
|
168
|
-
def __getitem__(self, item: Any):
|
|
169
|
-
if not isinstance(item, type) or not issubclass(item, Extension):
|
|
170
|
-
raise KeyError(f"{item} is not a valid extension type")
|
|
171
|
-
|
|
172
|
-
return getattr(self, item.__name__)
|
|
173
|
-
|
|
174
|
-
def __setitem__(self, item: Any, value: "Resource"):
|
|
175
|
-
if not isinstance(item, type) or not issubclass(item, Extension):
|
|
176
|
-
raise KeyError(f"{item} is not a valid extension type")
|
|
177
|
-
|
|
178
|
-
setattr(self, item.__name__, value)
|
|
179
|
-
|
|
180
|
-
@classmethod
|
|
181
|
-
def get_extension_models(cls) -> dict[str, type[Extension]]:
|
|
182
|
-
"""Return extension a dict associating extension models with their schemas."""
|
|
183
|
-
extension_models = cls.__pydantic_generic_metadata__.get("args", [])
|
|
184
|
-
extension_models = (
|
|
185
|
-
get_args(extension_models[0])
|
|
186
|
-
if len(extension_models) == 1
|
|
187
|
-
and get_origin(extension_models[0]) in UNION_TYPES
|
|
188
|
-
else extension_models
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
by_schema = {
|
|
192
|
-
ext.model_fields["schemas"].default[0]: ext for ext in extension_models
|
|
193
|
-
}
|
|
194
|
-
return by_schema
|
|
195
|
-
|
|
196
|
-
@classmethod
|
|
197
|
-
def get_extension_model(cls, name_or_schema) -> Optional[type[Extension]]:
|
|
198
|
-
"""Return an extension by its name or schema."""
|
|
199
|
-
for schema, extension in cls.get_extension_models().items():
|
|
200
|
-
if schema == name_or_schema or extension.__name__ == name_or_schema:
|
|
201
|
-
return extension
|
|
202
|
-
return None
|
|
203
|
-
|
|
204
|
-
@staticmethod
|
|
205
|
-
def get_by_schema(
|
|
206
|
-
resource_types: list[type[BaseModel]], schema: str, with_extensions=True
|
|
207
|
-
) -> Optional[type]:
|
|
208
|
-
"""Given a resource type list and a schema, find the matching resource type."""
|
|
209
|
-
by_schema = {
|
|
210
|
-
resource_type.model_fields["schemas"].default[0].lower(): resource_type
|
|
211
|
-
for resource_type in (resource_types or [])
|
|
212
|
-
}
|
|
213
|
-
if with_extensions:
|
|
214
|
-
for resource_type in list(by_schema.values()):
|
|
215
|
-
by_schema.update(
|
|
216
|
-
{
|
|
217
|
-
schema.lower(): extension
|
|
218
|
-
for schema, extension in resource_type.get_extension_models().items()
|
|
219
|
-
}
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
return by_schema.get(schema.lower())
|
|
223
|
-
|
|
224
|
-
@staticmethod
|
|
225
|
-
def get_by_payload(resource_types: list[type], payload: dict, **kwargs):
|
|
226
|
-
"""Given a resource type list and a payload, find the matching resource type."""
|
|
227
|
-
if not payload or not payload.get("schemas"):
|
|
228
|
-
return None
|
|
229
|
-
|
|
230
|
-
schema = payload["schemas"][0]
|
|
231
|
-
return Resource.get_by_schema(resource_types, schema, **kwargs)
|
|
232
|
-
|
|
233
|
-
@field_serializer("schemas")
|
|
234
|
-
def set_extension_schemas(self, schemas: Annotated[list[str], Required.true]):
|
|
235
|
-
"""Add model extension ids to the 'schemas' attribute."""
|
|
236
|
-
extension_schemas = self.get_extension_models().keys()
|
|
237
|
-
schemas = self.schemas + [
|
|
238
|
-
schema for schema in extension_schemas if schema not in self.schemas
|
|
239
|
-
]
|
|
240
|
-
return schemas
|
|
241
|
-
|
|
242
|
-
@classmethod
|
|
243
|
-
def to_schema(cls):
|
|
244
|
-
"""Build a :class:`~scim2_models.Schema` from the current resource class."""
|
|
245
|
-
return model_to_schema(cls)
|
|
246
|
-
|
|
247
|
-
@classmethod
|
|
248
|
-
def from_schema(cls, schema) -> "Resource":
|
|
249
|
-
"""Build a :class:`scim2_models.Resource` subclass from the schema definition."""
|
|
250
|
-
from .schema import make_python_model
|
|
251
|
-
|
|
252
|
-
return make_python_model(schema, cls)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
AnyResource = TypeVar("AnyResource", bound="Resource")
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
def dedicated_attributes(model, excluded_models):
|
|
259
|
-
"""Return attributes that are not members the parent 'excluded_models'."""
|
|
260
|
-
|
|
261
|
-
def compare_field_infos(fi1, fi2):
|
|
262
|
-
return (
|
|
263
|
-
fi1
|
|
264
|
-
and fi2
|
|
265
|
-
and fi1.__slotnames__ == fi2.__slotnames__
|
|
266
|
-
and all(
|
|
267
|
-
getattr(fi1, attr) == getattr(fi2, attr) for attr in fi1.__slotnames__
|
|
268
|
-
)
|
|
269
|
-
)
|
|
270
|
-
|
|
271
|
-
parent_field_infos = {
|
|
272
|
-
field_name: field_info
|
|
273
|
-
for excluded_model in excluded_models
|
|
274
|
-
for field_name, field_info in excluded_model.model_fields.items()
|
|
275
|
-
}
|
|
276
|
-
field_infos = {
|
|
277
|
-
field_name: field_info
|
|
278
|
-
for field_name, field_info in model.model_fields.items()
|
|
279
|
-
if not compare_field_infos(field_info, parent_field_infos.get(field_name))
|
|
280
|
-
}
|
|
281
|
-
return field_infos
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
def model_to_schema(model: type[BaseModel]):
|
|
285
|
-
from scim2_models.rfc7643.schema import Schema
|
|
286
|
-
|
|
287
|
-
schema_urn = model.model_fields["schemas"].default[0]
|
|
288
|
-
field_infos = dedicated_attributes(model, [Resource])
|
|
289
|
-
attributes = [
|
|
290
|
-
model_attribute_to_attribute(model, attribute_name)
|
|
291
|
-
for attribute_name in field_infos
|
|
292
|
-
if attribute_name != "schemas"
|
|
293
|
-
]
|
|
294
|
-
schema = Schema(
|
|
295
|
-
name=model.__name__,
|
|
296
|
-
id=schema_urn,
|
|
297
|
-
description=model.__doc__ or model.__name__,
|
|
298
|
-
attributes=attributes,
|
|
299
|
-
)
|
|
300
|
-
return schema
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
def get_reference_types(type) -> list[str]:
|
|
304
|
-
first_arg = get_args(type)[0]
|
|
305
|
-
types = get_args(first_arg) if get_origin(first_arg) in UNION_TYPES else [first_arg]
|
|
306
|
-
|
|
307
|
-
def serialize_ref_type(ref_type):
|
|
308
|
-
if ref_type == URIReference:
|
|
309
|
-
return "uri"
|
|
310
|
-
|
|
311
|
-
elif ref_type == ExternalReference:
|
|
312
|
-
return "external"
|
|
313
|
-
|
|
314
|
-
return get_args(ref_type)[0]
|
|
315
|
-
|
|
316
|
-
return list(map(serialize_ref_type, types))
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
def model_attribute_to_attribute(model, attribute_name):
|
|
320
|
-
from scim2_models.rfc7643.schema import Attribute
|
|
321
|
-
|
|
322
|
-
field_info = model.model_fields[attribute_name]
|
|
323
|
-
root_type = model.get_field_root_type(attribute_name)
|
|
324
|
-
attribute_type = Attribute.Type.from_python(root_type)
|
|
325
|
-
sub_attributes = (
|
|
326
|
-
[
|
|
327
|
-
model_attribute_to_attribute(root_type, sub_attribute_name)
|
|
328
|
-
for sub_attribute_name in dedicated_attributes(
|
|
329
|
-
root_type, [MultiValuedComplexAttribute]
|
|
330
|
-
)
|
|
331
|
-
if (
|
|
332
|
-
attribute_name != "sub_attributes"
|
|
333
|
-
or sub_attribute_name != "sub_attributes"
|
|
334
|
-
)
|
|
335
|
-
]
|
|
336
|
-
if is_complex_attribute(root_type)
|
|
337
|
-
else None
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
return Attribute(
|
|
341
|
-
name=field_info.serialization_alias or attribute_name,
|
|
342
|
-
type=attribute_type,
|
|
343
|
-
multi_valued=model.get_field_multiplicity(attribute_name),
|
|
344
|
-
description=field_info.description,
|
|
345
|
-
canonical_values=field_info.examples,
|
|
346
|
-
required=model.get_field_annotation(attribute_name, Required),
|
|
347
|
-
case_exact=model.get_field_annotation(attribute_name, CaseExact),
|
|
348
|
-
mutability=model.get_field_annotation(attribute_name, Mutability),
|
|
349
|
-
returned=model.get_field_annotation(attribute_name, Returned),
|
|
350
|
-
uniqueness=model.get_field_annotation(attribute_name, Uniqueness),
|
|
351
|
-
sub_attributes=sub_attributes,
|
|
352
|
-
reference_types=get_reference_types(root_type)
|
|
353
|
-
if attribute_type == Attribute.Type.reference
|
|
354
|
-
else None,
|
|
355
|
-
)
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
from typing import Annotated
|
|
2
|
-
from typing import Any
|
|
3
|
-
from typing import Generic
|
|
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 Field
|
|
11
|
-
from pydantic import Tag
|
|
12
|
-
from pydantic import ValidationInfo
|
|
13
|
-
from pydantic import ValidatorFunctionWrapHandler
|
|
14
|
-
from pydantic import model_validator
|
|
15
|
-
from pydantic_core import PydanticCustomError
|
|
16
|
-
from typing_extensions import Self
|
|
17
|
-
|
|
18
|
-
from ..base import BaseModel
|
|
19
|
-
from ..base import BaseModelType
|
|
20
|
-
from ..base import Context
|
|
21
|
-
from ..base import Required
|
|
22
|
-
from ..rfc7643.resource import AnyResource
|
|
23
|
-
from ..utils import UNION_TYPES
|
|
24
|
-
from .message import Message
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class ListResponseMetaclass(BaseModelType):
|
|
28
|
-
def tagged_resource_union(resource_union):
|
|
29
|
-
"""Build Discriminated Unions, so pydantic can guess which class are needed to instantiate by inspecting a payload.
|
|
30
|
-
|
|
31
|
-
https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions
|
|
32
|
-
"""
|
|
33
|
-
if get_origin(resource_union) not in UNION_TYPES:
|
|
34
|
-
return resource_union
|
|
35
|
-
|
|
36
|
-
resource_types = get_args(resource_union)
|
|
37
|
-
|
|
38
|
-
def get_schema_from_payload(payload: Any) -> Optional[str]:
|
|
39
|
-
if not payload:
|
|
40
|
-
return None
|
|
41
|
-
|
|
42
|
-
payload_schemas = (
|
|
43
|
-
payload.get("schemas", [])
|
|
44
|
-
if isinstance(payload, dict)
|
|
45
|
-
else payload.schemas
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
resource_types_schemas = [
|
|
49
|
-
resource_type.model_fields["schemas"].default[0]
|
|
50
|
-
for resource_type in resource_types
|
|
51
|
-
]
|
|
52
|
-
common_schemas = [
|
|
53
|
-
schema for schema in payload_schemas if schema in resource_types_schemas
|
|
54
|
-
]
|
|
55
|
-
return common_schemas[0] if common_schemas else None
|
|
56
|
-
|
|
57
|
-
discriminator = Discriminator(get_schema_from_payload)
|
|
58
|
-
|
|
59
|
-
def get_tag(resource_type: type[BaseModel]) -> Tag:
|
|
60
|
-
return Tag(resource_type.model_fields["schemas"].default[0])
|
|
61
|
-
|
|
62
|
-
tagged_resources = [
|
|
63
|
-
Annotated[resource_type, get_tag(resource_type)]
|
|
64
|
-
for resource_type in resource_types
|
|
65
|
-
]
|
|
66
|
-
union = Union[tuple(tagged_resources)]
|
|
67
|
-
return Annotated[union, discriminator]
|
|
68
|
-
|
|
69
|
-
def __new__(cls, name, bases, attrs, **kwargs):
|
|
70
|
-
if kwargs.get("__pydantic_generic_metadata__") and kwargs[
|
|
71
|
-
"__pydantic_generic_metadata__"
|
|
72
|
-
].get("args"):
|
|
73
|
-
tagged_union = cls.tagged_resource_union(
|
|
74
|
-
kwargs["__pydantic_generic_metadata__"]["args"][0]
|
|
75
|
-
)
|
|
76
|
-
kwargs["__pydantic_generic_metadata__"]["args"] = (tagged_union,)
|
|
77
|
-
|
|
78
|
-
klass = super().__new__(cls, name, bases, attrs, **kwargs)
|
|
79
|
-
return klass
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
class ListResponse(Message, Generic[AnyResource], metaclass=ListResponseMetaclass):
|
|
83
|
-
schemas: Annotated[list[str], Required.true] = [
|
|
84
|
-
"urn:ietf:params:scim:api:messages:2.0:ListResponse"
|
|
85
|
-
]
|
|
86
|
-
|
|
87
|
-
total_results: Optional[int] = None
|
|
88
|
-
"""The total number of results returned by the list or query operation."""
|
|
89
|
-
|
|
90
|
-
start_index: Optional[int] = None
|
|
91
|
-
"""The 1-based index of the first result in the current set of list
|
|
92
|
-
results."""
|
|
93
|
-
|
|
94
|
-
items_per_page: Optional[int] = None
|
|
95
|
-
"""The number of resources returned in a list response page."""
|
|
96
|
-
|
|
97
|
-
resources: Optional[list[AnyResource]] = Field(
|
|
98
|
-
None, serialization_alias="Resources"
|
|
99
|
-
)
|
|
100
|
-
"""A multi-valued list of complex objects containing the requested
|
|
101
|
-
resources."""
|
|
102
|
-
|
|
103
|
-
@model_validator(mode="wrap")
|
|
104
|
-
@classmethod
|
|
105
|
-
def check_results_number(
|
|
106
|
-
cls, value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
|
|
107
|
-
) -> Self:
|
|
108
|
-
"""Validate result numbers.
|
|
109
|
-
|
|
110
|
-
:rfc:`RFC7644 §3.4.2 <7644#section-3.4.2.4>` indicates that:
|
|
111
|
-
|
|
112
|
-
- 'totalResults' is required
|
|
113
|
-
- 'resources' must be set if 'totalResults' is non-zero.
|
|
114
|
-
"""
|
|
115
|
-
obj = handler(value)
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
not info.context
|
|
119
|
-
or not info.context.get("scim")
|
|
120
|
-
or not Context.is_response(info.context["scim"])
|
|
121
|
-
):
|
|
122
|
-
return obj
|
|
123
|
-
|
|
124
|
-
if obj.total_results is None:
|
|
125
|
-
raise PydanticCustomError(
|
|
126
|
-
"required_error",
|
|
127
|
-
"Field 'total_results' is required but value is missing or null",
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
if obj.total_results > 0 and not obj.resources:
|
|
131
|
-
raise PydanticCustomError(
|
|
132
|
-
"no_resource_error",
|
|
133
|
-
"Field 'resources' is missing or null but 'total_results' is non-zero.",
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
return obj
|
scim2_models/rfc7644/message.py
DELETED
scim2_models/rfc7644/patch_op.py
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
|
-
from typing import Annotated
|
|
3
|
-
from typing import Any
|
|
4
|
-
from typing import Optional
|
|
5
|
-
|
|
6
|
-
from pydantic import Field
|
|
7
|
-
from pydantic import field_validator
|
|
8
|
-
|
|
9
|
-
from ..base import ComplexAttribute
|
|
10
|
-
from ..base import Required
|
|
11
|
-
from .message import Message
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class PatchOperation(ComplexAttribute):
|
|
15
|
-
class Op(str, Enum):
|
|
16
|
-
replace_ = "replace"
|
|
17
|
-
remove = "remove"
|
|
18
|
-
add = "add"
|
|
19
|
-
|
|
20
|
-
op: Optional[Optional[Op]] = None
|
|
21
|
-
"""Each PATCH operation object MUST have exactly one "op" member, whose
|
|
22
|
-
value indicates the operation to perform and MAY be one of "add", "remove",
|
|
23
|
-
or "replace".
|
|
24
|
-
|
|
25
|
-
.. note::
|
|
26
|
-
|
|
27
|
-
For the sake of compatibility with Microsoft Entra,
|
|
28
|
-
despite :rfc:`RFC7644 §3.5.2 <7644#section-3.5.2>`, op is case-insensitive.
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
path: Optional[str] = None
|
|
32
|
-
"""The "path" attribute value is a String containing an attribute path
|
|
33
|
-
describing the target of the operation."""
|
|
34
|
-
|
|
35
|
-
value: Optional[Any] = None
|
|
36
|
-
|
|
37
|
-
@field_validator("op", mode="before")
|
|
38
|
-
@classmethod
|
|
39
|
-
def normalize_op(cls, v):
|
|
40
|
-
"""Ignorecase for op.
|
|
41
|
-
|
|
42
|
-
This brings
|
|
43
|
-
`compatibility with Microsoft Entra <https://learn.microsoft.com/en-us/entra/identity/app-provisioning/use-scim-to-provision-users-and-groups#general>`_:
|
|
44
|
-
|
|
45
|
-
Don't require a case-sensitive match on structural elements in SCIM,
|
|
46
|
-
in particular PATCH op operation values, as defined in section 3.5.2.
|
|
47
|
-
Microsoft Entra ID emits the values of op as Add, Replace, and Remove.
|
|
48
|
-
"""
|
|
49
|
-
if isinstance(v, str):
|
|
50
|
-
return v.lower()
|
|
51
|
-
return v
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
class PatchOp(Message):
|
|
55
|
-
"""Patch Operation as defined in :rfc:`RFC7644 §3.5.2 <7644#section-3.5.2>`.
|
|
56
|
-
|
|
57
|
-
.. todo::
|
|
58
|
-
|
|
59
|
-
The models for Patch operations are defined, but their behavior is not implemented nor tested yet.
|
|
60
|
-
"""
|
|
61
|
-
|
|
62
|
-
schemas: Annotated[list[str], Required.true] = [
|
|
63
|
-
"urn:ietf:params:scim:api:messages:2.0:PatchOp"
|
|
64
|
-
]
|
|
65
|
-
|
|
66
|
-
operations: Optional[list[PatchOperation]] = Field(
|
|
67
|
-
None, serialization_alias="Operations"
|
|
68
|
-
)
|
|
69
|
-
"""The body of an HTTP PATCH request MUST contain the attribute
|
|
70
|
-
"Operations", whose value is an array of one or more PATCH operations."""
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
scim2_models/__init__.py,sha256=Y06vTA_51lXfv9zk_dTzyIIqEo1H2bcencvMM5KAwn8,3063
|
|
2
|
-
scim2_models/base.py,sha256=NV-uaDWG_pN6Cl_PT1E9PqdG7QH7jMMADGi3PVVurHQ,34917
|
|
3
|
-
scim2_models/constants.py,sha256=SuMGFtVNletdV5ZJRUcIq7o2CqZCRvOurnIdLE_cakE,540
|
|
4
|
-
scim2_models/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
scim2_models/utils.py,sha256=MzZz212-lkVWgXcpXvNwoi_u28wBTpkTwPrfYC5v92A,2771
|
|
6
|
-
scim2_models/rfc7643/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
scim2_models/rfc7643/enterprise_user.py,sha256=EaxdHH2dcBrwWwGpaZC6iZ9dbcaVN1NpoRLAdkTYssQ,1781
|
|
8
|
-
scim2_models/rfc7643/group.py,sha256=JML8OtgKjvHHv2YufiYcTtbZuN5GFVQTbm1J6MLtp8o,1427
|
|
9
|
-
scim2_models/rfc7643/resource.py,sha256=oEm0RVBFonTrrhS-iYQquAdiy4UW4nXHbk_Bf--fR3g,12849
|
|
10
|
-
scim2_models/rfc7643/resource_type.py,sha256=vT2ItHvBzmsFVt3zbIJABxjcnSLUK-mW-zWduHxGE3k,3570
|
|
11
|
-
scim2_models/rfc7643/schema.py,sha256=B7TzMbT6ngYQrMqvqW5_LberN6EaqtZaFwBVsgoA3S0,10388
|
|
12
|
-
scim2_models/rfc7643/service_provider_config.py,sha256=deMNCXlqiNzuLcVRN9mdHiTUxhczDnvi-oO6k-Anj8U,5402
|
|
13
|
-
scim2_models/rfc7643/user.py,sha256=EEje4V_zbMVwVYOu2Gj1W7GjD90WnC0lUK3FJuh4jWE,10607
|
|
14
|
-
scim2_models/rfc7644/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
scim2_models/rfc7644/bulk.py,sha256=I6S40kyJPwDQwPFi668wFFDVTST7z6QTe9HTL5QUViI,2577
|
|
16
|
-
scim2_models/rfc7644/error.py,sha256=l4-vtGQYm5u13-ParhbHSeqXEil0E09QXSO9twAT3SU,6185
|
|
17
|
-
scim2_models/rfc7644/list_response.py,sha256=GNfRKpL2WK2KHGC3kg7ldaSiPwCXl0AxDfhnbHByEAY,4589
|
|
18
|
-
scim2_models/rfc7644/message.py,sha256=F4kPqbHAka3-wZzap9a45noQZw-o1vznTJypNABBF7w,253
|
|
19
|
-
scim2_models/rfc7644/patch_op.py,sha256=OE-ixDanTkY5zQP7EK7OAp88uE_fMk03mqmaZHxgJ-g,2210
|
|
20
|
-
scim2_models/rfc7644/search_request.py,sha256=DRGlixcWtYtbUuP9MT7PsnvyxlONLcXGEcQveWdqQng,3003
|
|
21
|
-
scim2_models-0.3.6.dist-info/METADATA,sha256=ihhjxgKo0dF-ufWhrgCdsNiH6vGXOplFlga07UVHrLw,16288
|
|
22
|
-
scim2_models-0.3.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
23
|
-
scim2_models-0.3.6.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
24
|
-
scim2_models-0.3.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|