scim2-models 0.3.6__py3-none-any.whl → 0.3.7__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/base.py CHANGED
@@ -1,20 +1,13 @@
1
- from collections import UserString
2
- from enum import Enum
3
- from enum import auto
4
1
  from inspect import isclass
5
- from typing import Annotated
6
2
  from typing import Any
7
- from typing import Generic
8
3
  from typing import Optional
9
- from typing import TypeVar
10
4
  from typing import get_args
11
5
  from typing import get_origin
12
6
 
13
7
  from pydantic import AliasGenerator
14
8
  from pydantic import BaseModel as PydanticBaseModel
15
9
  from pydantic import ConfigDict
16
- from pydantic import Field
17
- from pydantic import GetCoreSchemaHandler
10
+ from pydantic import FieldSerializationInfo
18
11
  from pydantic import SerializationInfo
19
12
  from pydantic import SerializerFunctionWrapHandler
20
13
  from pydantic import ValidationInfo
@@ -24,382 +17,27 @@ from pydantic import field_validator
24
17
  from pydantic import model_serializer
25
18
  from pydantic import model_validator
26
19
  from pydantic_core import PydanticCustomError
27
- from pydantic_core import core_schema
28
- from typing_extensions import NewType
29
20
  from typing_extensions import Self
30
21
 
22
+ from scim2_models.annotations import Mutability
23
+ from scim2_models.annotations import Required
24
+ from scim2_models.annotations import Returned
25
+ from scim2_models.context import Context
31
26
  from scim2_models.utils import normalize_attribute_name
32
27
  from scim2_models.utils import to_camel
33
28
 
34
29
  from .utils import UNION_TYPES
35
30
 
36
- ReferenceTypes = TypeVar("ReferenceTypes")
37
- URIReference = NewType("URIReference", str)
38
- ExternalReference = NewType("ExternalReference", str)
39
31
 
40
-
41
- def validate_model_attribute(model: type["BaseModel"], attribute_base: str) -> None:
42
- """Validate that an attribute name or a sub-attribute path exist for a given model."""
43
- from scim2_models.base import BaseModel
44
-
45
- attribute_name, *sub_attribute_blocks = attribute_base.split(".")
46
- sub_attribute_base = ".".join(sub_attribute_blocks)
47
-
48
- aliases = {field.validation_alias for field in model.model_fields.values()}
49
-
50
- if normalize_attribute_name(attribute_name) not in aliases:
51
- raise ValueError(
52
- f"Model '{model.__name__}' has no attribute named '{attribute_name}'"
53
- )
54
-
55
- if sub_attribute_base:
56
- attribute_type = model.get_field_root_type(attribute_name)
57
-
58
- if not attribute_type or not issubclass(attribute_type, BaseModel):
59
- raise ValueError(
60
- f"Attribute '{attribute_name}' is not a complex attribute, and cannot have a '{sub_attribute_base}' sub-attribute"
61
- )
62
-
63
- validate_model_attribute(attribute_type, sub_attribute_base)
64
-
65
-
66
- def extract_schema_and_attribute_base(attribute_urn: str) -> tuple[str, str]:
67
- # Extract the schema urn part and the attribute name part from attribute
68
- # name, as defined in :rfc:`RFC7644 §3.10 <7644#section-3.10>`.
69
-
70
- *urn_blocks, attribute_base = attribute_urn.split(":")
71
- schema = ":".join(urn_blocks)
72
- return schema, attribute_base
73
-
74
-
75
- def validate_attribute_urn(
76
- attribute_name: str,
77
- default_resource: Optional[type["BaseModel"]] = None,
78
- resource_types: Optional[list[type["BaseModel"]]] = None,
79
- ) -> str:
80
- """Validate that an attribute urn is valid or not.
81
-
82
- :param attribute_name: The attribute urn to check.
83
- :default_resource: The default resource if `attribute_name` is not an absolute urn.
84
- :resource_types: The available resources in which to look for the attribute.
85
- :return: The normalized attribute URN.
86
- """
87
- from scim2_models.rfc7643.resource import Resource
88
-
89
- if not resource_types:
90
- resource_types = []
91
-
92
- if default_resource and default_resource not in resource_types:
93
- resource_types.append(default_resource)
94
-
95
- default_schema = (
96
- default_resource.model_fields["schemas"].default[0]
97
- if default_resource
98
- else None
99
- )
100
-
101
- schema: Optional[Any]
102
- schema, attribute_base = extract_schema_and_attribute_base(attribute_name)
103
- if not schema:
104
- schema = default_schema
105
-
106
- if not schema:
107
- raise ValueError("No default schema and relative URN")
108
-
109
- resource = Resource.get_by_schema(resource_types, schema)
110
- if not resource:
111
- raise ValueError(f"No resource matching schema '{schema}'")
112
-
113
- validate_model_attribute(resource, attribute_base)
114
-
115
- return f"{schema}:{attribute_base}"
116
-
117
-
118
- def contains_attribute_or_subattributes(attribute_urns: list[str], attribute_urn: str):
32
+ def contains_attribute_or_subattributes(
33
+ attribute_urns: list[str], attribute_urn: str
34
+ ) -> bool:
119
35
  return attribute_urn in attribute_urns or any(
120
36
  item.startswith(f"{attribute_urn}.") or item.startswith(f"{attribute_urn}:")
121
37
  for item in attribute_urns
122
38
  )
