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.
@@ -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_, (ComplexAttribute, MultiValuedComplexAttribute))
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
 
@@ -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
@@ -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[BaseModel], field_name: str
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[BaseModel], field_name: str
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__(cls, item):
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
- # Allow TypeVar as type parameter
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 item.__bound__ is not None and (
175
- item.__bound__ is Resource
176
- or (isclass(item.__bound__) and issubclass(item.__bound__, Resource))
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__(item)
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 {item}. "
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 item is Resource:
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
- if not (isclass(item) and issubclass(item, Resource) and item is not Resource):
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 {item}. "
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__(item)
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(self, resource: Resource, operation: PatchOperation) -> bool:
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(self, resource: Resource, operation: PatchOperation) -> bool:
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
- def _apply_root_attributes(self, resource: BaseModel, value: Any) -> bool:
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
- self, resource: Resource, path: str, value: Any, is_add: bool
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 attr_path or not target:
343
+ if not target:
322
344
  raise ValueError(Error.make_invalid_path_error().detail)
323
345
 
324
- path_parts = attr_path.split(".")
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 self._set_simple_attribute(target, path_parts[0], value, is_add)
357
+ return cls._set_simple_attribute(target, path_parts[0], value, is_add)
327
358
 
328
- return self._set_complex_attribute(target, path_parts, value, is_add)
359
+ return cls._set_complex_attribute(target, path_parts, value, is_add)
329
360
 
361
+ @classmethod
330
362
  def _set_simple_attribute(
331
- self, resource: BaseModel, attr_name: str, value: Any, is_add: bool
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 self._is_multivalued_field(resource, field_name):
340
- return self._handle_multivalued_add(resource, field_name, value)
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
- self, resource: BaseModel, path_parts: list[str], value: Any, is_add: bool
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 = self._create_parent_object(resource, parent_field_name)
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 self._set_value_at_path(parent_obj, sub_path, value, is_add)
399
+ return cls._set_value_at_path(parent_obj, sub_path, value, is_add)
367
400
 
368
- def _is_multivalued_field(self, resource: BaseModel, field_name: str) -> bool:
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
- self, resource: BaseModel, field_name: str, value: Any
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 self._add_multiple_values(resource, field_name, current_list, value)
417
+ return cls._add_multiple_values(resource, field_name, current_list, value)
383
418
 
384
- return self._add_single_value(resource, field_name, current_list, value)
419
+ return cls._add_single_value(resource, field_name, current_list, value)
385
420
 
421
+ @classmethod
386
422
  def _add_multiple_values(
387
- self, resource: BaseModel, field_name: str, current_list: list, values: list
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 self._value_exists_in_list(current_list, new_val):
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
- self, resource: BaseModel, field_name: str, current_list: list, value: Any
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 self._value_exists_in_list(current_list, value):
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
- def _value_exists_in_list(self, current_list: list, new_value: Any) -> bool:
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(self._values_match(item, new_value) for item in current_list)
458
+ return any(cls._values_match(item, new_value) for item in current_list)
417
459
 
418
- def _create_parent_object(self, resource: BaseModel, parent_field_name: str) -> Any:
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
- def _remove_value_at_path(self, resource: Resource, path: str) -> bool:
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.split(".")
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 self._remove_value_at_path(parent_obj, sub_path)
495
+ return cls._remove_value_at_path(parent_obj, sub_path)
452
496
 
497
+ @classmethod
453
498
  def _remove_specific_value(
454
- self, resource: Resource, path: str, value_to_remove: Any
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 self._values_match(item, value_to_remove):
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
- def _values_match(self, value1: Any, value2: Any) -> bool:
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 = (
@@ -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(MultiValuedComplexAttribute):
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 _dedicated_attributes(
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]
@@ -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: MultiValuedComplexAttribute
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(bool(self.multi_valued), self.reference_types)
213
+ attr_type = self.type._to_python(self.reference_types)
219
214
 
220
- if attr_type in (ComplexAttribute, MultiValuedComplexAttribute):
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
- """A Boolean value specifying whether or not the operation is supported."""
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", "oauthbreakertoken", "httpbasic", "httpdigest"],
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
  ]