Skip to content

Commit

Permalink
Revert "refactor: Resources must define a 'scim_schema' attribute"
Browse files Browse the repository at this point in the history
This reverts commit 7514c8b.

This was not compatible with Pydantic 2.10
  • Loading branch information
azmeuk committed Dec 2, 2024
1 parent 7514c8b commit d082e9d
Show file tree
Hide file tree
Showing 23 changed files with 75 additions and 153 deletions.
22 changes: 0 additions & 22 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,28 +1,6 @@
Changelog
=========

[0.3.0] - Unreleased
--------------------

.. warning::

This version comes with breaking changes:

- :class:`~scim2_models.Resource`, :class:`~scim2_models.Extension` and :class:`~scim2_models.Message` must define a ``scim_schema`` attribute.

.. code-block:: python
:caption: Before
class MyResource(Resource):
schemas : list[str] = ["example:schema:MyResource"]
.. code-block:: python
:caption: After
class MyResource(Resource):
scim_schema: ClassVar[str] = "example:schema:MyResource"
[0.2.7] - 2024-11-30
--------------------

Expand Down
9 changes: 3 additions & 6 deletions doc/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -272,12 +272,11 @@ Custom models

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

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

>>> from typing import Literal
>>> class PetOwner(Resource):
... scim_schema : ClassVar[str] = "examples:schema.PetOwner"
...
... pet: Reference[Literal["Pet"]]