123
39
 
124
40
 
125
- class Reference(UserString, Generic[ReferenceTypes]):
126
- """Reference type as defined in :rfc:`RFC7643 §2.3.7 <7643#section-2.3.7>`.
127
-
128
- References can take different type parameters:
129
-
130
- - Any :class:`~scim2_models.Resource` subtype, or :class:`~typing.ForwardRef` of a Resource subtype, or :data:`~typing.Union` of those,
131
- - :data:`~scim2_models.ExternalReference`
132
- - :data:`~scim2_models.URIReference`
133
-
134
- Examples
135
- --------
136
-
137
- .. code-block:: python
138
-
139
- class Foobar(Resource):
140
- bff: Reference[User]
141
- managers: Reference[Union["User", "Group"]]
142
- photo: Reference[ExternalReference]
143
- website: Reference[URIReference]
144
-
145
- """
146
-
147
- @classmethod
148
- def __get_pydantic_core_schema__(
149
- cls,
150
- _source: type[Any],
151
- _handler: GetCoreSchemaHandler,
152
- ) -> core_schema.CoreSchema:
153
- return core_schema.no_info_after_validator_function(
154
- cls._validate, core_schema.str_schema()
155
- )
156
-
157
- @classmethod
158
- def _validate(cls, input_value: str, /) -> str:
159
- return input_value
160
-
161
-
162
- class Context(Enum):
163
- """Represent the different HTTP contexts detailed in :rfc:`RFC7644 §3.2 <7644#section-3.2>`.
164
-
165
- Contexts are intended to be used during model validation and serialization.
166
- For instance a client preparing a resource creation POST request can use
167
- :code:`resource.model_dump(Context.RESOURCE_CREATION_REQUEST)` and
168
- the server can then validate it with
169
- :code:`resource.model_validate(Context.RESOURCE_CREATION_REQUEST)`.
170
- """
171
-
172
- DEFAULT = auto()
173
- """The default context.
174
-
175
- All fields are accepted during validation, and all fields are
176
- serialized during a dump.
177
- """
178
-
179
- RESOURCE_CREATION_REQUEST = auto()
180
- """The resource creation request context.
181
-
182
- Should be used for clients building a payload for a resource creation request,
183
- and servers validating resource creation request payloads.
184
-
185
- - When used for serialization, it will not dump attributes annotated with :attr:`~scim2_models.Mutability.read_only`.
186
- - When used for validation, it will raise a :class:`~pydantic.ValidationError`:
187
- - when finding attributes annotated with :attr:`~scim2_models.Mutability.read_only`,
188
- - when attributes annotated with :attr:`Required.true <scim2_models.Required.true>` are missing on null.
189
- """
190
-
191
- RESOURCE_CREATION_RESPONSE = auto()
192
- """The resource creation response context.
193
-
194
- Should be used for servers building a payload for a resource
195
- creation response, and clients validating resource creation response
196
- payloads.
197
-
198
- - When used for validation, it will raise a :class:`~pydantic.ValidationError` when finding attributes annotated with :attr:`~scim2_models.Returned.never` or when attributes annotated with :attr:`~scim2_models.Returned.always` are missing or :data:`None`;
199
- - When used for serialization, it will:
200
- - always dump attributes annotated with :attr:`~scim2_models.Returned.always`;
201
- - never dump attributes annotated with :attr:`~scim2_models.Returned.never`;
202
- - dump attributes annotated with :attr:`~scim2_models.Returned.default` unless they are explicitly excluded;
203
- - not dump attributes annotated with :attr:`~scim2_models.Returned.request` unless they are explicitly included.
204
- """
205
-
206
- RESOURCE_QUERY_REQUEST = auto()
207
- """The resource query request context.
208
-
209
- Should be used for clients building a payload for a resource query request,
210
- and servers validating resource query request payloads.
211
-
212
- - When used for serialization, it will not dump attributes annotated with :attr:`~scim2_models.Mutability.write_only`.
213
- - When used for validation, it will raise a :class:`~pydantic.ValidationError` when finding attributes annotated with :attr:`~scim2_models.Mutability.write_only`.
214
- """
215
-
216
- RESOURCE_QUERY_RESPONSE = auto()
217
- """The resource query response context.
218
-
219
- Should be used for servers building a payload for a resource query
220
- response, and clients validating resource query response payloads.
221
-
222
- - When used for validation, it will raise a :class:`~pydantic.ValidationError` when finding attributes annotated with :attr:`~scim2_models.Returned.never` or when attributes annotated with :attr:`~scim2_models.Returned.always` are missing or :data:`None`;
223
- - When used for serialization, it will:
224
- - always dump attributes annotated with :attr:`~scim2_models.Returned.always`;
225
- - never dump attributes annotated with :attr:`~scim2_models.Returned.never`;
226
- - dump attributes annotated with :attr:`~scim2_models.Returned.default` unless they are explicitly excluded;
227
- - not dump attributes annotated with :attr:`~scim2_models.Returned.request` unless they are explicitly included.
228
- """
229
-
230
- RESOURCE_REPLACEMENT_REQUEST = auto()
231
- """The resource replacement request context.
232
-
233
- Should be used for clients building a payload for a resource replacement request,
234
- and servers validating resource replacement request payloads.
235
-
236
- - When used for serialization, it will not dump attributes annotated with :attr:`~scim2_models.Mutability.read_only`.
237
- - When used for validation, it will ignore attributes annotated with :attr:`scim2_models.Mutability.read_only` and raise a :class:`~pydantic.ValidationError`:
238
- - when finding attributes annotated with :attr:`~scim2_models.Mutability.immutable` different than :paramref:`~scim2_models.BaseModel.model_validate.original`:
239
- - when attributes annotated with :attr:`Required.true <scim2_models.Required.true>` are missing on null.
240
- """
241
-
242
- RESOURCE_REPLACEMENT_RESPONSE = auto()
243
- """The resource replacement response context.
244
-
245
- Should be used for servers building a payload for a resource
246
- replacement response, and clients validating resource query
247
- replacement payloads.
248
-
249
- - When used for validation, it will raise a :class:`~pydantic.ValidationError` when finding attributes annotated with :attr:`~scim2_models.Returned.never` or when attributes annotated with :attr:`~scim2_models.Returned.always` are missing or :data:`None`;
250
- - When used for serialization, it will:
251
- - always dump attributes annotated with :attr:`~scim2_models.Returned.always`;
252
- - never dump attributes annotated with :attr:`~scim2_models.Returned.never`;
253
- - dump attributes annotated with :attr:`~scim2_models.Returned.default` unless they are explicitly excluded;
254
- - not dump attributes annotated with :attr:`~scim2_models.Returned.request` unless they are explicitly included.
255
- """
256
-
257
- SEARCH_REQUEST = auto()
258
- """The search request context.
259
-
260
- Should be used for clients building a payload for a search request,
261
- and servers validating search request payloads.
262
-
263
- - When used for serialization, it will not dump attributes annotated with :attr:`~scim2_models.Mutability.write_only`.
264
- - When used for validation, it will raise a :class:`~pydantic.ValidationError` when finding attributes annotated with :attr:`~scim2_models.Mutability.write_only`.
265
- """
266
-
267
- SEARCH_RESPONSE = auto()
268
- """The resource query response context.
269
-
270
- Should be used for servers building a payload for a search response,
271
- and clients validating resource search payloads.
272
-
273
- - When used for validation, it will raise a :class:`~pydantic.ValidationError` when finding attributes annotated with :attr:`~scim2_models.Returned.never` or when attributes annotated with :attr:`~scim2_models.Returned.always` are missing or :data:`None`;
274
- - When used for serialization, it will:
275
- - always dump attributes annotated with :attr:`~scim2_models.Returned.always`;
276
- - never dump attributes annotated with :attr:`~scim2_models.Returned.never`;
277
- - dump attributes annotated with :attr:`~scim2_models.Returned.default` unless they are explicitly excluded;
278
- - not dump attributes annotated with :attr:`~scim2_models.Returned.request` unless they are explicitly included.
279
- """
280
-
281
- @classmethod
282
- def is_request(cls, ctx: "Context") -> bool:
283
- return ctx in (
284
- cls.RESOURCE_CREATION_REQUEST,
285
- cls.RESOURCE_QUERY_REQUEST,
286
- cls.RESOURCE_REPLACEMENT_REQUEST,
287
- cls.SEARCH_REQUEST,
288
- )
289
-
290
- @classmethod
291
- def is_response(cls, ctx: "Context") -> bool:
292
- return ctx in (
293
- cls.RESOURCE_CREATION_RESPONSE,
294
- cls.RESOURCE_QUERY_RESPONSE,
295
- cls.RESOURCE_REPLACEMENT_RESPONSE,
296
- cls.SEARCH_RESPONSE,
297
- )
298
-
299
-
300
- class Mutability(str, Enum):
301
- """A single keyword indicating the circumstances under which the value of the attribute can be (re)defined."""
302
-
303
- read_only = "readOnly"
304
- """The attribute SHALL NOT be modified."""
305
-
306
- read_write = "readWrite"
307
- """The attribute MAY be updated and read at any time."""
308
-
309
- immutable = "immutable"
310
- """The attribute MAY be defined at resource creation (e.g., POST) or at
311
- record replacement via a request (e.g., a PUT).
312
-
313
- The attribute SHALL NOT be updated.
314
- """
315
-
316
- write_only = "writeOnly"
317
- """The attribute MAY be updated at any time.
318
-
319
- Attribute values SHALL NOT be returned (e.g., because the value is a
320
- stored hash). Note: An attribute with a mutability of "writeOnly"
321
- usually also has a returned setting of "never".
322
- """
323
-
324
- _default = read_write
325
-
326
-
327
- class Returned(str, Enum):
328
- """A single keyword that indicates when an attribute and associated values are returned in response to a GET request or in response to a PUT, POST, or PATCH request."""
329
-
330
- always = "always" # cannot be excluded
331
- """The attribute is always returned, regardless of the contents of the
332
- "attributes" parameter.
333
-
334
- For example, "id" is always returned to identify a SCIM resource.
335
- """
336
-
337
- never = "never" # always excluded
338
- """The attribute is never returned, regardless of the contents of the
339
- "attributes" parameter."""
340
-
341
- default = "default" # included by default but can be excluded
342
- """The attribute is returned by default in all SCIM operation responses
343
- where attribute values are returned, unless it is explicitly excluded."""
344
-
345
- request = "request" # excluded by default but can be included
346
- """The attribute is returned in response to any PUT, POST, or PATCH
347
- operations if specified in the "attributes" parameter."""
348
-
349
- _default = default
350
-
351
-
352
- class Uniqueness(str, Enum):
353
- """A single keyword value that specifies how the service provider enforces uniqueness of attribute values."""
354
-
355
- none = "none"
356
- """The values are not intended to be unique in any way."""
357
-
358
- server = "server"
359
- """The value SHOULD be unique within the context of the current SCIM
360
- endpoint (or tenancy) and MAY be globally unique (e.g., a "username", email
361
- address, or other server-generated key or counter).
362
-
363
- No two resources on the same server SHOULD possess the same value.
364
- """
365
-
366
- global_ = "global"
367
- """The value SHOULD be globally unique (e.g., an email address, a GUID, or
368
- other value).
369
-
370
- No two resources on any server SHOULD possess the same value.
371
- """
372
-
373
- _default = none
374
-
375
-
376
- class Required(Enum):
377
- """A Boolean value that specifies whether the attribute is required or not.
378
-
379
- Missing required attributes raise a :class:`~pydantic.ValidationError` on :attr:`~scim2_models.Context.RESOURCE_CREATION_REQUEST` and :attr:`~scim2_models.Context.RESOURCE_REPLACEMENT_REQUEST` validations.
380
- """
381
-
382
- true = True
383
- false = False
384
-
385
- _default = false
386
-
387
- def __bool__(self):
388
- return self.value
389
-
390
-
391
- class CaseExact(Enum):
392
- """A Boolean value that specifies whether a string attribute is case- sensitive or not."""
393
-
394
- true = True
395
- false = False
396
-
397
- _default = false
398
-
399
- def __bool__(self):
400
- return self.value
401
-
402
-
403
41
  class BaseModel(PydanticBaseModel):
