Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions scim2_models/base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from enum import Enum
from enum import auto
from inspect import isclass
from typing import Any
from typing import Dict
from typing import List
Expand Down Expand Up @@ -432,6 +433,35 @@ def check_response_attributes_necessity(

return handler(value)

def mark_with_schema(self):
"""Navigate through attributes and subattributes of type
ComplexAttribute, and mark them with a '_schema' attribute.

'_schema' will later be used by 'get_attribute_urn'.
"""

from scim2_models.rfc7643.resource import Resource

for field_name, field in self.model_fields.items():
attr_type = self.get_field_root_type(field_name)
if not isclass(attr_type) or not issubclass(attr_type, ComplexAttribute):
continue

if isinstance(self, ComplexAttribute):
main_schema = self._schema
else:
main_schema = 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._schema = schema
else:
attr_value._schema = schema

@field_serializer("*", mode="wrap")
def scim_serializer(
self,
Expand Down Expand Up @@ -568,8 +598,6 @@ def get_attribute_urn(self, field_name: str) -> Returned:
"""Build the full URN of the attribute.

See :rfc:`RFC7644 §3.12 <7644#section-3.12>`.

.. todo:: Actually *guess* the URN instead of using the hacky `_schema` attribute.
"""
main_schema = self.model_fields["schemas"].default[0]
alias = self.model_fields[field_name].alias or field_name
Expand All @@ -580,15 +608,15 @@ class ComplexAttribute(BaseModel):
"""A complex attribute as defined in :rfc:`RFC7643 §2.3.8
<7643#section-2.3.8>`."""

_schema: str

def get_attribute_urn(self, field_name: str) -> Returned:
"""Build the full URN of the attribute.

See :rfc:`RFC7644 §3.12 <7644#section-3.12>`.

.. todo:: Actually *guess* the URN instead of using the hacky `_attribute_urn` attribute.
"""
alias = self.model_fields[field_name].alias or field_name
return f"{self._attribute_urn}.{alias}"
return f"{self._schema}.{alias}"


AnyModel = TypeVar("AnyModel", bound=BaseModel)
4 changes: 0 additions & 4 deletions scim2_models/rfc7643/enterprise_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@


class Manager(ComplexAttribute):
_attribute_urn: str = (
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager"
)

value: Optional[str] = None
"""The id of the SCIM resource representingthe User's manager."""

Expand Down
2 changes: 0 additions & 2 deletions scim2_models/rfc7643/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@


class GroupMember(ComplexAttribute):
_attribute_urn: str = "urn:ietf:params:scim:schemas:core:2.0:Group.members"

value: Annotated[Optional[str], Mutability.immutable] = None
"""Identifier of the member of this Group."""

Expand Down
18 changes: 18 additions & 0 deletions scim2_models/rfc7643/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from pydantic import ConfigDict
from pydantic import Discriminator
from pydantic import Tag
from pydantic import ValidationInfo
from pydantic import ValidatorFunctionWrapHandler
from pydantic import field_serializer
from pydantic import model_validator
from typing_extensions import Self
Expand Down Expand Up @@ -197,6 +199,22 @@ def set_extension_schemas(self, schemas: List[str]):
]
return schemas

@model_validator(mode="wrap")
@classmethod
def attribute_urn_marker(
cls, value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
) -> Self:
"""Navigate through attributes and subattributes of type
ComplexAttribute, and mark them with a '_schema' attribute.

'_schema' will later be used by 'get_attribute_urn'.
"""

obj = handler(value)
obj.mark_with_schema()

return obj


AnyResource = TypeVar("AnyResource", bound="Resource")

Expand Down
4 changes: 0 additions & 4 deletions scim2_models/rfc7643/resource_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@


class SchemaExtension(ComplexAttribute):
_attribute_urn: str = (
"urn:ietf:params:scim:schemas:core:2.0:ResourceType.schemaExtensions"
)

schema_: Annotated[AnyUrl, Mutability.read_only, Required.true, CaseExact.true] = (
Field(..., alias="schema")
)
Expand Down
2 changes: 0 additions & 2 deletions scim2_models/rfc7643/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@


class Attribute(ComplexAttribute):
_attribute_urn: str = "urn:ietf:params:scim:schemas:core:2.0:Schema.attributes"

class Type(str, Enum):
string = "string"
boolean = "boolean"
Expand Down
18 changes: 0 additions & 18 deletions scim2_models/rfc7643/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@


class Name(ComplexAttribute):
_attribute_urn: str = "urn:ietf:params:scim:schemas:core:2.0:User.name"

formatted: Optional[str] = None
"""The full name, including all middle names, titles, and suffixes as
appropriate, formatted for display (e.g., 'Ms. Barbara J Jensen, III')."""
Expand All @@ -43,8 +41,6 @@ class Name(ComplexAttribute):


class Email(ComplexAttribute):
_attribute_urn: str = "urn:ietf:params:scim:schemas:core:2.0:User.emails"

class Type(str, Enum):
work = "work"
home = "home"
Expand All @@ -66,8 +62,6 @@ class Type(str, Enum):


class PhoneNumber(ComplexAttribute):
_attribute_urn: str = "urn:ietf:params:scim:schemas:core:2.0:User.phoneNumbers"

class Type(str, Enum):
work = "work"
home = "home"
Expand All @@ -93,8 +87,6 @@ class Type(str, Enum):


class Im(ComplexAttribute):
_attribute_urn: str = "urn:ietf:params:scim:schemas:core:2.0:User.ims"

class Type(str, Enum):
aim = "aim"
gtalk = "gtalk"
Expand All @@ -121,8 +113,6 @@ class Type(str, Enum):


class Photo(ComplexAttribute):
_attribute_urn: str = "urn:ietf:params:scim:schemas:core:2.0:User.photos"

class Type(str, Enum):
photo = "photo"
thumbnail = "thumbnail"
Expand All @@ -143,8 +133,6 @@ class Type(str, Enum):


class Address(ComplexAttribute):
_attribute_urn: str = "urn:ietf:params:scim:schemas:core:2.0:User.addresses"

class Type(str, Enum):
work = "work"
home = "home"
Expand Down Expand Up @@ -182,8 +170,6 @@ class Type(str, Enum):


class Entitlement(ComplexAttribute):
_attribute_urn: str = "urn:ietf:params:scim:schemas:core:2.0:User.entitlements"

value: Optional[str] = None
"""The value of an entitlement."""

Expand All @@ -199,8 +185,6 @@ class Entitlement(ComplexAttribute):


class Role(ComplexAttribute):
_attribute_urn: str = "urn:ietf:params:scim:schemas:core:2.0:User.roles"

value: Optional[str] = None
"""The value of a role."""

Expand All @@ -216,8 +200,6 @@ class Role(ComplexAttribute):


class X509Certificate(ComplexAttribute):
_attribute_urn: str = "urn:ietf:params:scim:schemas:core:2.0:User.x509Certificates"

value: Optional[str] = None
"""The value of an X.509 certificate."""

Expand Down
6 changes: 1 addition & 5 deletions tests/test_model_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

def test_get_attribute_urn():
class Sub(ComplexAttribute):
_attribute_urn = "urn:example:2.0:Sup:sub"
dummy: str

class Sup(Resource):
Expand All @@ -28,14 +27,11 @@ class Sup(Resource):
assert sup.get_attribute_urn("dummy") == "urn:example:2.0:Sup:dummy"
assert sup.get_attribute_urn("sub") == "urn:example:2.0:Sup:sub"
assert sup.sub.get_attribute_urn("dummy") == "urn:example:2.0:Sup:sub.dummy"

# TODO: fix this by dynamically guess attribute urns
# assert sup.subs[0].get_attribute_urn("dummy") == "urn:example:2.0:Bar:subs.dummy"
assert sup.subs[0].get_attribute_urn("dummy") == "urn:example:2.0:Sup:subs.dummy"


def test_guess_root_type():
class Sub(ComplexAttribute):
_attribute_urn = "urn:example:2.0:Sup:sub"
dummy: str

class Sup(Resource):
Expand Down
2 changes: 0 additions & 2 deletions tests/test_model_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@


class SubRetModel(ComplexAttribute):
_attribute_urn: str = "org:example:SupRetResource:sub"

always_returned: Annotated[Optional[str], Returned.always] = None
never_returned: Annotated[Optional[str], Returned.never] = None
default_returned: Annotated[Optional[str], Returned.default] = None
Expand Down