scim2-models 0.4.1__py3-none-any.whl → 0.4.2__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 +1 -1
- scim2_models/base.py +2 -2
- scim2_models/messages/message.py +2 -2
- scim2_models/messages/patch_op.py +77 -46
- scim2_models/messages/search_request.py +0 -2
- scim2_models/reference.py +0 -2
- scim2_models/resources/group.py +2 -1
- scim2_models/resources/resource.py +7 -7
- scim2_models/resources/resource_type.py +3 -2
- scim2_models/resources/schema.py +1 -1
- scim2_models/resources/service_provider_config.py +2 -1
- scim2_models/urn.py +7 -5
- scim2_models/utils.py +9 -13
- {scim2_models-0.4.1.dist-info → scim2_models-0.4.2.dist-info}/METADATA +1 -1
- scim2_models-0.4.2.dist-info/RECORD +30 -0
- scim2_models-0.4.1.dist-info/RECORD +0 -30
- {scim2_models-0.4.1.dist-info → scim2_models-0.4.2.dist-info}/WHEEL +0 -0
- {scim2_models-0.4.1.dist-info → scim2_models-0.4.2.dist-info}/licenses/LICENSE +0 -0
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
|
|
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
|
|
@@ -26,7 +27,7 @@ from .error import Error
|
|
|
26
27
|
from .message import Message
|
|
27
28
|
from .message import _get_resource_class
|
|
28
29
|
|
|
29
|
-
T = TypeVar("T", bound=Resource)
|
|
30
|
+
T = TypeVar("T", bound=Resource[Any])
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
class PatchOperation(ComplexAttribute):
|
|
@@ -51,7 +52,7 @@ class PatchOperation(ComplexAttribute):
|
|
|
51
52
|
describing the target of the operation."""
|
|
52
53
|
|
|
53
54
|
def _validate_mutability(
|
|
54
|
-
self, resource_class: type[
|
|
55
|
+
self, resource_class: type[Resource[Any]], field_name: str
|
|
55
56
|
) -> None:
|
|
56
57
|
"""Validate mutability constraints."""
|
|
57
58
|
# RFC 7644 Section 3.5.2: "Servers should be tolerant of schema extensions"
|
|
@@ -72,7 +73,7 @@ class PatchOperation(ComplexAttribute):
|
|
|
72
73
|
raise ValueError(Error.make_mutability_error().detail)
|
|
73
74
|
|
|
74
75
|
def _validate_required_attribute(
|
|
75
|
-
self, resource_class: type[
|
|
76
|
+
self, resource_class: type[Resource[Any]], field_name: str
|
|
76
77
|
) -> None:
|
|
77
78
|
"""Validate required attribute constraints for remove operations."""
|
|
78
79
|
# RFC 7644 Section 3.5.2.3: Only validate for remove operations
|
|
@@ -143,7 +144,7 @@ class PatchOp(Message, Generic[T]):
|
|
|
143
144
|
- Using PatchOp without a type parameter raises TypeError
|
|
144
145
|
"""
|
|
145
146
|
|
|
146
|
-
def __new__(cls, *args: Any, **kwargs: Any):
|
|
147
|
+
def __new__(cls, *args: Any, **kwargs: Any) -> Self:
|
|
147
148
|
"""Create new PatchOp instance with type parameter validation.
|
|
148
149
|
|
|
149
150
|
Only handles the case of direct instantiation without type parameter (PatchOp()).
|
|
@@ -162,39 +163,48 @@ class PatchOp(Message, Generic[T]):
|
|
|
162
163
|
|
|
163
164
|
return super().__new__(cls)
|
|
164
165
|
|
|
165
|
-
def __class_getitem__(
|
|
166
|
+
def __class_getitem__(
|
|
167
|
+
cls, typevar_values: Union[type[Resource[Any]], tuple[type[Resource[Any]], ...]]
|
|
168
|
+
) -> Any:
|
|
166
169
|
"""Validate type parameter when creating parameterized type.
|
|
167
170
|
|
|
168
171
|
Ensures the type parameter is a concrete Resource subclass (not Resource itself)
|
|
169
172
|
or a TypeVar bound to Resource. Rejects invalid types (str, int, etc.) and Union types.
|
|
170
173
|
"""
|
|
171
|
-
|
|
172
|
-
if isinstance(item, TypeVar):
|
|
174
|
+
if isinstance(typevar_values, TypeVar):
|
|
173
175
|
# Check if TypeVar is bound to Resource or its subclass
|
|
174
|
-
if
|
|
175
|
-
|
|
176
|
-
or (
|
|
176
|
+
if typevar_values.__bound__ is not None and (
|
|
177
|
+
typevar_values.__bound__ is Resource
|
|
178
|
+
or (
|
|
179
|
+
isclass(typevar_values.__bound__)
|
|
180
|
+
and issubclass(typevar_values.__bound__, Resource)
|
|
181
|
+
)
|
|
177
182
|
):
|
|
178
|
-
return super().__class_getitem__(
|
|
183
|
+
return super().__class_getitem__(typevar_values)
|
|
179
184
|
else:
|
|
180
185
|
raise TypeError(
|
|
181
|
-
f"PatchOp TypeVar must be bound to Resource or its subclass, got {
|
|
186
|
+
f"PatchOp TypeVar must be bound to Resource or its subclass, got {typevar_values}. "
|
|
182
187
|
"Example: T = TypeVar('T', bound=Resource)"
|
|
183
188
|
)
|
|
184
189
|
|
|
185
190
|
# Check if type parameter is a concrete Resource subclass (not Resource itself)
|
|
186
|
-
if
|
|
191
|
+
if typevar_values is Resource:
|
|
187
192
|
raise TypeError(
|
|
188
193
|
"PatchOp requires a concrete Resource subclass, not Resource itself. "
|
|
189
194
|
"Use PatchOp[User], PatchOp[Group], etc. instead of PatchOp[Resource]."
|
|
190
195
|
)
|
|
191
|
-
|
|
196
|
+
|
|
197
|
+
if not (
|
|
198
|
+
isclass(typevar_values)
|
|
199
|
+
and issubclass(typevar_values, Resource)
|
|
200
|
+
and typevar_values is not Resource
|
|
201
|
+
):
|
|
192
202
|
raise TypeError(
|
|
193
|
-
f"PatchOp type parameter must be a concrete Resource subclass or TypeVar, got {
|
|
203
|
+
f"PatchOp type parameter must be a concrete Resource subclass or TypeVar, got {typevar_values}. "
|
|
194
204
|
"Use PatchOp[User], PatchOp[Group], etc."
|
|
195
205
|
)
|
|
196
206
|
|
|
197
|
-
return super().__class_getitem__(
|
|
207
|
+
return super().__class_getitem__(typevar_values)
|
|
198
208
|
|
|
199
209
|
schemas: Annotated[list[str], Required.true] = [
|
|
200
210
|
"urn:ietf:params:scim:api:messages:2.0:PatchOp"
|
|
@@ -254,7 +264,9 @@ class PatchOp(Message, Generic[T]):
|
|
|
254
264
|
|
|
255
265
|
return modified
|
|
256
266
|
|
|
257
|
-
def _apply_operation(
|
|
267
|
+
def _apply_operation(
|
|
268
|
+
self, resource: Resource[Any], operation: PatchOperation
|
|
269
|
+
) -> bool:
|
|
258
270
|
"""Apply a single patch operation to a resource.
|
|
259
271
|
|
|
260
272
|
:return: :data:`True` if the resource was modified, else :data:`False`.
|
|
@@ -266,7 +278,9 @@ class PatchOp(Message, Generic[T]):
|
|
|
266
278
|
|
|
267
279
|
raise ValueError(Error.make_invalid_value_error().detail)
|
|
268
280
|
|
|
269
|
-
def _apply_add_replace(
|
|
281
|
+
def _apply_add_replace(
|
|
282
|
+
self, resource: Resource[Any], operation: PatchOperation
|
|
283
|
+
) -> bool:
|
|
270
284
|
"""Apply an add or replace operation."""
|
|
271
285
|
# RFC 7644 Section 3.5.2.1: "If path is specified, add/replace at that path"
|
|
272
286
|
if operation.path is not None:
|
|
@@ -280,7 +294,7 @@ class PatchOp(Message, Generic[T]):
|
|
|
280
294
|
# RFC 7644 Section 3.5.2.1: "If no path specified, add/replace at root level"
|
|
281
295
|
return self._apply_root_attributes(resource, operation.value)
|
|
282
296
|
|
|
283
|
-
def _apply_remove(self, resource: Resource, operation: PatchOperation) -> bool:
|
|
297
|
+
def _apply_remove(self, resource: Resource[Any], operation: PatchOperation) -> bool:
|
|
284
298
|
"""Apply a remove operation."""
|
|
285
299
|
# RFC 7644 Section 3.5.2.3: "Path is required for remove operations"
|
|
286
300
|
if operation.path is None:
|
|
@@ -294,7 +308,8 @@ class PatchOp(Message, Generic[T]):
|
|
|
294
308
|
|
|
295
309
|
return self._remove_value_at_path(resource, operation.path)
|
|
296
310
|
|
|
297
|
-
|
|
311
|
+
@classmethod
|
|
312
|
+
def _apply_root_attributes(cls, resource: BaseModel, value: Any) -> bool:
|
|
298
313
|
"""Apply attributes to the resource root."""
|
|
299
314
|
if not isinstance(value, dict):
|
|
300
315
|
return False
|
|
@@ -312,8 +327,9 @@ class PatchOp(Message, Generic[T]):
|
|
|
312
327
|
|
|
313
328
|
return modified
|
|
314
329
|
|
|
330
|
+
@classmethod
|
|
315
331
|
def _set_value_at_path(
|
|
316
|
-
|
|
332
|
+
cls, resource: Resource[Any], path: str, value: Any, is_add: bool
|
|
317
333
|
) -> bool:
|
|
318
334
|
"""Set a value at a specific path."""
|
|
319
335
|
target, attr_path = _resolve_path_to_target(resource, path)
|
|
@@ -323,12 +339,13 @@ class PatchOp(Message, Generic[T]):
|
|
|
323
339
|
|
|
324
340
|
path_parts = attr_path.split(".")
|
|
325
341
|
if len(path_parts) == 1:
|
|
326
|
-
return
|
|
342
|
+
return cls._set_simple_attribute(target, path_parts[0], value, is_add)
|
|
327
343
|
|
|
328
|
-
return
|
|
344
|
+
return cls._set_complex_attribute(target, path_parts, value, is_add)
|
|
329
345
|
|
|
346
|
+
@classmethod
|
|
330
347
|
def _set_simple_attribute(
|
|
331
|
-
|
|
348
|
+
cls, resource: BaseModel, attr_name: str, value: Any, is_add: bool
|
|
332
349
|
) -> bool:
|
|
333
350
|
"""Set a value on a simple (non-nested) attribute."""
|
|
334
351
|
field_name = _find_field_name(type(resource), attr_name)
|
|
@@ -336,8 +353,8 @@ class PatchOp(Message, Generic[T]):
|
|
|
336
353
|
raise ValueError(Error.make_no_target_error().detail)
|
|
337
354
|
|
|
338
355
|
# RFC 7644 Section 3.5.2.1: "For multi-valued attributes, add operation appends values"
|
|
339
|
-
if is_add and
|
|
340
|
-
return
|
|
356
|
+
if is_add and cls._is_multivalued_field(resource, field_name):
|
|
357
|
+
return cls._handle_multivalued_add(resource, field_name, value)
|
|
341
358
|
|
|
342
359
|
old_value = getattr(resource, field_name)
|
|
343
360
|
if old_value == value:
|
|
@@ -346,8 +363,9 @@ class PatchOp(Message, Generic[T]):
|
|
|
346
363
|
setattr(resource, field_name, value)
|
|
347
364
|
return True
|
|
348
365
|
|
|
366
|
+
@classmethod
|
|
349
367
|
def _set_complex_attribute(
|
|
350
|
-
|
|
368
|
+
cls, resource: BaseModel, path_parts: list[str], value: Any, is_add: bool
|
|
351
369
|
) -> bool:
|
|
352
370
|
"""Set a value on a complex (nested) attribute."""
|
|
353
371
|
parent_attr = path_parts[0]
|
|
@@ -359,38 +377,45 @@ class PatchOp(Message, Generic[T]):
|
|
|
359
377
|
|
|
360
378
|
parent_obj = getattr(resource, parent_field_name)
|
|
361
379
|
if parent_obj is None:
|
|
362
|
-
parent_obj =
|
|
380
|
+
parent_obj = cls._create_parent_object(resource, parent_field_name)
|
|
363
381
|
if parent_obj is None:
|
|
364
382
|
return False
|
|
365
383
|
|
|
366
|
-
return
|
|
384
|
+
return cls._set_value_at_path(parent_obj, sub_path, value, is_add)
|
|
367
385
|
|
|
368
|
-
|
|
386
|
+
@classmethod
|
|
387
|
+
def _is_multivalued_field(cls, resource: BaseModel, field_name: str) -> bool:
|
|
369
388
|
"""Check if a field is multi-valued."""
|
|
370
389
|
return hasattr(resource, field_name) and type(resource).get_field_multiplicity(
|
|
371
390
|
field_name
|
|
372
391
|
)
|
|
373
392
|
|
|
393
|
+
@classmethod
|
|
374
394
|
def _handle_multivalued_add(
|
|
375
|
-
|
|
395
|
+
cls, resource: BaseModel, field_name: str, value: Any
|
|
376
396
|
) -> bool:
|
|
377
397
|
"""Handle adding values to a multi-valued attribute."""
|
|
378
398
|
current_list = getattr(resource, field_name) or []
|
|
379
399
|
|
|
380
400
|
# RFC 7644 Section 3.5.2.1: "Add operation appends values to multi-valued attributes"
|
|
381
401
|
if isinstance(value, list):
|
|
382
|
-
return
|
|
402
|
+
return cls._add_multiple_values(resource, field_name, current_list, value)
|
|
383
403
|
|
|
384
|
-
return
|
|
404
|
+
return cls._add_single_value(resource, field_name, current_list, value)
|
|
385
405
|
|
|
406
|
+
@classmethod
|
|
386
407
|
def _add_multiple_values(
|
|
387
|
-
|
|
408
|
+
cls,
|
|
409
|
+
resource: BaseModel,
|
|
410
|
+
field_name: str,
|
|
411
|
+
current_list: list[Any],
|
|
412
|
+
values: list[Any],
|
|
388
413
|
) -> bool:
|
|
389
414
|
"""Add multiple values to a multi-valued attribute."""
|
|
390
415
|
new_values = []
|
|
391
416
|
# RFC 7644 Section 3.5.2.1: "Do not add duplicate values"
|
|
392
417
|
for new_val in values:
|
|
393
|
-
if not
|
|
418
|
+
if not cls._value_exists_in_list(current_list, new_val):
|
|
394
419
|
new_values.append(new_val)
|
|
395
420
|
|
|
396
421
|
if not new_values:
|
|
@@ -399,23 +424,26 @@ class PatchOp(Message, Generic[T]):
|
|
|
399
424
|
setattr(resource, field_name, current_list + new_values)
|
|
400
425
|
return True
|
|
401
426
|
|
|
427
|
+
@classmethod
|
|
402
428
|
def _add_single_value(
|
|
403
|
-
|
|
429
|
+
cls, resource: BaseModel, field_name: str, current_list: list[Any], value: Any
|
|
404
430
|
) -> bool:
|
|
405
431
|
"""Add a single value to a multi-valued attribute."""
|
|
406
432
|
# RFC 7644 Section 3.5.2.1: "Do not add duplicate values"
|
|
407
|
-
if
|
|
433
|
+
if cls._value_exists_in_list(current_list, value):
|
|
408
434
|
return False
|
|
409
435
|
|
|
410
436
|
current_list.append(value)
|
|
411
437
|
setattr(resource, field_name, current_list)
|
|
412
438
|
return True
|
|
413
439
|
|
|
414
|
-
|
|
440
|
+
@classmethod
|
|
441
|
+
def _value_exists_in_list(cls, current_list: list[Any], new_value: Any) -> bool:
|
|
415
442
|
"""Check if a value already exists in a list."""
|
|
416
|
-
return any(
|
|
443
|
+
return any(cls._values_match(item, new_value) for item in current_list)
|
|
417
444
|
|
|
418
|
-
|
|
445
|
+
@classmethod
|
|
446
|
+
def _create_parent_object(cls, resource: BaseModel, parent_field_name: str) -> Any:
|
|
419
447
|
"""Create a parent object if it doesn't exist."""
|
|
420
448
|
parent_class = type(resource).get_field_root_type(parent_field_name)
|
|
421
449
|
if not parent_class or not isclass(parent_class):
|
|
@@ -425,7 +453,8 @@ class PatchOp(Message, Generic[T]):
|
|
|
425
453
|
setattr(resource, parent_field_name, parent_obj)
|
|
426
454
|
return parent_obj
|
|
427
455
|
|
|
428
|
-
|
|
456
|
+
@classmethod
|
|
457
|
+
def _remove_value_at_path(cls, resource: Resource[Any], path: str) -> bool:
|
|
429
458
|
"""Remove a value at a specific path."""
|
|
430
459
|
target, attr_path = _resolve_path_to_target(resource, path)
|
|
431
460
|
|
|
@@ -448,10 +477,11 @@ class PatchOp(Message, Generic[T]):
|
|
|
448
477
|
return True
|
|
449
478
|
|
|
450
479
|
sub_path = ".".join(path_parts)
|
|
451
|
-
return
|
|
480
|
+
return cls._remove_value_at_path(parent_obj, sub_path)
|
|
452
481
|
|
|
482
|
+
@classmethod
|
|
453
483
|
def _remove_specific_value(
|
|
454
|
-
|
|
484
|
+
cls, resource: Resource[Any], path: str, value_to_remove: Any
|
|
455
485
|
) -> bool:
|
|
456
486
|
"""Remove a specific value from a multi-valued attribute."""
|
|
457
487
|
target, attr_path = _resolve_path_to_target(resource, path)
|
|
@@ -472,7 +502,7 @@ class PatchOp(Message, Generic[T]):
|
|
|
472
502
|
modified = False
|
|
473
503
|
# RFC 7644 Section 3.5.2.3: "Remove matching values from multi-valued attributes"
|
|
474
504
|
for item in current_list:
|
|
475
|
-
if not
|
|
505
|
+
if not cls._values_match(item, value_to_remove):
|
|
476
506
|
new_list.append(item)
|
|
477
507
|
else:
|
|
478
508
|
modified = True
|
|
@@ -483,10 +513,11 @@ class PatchOp(Message, Generic[T]):
|
|
|
483
513
|
|
|
484
514
|
return False
|
|
485
515
|
|
|
486
|
-
|
|
516
|
+
@classmethod
|
|
517
|
+
def _values_match(cls, value1: Any, value2: Any) -> bool:
|
|
487
518
|
"""Check if two values match, converting BaseModel to dict for comparison."""
|
|
488
519
|
|
|
489
|
-
def to_dict(value):
|
|
520
|
+
def to_dict(value: Any) -> dict[str, Any]:
|
|
490
521
|
return value.model_dump() if isinstance(value, BaseModel) else value
|
|
491
522
|
|
|
492
523
|
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
|
|
@@ -33,7 +34,7 @@ class GroupMember(MultiValuedComplexAttribute):
|
|
|
33
34
|
display: Annotated[Optional[str], Mutability.read_only] = None
|
|
34
35
|
|
|
35
36
|
|
|
36
|
-
class Group(Resource):
|
|
37
|
+
class Group(Resource[Any]):
|
|
37
38
|
schemas: Annotated[list[str], Required.true] = [
|
|
38
39
|
"urn:ietf:params:scim:schemas:core:2.0:Group"
|
|
39
40
|
]
|
|
@@ -148,7 +148,7 @@ class Resource(ScimObject, Generic[AnyExtension]):
|
|
|
148
148
|
"""A complex attribute containing resource metadata."""
|
|
149
149
|
|
|
150
150
|
@classmethod
|
|
151
|
-
def __class_getitem__(cls, item: Any) -> type["Resource"]:
|
|
151
|
+
def __class_getitem__(cls, item: Any) -> type["Resource[Any]"]:
|
|
152
152
|
"""Create a Resource class with extension fields dynamically added."""
|
|
153
153
|
if hasattr(cls, "__scim_extension_metadata__"):
|
|
154
154
|
return cls
|
|
@@ -241,12 +241,12 @@ class Resource(ScimObject, Generic[AnyExtension]):
|
|
|
241
241
|
|
|
242
242
|
@staticmethod
|
|
243
243
|
def get_by_schema(
|
|
244
|
-
resource_types: list[type["Resource"]],
|
|
244
|
+
resource_types: list[type["Resource[Any]"]],
|
|
245
245
|
schema: str,
|
|
246
246
|
with_extensions: bool = True,
|
|
247
|
-
) -> Optional[Union[type["Resource"], type["Extension"]]]:
|
|
247
|
+
) -> Optional[Union[type["Resource[Any]"], type["Extension"]]]:
|
|
248
248
|
"""Given a resource type list and a schema, find the matching resource type."""
|
|
249
|
-
by_schema: dict[str, Union[type[Resource], type[Extension]]] = {
|
|
249
|
+
by_schema: dict[str, Union[type[Resource[Any]], type[Extension]]] = {
|
|
250
250
|
resource_type.model_fields["schemas"].default[0].lower(): resource_type
|
|
251
251
|
for resource_type in (resource_types or [])
|
|
252
252
|
}
|
|
@@ -263,7 +263,7 @@ class Resource(ScimObject, Generic[AnyExtension]):
|
|
|
263
263
|
|
|
264
264
|
@staticmethod
|
|
265
265
|
def get_by_payload(
|
|
266
|
-
resource_types: list[type["Resource"]],
|
|
266
|
+
resource_types: list[type["Resource[Any]"]],
|
|
267
267
|
payload: dict[str, Any],
|
|
268
268
|
**kwargs: Any,
|
|
269
269
|
) -> Optional[type]:
|
|
@@ -291,7 +291,7 @@ class Resource(ScimObject, Generic[AnyExtension]):
|
|
|
291
291
|
return _model_to_schema(cls)
|
|
292
292
|
|
|
293
293
|
@classmethod
|
|
294
|
-
def from_schema(cls, schema: "Schema") -> type["Resource"]:
|
|
294
|
+
def from_schema(cls, schema: "Schema") -> type["Resource[Any]"]:
|
|
295
295
|
"""Build a :class:`scim2_models.Resource` subclass from the schema definition."""
|
|
296
296
|
from .schema import _make_python_model
|
|
297
297
|
|
|
@@ -372,7 +372,7 @@ class Resource(ScimObject, Generic[AnyExtension]):
|
|
|
372
372
|
return super(ScimObject, self).model_dump_json(*args, **dump_kwargs)
|
|
373
373
|
|
|
374
374
|
|
|
375
|
-
AnyResource = TypeVar("AnyResource", bound="Resource")
|
|
375
|
+
AnyResource = TypeVar("AnyResource", bound="Resource[Any]")
|
|
376
376
|
|
|
377
377
|
|
|
378
378
|
def _dedicated_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
|
@@ -258,7 +258,7 @@ class Attribute(ComplexAttribute):
|
|
|
258
258
|
raise KeyError(f"This attribute has no '{name}' sub-attribute")
|
|
259
259
|
|
|
260
260
|
|
|
261
|
-
class Schema(Resource):
|
|
261
|
+
class Schema(Resource[Any]):
|
|
262
262
|
schemas: Annotated[list[str], Required.true] = [
|
|
263
263
|
"urn:ietf:params:scim:schemas:core:2.0:Schema"
|
|
264
264
|
]
|
|
@@ -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
|
|
@@ -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
|
]
|
scim2_models/urn.py
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING
|
|
2
2
|
from typing import Any
|
|
3
3
|
from typing import Optional
|
|
4
|
+
from typing import Union
|
|
4
5
|
|
|
5
6
|
from .base import BaseModel
|
|
6
7
|
from .utils import _normalize_attribute_name
|
|
7
8
|
|
|
8
9
|
if TYPE_CHECKING:
|
|
9
10
|
from .base import BaseModel
|
|
11
|
+
from .resources.resource import Extension
|
|
10
12
|
from .resources.resource import Resource
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
def _get_or_create_extension_instance(
|
|
14
|
-
model: "Resource", extension_class: type
|
|
15
|
-
) -> "
|
|
16
|
+
model: "Resource[Any]", extension_class: type
|
|
17
|
+
) -> "Extension":
|
|
16
18
|
"""Get existing extension instance or create a new one."""
|
|
17
19
|
extension_instance = model[extension_class]
|
|
18
20
|
if extension_instance is None:
|
|
@@ -62,7 +64,7 @@ def _validate_model_attribute(model: type["BaseModel"], attribute_base: str) ->
|
|
|
62
64
|
|
|
63
65
|
|
|
64
66
|
def _validate_attribute_urn(
|
|
65
|
-
attribute_name: str, resource: type["Resource"]
|
|
67
|
+
attribute_name: str, resource: type["Resource[Any]"]
|
|
66
68
|
) -> Optional[str]:
|
|
67
69
|
"""Validate that an attribute urn is valid or not.
|
|
68
70
|
|
|
@@ -87,8 +89,8 @@ def _validate_attribute_urn(
|
|
|
87
89
|
|
|
88
90
|
|
|
89
91
|
def _resolve_path_to_target(
|
|
90
|
-
resource: "Resource", path: str
|
|
91
|
-
) -> tuple[Optional["
|
|
92
|
+
resource: "Resource[Any]", path: str
|
|
93
|
+
) -> tuple[Optional[Union["Resource[Any]", "Extension"]], str]:
|
|
92
94
|
"""Resolve a path to a target and an attribute_path.
|
|
93
95
|
|
|
94
96
|
The target can be the resource itself, or an extension object.
|
scim2_models/utils.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import re
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
3
4
|
from typing import Annotated
|
|
4
5
|
from typing import Literal
|
|
5
6
|
from typing import Optional
|
|
@@ -10,6 +11,9 @@ from pydantic import EncoderProtocol
|
|
|
10
11
|
from pydantic.alias_generators import to_snake
|
|
11
12
|
from pydantic_core import PydanticCustomError
|
|
12
13
|
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .base import BaseModel
|
|
16
|
+
|
|
13
17
|
try:
|
|
14
18
|
from types import UnionType
|
|
15
19
|
|
|
@@ -103,9 +107,7 @@ def _validate_scim_path_syntax(path: str) -> bool:
|
|
|
103
107
|
"""Check if path syntax is valid according to RFC 7644 simplified rules.
|
|
104
108
|
|
|
105
109
|
:param path: The path to validate
|
|
106
|
-
:type path: str
|
|
107
110
|
:return: True if path syntax is valid, False otherwise
|
|
108
|
-
:rtype: bool
|
|
109
111
|
"""
|
|
110
112
|
if not path or not path.strip():
|
|
111
113
|
return False
|
|
@@ -135,9 +137,7 @@ def _validate_scim_urn_syntax(path: str) -> bool:
|
|
|
135
137
|
"""Validate URN-based path format.
|
|
136
138
|
|
|
137
139
|
:param path: The URN path to validate
|
|
138
|
-
:type path: str
|
|
139
140
|
:return: True if URN path format is valid, False otherwise
|
|
140
|
-
:rtype: bool
|
|
141
141
|
"""
|
|
142
142
|
# Basic URN validation: should start with urn:
|
|
143
143
|
if not path.startswith("urn:"):
|
|
@@ -181,20 +181,16 @@ def _extract_field_name(path: str) -> Optional[str]:
|
|
|
181
181
|
return path
|
|
182
182
|
|
|
183
183
|
|
|
184
|
-
def _find_field_name(
|
|
184
|
+
def _find_field_name(model_class: type["BaseModel"], attr_name: str) -> Optional[str]:
|
|
185
185
|
"""Find the actual field name in a resource class from an attribute name.
|
|
186
186
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
Returns:
|
|
192
|
-
The actual field name if found (e.g., "nick_name"), None otherwise
|
|
193
|
-
|
|
187
|
+
:param resource_class: The resource class to search in
|
|
188
|
+
:param attr_name: The attribute name to find (e.g., "nickName")
|
|
189
|
+
:returns: The actual field name if found (e.g., "nick_name"), None otherwise
|
|
194
190
|
"""
|
|
195
191
|
normalized_attr_name = _normalize_attribute_name(attr_name)
|
|
196
192
|
|
|
197
|
-
for field_key in
|
|
193
|
+
for field_key in model_class.model_fields:
|
|
198
194
|
if _normalize_attribute_name(field_key) == normalized_attr_name:
|
|
199
195
|
return field_key
|
|
200
196
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scim2-models
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: SCIM2 models serialization and validation with pydantic
|
|
5
5
|
Project-URL: documentation, https://scim2-models.readthedocs.io
|
|
6
6
|
Project-URL: repository, https://github.com/python-scim/scim2-models
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
scim2_models/__init__.py,sha256=IACL_c94UhKq0ZEXOb3LMZgeEQMfQhdOVpWGDnHSpXo,3201
|
|
2
|
+
scim2_models/annotations.py,sha256=oRjlKL1fqrYfa9UtaMdxF5fOT8CUUN3m-rdzvf7aiSA,3304
|
|
3
|
+
scim2_models/attributes.py,sha256=ISPpiCLPlDXq_NTudSs2186fvy2SRxmFXJNeVHEvbsY,1790
|
|
4
|
+
scim2_models/base.py,sha256=TGEUVvaXZv5uuR-J_tR6MKeaPjZeGK3cLN6TPotwJnY,20409
|
|
5
|
+
scim2_models/constants.py,sha256=9egq8JW0dFAqPng85CiHoH5T6pRtYL87-gC0C-IMGsk,573
|
|
6
|
+
scim2_models/context.py,sha256=RjgMIvWPr8f41qbVL1sjaDnm9GRKyrCrgfC4npwwcMg,9149
|
|
7
|
+
scim2_models/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
scim2_models/reference.py,sha256=HwbY0PLF16Bu_1zaxXbspjBP_CtTrQkaK2KLmD1ycD0,2422
|
|
9
|
+
scim2_models/scim_object.py,sha256=6a-lf8iIQhlMY7lCM7RcSde4f1kECncE0Ae8uJ4RaaA,2404
|
|
10
|
+
scim2_models/urn.py,sha256=c5jJXZCsik4-QBV1-z3vXTB__DKY7xYZqDPMmOsIbEo,3669
|
|
11
|
+
scim2_models/utils.py,sha256=sGnoY9KJS0elxfgPTp9S0CqzXXoxt5hIVUSjkPCaAh0,5691
|
|
12
|
+
scim2_models/messages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
scim2_models/messages/bulk.py,sha256=snPB722V_Msg1JBFcaTeEW_sa-jt-KHv6fh071NoKCg,2592
|
|
14
|
+
scim2_models/messages/error.py,sha256=_IHUoFwl-QJiICIcRkhaXf_9C7RXgiX7TL_26QXDo2c,6304
|
|
15
|
+
scim2_models/messages/list_response.py,sha256=B-XELBTks6I45pKheAS5Go9x5IDKhrLJWsqC8y3G00s,2400
|
|
16
|
+
scim2_models/messages/message.py,sha256=rs4Zcr5gXl8KDMscLOYKp4spg9lvH_o3w-Rn2R76eN0,4118
|
|
17
|
+
scim2_models/messages/patch_op.py,sha256=uvLq8-kfQgit_sIE2jzQ7FFWE-o7J5aPDjpCPsEPV_g,20386
|
|
18
|
+
scim2_models/messages/search_request.py,sha256=7v_ALuJV6qgJ_VD5hcdq9KoxIFiwCgGj9QqrlC695Co,4571
|
|
19
|
+
scim2_models/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
scim2_models/resources/enterprise_user.py,sha256=TVa5aS-eLHcDkwyr58hZYsRKk0AwjUaUaSFhU51mn5E,1806
|
|
21
|
+
scim2_models/resources/group.py,sha256=_7zkfVsdVXmPXoexL0xDXLfuKV7gR71RiBzR-RCPC4I,1486
|
|
22
|
+
scim2_models/resources/resource.py,sha256=pVciWEM8yW13iOOheetXkJ573uZ4kJbexgxBJU4x8_4,17650
|
|
23
|
+
scim2_models/resources/resource_type.py,sha256=DgGefRQ9bXDFl1AsMPbfuznJqMshfKyUWbpo0vzsyes,3347
|
|
24
|
+
scim2_models/resources/schema.py,sha256=zVq_D_JLDl3w1JjgEtfAQaqrLup8APtQCmz9RBrviUw,10664
|
|
25
|
+
scim2_models/resources/service_provider_config.py,sha256=yHzpLZIQ7r0nooWOcK2Sq6Q53sdcy443Pq4LverRlGI,5474
|
|
26
|
+
scim2_models/resources/user.py,sha256=ErOghhilUF7fipwDRqARyLwJhbntQx4GJG3u2sZNJXs,10664
|
|
27
|
+
scim2_models-0.4.2.dist-info/METADATA,sha256=Y77SSW3WFIIlJQG_WjWzX_pXhFEBMP9WhGdYUvTHJFA,16288
|
|
28
|
+
scim2_models-0.4.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
29
|
+
scim2_models-0.4.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
30
|
+
scim2_models-0.4.2.dist-info/RECORD,,
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
scim2_models/__init__.py,sha256=IACL_c94UhKq0ZEXOb3LMZgeEQMfQhdOVpWGDnHSpXo,3201
|
|
2
|
-
scim2_models/annotations.py,sha256=oRjlKL1fqrYfa9UtaMdxF5fOT8CUUN3m-rdzvf7aiSA,3304
|
|
3
|
-
scim2_models/attributes.py,sha256=REp_WMTxs02NDcJJrSCbplCqnhVtExvfzozp_JJ_BdY,1785
|
|
4
|
-
scim2_models/base.py,sha256=8Nh-R8vN_Gd9_NU9Oq-heD5h2MzjT_X9djidG3OeO0g,20389
|
|
5
|
-
scim2_models/constants.py,sha256=9egq8JW0dFAqPng85CiHoH5T6pRtYL87-gC0C-IMGsk,573
|
|
6
|
-
scim2_models/context.py,sha256=RjgMIvWPr8f41qbVL1sjaDnm9GRKyrCrgfC4npwwcMg,9149
|
|
7
|
-
scim2_models/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
scim2_models/reference.py,sha256=EQM8bbSr_kxbFMNlWYf_4sAJlSsOS5wUrn-9_eF0Ykc,2483
|
|
9
|
-
scim2_models/scim_object.py,sha256=6a-lf8iIQhlMY7lCM7RcSde4f1kECncE0Ae8uJ4RaaA,2404
|
|
10
|
-
scim2_models/urn.py,sha256=n94gnOBMJguXfyczcFvkn8ipvZuRXoxYPC7QeXe9HoY,3559
|
|
11
|
-
scim2_models/utils.py,sha256=3yvU261wl5IR-zPcNhcNcQFwCd0h--k07mfTSgUY1nE,5681
|
|
12
|
-
scim2_models/messages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
scim2_models/messages/bulk.py,sha256=snPB722V_Msg1JBFcaTeEW_sa-jt-KHv6fh071NoKCg,2592
|
|
14
|
-
scim2_models/messages/error.py,sha256=_IHUoFwl-QJiICIcRkhaXf_9C7RXgiX7TL_26QXDo2c,6304
|
|
15
|
-
scim2_models/messages/list_response.py,sha256=B-XELBTks6I45pKheAS5Go9x5IDKhrLJWsqC8y3G00s,2400
|
|
16
|
-
scim2_models/messages/message.py,sha256=sjo_iFMJelZkssvA4O5xTqIt-urDxUbSP6N_WsnZ63Y,4081
|
|
17
|
-
scim2_models/messages/patch_op.py,sha256=yT_5gDcXWeSOdxvpYrdHh5nD2uD-B9kdfQ5qjC-JUkY,19726
|
|
18
|
-
scim2_models/messages/search_request.py,sha256=BDTDzvtfCuFZDFsZBHl8ys8Te02jLuvGaTNipBQByrM,4632
|
|
19
|
-
scim2_models/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
-
scim2_models/resources/enterprise_user.py,sha256=TVa5aS-eLHcDkwyr58hZYsRKk0AwjUaUaSFhU51mn5E,1806
|
|
21
|
-
scim2_models/resources/group.py,sha256=lJXKopa__LoWhkaNqu0JFVyQaOMe-AF4vISaq0Gg7cE,1458
|
|
22
|
-
scim2_models/resources/resource.py,sha256=GZaV_bi6r2A_UfPSQgEb3jGEeEqMhw2YGImRIhfKOY0,17615
|
|
23
|
-
scim2_models/resources/resource_type.py,sha256=scLqbD3HX3fXT2JOXY9OUVnZz7i5ty2lx4VuiVxt6DE,3314
|
|
24
|
-
scim2_models/resources/schema.py,sha256=KBqDRr2sgLrqL8UYWon-uxBzIA0118srJ8tXobs950E,10659
|
|
25
|
-
scim2_models/resources/service_provider_config.py,sha256=6xJ182T-1szEQnN5Zb1cTdQCgTYIFi4XKygbvDlTKTM,5446
|
|
26
|
-
scim2_models/resources/user.py,sha256=ErOghhilUF7fipwDRqARyLwJhbntQx4GJG3u2sZNJXs,10664
|
|
27
|
-
scim2_models-0.4.1.dist-info/METADATA,sha256=zu1Txl8FZcnVF6WYYkBl5g7O1rvfcnaG1Dk987DtIRM,16288
|
|
28
|
-
scim2_models-0.4.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
29
|
-
scim2_models-0.4.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
30
|
-
scim2_models-0.4.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|