404
42
  """Base Model for everything."""
405
43
 
@@ -421,7 +59,7 @@ class BaseModel(PydanticBaseModel):
421
59
 
422
60
  default_value = getattr(annotation_type, "_default", None)
423
61
 
424
- def annotation_type_filter(item):
62
+ def annotation_type_filter(item: Any) -> bool:
425
63
  return isinstance(item, annotation_type)
426
64
 
427
65
  field_annotation = next(
@@ -469,6 +107,7 @@ class BaseModel(PydanticBaseModel):
469
107
  """Check and fix that the field mutability is expected according to the requests validation context, as defined in :rfc:`RFC7643 §7 <7653#section-7>`."""
470
108
  if (
471
109
  not info.context
110
+ or not info.field_name
472
111
  or not info.context.get("scim")
473
112
  or not Context.is_request(info.context["scim"])
474
113
  ):
@@ -522,7 +161,9 @@ class BaseModel(PydanticBaseModel):
522
161
  return value
523
162
 
524
163
  normalized_value = normalize_value(value)
525
- return handler(normalized_value)
164
+ obj = handler(normalized_value)
165
+ assert isinstance(obj, cls)
166
+ return obj
526
167
 
527
168
  @model_validator(mode="wrap")
528
169
  @classmethod
@@ -530,19 +171,20 @@ class BaseModel(PydanticBaseModel):
530
171
  cls, value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
531
172
  ) -> Self:
