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.
@@ -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
 
@@ -112,8 +112,8 @@ class _GenericMessageMetaclass(ModelMetaclass):
112
112
  return klass
113
113
 
114
114
 
115
- def _get_resource_class(obj) -> Optional[type[Resource]]:
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[BaseModel], field_name: str
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[BaseModel], field_name: str
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__(cls, item):
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
- # Allow TypeVar as type parameter
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 item.__bound__ is not None and (
175
- item.__bound__ is Resource
176
- or (isclass(item.__bound__) and issubclass(item.__bound__, Resource))
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__(item)
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 {item}. "
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 item is Resource:
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
- if not (isclass(item) and issubclass(item, Resource) and item is not Resource):
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 {item}. "
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__(item)
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(self, resource: Resource, operation: PatchOperation) -> bool:
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(self, resource: Resource, operation: PatchOperation) -> bool:
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
- def _apply_root_attributes(self, resource: BaseModel, value: Any) -> bool:
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
- self, resource: Resource, path: str, value: Any, is_add: bool
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 self._set_simple_attribute(target, path_parts[0], value, is_add)
342
+ return cls._set_simple_attribute(target, path_parts[0], value, is_add)
327
343
 
328
- return self._set_complex_attribute(target, path_parts, value, is_add)
344
+ return cls._set_complex_attribute(target, path_parts, value, is_add)
329
345
 
346
+ @classmethod
330
347
  def _set_simple_attribute(
331
- self, resource: BaseModel, attr_name: str, value: Any, is_add: bool
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 self._is_multivalued_field(resource, field_name):
340
- return self._handle_multivalued_add(resource, field_name, value)
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
- self, resource: BaseModel, path_parts: list[str], value: Any, is_add: bool
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 = self._create_parent_object(resource, parent_field_name)
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 self._set_value_at_path(parent_obj, sub_path, value, is_add)
384
+ return cls._set_value_at_path(parent_obj, sub_path, value, is_add)
367
385
 
368
- def _is_multivalued_field(self, resource: BaseModel, field_name: str) -> bool:
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
- self, resource: BaseModel, field_name: str, value: Any
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 self._add_multiple_values(resource, field_name, current_list, value)
402
+ return cls._add_multiple_values(resource, field_name, current_list, value)
383
403
 
384
- return self._add_single_value(resource, field_name, current_list, value)
404
+ return cls._add_single_value(resource, field_name, current_list, value)
385
405
 
406
+ @classmethod
386
407
  def _add_multiple_values(
387
- self, resource: BaseModel, field_name: str, current_list: list, values: list
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 self._value_exists_in_list(current_list, new_val):
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
- self, resource: BaseModel, field_name: str, current_list: list, value: Any
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 self._value_exists_in_list(current_list, value):
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
- def _value_exists_in_list(self, current_list: list, new_value: Any) -> bool:
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(self._values_match(item, new_value) for item in current_list)
443
+ return any(cls._values_match(item, new_value) for item in current_list)
417
444
 
418
- def _create_parent_object(self, resource: BaseModel, parent_field_name: str) -> Any:
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
- def _remove_value_at_path(self, resource: Resource, path: str) -> bool:
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 self._remove_value_at_path(parent_obj, sub_path)
480
+ return cls._remove_value_at_path(parent_obj, sub_path)
452
481
 
482
+ @classmethod
453
483
  def _remove_specific_value(
454
- self, resource: Resource, path: str, value_to_remove: Any
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 self._values_match(item, value_to_remove):
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
- def _values_match(self, value1: Any, value2: Any) -> bool:
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 = (
@@ -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]
@@ -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
- ) -> "BaseModel":
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["BaseModel"], str]:
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(resource_class, attr_name: str) -> Optional[str]:
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
- Args:
188
- resource_class: The resource class to search in
189
- attr_name: The attribute name to find (e.g., "nickName")
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 resource_class.model_fields:
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.1
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,,