Source code for scim2_models.reference

import warnings
from typing import Any
from typing import Generic
from typing import Literal
from typing import TypeVar
from typing import get_args
from typing import get_origin

from pydantic import GetCoreSchemaHandler
from pydantic import GetJsonSchemaHandler
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import Url
from pydantic_core import ValidationError
from pydantic_core import core_schema

from .utils import UNION_TYPES

ReferenceTypes = TypeVar("ReferenceTypes")


[docs] class External: """Marker for external references per :rfc:`RFC7643 §7 <7643#section-7>`. Use with :class:`Reference` to type external resource URLs (photos, websites):: profile_url: Reference[External] | None = None """
[docs] class URI: """Marker for URI references per :rfc:`RFC7643 §7 <7643#section-7>`. Use with :class:`Reference` to type URI identifiers (schema URNs, endpoints):: endpoint: Reference[URI] | None = None """
[docs] class ExternalReference: """Deprecated. Use :class:`External` instead."""
[docs] class URIReference: """Deprecated. Use :class:`URI` instead."""
[docs] class Reference(str, Generic[ReferenceTypes]): """Reference type as defined in :rfc:`RFC7643 §2.3.7 <7643#section-2.3.7>`. References can take different type parameters: - :class:`~scim2_models.External` for external resources (photos, websites) - :class:`~scim2_models.URI` for URI identifiers (schema URNs, endpoints) - String forward references for SCIM resource types (``"User"``, ``"Group"``) - Resource classes directly if imports allow Examples:: class Foobar(Resource): photo: Reference[External] | None = None website: Reference[URI] | None = None manager: Reference["User"] | None = None members: Reference[Union["User", "Group"]] | None = None .. versionchanged:: 0.6.0 - ``Reference[ExternalReference]`` becomes ``Reference[External]`` - ``Reference[URIReference]`` becomes ``Reference[URI]`` - ``Reference[Literal["User"]]`` becomes ``Reference["User"]`` - ``Reference[Literal["User"] | Literal["Group"]]`` becomes ``Reference[Union["User", "Group"]]`` """ __slots__ = () __reference_types__: tuple[str, ...] = () _cache: dict[tuple[str, ...], type["Reference[Any]"]] = {} def __class_getitem__(cls, item: Any) -> type["Reference[Any]"]: if get_origin(item) in UNION_TYPES: items = get_args(item) else: items = (item,) type_strings = tuple(_to_type_string(i) for i in items) if type_strings in cls._cache: return cls._cache[type_strings] class TypedReference(cls): # type: ignore[valid-type,misc] __reference_types__ = type_strings TypedReference.__name__ = f"Reference[{' | '.join(type_strings)}]" TypedReference.__qualname__ = TypedReference.__name__ cls._cache[type_strings] = TypedReference return TypedReference @classmethod def __get_pydantic_core_schema__( cls, source_type: type[Any], handler: GetCoreSchemaHandler, ) -> core_schema.CoreSchema: ref_types = getattr(source_type, "__reference_types__", ()) def validate(value: Any) -> "Reference[Any]": if not isinstance(value, str): raise ValueError(f"Expected string, got {type(value).__name__}") if "external" in ref_types or "uri" in ref_types: _validate_uri(value) return source_type(value) # type: ignore[no-any-return] return core_schema.no_info_plain_validator_function( validate, serialization=core_schema.plain_serializer_function_ser_schema(str), ) @classmethod def __get_pydantic_json_schema__( cls, _core_schema: core_schema.CoreSchema, _handler: GetJsonSchemaHandler, ) -> JsonSchemaValue: return {"type": "string", "format": "uri"}
[docs] @classmethod def get_scim_reference_types(cls) -> list[str]: """Return referenceTypes for SCIM schema generation.""" return list(cls.__reference_types__)
def _to_type_string(item: Any) -> str: """Convert any type parameter to its SCIM referenceType string.""" if item is Any: return "uri" if item is External: return "external" if item is ExternalReference: warnings.warn( "Reference[ExternalReference] is deprecated, " "use Reference[External] instead. Will be removed in 0.7.0.", DeprecationWarning, stacklevel=4, ) return "external" if item is URI: return "uri" if item is URIReference: warnings.warn( "Reference[URIReference] is deprecated, " "use Reference[URI] instead. Will be removed in 0.7.0.", DeprecationWarning, stacklevel=4, ) return "uri" if isinstance(item, str): return item if isinstance(item, type): return item.__name__ if hasattr(item, "__forward_arg__"): return item.__forward_arg__ # type: ignore[no-any-return] # Support Literal["User"] for backwards compatibility if get_origin(item) is Literal: value = get_args(item)[0] warnings.warn( f'Reference[Literal["{value}"]] is deprecated, ' f'use Reference["{value}"] instead. Will be removed in 0.7.0.', DeprecationWarning, stacklevel=4, ) return value # type: ignore[no-any-return] raise TypeError(f"Invalid reference type: {item!r}") def _validate_uri(value: str) -> None: """Validate URI format, allowing relative URIs per RFC 7643.""" if value.startswith("/"): return try: Url(value) except ValidationError as e: raise ValueError(f"Invalid URI: {value}") from e