532
173
  """Check that the fields returnability is expected according to the responses validation context, as defined in :rfc:`RFC7643 §7 <7653#section-7>`."""
533
- value = handler(value)
174
+ obj = handler(value)
175
+ assert isinstance(obj, cls)
534
176
 
535
177
  if (
536
178
  not info.context
537
179
  or not info.context.get("scim")
538
180
  or not Context.is_response(info.context["scim"])
539
181
  ):
540
- return value
182
+ return obj
541
183
 
542
184
  for field_name in cls.model_fields:
543
185
  returnability = cls.get_field_annotation(field_name, Returned)
544
186
 
545
- if returnability == Returned.always and getattr(value, field_name) is None:
187
+ if returnability == Returned.always and getattr(obj, field_name) is None:
546
188
  raise PydanticCustomError(
547
189
  "returned_error",
548
190
  "Field '{field_name}' has returnability 'always' but value is missing or null",
@@ -551,10 +193,7 @@ class BaseModel(PydanticBaseModel):
551
193
  },
552
194
  )
553
195
 
554
- if (
555
- returnability == Returned.never
556
- and getattr(value, field_name) is not None
557
- ):
196
+ if returnability == Returned.never and getattr(obj, field_name) is not None:
558
197
  raise PydanticCustomError(
559
198
  "returned_error",
560
199
  "Field '{field_name}' has returnability 'never' but value is set",
@@ -563,7 +202,7 @@ class BaseModel(PydanticBaseModel):
563
202
  },
