Skip to content

Commit d082e9d

Browse files
committed
Revert "refactor: Resources must define a 'scim_schema' attribute"
This reverts commit 7514c8b. This was not compatible with Pydantic 2.10
1 parent 7514c8b commit d082e9d

23 files changed

+75
-153
lines changed

doc/changelog.rst

-22
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,6 @@
11
Changelog
22
=========
33

4-
[0.3.0] - Unreleased
5-
--------------------
6-
7-
.. warning::
8-
9-
This version comes with breaking changes:
10-
11-
- :class:`~scim2_models.Resource`, :class:`~scim2_models.Extension` and :class:`~scim2_models.Message` must define a ``scim_schema`` attribute.
12-
13-
.. code-block:: python
14-
:caption: Before
15-
16-
class MyResource(Resource):
17-
schemas : list[str] = ["example:schema:MyResource"]
18-
19-
.. code-block:: python
20-
:caption: After
21-
22-
class MyResource(Resource):
23-
scim_schema: ClassVar[str] = "example:schema:MyResource"
24-
25-
264
[0.2.7] - 2024-11-30
275
--------------------
286

doc/tutorial.rst

+3-6
Original file line numberDiff line numberDiff line change
@@ -272,12 +272,11 @@ Custom models
272272

273273
You can write your own model and use it the same way than the other scim2-models models.
274274
Just inherit from :class:`~scim2_models.Resource` for your main resource, or :class:`~scim2_models.Extension` for extensions.
275-
Then you need to define a ``scim_schema`` attribute, that is a class variable detailing the schema identifier of your model.
276275
Use :class:`~scim2_models.ComplexAttribute` as base class for complex attributes:
277276

278277
.. code-block:: python
279278
280-
>>> from typing import Annotated, ClassVar, Optional, List
279+
>>> from typing import Annotated, Optional, List
281280
>>> from scim2_models import Resource, Returned, Mutability, ComplexAttribute
282281
>>> from enum import Enum
283282
@@ -289,7 +288,7 @@ Use :class:`~scim2_models.ComplexAttribute` as base class for complex attributes
289288
... """The pet color."""
290289
291290
>>> class Pet(Resource):
292-
... scim_schema : ClassVar[str] = "example:schemas:Pet"
291+
... schemas: List[str] = ["example:schemas:Pet"]
293292
...
294293
... name: Annotated[Optional[str], Mutability.immutable, Returned.always]
295294
... """The name of the pet."""
@@ -310,8 +309,6 @@ that can take type parameters to represent :rfc:`RFC7643 §7 'referenceTypes'<7
310309

311310
>>> from typing import Literal
312311
>>> class PetOwner(Resource):
313-
... scim_schema : ClassVar[str] = "examples:schema.PetOwner"
314-
...
315312
... pet: Reference[Literal["Pet"]]
316313

317314
:class:`~scim2_models.Reference` has two special type parameters :data:`~scim2_models.ExternalReference` and :data:`~scim2_models.URIReference` that matches :rfc:`RFC7643 §7 <7643#section-7>` external and URI reference types.
@@ -328,7 +325,7 @@ This is useful for server implementations, so custom models or models provided b
328325
>>> class MyCustomResource(Resource):
329326
... """My awesome custom schema."""
330327
...
331-
... scim_schema: ClassVar[str] = "example:schemas:MyCustomResource"
328+
... schemas: List[str] = ["example:schemas:MyCustomResource"]
332329
...
333330
... foobar: Optional[str]
334331
...

scim2_models/base.py

