-
-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathreference.py
More file actions
181 lines (143 loc) · 5.82 KB
/
reference.py
File metadata and controls
181 lines (143 loc) · 5.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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")
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
"""
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
"""
class ExternalReference:
"""Deprecated. Use :class:`External` instead."""
class URIReference:
"""Deprecated. Use :class:`URI` instead."""
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"}
@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