564
203
  )
565
204
 
566
- return value
205
+ return obj
567
206
 
568
207
  @model_validator(mode="wrap")
569
208
  @classmethod
@@ -571,7 +210,8 @@ class BaseModel(PydanticBaseModel):
571
210
  cls, value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
572
211
  ) -> Self:
573
212
  """Check that the required attributes are present in creations and replacement requests."""
574
- value = handler(value)
213
+ obj = handler(value)
214
+ assert isinstance(obj, cls)
575
215
 
576
216
  if (
577
217
  not info.context
@@ -582,12 +222,12 @@ class BaseModel(PydanticBaseModel):
582
222
  Context.RESOURCE_REPLACEMENT_REQUEST,
583
223
  )
584
224
  ):
585
- return value
225
+ return obj
586
226
 
587
227
  for field_name in cls.model_fields:
588
228
  necessity = cls.get_field_annotation(field_name, Required)
589
229
 
590
- if necessity == Required.true and getattr(value, field_name) is None:
230
+ if necessity == Required.true and getattr(obj, field_name) is None:
591
231
  raise PydanticCustomError(
592
232
  "required_error",
593
233
  "Field '{field_name}' is required but value is missing or null",
@@ -596,7 +236,7 @@ class BaseModel(PydanticBaseModel):
596
236
  },
