scim2-models 0.4.1__py3-none-any.whl → 0.5.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/attributes.py +2 -2
- scim2_models/base.py +2 -2
- scim2_models/messages/message.py +2 -2
- scim2_models/messages/patch_op.py +96 -50
- scim2_models/messages/search_request.py +0 -2
- scim2_models/reference.py +0 -2
- scim2_models/resources/group.py +3 -3
- scim2_models/resources/resource.py +8 -12
- scim2_models/resources/resource_type.py +3 -2
- scim2_models/resources/schema.py +5 -12
- scim2_models/resources/service_provider_config.py +4 -3
- scim2_models/resources/user.py +43 -12
- scim2_models/urn.py +30 -12
- scim2_models/utils.py +13 -13
- scim2_models-0.5.0.dist-info/METADATA +280 -0
- scim2_models-0.5.0.dist-info/RECORD +29 -0
- scim2_models-0.5.0.dist-info/WHEEL +4 -0
- scim2_models-0.4.1.dist-info/METADATA +0 -280
- scim2_models-0.4.1.dist-info/RECORD +0 -30
- scim2_models-0.4.1.dist-info/WHEEL +0 -4
- scim2_models-0.4.1.dist-info/licenses/LICENSE +0 -201
scim2_models/attributes.py
CHANGED
|
@@ -43,7 +43,7 @@ class MultiValuedComplexAttribute(ComplexAttribute):
|
|
|
43
43
|
value: Optional[Any] = None
|
|
44
44
|
"""The value of an entitlement."""
|
|
45
45
|
|
|
46
|
-
ref: Optional[Reference] = Field(None, serialization_alias="$ref")
|
|
46
|
+
ref: Optional[Reference[Any]] = Field(None, serialization_alias="$ref")
|
|
47
47
|
"""The reference URI of a target resource, if the attribute is a
|
|
48
48
|
reference."""
|
|
49
49
|
|
|
@@ -53,5 +53,5 @@ def is_complex_attribute(type_: type) -> bool:
|
|
|
53
53
|
return (
|
|
54
54
|
get_origin(type_) != Reference
|
|
55
55
|
and isclass(type_)
|
|
56
|
-
and issubclass(type_,
|
|
56
|
+
and issubclass(type_, ComplexAttribute)
|
|
57
57
|
)
|
scim2_models/base.py
CHANGED
|
@@ -220,8 +220,8 @@ class BaseModel(PydanticBaseModel):
|
|
|
220
220
|
"""
|
|
221
221
|
|
|
222
222
|
def normalize_dict_keys(
|
|
223
|
-
input_dict: dict, model_class: type["BaseModel"]
|
|
224
|
-
) -> dict:
|
|
223
|
+
input_dict: dict[str, Any], model_class: type["BaseModel"]
|
|
224
|
+
) -> dict[str, Any]:
|
|
225
225
|
"""Normalize dictionary keys, preserving case for Any fields."""
|
|
226
226
|
result = {}
|
|
227
227
|
|
scim2_models/messages/message.py
CHANGED
|
@@ -112,8 +112,8 @@ class _GenericMessageMetaclass(ModelMetaclass):
|
|
|
112
112
|
return klass
|
|
113
113
|
|
|
114
114
|
|
|
115
|
-
def _get_resource_class(obj) ->
|
|
115
|
+
def _get_resource_class(obj: BaseModel) -> type[Resource[Any]]:
|
|
116
116
|
"""Extract the resource class from generic type parameter."""
|
|
117
117
|
metadata = getattr(obj.__class__, "__pydantic_generic_metadata__", {"args": [None]})
|
|
118
118
|
resource_class = metadata["args"][0]
|
|
119
|
-
return resource_class
|
|
119
|
+
return resource_class # type: ignore[no-any-return]
|
|
@@ -5,6 +5,7 @@ from typing import Any
|
|
|
5
5
|
from typing import Generic
|
|
6
6
|
from typing import Optional
|
|
7
7
|
from typing import TypeVar
|
|
8
|
+
from typing import Union
|
|
8
9
|
|
|
9
10
|
from pydantic import Field
|
|
10
11
|
from pydantic import ValidationInfo
|
|
@@ -21,12 +22,13 @@ from ..resources.resource import Resource
|
|
|
21
22
|
from ..urn import _resolve_path_to_target
|
|
22
23
|
from ..utils import _extract_field_name
|
|
23
24
|
from ..utils import _find_field_name
|
|
25
|
+
from ..utils import _get_path_parts
|
|
24
26
|
from ..utils import _validate_scim_path_syntax
|
|
25
27
|
from .error import Error
|
|
26
28
|
from .message import Message
|
|
27
29
|
from .message import _get_resource_class
|
|
28
30
|
|
|
29
|
-
T = TypeVar("T", bound=Resource)
|
|
31
|
+
T = TypeVar("T", bound=Resource[Any])
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
class PatchOperation(ComplexAttribute):
|
|
@@ -51,7 +53,7 @@ class PatchOperation(ComplexAttribute):
|
|
|
51
53
|
describing the target of the operation."""
|
|
52
54
|
|
|
53
55
|
def _validate_mutability(
|
|
54
|
-
self, resource_class: type[
|
|
56
|
+
self, resource_class: type[Resource[Any]], field_name: str
|
|
55
57
|
) -> None:
|
|
56
58
|
"""Validate mutability constraints."""
|
|
57
59
|
# RFC 7644 Section 3.5.2: "Servers should be tolerant of schema extensions"
|
|
@@ -72,7 +74,7 @@ class PatchOperation(ComplexAttribute):
|
|
|
72
74
|
raise ValueError(Error.make_mutability_error().detail)
|
|
73
75
|
|
|
74
76
|
def _validate_required_attribute(
|
|
75
|
-
self, resource_class: type[
|
|
77
|
+
self, resource_class: type[Resource[Any]], field_name: str
|
|
76
78
|
) -> None:
|
|
77
79
|
"""Validate required attribute constraints for remove operations."""
|
|
78
80
|
# RFC 7644 Section 3.5.2.3: Only validate for remove operations
|
|
@@ -143,7 +145,7 @@ class PatchOp(Message, Generic[T]):
|
|
|
143
145
|
- Using PatchOp without a type parameter raises TypeError
|
|
144
146
|
"""
|
|
145
147
|
|
|
146
|
-
def __new__(cls, *args: Any, **kwargs: Any):
|
|
148
|
+
def __new__(cls, *args: Any, **kwargs: Any) -> Self:
|
|
147
149
|
"""Create new PatchOp instance with type parameter validation.
|
|
148
150
|
|
|
149
151
|
Only handles the case of direct instantiation without type parameter (PatchOp()).
|
|
@@ -162,39 +164,48 @@ class PatchOp(Message, Generic[T]):
|
|
|
162
164
|
|
|
163
165
|
return super().__new__(cls)
|
|
164
166
|
|
|
165
|
-
def __class_getitem__(
|
|
167
|
+
def __class_getitem__(
|
|
168
|
+
cls, typevar_values: Union[type[Resource[Any]], tuple[type[Resource[Any]], ...]]
|
|
169
|
+
) -> Any:
|
|
166
170
|
"""Validate type parameter when creating parameterized type.
|
|
167
171
|
|
|
168
172
|
Ensures the type parameter is a concrete Resource subclass (not Resource itself)
|
|
169
173
|
or a TypeVar bound to Resource. Rejects invalid types (str, int, etc.) and Union types.
|
|
170
174
|
"""
|
|
171
|
-
|
|
172
|
-
if isinstance(item, TypeVar):
|
|
175
|
+
if isinstance(typevar_values, TypeVar):
|
|
173
176
|
# Check if TypeVar is bound to Resource or its subclass
|
|
174
|
-
if
|
|
175
|
-
|
|
176
|
-
or (
|
|
177
|
+
if typevar_values.__bound__ is not None and (
|
|
178
|
+
typevar_values.__bound__ is Resource
|
|
179
|
+
or (
|
|
180
|
+
isclass(typevar_values.__bound__)
|
|
181
|
+
and issubclass(typevar_values.__bound__, Resource)
|
|
182
|
+
)
|
|
177
183
|
):
|
|
178
|
-
return super().__class_getitem__(
|
|
184
|
+
return super().__class_getitem__(typevar_values)
|
|
179
185
|
else:
|
|
180
186
|
raise TypeError(
|
|
181
|
-
f"PatchOp TypeVar must be bound to Resource or its subclass, got {
|
|
187
|
+
f"PatchOp TypeVar must be bound to Resource or its subclass, got {typevar_values}. "
|
|
182
188
|
"Example: T = TypeVar('T', bound=Resource)"
|
|
183
189
|
)
|
|
184
190
|
|
|
185
191
|
# Check if type parameter is a concrete Resource subclass (not Resource itself)
|
|
186
|
-
if
|
|
192
|
+
if typevar_values is Resource:
|
|
187
193
|
raise TypeError(
|
|
188
194
|
"PatchOp requires a concrete Resource subclass, not Resource itself. "
|
|
189
195
|
"Use PatchOp[User], PatchOp[Group], etc. instead of PatchOp[Resource]."
|
|
190
196
|
)
|
|
191
|
-
|
|
197
|
+
|
|
198
|
+
if not (
|
|
199
|
+
isclass(typevar_values)
|
|
200
|
+
and issubclass(typevar_values, Resource)
|
|
201
|
+
and typevar_values is not Resource
|
|
202
|
+
):
|
|
192
203
|
raise TypeError(
|
|
193
|
-
f"PatchOp type parameter must be a concrete Resource subclass or TypeVar, got {
|
|
204
|
+
f"PatchOp type parameter must be a concrete Resource subclass or TypeVar, got {typevar_values}. "
|
|
194
205
|
"Use PatchOp[User], PatchOp[Group], etc."
|
|
195
206
|
)
|
|
196
207
|
|
|
197
|
-
return super().__class_getitem__(
|
|
208
|
+
return super().__class_getitem__(typevar_values)
|
|
198
209
|
|
|
199
210
|
schemas: Annotated[list[str], Required.true] = [
|
|
200
211
|
"urn:ietf:params:scim:api:messages:2.0:PatchOp"
|
|
@@ -207,12 +218,17 @@ class PatchOp(Message, Generic[T]):
|
|
|
207
218
|
"Operations", whose value is an array of one or more PATCH operations."""
|
|
208
219
|
|
|
209
220
|
@model_validator(mode="after")
|
|
210
|
-
def validate_operations(self) -> Self:
|
|
221
|
+
def validate_operations(self, info: ValidationInfo) -> Self:
|
|
211
222
|
"""Validate operations against resource type metadata if available.
|
|
212
223
|
|
|
213
224
|
When PatchOp is used with a specific resource type (e.g., PatchOp[User]),
|
|
214
225
|
this validator will automatically check mutability and required constraints.
|
|
215
226
|
"""
|
|
227
|
+
# RFC 7644: The body of an HTTP PATCH request MUST contain the attribute "Operations"
|
|
228
|
+
scim_ctx = info.context.get("scim") if info.context else None
|
|
229
|
+
if scim_ctx == Context.RESOURCE_PATCH_REQUEST and self.operations is None:
|
|
230
|
+
raise ValueError(Error.make_invalid_value_error().detail)
|
|
231
|
+
|
|
216
232
|
resource_class = _get_resource_class(self)
|
|
217
233
|
if resource_class is None or not self.operations:
|
|
218
234
|
return self
|
|
@@ -254,7 +270,9 @@ class PatchOp(Message, Generic[T]):
|
|
|
254
270
|
|
|
255
271
|
return modified
|
|
256
272
|
|
|
257
|
-
def _apply_operation(
|
|
273
|
+
def _apply_operation(
|
|
274
|
+
self, resource: Resource[Any], operation: PatchOperation
|
|
275
|
+
) -> bool:
|
|
258
276
|
"""Apply a single patch operation to a resource.
|
|
259
277
|
|
|
260
278
|
:return: :data:`True` if the resource was modified, else :data:`False`.
|
|
@@ -266,7 +284,9 @@ class PatchOp(Message, Generic[T]):
|
|
|
266
284
|
|
|
267
285
|
raise ValueError(Error.make_invalid_value_error().detail)
|
|
268
286
|
|
|
269
|
-
def _apply_add_replace(
|
|
287
|
+
def _apply_add_replace(
|
|
288
|
+
self, resource: Resource[Any], operation: PatchOperation
|
|
289
|
+
) -> bool:
|
|
270
290
|
"""Apply an add or replace operation."""
|
|
271
291
|
# RFC 7644 Section 3.5.2.1: "If path is specified, add/replace at that path"
|
|
272
292
|
if operation.path is not None:
|
|
@@ -280,7 +300,7 @@ class PatchOp(Message, Generic[T]):
|
|
|
280
300
|
# RFC 7644 Section 3.5.2.1: "If no path specified, add/replace at root level"
|
|
281
301
|
return self._apply_root_attributes(resource, operation.value)
|
|
282
302
|
|
|
283
|
-
def _apply_remove(self, resource: Resource, operation: PatchOperation) -> bool:
|
|
303
|
+
def _apply_remove(self, resource: Resource[Any], operation: PatchOperation) -> bool:
|
|
284
304
|
"""Apply a remove operation."""
|
|
285
305
|
# RFC 7644 Section 3.5.2.3: "Path is required for remove operations"
|
|
286
306
|
if operation.path is None:
|
|
@@ -294,7 +314,8 @@ class PatchOp(Message, Generic[T]):
|
|
|
294
314
|
|
|
295
315
|
return self._remove_value_at_path(resource, operation.path)
|
|
296
316
|
|
|
297
|
-
|
|
317
|
+
@classmethod
|
|
318
|
+
def _apply_root_attributes(cls, resource: BaseModel, value: Any) -> bool:
|
|
298
319
|
"""Apply attributes to the resource root."""
|
|
299
320
|
if not isinstance(value, dict):
|
|
300
321
|
return False
|
|
@@ -312,23 +333,34 @@ class PatchOp(Message, Generic[T]):
|
|
|
312
333
|
|
|
313
334
|
return modified
|
|
314
335
|
|
|
336
|
+
@classmethod
|
|
315
337
|
def _set_value_at_path(
|
|
316
|
-
|
|
338
|
+
cls, resource: Resource[Any], path: str, value: Any, is_add: bool
|
|
317
339
|
) -> bool:
|
|
318
340
|
"""Set a value at a specific path."""
|
|
319
341
|
target, attr_path = _resolve_path_to_target(resource, path)
|
|
320
342
|
|
|
321
|
-
if not
|
|
343
|
+
if not target:
|
|
322
344
|
raise ValueError(Error.make_invalid_path_error().detail)
|
|
323
345
|
|
|
324
|
-
|
|
346
|
+
if not attr_path:
|
|
347
|
+
if not isinstance(value, dict):
|
|
348
|
+
raise ValueError(Error.make_invalid_path_error().detail)
|
|
349
|
+
|
|
350
|
+
updated_data = {**target.model_dump(), **value}
|
|
351
|
+
updated_target = type(target).model_validate(updated_data)
|
|
352
|
+
target.__dict__.update(updated_target.__dict__)
|
|
353
|
+
return True
|
|
354
|
+
|
|
355
|
+
path_parts = _get_path_parts(attr_path)
|
|
325
356
|
if len(path_parts) == 1:
|
|
326
|
-
return
|
|
357
|
+
return cls._set_simple_attribute(target, path_parts[0], value, is_add)
|
|
327
358
|
|
|
328
|
-
return
|
|
359
|
+
return cls._set_complex_attribute(target, path_parts, value, is_add)
|
|
329
360
|
|
|
361
|
+
@classmethod
|
|
330
362
|
def _set_simple_attribute(
|
|
331
|
-
|
|
363
|
+
cls, resource: BaseModel, attr_name: str, value: Any, is_add: bool
|
|
332
364
|
) -> bool:
|
|
333
365
|
"""Set a value on a simple (non-nested) attribute."""
|
|
334
366
|
field_name = _find_field_name(type(resource), attr_name)
|
|
@@ -336,8 +368,8 @@ class PatchOp(Message, Generic[T]):
|
|
|
336
368
|
raise ValueError(Error.make_no_target_error().detail)
|
|
337
369
|
|
|
338
370
|
# RFC 7644 Section 3.5.2.1: "For multi-valued attributes, add operation appends values"
|
|
339
|
-
if is_add and
|
|
340
|
-
return
|
|
371
|
+
if is_add and cls._is_multivalued_field(resource, field_name):
|
|
372
|
+
return cls._handle_multivalued_add(resource, field_name, value)
|
|
341
373
|
|
|
342
374
|
old_value = getattr(resource, field_name)
|
|
343
375
|
if old_value == value:
|
|
@@ -346,8 +378,9 @@ class PatchOp(Message, Generic[T]):
|
|
|
346
378
|
setattr(resource, field_name, value)
|
|
347
379
|
return True
|
|
348
380
|
|
|
381
|
+
@classmethod
|
|
349
382
|
def _set_complex_attribute(
|
|
350
|
-
|
|
383
|
+
cls, resource: BaseModel, path_parts: list[str], value: Any, is_add: bool
|
|
351
384
|
) -> bool:
|
|
352
385
|
"""Set a value on a complex (nested) attribute."""
|
|
353
386
|
parent_attr = path_parts[0]
|
|
@@ -359,38 +392,45 @@ class PatchOp(Message, Generic[T]):
|
|
|
359
392
|
|
|
360
393
|
parent_obj = getattr(resource, parent_field_name)
|
|
361
394
|
if parent_obj is None:
|
|
362
|
-
parent_obj =
|
|
395
|
+
parent_obj = cls._create_parent_object(resource, parent_field_name)
|
|
363
396
|
if parent_obj is None:
|
|
364
397
|
return False
|
|
365
398
|
|
|
366
|
-
return
|
|
399
|
+
return cls._set_value_at_path(parent_obj, sub_path, value, is_add)
|
|
367
400
|
|
|
368
|
-
|
|
401
|
+
@classmethod
|
|
402
|
+
def _is_multivalued_field(cls, resource: BaseModel, field_name: str) -> bool:
|
|
369
403
|
"""Check if a field is multi-valued."""
|
|
370
404
|
return hasattr(resource, field_name) and type(resource).get_field_multiplicity(
|
|
371
405
|
field_name
|
|
372
406
|
)
|
|
373
407
|
|
|
408
|
+
@classmethod
|
|
374
409
|
def _handle_multivalued_add(
|
|
375
|
-
|
|
410
|
+
cls, resource: BaseModel, field_name: str, value: Any
|
|
376
411
|
) -> bool:
|
|
377
412
|
"""Handle adding values to a multi-valued attribute."""
|
|
378
413
|
current_list = getattr(resource, field_name) or []
|
|
379
414
|
|
|
380
415
|
# RFC 7644 Section 3.5.2.1: "Add operation appends values to multi-valued attributes"
|
|
381
416
|
if isinstance(value, list):
|
|
382
|
-
return
|
|
417
|
+
return cls._add_multiple_values(resource, field_name, current_list, value)
|
|
383
418
|
|
|
384
|
-
return
|
|
419
|
+
return cls._add_single_value(resource, field_name, current_list, value)
|
|
385
420
|
|
|
421
|
+
@classmethod
|
|
386
422
|
def _add_multiple_values(
|
|
387
|
-
|
|
423
|
+
cls,
|
|
424
|
+
resource: BaseModel,
|
|
425
|
+
field_name: str,
|
|
426
|
+
current_list: list[Any],
|
|
427
|
+
values: list[Any],
|
|
388
428
|
) -> bool:
|
|
389
429
|
"""Add multiple values to a multi-valued attribute."""
|
|
390
430
|
new_values = []
|
|
391
431
|
# RFC 7644 Section 3.5.2.1: "Do not add duplicate values"
|
|
392
432
|
for new_val in values:
|
|
393
|
-
if not
|
|
433
|
+
if not cls._value_exists_in_list(current_list, new_val):
|
|
394
434
|
new_values.append(new_val)
|
|
395
435
|
|
|
396
436
|
if not new_values:
|
|
@@ -399,23 +439,26 @@ class PatchOp(Message, Generic[T]):
|
|
|
399
439
|
setattr(resource, field_name, current_list + new_values)
|
|
400
440
|
return True
|
|
401
441
|
|
|
442
|
+
@classmethod
|
|
402
443
|
def _add_single_value(
|
|
403
|
-
|
|
444
|
+
cls, resource: BaseModel, field_name: str, current_list: list[Any], value: Any
|
|
404
445
|
) -> bool:
|
|
405
446
|
"""Add a single value to a multi-valued attribute."""
|
|
406
447
|
# RFC 7644 Section 3.5.2.1: "Do not add duplicate values"
|
|
407
|
-
if
|
|
448
|
+
if cls._value_exists_in_list(current_list, value):
|
|
408
449
|
return False
|
|
409
450
|
|
|
410
451
|
current_list.append(value)
|
|
411
452
|
setattr(resource, field_name, current_list)
|
|
412
453
|
return True
|
|
413
454
|
|
|
414
|
-
|
|
455
|
+
@classmethod
|
|
456
|
+
def _value_exists_in_list(cls, current_list: list[Any], new_value: Any) -> bool:
|
|
415
457
|
"""Check if a value already exists in a list."""
|
|
416
|
-
return any(
|
|
458
|
+
return any(cls._values_match(item, new_value) for item in current_list)
|
|
417
459
|
|
|
418
|
-
|
|
460
|
+
@classmethod
|
|
461
|
+
def _create_parent_object(cls, resource: BaseModel, parent_field_name: str) -> Any:
|
|
419
462
|
"""Create a parent object if it doesn't exist."""
|
|
420
463
|
parent_class = type(resource).get_field_root_type(parent_field_name)
|
|
421
464
|
if not parent_class or not isclass(parent_class):
|
|
@@ -425,7 +468,8 @@ class PatchOp(Message, Generic[T]):
|
|
|
425
468
|
setattr(resource, parent_field_name, parent_obj)
|
|
426
469
|
return parent_obj
|
|
427
470
|
|
|
428
|
-
|
|
471
|
+
@classmethod
|
|
472
|
+
def _remove_value_at_path(cls, resource: Resource[Any], path: str) -> bool:
|
|
429
473
|
"""Remove a value at a specific path."""
|
|
430
474
|
target, attr_path = _resolve_path_to_target(resource, path)
|
|
431
475
|
|
|
@@ -433,7 +477,7 @@ class PatchOp(Message, Generic[T]):
|
|
|
433
477
|
if not attr_path or not target:
|
|
434
478
|
raise ValueError(Error.make_invalid_path_error().detail)
|
|
435
479
|
|
|
436
|
-
parent_attr, *path_parts = attr_path
|
|
480
|
+
parent_attr, *path_parts = _get_path_parts(attr_path)
|
|
437
481
|
field_name = _find_field_name(type(target), parent_attr)
|
|
438
482
|
if not field_name:
|
|
439
483
|
raise ValueError(Error.make_no_target_error().detail)
|
|
@@ -448,10 +492,11 @@ class PatchOp(Message, Generic[T]):
|
|
|
448
492
|
return True
|
|
449
493
|
|
|
450
494
|
sub_path = ".".join(path_parts)
|
|
451
|
-
return
|
|
495
|
+
return cls._remove_value_at_path(parent_obj, sub_path)
|
|
452
496
|
|
|
497
|
+
@classmethod
|
|
453
498
|
def _remove_specific_value(
|
|
454
|
-
|
|
499
|
+
cls, resource: Resource[Any], path: str, value_to_remove: Any
|
|
455
500
|
) -> bool:
|
|
456
501
|
"""Remove a specific value from a multi-valued attribute."""
|
|
457
502
|
target, attr_path = _resolve_path_to_target(resource, path)
|
|
@@ -472,7 +517,7 @@ class PatchOp(Message, Generic[T]):
|
|
|
472
517
|
modified = False
|
|
473
518
|
# RFC 7644 Section 3.5.2.3: "Remove matching values from multi-valued attributes"
|
|
474
519
|
for item in current_list:
|
|
475
|
-
if not
|
|
520
|
+
if not cls._values_match(item, value_to_remove):
|
|
476
521
|
new_list.append(item)
|
|
477
522
|
else:
|
|
478
523
|
modified = True
|
|
@@ -483,10 +528,11 @@ class PatchOp(Message, Generic[T]):
|
|
|
483
528
|
|
|
484
529
|
return False
|
|
485
530
|
|
|
486
|
-
|
|
531
|
+
@classmethod
|
|
532
|
+
def _values_match(cls, value1: Any, value2: Any) -> bool:
|
|
487
533
|
"""Check if two values match, converting BaseModel to dict for comparison."""
|
|
488
534
|
|
|
489
|
-
def to_dict(value):
|
|
535
|
+
def to_dict(value: Any) -> dict[str, Any]:
|
|
490
536
|
return value.model_dump() if isinstance(value, BaseModel) else value
|
|
491
537
|
|
|
492
538
|
return to_dict(value1) == to_dict(value2)
|
|
@@ -71,9 +71,7 @@ class SearchRequest(Message):
|
|
|
71
71
|
"""Validate syntax of sort_by attribute path.
|
|
72
72
|
|
|
73
73
|
:param v: The sort_by attribute path to validate
|
|
74
|
-
:type v: Optional[str]
|
|
75
74
|
:return: The validated sort_by attribute path
|
|
76
|
-
:rtype: Optional[str]
|
|
77
75
|
:raises ValueError: If sort_by attribute path has invalid syntax
|
|
78
76
|
"""
|
|
79
77
|
if v is None:
|
scim2_models/reference.py
CHANGED
|
@@ -61,9 +61,7 @@ class Reference(UserString, Generic[ReferenceTypes]):
|
|
|
61
61
|
"""Get reference types from a type annotation.
|
|
62
62
|
|
|
63
63
|
:param type_annotation: Type annotation to extract reference types from
|
|
64
|
-
:type type_annotation: Any
|
|
65
64
|
:return: List of reference type strings
|
|
66
|
-
:rtype: list[str]
|
|
67
65
|
"""
|
|
68
66
|
first_arg = get_args(type_annotation)[0]
|
|
69
67
|
types = (
|
scim2_models/resources/group.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from typing import Annotated
|
|
2
|
+
from typing import Any
|
|
2
3
|
from typing import ClassVar
|
|
3
4
|
from typing import Literal
|
|
4
5
|
from typing import Optional
|
|
@@ -9,12 +10,11 @@ from pydantic import Field
|
|
|
9
10
|
from ..annotations import Mutability
|
|
10
11
|
from ..annotations import Required
|
|
11
12
|
from ..attributes import ComplexAttribute
|
|
12
|
-
from ..attributes import MultiValuedComplexAttribute
|
|
13
13
|
from ..reference import Reference
|
|
14
14
|
from .resource import Resource
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
class GroupMember(
|
|
17
|
+
class GroupMember(ComplexAttribute):
|
|
18
18
|
value: Annotated[Optional[str], Mutability.immutable] = None
|
|
19
19
|
"""Identifier of the member of this Group."""
|
|
20
20
|
|
|
@@ -33,7 +33,7 @@ class GroupMember(MultiValuedComplexAttribute):
|
|
|
33
33
|
display: Annotated[Optional[str], Mutability.read_only] = None
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
class Group(Resource):
|
|
36
|
+
class Group(Resource[Any]):
|
|
37
37
|
schemas: Annotated[list[str], Required.true] = [
|
|
38
38
|
"urn:ietf:params:scim:schemas:core:2.0:Group"
|
|
39
39
|
]
|
|
@@ -22,7 +22,6 @@ from ..annotations import Required
|
|
|
22
22
|
from ..annotations import Returned
|
|
23
23
|
from ..annotations import Uniqueness
|
|
24
24
|
from ..attributes import ComplexAttribute
|
|
25
|
-
from ..attributes import MultiValuedComplexAttribute
|
|
26
25
|
from ..attributes import is_complex_attribute
|
|
27
26
|
from ..base import BaseModel
|
|
28
27
|
from ..context import Context
|
|
@@ -148,7 +147,7 @@ class Resource(ScimObject, Generic[AnyExtension]):
|
|
|
148
147
|
"""A complex attribute containing resource metadata."""
|
|
149
148
|
|
|
150
149
|
@classmethod
|
|
151
|
-
def __class_getitem__(cls, item: Any) -> type["Resource"]:
|
|
150
|
+
def __class_getitem__(cls, item: Any) -> type["Resource[Any]"]:
|
|
152
151
|
"""Create a Resource class with extension fields dynamically added."""
|
|
153
152
|
if hasattr(cls, "__scim_extension_metadata__"):
|
|
154
153
|
return cls
|
|
@@ -241,12 +240,12 @@ class Resource(ScimObject, Generic[AnyExtension]):
|
|
|
241
240
|
|
|
242
241
|
@staticmethod
|
|
243
242
|
def get_by_schema(
|
|
244
|
-
resource_types: list[type["Resource"]],
|
|
243
|
+
resource_types: list[type["Resource[Any]"]],
|
|
245
244
|
schema: str,
|
|
246
245
|
with_extensions: bool = True,
|
|
247
|
-
) -> Optional[Union[type["Resource"], type["Extension"]]]:
|
|
246
|
+
) -> Optional[Union[type["Resource[Any]"], type["Extension"]]]:
|
|
248
247
|
"""Given a resource type list and a schema, find the matching resource type."""
|
|
249
|
-
by_schema: dict[str, Union[type[Resource], type[Extension]]] = {
|
|
248
|
+
by_schema: dict[str, Union[type[Resource[Any]], type[Extension]]] = {
|
|
250
249
|
resource_type.model_fields["schemas"].default[0].lower(): resource_type
|
|
251
250
|
for resource_type in (resource_types or [])
|
|
252
251
|
}
|
|
@@ -263,7 +262,7 @@ class Resource(ScimObject, Generic[AnyExtension]):
|
|
|
263
262
|
|
|
264
263
|
@staticmethod
|
|
265
264
|
def get_by_payload(
|
|
266
|
-
resource_types: list[type["Resource"]],
|
|
265
|
+
resource_types: list[type["Resource[Any]"]],
|
|
267
266
|
payload: dict[str, Any],
|
|
268
267
|
**kwargs: Any,
|
|
269
268
|
) -> Optional[type]:
|
|
@@ -291,7 +290,7 @@ class Resource(ScimObject, Generic[AnyExtension]):
|
|
|
291
290
|
return _model_to_schema(cls)
|
|
292
291
|
|
|
293
292
|
@classmethod
|
|
294
|
-
def from_schema(cls, schema: "Schema") -> type["Resource"]:
|
|
293
|
+
def from_schema(cls, schema: "Schema") -> type["Resource[Any]"]:
|
|
295
294
|
"""Build a :class:`scim2_models.Resource` subclass from the schema definition."""
|
|
296
295
|
from .schema import _make_python_model
|
|
297
296
|
|
|
@@ -372,7 +371,7 @@ class Resource(ScimObject, Generic[AnyExtension]):
|
|
|
372
371
|
return super(ScimObject, self).model_dump_json(*args, **dump_kwargs)
|
|
373
372
|
|
|
374
373
|
|
|
375
|
-
AnyResource = TypeVar("AnyResource", bound="Resource")
|
|
374
|
+
AnyResource = TypeVar("AnyResource", bound="Resource[Any]")
|
|
376
375
|
|
|
377
376
|
|
|
378
377
|
def _dedicated_attributes(
|
|
@@ -437,10 +436,7 @@ def _model_attribute_to_scim_attribute(
|
|
|
437
436
|
sub_attributes = (
|
|
438
437
|
[
|
|
439
438
|
_model_attribute_to_scim_attribute(root_type, sub_attribute_name)
|
|
440
|
-
for sub_attribute_name in
|
|
441
|
-
root_type,
|
|
442
|
-
[MultiValuedComplexAttribute],
|
|
443
|
-
)
|
|
439
|
+
for sub_attribute_name in root_type.model_fields # type: ignore
|
|
444
440
|
if (
|
|
445
441
|
attribute_name != "sub_attributes"
|
|
446
442
|
or sub_attribute_name != "sub_attributes"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from typing import Annotated
|
|
2
|
+
from typing import Any
|
|
2
3
|
from typing import Optional
|
|
3
4
|
|
|
4
5
|
from pydantic import Field
|
|
@@ -34,7 +35,7 @@ class SchemaExtension(ComplexAttribute):
|
|
|
34
35
|
"""
|
|
35
36
|
|
|
36
37
|
|
|
37
|
-
class ResourceType(Resource):
|
|
38
|
+
class ResourceType(Resource[Any]):
|
|
38
39
|
schemas: Annotated[list[str], Required.true] = [
|
|
39
40
|
"urn:ietf:params:scim:schemas:core:2.0:ResourceType"
|
|
40
41
|
]
|
|
@@ -78,7 +79,7 @@ class ResourceType(Resource):
|
|
|
78
79
|
"""A list of URIs of the resource type's schema extensions."""
|
|
79
80
|
|
|
80
81
|
@classmethod
|
|
81
|
-
def from_resource(cls, resource_model: type[Resource]) -> Self:
|
|
82
|
+
def from_resource(cls, resource_model: type[Resource[Any]]) -> Self:
|
|
82
83
|
"""Build a naive ResourceType from a resource model."""
|
|
83
84
|
schema = resource_model.model_fields["schemas"].default[0]
|
|
84
85
|
name = schema.split(":")[-1]
|
scim2_models/resources/schema.py
CHANGED
|
@@ -23,7 +23,6 @@ from ..annotations import Required
|
|
|
23
23
|
from ..annotations import Returned
|
|
24
24
|
from ..annotations import Uniqueness
|
|
25
25
|
from ..attributes import ComplexAttribute
|
|
26
|
-
from ..attributes import MultiValuedComplexAttribute
|
|
27
26
|
from ..attributes import is_complex_attribute
|
|
28
27
|
from ..base import BaseModel
|
|
29
28
|
from ..constants import RESERVED_WORDS
|
|
@@ -49,7 +48,6 @@ def _make_python_identifier(identifier: str) -> str:
|
|
|
49
48
|
def _make_python_model(
|
|
50
49
|
obj: Union["Schema", "Attribute"],
|
|
51
50
|
base: type[T],
|
|
52
|
-
multiple: bool = False,
|
|
53
51
|
) -> type[T]:
|
|
54
52
|
"""Build a Python model from a Schema or an Attribute object."""
|
|
55
53
|
if isinstance(obj, Attribute):
|
|
@@ -99,7 +97,6 @@ class Attribute(ComplexAttribute):
|
|
|
99
97
|
|
|
100
98
|
def _to_python(
|
|
101
99
|
self,
|
|
102
|
-
multiple: bool = False,
|
|
103
100
|
reference_types: Optional[list[str]] = None,
|
|
104
101
|
) -> type:
|
|
105
102
|
if self.value == self.reference and reference_types is not None:
|
|
@@ -119,9 +116,7 @@ class Attribute(ComplexAttribute):
|
|
|
119
116
|
self.integer: int,
|
|
120
117
|
self.date_time: datetime,
|
|
121
118
|
self.binary: Base64Bytes,
|
|
122
|
-
self.complex:
|
|
123
|
-
if multiple
|
|
124
|
-
else ComplexAttribute,
|
|
119
|
+
self.complex: ComplexAttribute,
|
|
125
120
|
}
|
|
126
121
|
return attr_types[self.value]
|
|
127
122
|
|
|
@@ -215,12 +210,10 @@ class Attribute(ComplexAttribute):
|
|
|
215
210
|
if not self.name or not self.type:
|
|
216
211
|
return None
|
|
217
212
|
|
|
218
|
-
attr_type = self.type._to_python(
|
|
213
|
+
attr_type = self.type._to_python(self.reference_types)
|
|
219
214
|
|
|
220
|
-
if attr_type
|
|
221
|
-
attr_type = _make_python_model(
|
|
222
|
-
obj=self, base=attr_type, multiple=bool(self.multi_valued)
|
|
223
|
-
)
|
|
215
|
+
if attr_type == ComplexAttribute:
|
|
216
|
+
attr_type = _make_python_model(obj=self, base=attr_type)
|
|
224
217
|
|
|
225
218
|
if self.multi_valued:
|
|
226
219
|
attr_type = list[attr_type] # type: ignore
|
|
@@ -258,7 +251,7 @@ class Attribute(ComplexAttribute):
|
|
|
258
251
|
raise KeyError(f"This attribute has no '{name}' sub-attribute")
|
|
259
252
|
|
|
260
253
|
|
|
261
|
-
class Schema(Resource):
|
|
254
|
+
class Schema(Resource[Any]):
|
|
262
255
|
schemas: Annotated[list[str], Required.true] = [
|
|
263
256
|
"urn:ietf:params:scim:schemas:core:2.0:Schema"
|
|
264
257
|
]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
from typing import Annotated
|
|
3
|
+
from typing import Any
|
|
3
4
|
from typing import Optional
|
|
4
5
|
|
|
5
6
|
from pydantic import Field
|
|
@@ -37,7 +38,7 @@ class Filter(ComplexAttribute):
|
|
|
37
38
|
"""A Boolean value specifying whether or not the operation is supported."""
|
|
38
39
|
|
|
39
40
|
max_results: Annotated[Optional[int], Mutability.read_only, Required.true] = None
|
|
40
|
-
"""
|
|
41
|
+
"""An integer value specifying the maximum number of resources returned in a response."""
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
class ChangePassword(ComplexAttribute):
|
|
@@ -65,7 +66,7 @@ class AuthenticationScheme(ComplexAttribute):
|
|
|
65
66
|
|
|
66
67
|
type: Annotated[Optional[Type], Mutability.read_only, Required.true] = Field(
|
|
67
68
|
None,
|
|
68
|
-
examples=["oauth", "oauth2", "
|
|
69
|
+
examples=["oauth", "oauth2", "oauthbearertoken", "httpbasic", "httpdigest"],
|
|
69
70
|
)
|
|
70
71
|
"""The authentication scheme."""
|
|
71
72
|
|
|
@@ -93,7 +94,7 @@ class AuthenticationScheme(ComplexAttribute):
|
|
|
93
94
|
address."""
|
|
94
95
|
|
|
95
96
|
|
|
96
|
-
class ServiceProviderConfig(Resource):
|
|
97
|
+
class ServiceProviderConfig(Resource[Any]):
|
|
97
98
|
schemas: Annotated[list[str], Required.true] = [
|
|
98
99
|
"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"
|
|
99
100
|
]
|