scim2-models 0.5.2__py3-none-any.whl → 0.6.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
scim2_models/reference.py CHANGED
@@ -1,80 +1,171 @@
1
- from collections import UserString
1
+ import warnings
2
2
  from typing import Any
3
3
  from typing import Generic
4
- from typing import NewType
4
+ from typing import Literal
5
5
  from typing import TypeVar
6
6
  from typing import get_args
7
7
  from typing import get_origin
8
8
 
9
9
  from pydantic import GetCoreSchemaHandler
10
+ from pydantic_core import Url
11
+ from pydantic_core import ValidationError
10
12
  from pydantic_core import core_schema
11
13
 
12
14
  from .utils import UNION_TYPES
13
15
 
14
16
  ReferenceTypes = TypeVar("ReferenceTypes")
15
17
 
16
- URIReference = NewType("URIReference", str)
17
- ExternalReference = NewType("ExternalReference", str)
18
18
 
19
+ class External:
20
+ """Marker for external references per :rfc:`RFC7643 §7 <7643#section-7>`.
19
21
 
20
- class Reference(UserString, Generic[ReferenceTypes]):
22
+ Use with :class:`Reference` to type external resource URLs (photos, websites)::
23
+
24
+ profile_url: Reference[External] | None = None
25
+ """
26
+
27
+
28
+ class URI:
29
+ """Marker for URI references per :rfc:`RFC7643 §7 <7643#section-7>`.
30
+
31
+ Use with :class:`Reference` to type URI identifiers (schema URNs, endpoints)::
32
+
33
+ endpoint: Reference[URI] | None = None
34
+ """
35
+
36
+
37
+ class ExternalReference:
38
+ """Deprecated. Use :class:`External` instead."""
39
+
40
+
41
+ class URIReference:
42
+ """Deprecated. Use :class:`URI` instead."""
43
+
44
+
45
+ class Reference(str, Generic[ReferenceTypes]):
21
46
  """Reference type as defined in :rfc:`RFC7643 §2.3.7 <7643#section-2.3.7>`.
22
47
 
23
48
  References can take different type parameters:
24
49
 
25
- - Any :class:`~scim2_models.Resource` subtype, or :class:`~typing.ForwardRef` of a Resource subtype, or :data:`~typing.Union` of those,
26
- - :data:`~scim2_models.ExternalReference`
27
- - :data:`~scim2_models.URIReference`
28
-
29
- Examples
30
- --------
50
+ - :class:`~scim2_models.External` for external resources (photos, websites)
51
+ - :class:`~scim2_models.URI` for URI identifiers (schema URNs, endpoints)
52
+ - String forward references for SCIM resource types (``"User"``, ``"Group"``)
53
+ - Resource classes directly if imports allow
31
54
 
32
- .. code-block:: python
55
+ Examples::
33
56
 
34
57
  class Foobar(Resource):
35
- bff: Reference[User]
36
- managers: Reference[Union["User", "Group"]]
37
- photo: Reference[ExternalReference]
38
- website: Reference[URIReference]
39
-
58
+ photo: Reference[External] | None = None
59
+ website: Reference[URI] | None = None
60
+ manager: Reference["User"] | None = None
61
+ members: Reference[Union["User", "Group"]] | None = None
62
+
63
+ .. versionchanged:: 0.6.0
64
+
65
+ - ``Reference[ExternalReference]`` becomes ``Reference[External]``
66
+ - ``Reference[URIReference]`` becomes ``Reference[URI]``
67
+ - ``Reference[Literal["User"]]`` becomes ``Reference["User"]``
68
+ - ``Reference[Literal["User"] | Literal["Group"]]`` becomes
69
+ ``Reference[Union["User", "Group"]]``
40
70
  """
41
71
 