+14-7
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,11 @@ def validate_attribute_urn(
9292
if default_resource and default_resource not in resource_types:
9393
resource_types.append(default_resource)
9494

95-
default_schema = default_resource.scim_schema if default_resource else None
95+
default_schema = (
96+
default_resource.model_fields["schemas"].default[0]
97+
if default_resource
98+
else None
99+
)
96100

97101
schema: Optional[Any]
98102
schema, attribute_base = extract_schema_and_attribute_base(attribute_name)
@@ -609,17 +613,20 @@ def mark_with_schema(self):
609613
if not is_complex_attribute(attr_type):
610614
continue
611615

612-
main_schema = getattr(self, "_scim_schema", None) or self.scim_schema
616+
main_schema = (
617+
getattr(self, "_schema", None)
618+
or self.model_fields["schemas"].default[0]
619+
)
613620

614621
separator = ":" if isinstance(self, Resource) else "."
615622
schema = f"{main_schema}{separator}{field_name}"
616623

617624
if attr_value := getattr(self, field_name):
618625
if isinstance(attr_value, list):
619626
for item in attr_value:
620-
item._scim_schema = schema
627+
item._schema = schema
621628
else:
622-
attr_value._scim_schema = schema
629+
attr_value._schema = schema
623630

624631
@field_serializer("*", mode="wrap")
625632
def scim_serializer(
@@ -786,7 +793,7 @@ def get_attribute_urn(self, field_name: str) -> str:
786793
787794
See :rfc:`RFC7644 §3.10 <7644#section-3.10>`.
788795
"""
789-
main_schema = self.scim_schema
796+
main_schema = self.model_fields["schemas"].default[0]
790797
alias = self.model_fields[field_name].serialization_alias or field_name
791798

792799
# if alias contains a ':' this is an extension urn
@@ -797,15 +804,15 @@ def get_attribute_urn(self, field_name: str) -> str:
797804
class ComplexAttribute(BaseModel):
798805
"""A complex attribute as defined in :rfc:`RFC7643 §2.3.8 <7643#section-2.3.8>`."""
799806

800-
_scim_schema: Optional[str] = None
807+
_schema: Optional[str] = None
801808

802809
def get_attribute_urn(self, field_name: str) -> str:
803810
"""Build the full URN of the attribute.
804811
805812
See :rfc:`RFC7644 §3.10 <7644#section-3.10>`.
806813
"""
807814
alias = self.model_fields[field_name].serialization_alias or field_name
808-
return f"{self._scim_schema}.{alias}"
815+
return f"{self._schema}.{alias}"
809816

810817

811818
class MultiValuedComplexAttribute(ComplexAttribute):

scim2_models/rfc7643/enterprise_user.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from typing import Annotated
2-
from typing import ClassVar
32
from typing import Literal
43
from typing import Optional
54

@@ -27,9 +26,7 @@ class Manager(ComplexAttribute):
2726

2827

2928
class EnterpriseUser(Extension):
30-
scim_schema: ClassVar[str] = (
31-
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
32-
)
29+
schemas: list[str] = ["urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"]
3330

3431
employee_number: Optional[str] = None
3532
"""Numeric or alphanumeric identifier assigned to a person, typically based

scim2_models/rfc7643/group.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from typing import Annotated
2-
from typing import ClassVar
32
from typing import Literal
43
from typing import Optional
54
from typing import Union
@@ -32,7 +31,7 @@ class GroupMember(MultiValuedComplexAttribute):
3231

3332

3433
class Group(Resource):
35-
scim_schema: ClassVar[str] = "urn:ietf:params:scim:schemas:core:2.0:Group"
34+
schemas: list[str] = ["urn:ietf:params:scim:schemas:core:2.0:Group"]
3635

3736
display_name: Optional[str] = None
3837
"""A human-readable name for the Group."""

scim2_models/rfc7643/resource.py

+6-23
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,6 @@ class Meta(ComplexAttribute):
7878

7979

8080
class Extension(BaseModel):
81-
def __init_subclass__(cls, **kwargs):
82-
super().__init_subclass__(**kwargs)
83-
if not hasattr(cls, "scim_schema"):
84-
raise AttributeError(
85-
f"{cls.__name__} did not define a scim_schema attribute"
86-
)
87-
8881
@classmethod
8982
def to_schema(cls):
9083
"""Build a :class:`~scim2_models.Schema` from the current extension class."""
@@ -127,7 +120,7 @@ def __new__(cls, name, bases, attrs, **kwargs):
127120
else [extensions]
128121
)
129122
for extension in extensions:
130-
schema = extension.scim_schema
123+
schema = extension.model_fields["schemas"].default[0]
131124
attrs.setdefault("__annotations__", {})[extension.__name__] = Annotated[
132125
Optional[extension],
133126
WrapSerializer(extension_serializer),
@@ -143,18 +136,6 @@ def __new__(cls, name, bases, attrs, **kwargs):
143136

144137

145138
class Resource(BaseModel, Generic[AnyExtension], metaclass=ResourceMetaclass):
146-
def __init_subclass__(cls, **kwargs):
147-
super().__init_subclass__(**kwargs)
148-
if not hasattr(cls, "scim_schema"):
149-
raise AttributeError(
150-
f"{cls.__name__} did not define a scim_schema attribute"
151-
)
152-
153-
def init_schemas():
154-
return [cls.scim_schema]
155-
156-
cls.model_fields["schemas"].default_factory = init_schemas
157-
158139
schemas: list[str]
159140
"""The "schemas" attribute is a REQUIRED attribute and is an array of
160141
Strings containing URIs that are used to indicate the namespaces of the
@@ -205,7 +186,9 @@ def get_extension_models(cls) -> dict[str, type]:
205186
else extension_models
206187
)
207188

208-
by_schema = {ext.scim_schema: ext for ext in extension_models}
189+
by_schema = {
190+
ext.model_fields["schemas"].default[0]: ext for ext in extension_models
191+
}
209192
return by_schema
210193

211194
@staticmethod
@@ -214,7 +197,7 @@ def get_by_schema(
214197
) -> Optional[type]:
215198
"""Given a resource type list and a schema, find the matching resource type."""
216199
by_schema = {
217-
resource_type.scim_schema.lower(): resource_type
200+
resource_type.model_fields["schemas"].default[0].lower(): resource_type
218201
for resource_type in (resource_types or [])
219202
}
220203
if with_extensions:
@@ -291,7 +274,7 @@ def compare_field_infos(fi1, fi2):
291274
def model_to_schema(model: type[BaseModel]):
292275
from scim2_models.rfc7643.schema import Schema
293276

294-
schema_urn = model.scim_schema
277+
schema_urn = model.model_fields["schemas"].default[0]
295278
field_infos = dedicated_attributes(model)
296279
attributes = [
297280
model_attribute_to_attribute(model, attribute_name)

scim2_models/rfc7643/resource_type.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from typing import Annotated
2-
from typing import ClassVar
32
from typing import Optional
43

54
from pydantic import Field
@@ -36,7 +35,7 @@ class SchemaExtension(ComplexAttribute):
3635

3736

3837
class ResourceType(Resource):
39-
scim_schema: ClassVar[str] = "urn:ietf:params:scim:schemas:core:2.0:ResourceType"
38+
schemas: list[str] = ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"]
4039

4140
name: Annotated[Optional[str], Mutability.read_only, Required.true] = None
4241
"""The resource type name.
@@ -79,7 +78,7 @@ class ResourceType(Resource):
7978
@classmethod
8079
def from_resource(cls, resource_model: type[Resource]) -> Self:
8180
"""Build a naive ResourceType from a resource model."""
82-
schema = resource_model.scim_schema
81+
schema = resource_model.model_fields["schemas"].default[0]
8382
name = schema.split(":")[-1]
8483
extensions = resource_model.__pydantic_generic_metadata__["args"]
8584
return ResourceType(
@@ -89,7 +88,9 @@ def from_resource(cls, resource_model: type[Resource]) -> Self:
8988
endpoint=f"/{name}s",
9089
schema_=schema,
9190
schema_extensions=[
92-
SchemaExtension(schema_=extension.scim_schema, required=False)
91+
SchemaExtension(
92+
schema_=extension.model_fields["schemas"].default[0], required=False
93+
)
9394
for extension in extensions
9495
],
9596
)

scim2_models/rfc7643/schema.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from enum import Enum
44
from typing import Annotated
55
from typing import Any
6-
from typing import ClassVar
76
from typing import List # noqa : UP005
87
from typing import Literal
98
from typing import Optional
@@ -65,7 +64,10 @@ def make_python_model(
6564
for attr in (obj.attributes or [])
6665
if attr.name
6766
}
68-
pydantic_attributes["scim_schema"] = (ClassVar[str], obj.id)
67+
pydantic_attributes["schemas"] = (
68+
Optional[list[str]],
69+
Field(default=[obj.id]),
70+
)
6971

7072
model_name = to_pascal(to_snake(obj.name))
7173
model = create_model(model_name, __base__=base, **pydantic_attributes)
@@ -238,7 +240,7 @@ def to_python(self) -> Optional[tuple[Any, Field]]:
238240

239241

240242
class Schema(Resource):
241-
scim_schema: ClassVar[str] = "urn:ietf:params:scim:schemas:core:2.0:Schema"
243+
schemas: list[str] = ["urn:ietf:params:scim:schemas:core:2.0:Schema"]
242244

243245
id: Annotated[Optional[str], Mutability.read_only, Required.true] = None
244246
"""The unique URI of the schema."""

scim2_models/rfc7643/service_provider_config.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from enum import Enum
22
from typing import Annotated
3-
from typing import ClassVar
43
from typing import Optional
54

65
from pydantic import Field
@@ -95,9 +94,7 @@ class Type(str, Enum):
9594

9695

9796
class ServiceProviderConfig(Resource):
98-
scim_schema: ClassVar[str] = (
99-
"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"
100-
)
97+
schemas: list[str] = ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"]
10198

10299
id: Annotated[
103100
Optional[str], Mutability.read_only, Returned.default, Uniqueness.global_

scim2_models/rfc7643/user.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from enum import Enum
22
from typing import Annotated
3-
from typing import ClassVar
43
from typing import Literal
54
from typing import Optional
65
from typing import Union
@@ -215,7 +214,7 @@ class X509Certificate(MultiValuedComplexAttribute):
215214

216215

217216
class User(Resource):
218-
scim_schema: ClassVar[str] = "urn:ietf:params:scim:schemas:core:2.0:User"
217+
schemas: list[str] = ["urn:ietf:params:scim:schemas:core:2.0:User"]
219218

220219
user_name: Annotated[Optional[str], Uniqueness.server, Required.true] = None
221220
"""Unique identifier for the User, typically used by the user to directly

scim2_models/rfc7644/bulk.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from enum import Enum
22
from typing import Annotated
33
from typing import Any
4-
from typing import ClassVar
54
from typing import Optional
65

76
from pydantic import Field
@@ -54,7 +53,7 @@ class BulkRequest(Message):
5453
The models for Bulk operations are defined, but their behavior is not implemented nor tested yet.
5554
"""
5655

57-
scim_schema: ClassVar[str] = "urn:ietf:params:scim:api:messages:2.0:BulkRequest"
56+
schemas: list[str] = ["urn:ietf:params:scim:api:messages:2.0:BulkRequest"]
5857

5958
fail_on_errors: Optional[int] = None
6059
"""An integer specifying the number of errors that the service provider
@@ -75,7 +74,7 @@ class BulkResponse(Message):
7574
The models for Bulk operations are defined, but their behavior is not implemented nor tested yet.
7675
"""
7776

78-
scim_schema: ClassVar[str] = "urn:ietf:params:scim:api:messages:2.0:BulkResponse"
77+
schemas: list[str] = ["urn:ietf:params:scim:api:messages:2.0:BulkResponse"]
7978

8079
operations: Optional[list[BulkOperation]] = Field(
8180
None, serialization_alias="Operations"

scim2_models/rfc7644/error.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from typing import Annotated
2-
from typing import ClassVar
32
from typing import Optional
43

54
from pydantic import PlainSerializer
@@ -11,7 +10,7 @@
1110
class Error(Message):
1211
"""Representation of SCIM API errors."""
1312

14-
scim_schema: ClassVar[str] = "urn:ietf:params:scim:api:messages:2.0:Error"
13+
schemas: list[str] = ["urn:ietf:params:scim:api:messages:2.0:Error"]
1514

1615
status: Annotated[Optional[int], PlainSerializer(int_to_str)] = None
1716
"""The HTTP status code (see Section 6 of [RFC7231]) expressed as a JSON

0 commit comments

Comments
 (0)