597
237
  )
598
238
 
599
- return value
239
+ return obj
600
240
 
601
241
  @model_validator(mode="wrap")
602
242
  @classmethod
@@ -606,7 +246,8 @@ class BaseModel(PydanticBaseModel):
606
246
  """Check if 'immutable' attributes have been mutated in replacement requests."""
607
247
  from scim2_models.rfc7643.resource import Resource
608
248
 
609
- value = handler(value)
249
+ obj = handler(value)
250
+ assert isinstance(obj, cls)
610
251
 
611
252
  context = info.context.get("scim") if info.context else None
612
253
  original = info.context.get("original") if info.context else None
@@ -615,12 +256,16 @@ class BaseModel(PydanticBaseModel):
615
256
  and issubclass(cls, Resource)
616
257
  and original is not None
617
258
  ):
618
- cls.check_mutability_issues(original, value)
619
- return value
259
+ cls.check_mutability_issues(original, obj)
260
+ return obj
620
261
 
621
262
  @classmethod
622
- def check_mutability_issues(cls, original: "BaseModel", replacement: "BaseModel"):
263
+ def check_mutability_issues(
264
+ cls, original: "BaseModel", replacement: "BaseModel"
265
+ ) -> None:
623
266
  """Compare two instances, and check for differences of values on the fields marked as immutable."""
267
+ from .attributes import is_complex_attribute
268
+
624
269
  model = replacement.__class__
625
270
  for field_name in model.model_fields:
626
271
  mutability = model.get_field_annotation(field_name, Mutability)
@@ -634,47 +279,51 @@ class BaseModel(PydanticBaseModel):
634
279
  )
635
280
 
636
281
  attr_type = model.get_field_root_type(field_name)