72
+ __slots__ = ()
73
+ __reference_types__: tuple[str, ...] = ()
74
+ _cache: dict[tuple[str, ...], type["Reference[Any]"]] = {}
75
+
76
+ def __class_getitem__(cls, item: Any) -> type["Reference[Any]"]:
77
+ if get_origin(item) in UNION_TYPES:
78
+ items = get_args(item)
79
+ else:
80
+ items = (item,)
81
+
82
+ type_strings = tuple(_to_type_string(i) for i in items)
83
+
84
+ if type_strings in cls._cache:
85
+ return cls._cache[type_strings]
86
+
87
+ class TypedReference(cls): # type: ignore[valid-type,misc]
88
+ __reference_types__ = type_strings
89
+
90
+ TypedReference.__name__ = f"Reference[{' | '.join(type_strings)}]"
91
+ TypedReference.__qualname__ = TypedReference.__name__
92
+ cls._cache[type_strings] = TypedReference
93
+ return TypedReference
94
+
42
95
  @classmethod
43
96
  def __get_pydantic_core_schema__(
44
97
  cls,
45
- _source: type[Any],
46
- _handler: GetCoreSchemaHandler,
98
+ source_type: type[Any],
99
+ handler: GetCoreSchemaHandler,
47
100
  ) -> core_schema.CoreSchema:
48
- return core_schema.no_info_after_validator_function(
49
- cls._validate,
50
- core_schema.union_schema(
51
- [core_schema.str_schema(), core_schema.is_instance_schema(cls)]
52
- ),
101
+ ref_types = getattr(source_type, "__reference_types__", ())
102
+
103
+ def validate(value: Any) -> "Reference[Any]":
104
+ if not isinstance(value, str):
105
+ raise ValueError(f"Expected string, got {type(value).__name__}")
106
+ if "external" in ref_types or "uri" in ref_types:
107
+ _validate_uri(value)
108
+ return source_type(value) # type: ignore[no-any-return]
109
+
110
+ return core_schema.no_info_plain_validator_function(
111
+ validate,
112
+ serialization=core_schema.plain_serializer_function_ser_schema(str),
53
113
  )
54
114
 
55
115
  @classmethod
56
- def _validate(cls, input_value: Any, /) -> str:
57
- return str(input_value)
58
-
59
- @classmethod
60
- def get_types(cls, type_annotation: Any) -> list[str]:
61
- """Get reference types from a type annotation.
62
-
63
- :param type_annotation: Type annotation to extract reference types from
64
- :return: List of reference type strings
65
- """
66
- first_arg = get_args(type_annotation)[0]
67
- types = (
68
- get_args(first_arg) if get_origin(first_arg) in UNION_TYPES else [first_arg]
116
+ def get_scim_reference_types(cls) -> list[str]:
117
+ """Return referenceTypes for SCIM schema generation."""
118
+ return list(cls.__reference_types__)
119
+
120
+
121
+ def _to_type_string(item: Any) -> str:
122
+ """Convert any type parameter to its SCIM referenceType string."""
123
+ if item is Any:
124
+ return "uri"
125
+ if item is External:
126
+ return "external"
127
+ if item is ExternalReference:
128
+ warnings.warn(
129
+ "Reference[ExternalReference] is deprecated, "
130
+ "use Reference[External] instead. Will be removed in 0.7.0.",
131
+ DeprecationWarning,
132
+ stacklevel=4,
69
133
  )
70
-
71
- def serialize_ref_type(ref_type: Any) -> str:
72
- if ref_type == URIReference:
73
- return "uri"
74
-
75
- elif ref_type == ExternalReference:
76
- return "external"
77
-
78
- return str(get_args(ref_type)[0])
79
-
80
- return list(map(serialize_ref_type, types))
134
+ return "external"
135
+ if item is URI:
136
+ return "uri"
137
+ if item is URIReference:
138
+ warnings.warn(
139
+ "Reference[URIReference] is deprecated, "
140
+ "use Reference[URI] instead. Will be removed in 0.7.0.",
141
+ DeprecationWarning,
142
+ stacklevel=4,
143
+ )
144
+ return "uri"
145
+ if isinstance(item, str):
146
+ return item
147
+ if isinstance(item, type):
148
+ return item.__name__
149
+ if hasattr(item, "__forward_arg__"):
150
+ return item.__forward_arg__ # type: ignore[no-any-return]
151
+ # Support Literal["User"] for backwards compatibility
152
+ if get_origin(item) is Literal:
153
+ value = get_args(item)[0]
154
+ warnings.warn(
155
+ f'Reference[Literal["{value}"]] is deprecated, '
156
+ f'use Reference["{value}"] instead. Will be removed in 0.7.0.',
157
+ DeprecationWarning,
158
+ stacklevel=4,
159
+ )
160
+ return value # type: ignore[no-any-return]
161
+ raise TypeError(f"Invalid reference type: {item!r}")
162
+
163
+
164
+ def _validate_uri(value: str) -> None:
165
+ """Validate URI format, allowing relative URIs per RFC 7643."""
166
+ if value.startswith("/"):
167
+ return
168
+ try:
169
+ Url(value)
170
+ except ValidationError as e:
171
+ raise ValueError(f"Invalid URI: {value}") from e
@@ -1,23 +1,27 @@
1
+ from typing import TYPE_CHECKING
1
2
  from typing import Annotated
2
- from typing import Literal
3
3
 
4
4
  from pydantic import Field
5
5
 
6
6
  from ..annotations import Mutability
7
7
  from ..annotations import Required
8
8
  from ..attributes import ComplexAttribute
9
+ from ..path import URN
9
10
  from ..reference import Reference
10
11
  from .resource import Extension
11
12
 
13
+ if TYPE_CHECKING:
14
+ from .user import User
15
+
12
16
 
13
17
  class Manager(ComplexAttribute):
14
18
  value: Annotated[str | None, Required.true] = None
15
19
  """The id of the SCIM resource representing the User's manager."""
16
20
 
17
- ref: Annotated[Reference[Literal["User"]] | None, Required.true] = Field(
18
- None,
19
- serialization_alias="$ref",
20
- )
21
+ ref: Annotated[ # type: ignore[type-arg]
22
+ Reference["User"] | None,
23
+ Required.true,
24
+ ] = Field(None, serialization_alias="$ref")
21
25
  """The URI of the SCIM resource representing the User's manager."""
22
26
 
23
27
  display_name: Annotated[str | None, Mutability.read_only] = None
@@ -25,9 +29,7 @@ class Manager(ComplexAttribute):
25
29
 
26
30
 
27
31
  class EnterpriseUser(Extension):
28
- schemas: Annotated[list[str], Required.true] = [
29
- "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
30
- ]
32
+ __schema__ = URN("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User")
31
33
 
32
34
  employee_number: str | None = None
33
35
  """Numeric or alphanumeric identifier assigned to a person, typically based
@@ -1,23 +1,27 @@
1
+ from typing import TYPE_CHECKING
1
2
  from typing import Annotated
2
3
  from typing import Any
3
4
  from typing import ClassVar
4
- from typing import Literal
5
+ from typing import Union
5
6
 
6
7
  from pydantic import Field
7
8
 
8
9
  from ..annotations import Mutability
9
- from ..annotations import Required
10
10
  from ..attributes import ComplexAttribute
11
+ from ..path import URN
11
12
  from ..reference import Reference
12
13
  from .resource import Resource
13
14
 
15
+ if TYPE_CHECKING:
16
+ from .user import User
17
+
14
18
 
15
19
  class GroupMember(ComplexAttribute):
16
20
  value: Annotated[str | None, Mutability.immutable] = None
17
21
  """Identifier of the member of this Group."""
18
22
 
19
- ref: Annotated[
20
- Reference[Literal["User"] | Literal["Group"]] | None,
23
+ ref: Annotated[ # type: ignore[type-arg]
24
+ Reference[Union["User", "Group"]] | None,
21
25
  Mutability.immutable,
22
26
  ] = Field(None, serialization_alias="$ref")
23
27
  """The reference URI of a target resource, if the attribute is a
@@ -32,9 +36,7 @@ class GroupMember(ComplexAttribute):
32
36
 
33
37
 
34
38
  class Group(Resource[Any]):
35
- schemas: Annotated[list[str], Required.true] = [
36
- "urn:ietf:params:scim:schemas:core:2.0:Group"
37
- ]
39
+ __schema__ = URN("urn:ietf:params:scim:schemas:core:2.0:Group")
38
40
 
39
41
  display_name: str | None = None
40
42
  """A human-readable name for the Group."""
@@ -5,15 +5,19 @@ from typing import Any
5
5
  from typing import Generic
6
6
  from typing import TypeVar
7
7
  from typing import Union
8
- from typing import cast
9
8
  from typing import get_args
10
9
  from typing import get_origin
11
10
 
12
11
  from pydantic import Field
13
12
  from pydantic import SerializationInfo
14
13
  from pydantic import SerializerFunctionWrapHandler
14
+ from pydantic import ValidationInfo
15
+ from pydantic import ValidatorFunctionWrapHandler
15
16
  from pydantic import WrapSerializer
16
17
  from pydantic import field_serializer
18
+ from pydantic import model_validator
19
+ from pydantic_core import PydanticCustomError
20
+ from typing_extensions import Self
17
21
 
18
22
  from ..annotations import CaseExact
19
23
  from ..annotations import Mutability
@@ -24,9 +28,9 @@ from ..attributes import ComplexAttribute
24
28
  from ..attributes import is_complex_attribute
25
29
  from ..base import BaseModel
26
30
  from ..context import Context
27
- from ..reference import Reference
31
+ from ..exceptions import InvalidPathException
32
+ from ..path import Path
28
33
  from ..scim_object import ScimObject
29
- from ..urn import _validate_attribute_urn
30
34
  from ..utils import UNION_TYPES
31
35
  from ..utils import _normalize_attribute_name
32
36
 
@@ -178,7 +182,7 @@ class Resource(ScimObject, Generic[AnyExtension]):
178
182
  class_attrs = {"__scim_extension_metadata__": valid_extensions}
179
183
 
180
184
  for extension in valid_extensions:
181
- schema = extension.model_fields["schemas"].default[0]
185
+ schema = extension.__schema__
182
186
  class_attrs[extension.__name__] = Field(
183
187
  default=None, # type: ignore[arg-type]
184
188
  serialization_alias=schema,
@@ -206,24 +210,79 @@ class Resource(ScimObject, Generic[AnyExtension]):
206
210
 
207
211
  return new_class
208
212
 
209
- def __getitem__(self, item: Any) -> Extension | None:
210
- if not isinstance(item, type) or not issubclass(item, Extension):
211
- raise KeyError(f"{item} is not a valid extension type")
213
+ def __getitem__(self, item: Any) -> Any:
214
+ """Get a value by extension type or path.
212
215
 
213
- return cast(Extension | None, getattr(self, item.__name__))
216
+ :param item: An Extension subclass or a path (string or Path).
217
+ :returns: The extension instance or the value at the path.
218
+ :raises KeyError: If the path references a non-existent field.
214
219
 
215
- def __setitem__(self, item: Any, value: "Extension") -> None:
216
- if not isinstance(item, type) or not issubclass(item, Extension):
217
- raise KeyError(f"{item} is not a valid extension type")
220
+ Examples::
218
221
 
219
- setattr(self, item.__name__, value)
222
+ user[EnterpriseUser] # Get extension
223
+ user["userName"] # Get attribute
224
+ user["name.familyName"] # Get nested attribute
225
+ """
226
+ if isinstance(item, type) and issubclass(item, Extension):
227
+ item = item.__schema__
228
+
229
+ bound_path = Path.__class_getitem__(type(self))
230
+ path = item if isinstance(item, Path) else bound_path(str(item))
231
+ try:
232
+ return path.get(self)
233
+ except InvalidPathException as exc:
234
+ raise KeyError(str(item)) from exc
235
+
236
+ def __setitem__(self, item: Any, value: Any) -> None:
237
+ """Set a value by extension type or path.
238
+
239
+ :param item: An Extension subclass or a path (string or Path).
240
+ :param value: The value to set.
241
+ :raises KeyError: If the path references a non-existent field.
242
+
243
+ Examples::
244
+
245
+ user[EnterpriseUser] = EnterpriseUser(employee_number="123")
246
+ user["displayName"] = "John Doe"
247
+ user["name.familyName"] = "Doe"
248
+ """
249
+ if isinstance(item, type) and issubclass(item, Extension):
250
+ item = item.__schema__
251
+
252
+ bound_path = Path.__class_getitem__(type(self))
253
+ path = item if isinstance(item, Path) else bound_path(str(item))
254
+ try:
255
+ path.set(self, value)
256
+ except InvalidPathException as exc:
257
+ raise KeyError(str(item)) from exc
258
+
259
+ def __delitem__(self, item: Any) -> None:
260
+ """Delete a value by extension type or path.
261
+
262
+ :param item: An Extension subclass or a path (string or Path).
263
+ :raises KeyError: If the path references a non-existent field.
264
+
265
+ Examples::
266
+
267
+ del user[EnterpriseUser] # Remove extension
268
+ del user["displayName"] # Remove attribute
269
+ """
270
+ if isinstance(item, type) and issubclass(item, Extension):
271
+ item = item.__schema__
272
+
273
+ bound_path = Path.__class_getitem__(type(self))
274
+ path = item if isinstance(item, Path) else bound_path(str(item))
275
+ try:
276
+ path.delete(self)
277
+ except InvalidPathException as exc:
278
+ raise KeyError(str(item)) from exc
220
279
 
221
280
  @classmethod
222
281
  def get_extension_models(cls) -> dict[str, type[Extension]]:
223
282
  """Return extension a dict associating extension models with their schemas."""
224
283
  extension_models = getattr(cls, "__scim_extension_metadata__", [])
225
- by_schema = {
226
- ext.model_fields["schemas"].default[0]: ext for ext in extension_models
284
+ by_schema: dict[str, type[Extension]] = {
285
+ ext.__schema__: ext for ext in extension_models
227
286
  }
228
287
  return by_schema
229
288
 
@@ -245,7 +304,7 @@ class Resource(ScimObject, Generic[AnyExtension]):
245
304
  ) -> type["Resource[Any]"] | type["Extension"] | None:
246
305
  """Given a resource type list and a schema, find the matching resource type."""
247
306
  by_schema: dict[str, type[Resource[Any]] | type[Extension]] = {
248
- resource_type.model_fields["schemas"].default[0].lower(): resource_type
307
+ getattr(resource_type, "__schema__", "").lower(): resource_type
249
308
  for resource_type in (resource_types or [])
250
309
  }
251
310
  if with_extensions:
@@ -283,6 +342,35 @@ class Resource(ScimObject, Generic[AnyExtension]):
283
342
  ]
284
343
  return schemas
285
344
 
345
+ @model_validator(mode="wrap")
346
+ @classmethod
347
+ def _validate_extension_schemas(
348
+ cls, value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
349
+ ) -> Self:
350
+ """Validate that extension schemas are known."""
351
+ obj: Self = handler(value)
352
+
353
+ scim_ctx = info.context.get("scim") if info.context else None
354
+ if scim_ctx is None or scim_ctx == Context.DEFAULT:
355
+ return obj
356
+
357
+ base_schema = getattr(cls, "__schema__", None)
358
+ if not base_schema:
359
+ return obj
360
+
361
+ allowed_extensions = set(cls.get_extension_models().keys())
362
+ provided_schemas = set(obj.schemas) - {base_schema}
363
+
364
+ unknown = provided_schemas - allowed_extensions
365
+ if unknown:
366
+ raise PydanticCustomError(
367
+ "unknown_extension_schema",
368
+ "Unknown extension schemas: {schemas}",
369
+ {"schemas": ", ".join(sorted(unknown))},
370
+ )
371
+
372
+ return obj
373
+
286
374
  @classmethod
287
375
  def to_schema(cls) -> "Schema":
288
376
  """Build a :class:`~scim2_models.Schema` from the current resource class."""
@@ -298,24 +386,23 @@ class Resource(ScimObject, Generic[AnyExtension]):
298
386
  def _prepare_model_dump(
299
387
  self,
300
388
  scim_ctx: Context | None = Context.DEFAULT,
301
- attributes: list[str] | None = None,
302
- excluded_attributes: list[str] | None = None,
389
+ attributes: list[str | Path[Any]] | None = None,
390
+ excluded_attributes: list[str | Path[Any]] | None = None,
303
391
  **kwargs: Any,
304
392
  ) -> dict[str, Any]:
305
393
  kwargs = super()._prepare_model_dump(scim_ctx, **kwargs)
306
394
 
307
395
  # RFC 7644: "SHOULD ignore any query parameters they do not recognize"
396
+ bound_path = Path.__class_getitem__(type(self))
308
397
  kwargs["context"]["scim_attributes"] = [
309
- valid_attr
398
+ urn
310
399
  for attribute in (attributes or [])
311
- if (valid_attr := _validate_attribute_urn(attribute, self.__class__))
312
- is not None
400
+ if (urn := bound_path(attribute).urn) is not None
313
401
  ]
314
402
  kwargs["context"]["scim_excluded_attributes"] = [
315
- valid_attr
403
+ urn
316
404
  for attribute in (excluded_attributes or [])
317
- if (valid_attr := _validate_attribute_urn(attribute, self.__class__))
318
- is not None
405
+ if (urn := bound_path(attribute).urn) is not None
319
406
  ]
320
407
  return kwargs
321
408
 
@@ -323,8 +410,8 @@ class Resource(ScimObject, Generic[AnyExtension]):
323
410
  self,
324
411
  *args: Any,
325
412
  scim_ctx: Context | None = Context.DEFAULT,
326
- attributes: list[str] | None = None,
327
- excluded_attributes: list[str] | None = None,
413
+ attributes: list[str | Path[Any]] | None = None,
414
+ excluded_attributes: list[str | Path[Any]] | None = None,
328
415
  **kwargs: Any,
329
416
  ) -> dict[str, Any]:
330
417
  """Create a model representation that can be included in SCIM messages by using Pydantic :code:`BaseModel.model_dump`.
@@ -349,8 +436,8 @@ class Resource(ScimObject, Generic[AnyExtension]):
349
436
  self,
350
437
  *args: Any,
351
438
  scim_ctx: Context | None = Context.DEFAULT,
352
- attributes: list[str] | None = None,
353
- excluded_attributes: list[str] | None = None,
439
+ attributes: list[str | Path[Any]] | None = None,
440
+ excluded_attributes: list[str | Path[Any]] | None = None,
354
441
  **kwargs: Any,
355
442
  ) -> str:
356
443
  """Create a JSON model representation that can be included in SCIM messages by using Pydantic :code:`BaseModel.model_dump_json`.
@@ -404,7 +491,7 @@ def _dedicated_attributes(
404
491
  def _model_to_schema(model: type[BaseModel]) -> "Schema":
405
492
  from scim2_models.resources.schema import Schema
406
493
 
407
- schema_urn = model.model_fields["schemas"].default[0]
494
+ schema_urn = getattr(model, "__schema__", "") or ""
408
495
  field_infos = _dedicated_attributes(model, [Resource])
409
496
  attributes = [
410
497
  _model_attribute_to_scim_attribute(model, attribute_name)
@@ -457,7 +544,7 @@ def _model_attribute_to_scim_attribute(
457
544
  returned=model.get_field_annotation(attribute_name, Returned),
458
545
  uniqueness=model.get_field_annotation(attribute_name, Uniqueness),
459
546
  sub_attributes=sub_attributes,
460
- reference_types=Reference.get_types(root_type)
547
+ reference_types=root_type.get_scim_reference_types() # type: ignore[attr-defined]
461
548
  if attribute_type == Attribute.Type.reference
462
549
  else None,
463
550
  )
@@ -9,14 +9,15 @@ from ..annotations import Mutability
9
9
  from ..annotations import Required
10
10
  from ..annotations import Returned
11
11
  from ..attributes import ComplexAttribute
12
+ from ..path import URN
13
+ from ..reference import URI
12
14
  from ..reference import Reference
13
- from ..reference import URIReference
14
15
  from .resource import Resource
15
16
 
16
17
 
17
18
  class SchemaExtension(ComplexAttribute):
18
19
  schema_: Annotated[
19
- Reference[URIReference] | None,
20
+ Reference[URI] | None,
20
21
  Mutability.read_only,
21
22
  Required.true,
22
23
  CaseExact.true,
@@ -35,9 +36,7 @@ class SchemaExtension(ComplexAttribute):
35
36
 
36
37
 
37
38
  class ResourceType(Resource[Any]):
38
- schemas: Annotated[list[str], Required.true] = [
39
- "urn:ietf:params:scim:schemas:core:2.0:ResourceType"
40
- ]
39
+ __schema__ = URN("urn:ietf:params:scim:schemas:core:2.0:ResourceType")
41
40
 
42
41
  name: Annotated[str | None, Mutability.read_only, Required.true] = None
43
42
  """The resource type name.
@@ -58,14 +57,14 @@ class ResourceType(Resource[Any]):
58
57
  This is often the same value as the "name" attribute.
59
58
  """
60
59
 
61
- endpoint: Annotated[
62
- Reference[URIReference] | None, Mutability.read_only, Required.true
63
- ] = None
60
+ endpoint: Annotated[Reference[URI] | None, Mutability.read_only, Required.true] = (
61
+ None
62
+ )
64
63
  """The resource type's HTTP-addressable endpoint relative to the Base URL,
65
64
  e.g., '/Users'."""
66
65
 
67
66
  schema_: Annotated[
68
- Reference[URIReference] | None,
67
+ Reference[URI] | None,
69
68
  Mutability.read_only,
70
69
  Required.true,
71
70
  CaseExact.true,
@@ -80,21 +79,23 @@ class ResourceType(Resource[Any]):
80
79
  @classmethod
81
80
  def from_resource(cls, resource_model: type[Resource[Any]]) -> Self:
82
81
  """Build a naive ResourceType from a resource model."""
83
- schema = resource_model.model_fields["schemas"].default[0]
82
+ schema = resource_model.__schema__
83
+ if schema is None:
84
+ raise ValueError(f"{resource_model.__name__} has no __schema__ defined")
84
85
  name = schema.split(":")[-1]
85
86
 
86
- # Get extensions from the metadata system
87
87
  extensions = getattr(resource_model, "__scim_extension_metadata__", [])
88
88
 
89
89
  return cls(
90
90
  id=name,
91
91
  name=name,
92
92
  description=name,
93
- endpoint=Reference[URIReference](f"/{name}s"),
94
- schema_=schema,
93
+ endpoint=Reference[URI](f"/{name}s"),
94
+ schema_=Reference[URI](schema),
95
95
  schema_extensions=[
96
96
  SchemaExtension(
97
- schema_=extension.model_fields["schemas"].default[0], required=False
97
+ schema_=Reference[URI](extension.__schema__),
98
+ required=False,
98
99
  )
99
100
  for extension in extensions
100
101
  ],