: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.
Expand All @@ -328,7 +325,7 @@ This is useful for server implementations, so custom models or models provided b
>>> class MyCustomResource(Resource):
... """My awesome custom schema."""
...
... scim_schema: ClassVar[str] = "example:schemas:MyCustomResource"
... schemas: List[str] = ["example:schemas:MyCustomResource"]
...
... foobar: Optional[str]
...
Expand Down
21 changes: 14 additions & 7 deletions scim2_models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,11 @@ def validate_attribute_urn(
if default_resource and default_resource not in resource_types:
resource_types.append(default_resource)

default_schema = default_resource.scim_schema if default_resource else None
default_schema = (
default_resource.model_fields["schemas"].default[0]
if default_resource
else None
)

schema: Optional[Any]
schema, attribute_base = extract_schema_and_attribute_base(attribute_name)
Expand Down Expand Up @@ -609,17 +613,20 @@ def mark_with_schema(self):
if not is_complex_attribute(attr_type):
continue

main_schema = getattr(self, "_scim_schema", None) or self.scim_schema
main_schema = (
getattr(self, "_schema", None)
or self.model_fields["schemas"].default[0]
)

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

if attr_value := getattr(self, field_name):
if isinstance(attr_value, list):
for item in attr_value:
item._scim_schema = schema
item._schema = schema
else:
attr_value._scim_schema = schema
attr_value._schema = schema

@field_serializer("*", mode="wrap")
def scim_serializer(
Expand Down Expand Up @@ -786,7 +793,7 @@ def get_attribute_urn(self, field_name: str) -> str:
See :rfc:`RFC7644 §3.10 <7644#section-3.10>`.
"""
main_schema = self.scim_schema
main_schema = self.model_fields["schemas"].default[0]
alias = self.model_fields[field_name].serialization_alias or field_name

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

_scim_schema: Optional[str] = None
_schema: Optional[str] = None

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


class MultiValuedComplexAttribute(ComplexAttribute):
Expand Down
5 changes: 1 addition & 4 deletions scim2_models/rfc7643/enterprise_user.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from typing import Annotated
from typing import ClassVar
from typing import Literal
from typing import Optional

Expand Down Expand Up @@ -27,9 +26,7 @@ class Manager(ComplexAttribute):


class EnterpriseUser(Extension):
scim_schema: ClassVar[str] = (
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
)
schemas: list[str] = ["urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"]

employee_number: Optional[str] = None
"""Numeric or alphanumeric identifier assigned to a person, typically based
Expand Down
3 changes: 1 addition & 2 deletions scim2_models/rfc7643/group.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from typing import Annotated
from typing import ClassVar
from typing import Literal
from typing import Optional
from typing import Union
Expand Down Expand Up @@ -32,7 +31,7 @@ class GroupMember(MultiValuedComplexAttribute):


class Group(Resource):
scim_schema: ClassVar[str] = "urn:ietf:params:scim:schemas:core:2.0:Group"
schemas: list[str] = ["urn:ietf:params:scim:schemas:core:2.0:Group"]

display_name: Optional[str] = None
"""A human-readable name for the Group."""
Expand Down
29 changes: 6 additions & 23 deletions scim2_models/rfc7643/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,6 @@ class Meta(ComplexAttribute):


class Extension(BaseModel):
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if not hasattr(cls, "scim_schema"):
raise AttributeError(
f"{cls.__name__} did not define a scim_schema attribute"
)

@classmethod
def to_schema(cls):
"""Build a :class:`~scim2_models.Schema` from the current extension class."""
Expand Down Expand Up @@ -127,7 +120,7 @@ def __new__(cls, name, bases, attrs, **kwargs):
else [extensions]
)
for extension in extensions:
schema = extension.scim_schema
schema = extension.model_fields["schemas"].default[0]
attrs.setdefault("__annotations__", {})[extension.__name__] = Annotated[
Optional[extension],
WrapSerializer(extension_serializer),
Expand All @@ -143,18 +136,6 @@ def __new__(cls, name, bases, attrs, **kwargs):


class Resource(BaseModel, Generic[AnyExtension], metaclass=ResourceMetaclass):
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if not hasattr(cls, "scim_schema"):
raise AttributeError(
f"{cls.__name__} did not define a scim_schema attribute"
)

def init_schemas():
return [cls.scim_schema]

cls.model_fields["schemas"].default_factory = init_schemas

schemas: list[str]
"""The "schemas" attribute is a REQUIRED attribute and is an array of
Strings containing URIs that are used to indicate the namespaces of the
Expand Down Expand Up @@ -205,7 +186,9 @@ def get_extension_models(cls) -> dict[str, type]:
else extension_models
)

by_schema = {ext.scim_schema: ext for ext in extension_models}
by_schema = {
ext.model_fields["schemas"].default[0]: ext for ext in extension_models
}
return by_schema

@staticmethod
Expand All @@ -214,7 +197,7 @@ def get_by_schema(
) -> Optional[type]:
"""Given a resource type list and a schema, find the matching resource type."""
by_schema = {
resource_type.scim_schema.lower(): resource_type
resource_type.model_fields["schemas"].default[0].lower(): resource_type
for resource_type in (resource_types or [])
}
if with_extensions:
Expand Down Expand Up @@ -291,7 +274,7 @@ def compare_field_infos(fi1, fi2):
def model_to_schema(model: type[BaseModel]):
from scim2_models.rfc7643.schema import Schema

schema_urn = model.scim_schema
schema_urn = model.model_fields["schemas"].default[0]
field_infos = dedicated_attributes(model)
attributes = [
model_attribute_to_attribute(model, attribute_name)
Expand Down
9 changes: 5 additions & 4 deletions scim2_models/rfc7643/resource_type.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from typing import Annotated
from typing import ClassVar
from typing import Optional

from pydantic import Field
Expand Down Expand Up @@ -36,7 +35,7 @@ class SchemaExtension(ComplexAttribute):


class ResourceType(Resource):
scim_schema: ClassVar[str] = "urn:ietf:params:scim:schemas:core:2.0:ResourceType"
schemas: list[str] = ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"]

name: Annotated[Optional[str], Mutability.read_only, Required.true] = None
"""The resource type name.
Expand Down Expand Up @@ -79,7 +78,7 @@ class ResourceType(Resource):
@classmethod
def from_resource(cls, resource_model: type[Resource]) -> Self:
"""Build a naive ResourceType from a resource model."""
schema = resource_model.scim_schema
schema = resource_model.model_fields["schemas"].default[0]
name = schema.split(":")[-1]
extensions = resource_model.__pydantic_generic_metadata__["args"]
return ResourceType(
Expand All @@ -89,7 +88,9 @@ def from_resource(cls, resource_model: type[Resource]) -> Self:
endpoint=f"/{name}s",
schema_=schema,
schema_extensions=[
SchemaExtension(schema_=extension.scim_schema, required=False)
SchemaExtension(
schema_=extension.model_fields["schemas"].default[0], required=False
)
for extension in extensions
],
)
8 changes: 5 additions & 3 deletions scim2_models/rfc7643/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from enum import Enum
from typing import Annotated
from typing import Any
from typing import ClassVar
from typing import List # noqa : UP005
from typing import Literal
from typing import Optional
Expand Down Expand Up @@ -65,7 +64,10 @@ def make_python_model(
for attr in (obj.attributes or [])
if attr.name
}
pydantic_attributes["scim_schema"] = (ClassVar[str], obj.id)
pydantic_attributes["schemas"] = (
Optional[list[str]],
Field(default=[obj.id]),
)

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


class Schema(Resource):
scim_schema: ClassVar[str] = "urn:ietf:params:scim:schemas:core:2.0:Schema"
schemas: list[str] = ["urn:ietf:params:scim:schemas:core:2.0:Schema"]

id: Annotated[Optional[str], Mutability.read_only, Required.true] = None
"""The unique URI of the schema."""
Expand Down
5 changes: 1 addition & 4 deletions scim2_models/rfc7643/service_provider_config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from enum import Enum
from typing import Annotated
from typing import ClassVar
from typing import Optional

from pydantic import Field
Expand Down Expand Up @@ -95,9 +94,7 @@ class Type(str, Enum):


class ServiceProviderConfig(Resource):
scim_schema: ClassVar[str] = (
"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"
)
schemas: list[str] = ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"]

id: Annotated[
Optional[str], Mutability.read_only, Returned.default, Uniqueness.global_
Expand Down
3 changes: 1 addition & 2 deletions scim2_models/rfc7643/user.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from enum import Enum
from typing import Annotated
from typing import ClassVar
from typing import Literal
from typing import Optional
from typing import Union
Expand Down Expand Up @@ -215,7 +214,7 @@ class X509Certificate(MultiValuedComplexAttribute):


class User(Resource):
scim_schema: ClassVar[str] = "urn:ietf:params:scim:schemas:core:2.0:User"
schemas: list[str] = ["urn:ietf:params:scim:schemas:core:2.0:User"]

user_name: Annotated[Optional[str], Uniqueness.server, Required.true] = None
"""Unique identifier for the User, typically used by the user to directly
Expand Down
5 changes: 2 additions & 3 deletions scim2_models/rfc7644/bulk.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from enum import Enum
from typing import Annotated
from typing import Any
from typing import ClassVar
from typing import Optional

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

scim_schema: ClassVar[str] = "urn:ietf:params:scim:api:messages:2.0:BulkRequest"
schemas: list[str] = ["urn:ietf:params:scim:api:messages:2.0:BulkRequest"]

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

scim_schema: ClassVar[str] = "urn:ietf:params:scim:api:messages:2.0:BulkResponse"
schemas: list[str] = ["urn:ietf:params:scim:api:messages:2.0:BulkResponse"]

operations: Optional[list[BulkOperation]] = Field(
None, serialization_alias="Operations"
Expand Down
3 changes: 1 addition & 2 deletions scim2_models/rfc7644/error.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from typing import Annotated
from typing import ClassVar
from typing import Optional

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

scim_schema: ClassVar[str] = "urn:ietf:params:scim:api:messages:2.0:Error"
schemas: list[str] = ["urn:ietf:params:scim:api:messages:2.0:Error"]

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

0 comments on commit d082e9d

Please sign in to comment.