637
- if is_complex_attribute(attr_type) and not model.get_field_multiplicity(
638
- field_name
282
+ if (
283
+ attr_type
284
+ and is_complex_attribute(attr_type)
285
+ and not model.get_field_multiplicity(field_name)
639
286
  ):
640
287
  original_val = getattr(original, field_name)
641
288
  replacement_value = getattr(replacement, field_name)
642
289
  if original_val is not None and replacement_value is not None:
643
290
  cls.check_mutability_issues(original_val, replacement_value)
644
291
 
645
- def mark_with_schema(self):
646
- """Navigate through attributes and sub-attributes of type ComplexAttribute, and mark them with a '_schema' attribute.
292
+ def set_complex_attribute_urns(self) -> None:
293
+ """Navigate through attributes and sub-attributes of type ComplexAttribute, and mark them with a 'attribute_urn' attribute.
647
294
 
648
- '_schema' will later be used by 'get_attribute_urn'.
295
+ 'attribute_urn' will later be used by 'get_attribute_urn'.
649
296
  """
650
- from scim2_models.rfc7643.resource import Resource
297
+ from .attributes import ComplexAttribute
298
+ from .attributes import is_complex_attribute
299
+
300
+ if isinstance(self, ComplexAttribute):
301
+ main_schema = self.attribute_urn
302
+ separator = "."
303
+ else:
304
+ main_schema = self.__class__.model_fields["schemas"].default[0]
305
+ separator = ":"
651
306
 
652
307
  for field_name in self.__class__.model_fields:
653
308
  attr_type = self.get_field_root_type(field_name)
654
- if not is_complex_attribute(attr_type):
309
+ if not attr_type or not is_complex_attribute(attr_type):
655
310
  continue
656
311
 
657
- main_schema = (
658
- getattr(self, "_schema", None)
659
- or self.__class__.model_fields["schemas"].default[0]
660
- )
661
-
662
- separator = ":" if isinstance(self, Resource) else "."
663
312
  schema = f"{main_schema}{separator}{field_name}"
664
313
 
665
314
  if attr_value := getattr(self, field_name):
666
315
  if isinstance(attr_value, list):
667
316
  for item in attr_value:
668
- item._schema = schema
317
+ item.attribute_urn = schema
669
318
  else:
670
- attr_value._schema = schema
319
+ attr_value.attribute_urn = schema
671
320
 
672
321
  @field_serializer("*", mode="wrap")
673
322
  def scim_serializer(
674
323
  self,
675
324
  value: Any,
676
325
  handler: SerializerFunctionWrapHandler,
677
- info: SerializationInfo,
326
+ info: FieldSerializationInfo,
678
327
  ) -> Any:
679
328
  """Serialize the fields according to mutability indications passed in the serialization context."""
680
329
  value = handler(value)
@@ -688,7 +337,7 @@ class BaseModel(PydanticBaseModel):
688
337
 
689
338
  return value
690
339
 
691
- def scim_request_serializer(self, value: Any, info: SerializationInfo) -> Any:
340
+ def scim_request_serializer(self, value: Any, info: FieldSerializationInfo) -> Any:
692
341
  """Serialize the fields according to mutability indications passed in the serialization context."""
693
342
  mutability = self.get_field_annotation(info.field_name, Mutability)
694
343
  scim_ctx = info.context.get("scim") if info.context else None
@@ -712,7 +361,7 @@ class BaseModel(PydanticBaseModel):
712
361
 
713
362
  return value
714
363
 
715
- def scim_response_serializer(self, value: Any, info: SerializationInfo) -> Any:
364
+ def scim_response_serializer(self, value: Any, info: FieldSerializationInfo) -> Any:
716
365
  """Serialize the fields according to returnability indications passed in the serialization context."""
717
366
  returnability = self.get_field_annotation(info.field_name, Returned)
718
367
  attribute_urn = self.get_attribute_urn(info.field_name)
@@ -746,20 +395,20 @@ class BaseModel(PydanticBaseModel):
746
395
 
747
396
  @model_serializer(mode="wrap")
748
397
  def model_serializer_exclude_none(
749
- self, handler, info: SerializationInfo
398
+ self, handler: SerializerFunctionWrapHandler, info: SerializationInfo
750
399
  ) -> dict[str, Any]:
751
400
  """Remove `None` values inserted by the :meth:`~scim2_models.base.BaseModel.scim_serializer`."""
752
- self.mark_with_schema()
401
+ self.set_complex_attribute_urns()
753
402
  result = handler(self)
754
403
  return {key: value for key, value in result.items() if value is not None}
755
404
 
756
405
  @classmethod
757
406
  def model_validate(
758
407
  cls,
759
- *args,
408
+ *args: Any,
760
409
  scim_ctx: Optional[Context] = Context.DEFAULT,
761
410
  original: Optional["BaseModel"] = None,
762
- **kwargs,
411
+ **kwargs: Any,
763
412
  ) -> Self:
764
413
  """Validate SCIM payloads and generate model representation by using Pydantic :code:`BaseModel.model_validate`.
765
414
 
@@ -779,126 +428,20 @@ class BaseModel(PydanticBaseModel):
779
428
 
780
429
  return super().model_validate(*args, **kwargs)
781
430
 
782
- def _prepare_model_dump(
783
- self,
784
- scim_ctx: Optional[Context] = Context.DEFAULT,
785
- attributes: Optional[list[str]] = None,
786
- excluded_attributes: Optional[list[str]] = None,
787
- **kwargs,
788
- ):
789
- kwargs.setdefault("context", {}).setdefault("scim", scim_ctx)
790
- kwargs["context"]["scim_attributes"] = [
791
- validate_attribute_urn(attribute, self.__class__)
792
- for attribute in (attributes or [])
793
- ]
794
- kwargs["context"]["scim_excluded_attributes"] = [
795
- validate_attribute_urn(attribute, self.__class__)
796
- for attribute in (excluded_attributes or [])
797
- ]
798
-
799
- if scim_ctx:
800
- kwargs.setdefault("exclude_none", True)
801
- kwargs.setdefault("by_alias", True)
802
-
803
- return kwargs
804
-
805
- def model_dump(
806
- self,
807
- *args,
808
- scim_ctx: Optional[Context] = Context.DEFAULT,
809
- attributes: Optional[list[str]] = None,
810
- excluded_attributes: Optional[list[str]] = None,
811
- **kwargs,
812
- ) -> dict:
813
- """Create a model representation that can be included in SCIM messages by using Pydantic :code:`BaseModel.model_dump`.
814
-
815
- :param scim_ctx: If a SCIM context is passed, some default values of
816
- Pydantic :code:`BaseModel.model_dump` are tuned to generate valid SCIM
817
- messages. Pass :data:`None` to get the default Pydantic behavior.
818
- """
819
- dump_kwargs = self._prepare_model_dump(
820
- scim_ctx, attributes, excluded_attributes, **kwargs
821
- )
822
- if scim_ctx:
823
- dump_kwargs.setdefault("mode", "json")
824
- return super().model_dump(*args, **dump_kwargs)
825
-
826
- def model_dump_json(
827
- self,
828
- *args,
829
- scim_ctx: Optional[Context] = Context.DEFAULT,
830
- attributes: Optional[list[str]] = None,
831
- excluded_attributes: Optional[list[str]] = None,
832
- **kwargs,
833
- ) -> dict:
834
- """Create a JSON model representation that can be included in SCIM messages by using Pydantic :code:`BaseModel.model_dump_json`.
835
-
836
- :param scim_ctx: If a SCIM context is passed, some default values of
837
- Pydantic :code:`BaseModel.model_dump` are tuned to generate valid SCIM
838
- messages. Pass :data:`None` to get the default Pydantic behavior.
839
- """
840
- dump_kwargs = self._prepare_model_dump(
841
- scim_ctx, attributes, excluded_attributes, **kwargs
842
- )
843
- return super().model_dump_json(*args, **dump_kwargs)
844
-
845
431
  def get_attribute_urn(self, field_name: str) -> str:
846
432
  """Build the full URN of the attribute.
847
433
 
848
434
  See :rfc:`RFC7644 §3.10 <7644#section-3.10>`.
849
435
  """
436
+ from scim2_models.rfc7643.resource import Extension
437
+
850
438
  main_schema = self.__class__.model_fields["schemas"].default[0]
851
- alias = (
852
- self.__class__.model_fields[field_name].serialization_alias or field_name
439
+ field = self.__class__.model_fields[field_name]
440
+ alias = field.serialization_alias or field_name
441
+ field_type = self.get_field_root_type(field_name)
442
+ full_urn = (
443
+ alias
444
+ if isclass(field_type) and issubclass(field_type, Extension)
445
+ else f"{main_schema}:{alias}"
853
446
  )
854
-
855
- # if alias contains a ':' this is an extension urn
856
- full_urn = alias if ":" in alias else f"{main_schema}:{alias}"
857
447
  return full_urn
858
-
859
-
860
- class ComplexAttribute(BaseModel):
861
- """A complex attribute as defined in :rfc:`RFC7643 §2.3.8 <7643#section-2.3.8>`."""
862
-
863
- _schema: Optional[str] = None
864
-
865
- def get_attribute_urn(self, field_name: str) -> str:
866
- """Build the full URN of the attribute.
867
-
868
- See :rfc:`RFC7644 §3.10 <7644#section-3.10>`.
869
- """
870
- alias = (
871
- self.__class__.model_fields[field_name].serialization_alias or field_name
872
- )
873
- return f"{self._schema}.{alias}"
874
-
875
-
876
- class MultiValuedComplexAttribute(ComplexAttribute):
877
- type: Optional[str] = None
878
- """A label indicating the attribute's function."""
879
-
880
- primary: Optional[bool] = None
881
- """A Boolean value indicating the 'primary' or preferred attribute value
882
- for this attribute."""
883
-
884
- display: Annotated[Optional[str], Mutability.immutable] = None
885
- """A human-readable name, primarily used for display purposes."""
886
-
887
- value: Optional[Any] = None
888
- """The value of an entitlement."""
889
-
890
- ref: Optional[Reference] = Field(None, serialization_alias="$ref")
891
- """The reference URI of a target resource, if the attribute is a
892
- reference."""
893
-
894
-
895
- def is_complex_attribute(type) -> bool:
896
- # issubclass raise a TypeError with 'Reference' on python < 3.11
897
- return (
898
- get_origin(type) != Reference
899
- and isclass(type)
900
- and issubclass(type, (ComplexAttribute, MultiValuedComplexAttribute))
901
- )
902
-
903
-
904
- BaseModelType: type = type(BaseModel)