scim2-models 0.3.5__py3-none-any.whl → 0.3.7__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.
@@ -1,31 +1,41 @@
1
1
  from datetime import datetime
2
+ from typing import TYPE_CHECKING
2
3
  from typing import Annotated
3
4
  from typing import Any
4
5
  from typing import Generic
5
6
  from typing import Optional
6
7
  from typing import TypeVar
7
8
  from typing import Union
9
+ from typing import cast
8
10
  from typing import get_args
9
11
  from typing import get_origin
10
12
 
11
13
  from pydantic import Field
14
+ from pydantic import SerializationInfo
15
+ from pydantic import SerializerFunctionWrapHandler
12
16
  from pydantic import WrapSerializer
13
17
  from pydantic import field_serializer
14
18
 
19
+ from ..annotations import CaseExact
20
+ from ..annotations import Mutability
21
+ from ..annotations import Required
22
+ from ..annotations import Returned
23
+ from ..annotations import Uniqueness
24
+ from ..attributes import ComplexAttribute
25
+ from ..attributes import MultiValuedComplexAttribute
26
+ from ..attributes import is_complex_attribute
15
27
  from ..base import BaseModel
16
- from ..base import BaseModelType
17
- from ..base import CaseExact
18
- from ..base import ComplexAttribute
19
- from ..base import ExternalReference
20
- from ..base import MultiValuedComplexAttribute
21
- from ..base import Mutability
22
- from ..base import Required
23
- from ..base import Returned
24
- from ..base import Uniqueness
25
- from ..base import URIReference
26
- from ..base import is_complex_attribute
28
+ from ..context import Context
29
+ from ..reference import Reference
30
+ from ..scim_object import ScimObject
31
+ from ..scim_object import validate_attribute_urn
32
+ from ..utils import UNION_TYPES
27
33
  from ..utils import normalize_attribute_name
28
34
 
35
+ if TYPE_CHECKING:
36
+ from .schema import Attribute
37
+ from .schema import Schema
38
+
29
39
 
30
40
  class Meta(ComplexAttribute):
31
41
  """All "meta" sub-attributes are assigned by the service provider (have a "mutability" of "readOnly"), and all of these sub-attributes have a "returned" characteristic of "default".
@@ -78,14 +88,14 @@ class Meta(ComplexAttribute):
78
88
  """
79
89
 
80
90
 
81
- class Extension(BaseModel):
91
+ class Extension(ScimObject):
82
92
  @classmethod
83
- def to_schema(cls):
93
+ def to_schema(cls) -> "Schema":
84
94
  """Build a :class:`~scim2_models.Schema` from the current extension class."""
85
95
  return model_to_schema(cls)
86
96
 
87
97
  @classmethod
88
- def from_schema(cls, schema) -> "Extension":
98
+ def from_schema(cls, schema: "Schema") -> type["Extension"]:
89
99
  """Build a :class:`~scim2_models.Extension` subclass from the schema definition."""
90
100
  from .schema import make_python_model
91
101
 
@@ -94,14 +104,18 @@ class Extension(BaseModel):
94
104
 
95
105
  AnyExtension = TypeVar("AnyExtension", bound="Extension")
96
106
 
107
+ _PARAMETERIZED_CLASSES: dict[tuple[type, tuple[Any, ...]], type] = {}
108
+
97
109
 
98
- def extension_serializer(value: Any, handler, info) -> Optional[dict[str, Any]]:
110
+ def extension_serializer(
111
+ value: Any, handler: SerializerFunctionWrapHandler, info: SerializationInfo
112
+ ) -> Optional[dict[str, Any]]:
99
113
  """Exclude the Resource attributes from the extension dump.
100
114
 
101
115
  For instance, attributes 'meta', 'id' or 'schemas' should not be
102
116
  dumped when the model is used as an extension for another model.
103
117
  """
104
- partial_result = handler(value, info)
118
+ partial_result = handler(value)
105
119
  result = {
106
120
  attr_name: value
107
121
  for attr_name, value in partial_result.items()
@@ -110,39 +124,7 @@ def extension_serializer(value: Any, handler, info) -> Optional[dict[str, Any]]:
110
124
  return result or None
111
125
 
112
126
 
113
- class ResourceMetaclass(BaseModelType):
114
- def __new__(cls, name, bases, attrs, **kwargs):
115
- """Dynamically add a field for each extension."""
116
- if "__pydantic_generic_metadata__" in kwargs:
117
- extensions = kwargs["__pydantic_generic_metadata__"]["args"][0]
118
- extensions = (
119
- get_args(extensions)
120
- if get_origin(extensions) == Union
121
- else [extensions]
122
- )
123
- for extension in extensions:
124
- schema = extension.model_fields["schemas"].default[0]
125
- attrs.setdefault("__annotations__", {})[extension.__name__] = Annotated[
126
- Optional[extension],
127
- WrapSerializer(extension_serializer),
128
- ]
129
- attrs[extension.__name__] = Field(
130
- None,
131
- serialization_alias=schema,
132
- validation_alias=normalize_attribute_name(schema),
133
- )
134
-
135
- klass = super().__new__(cls, name, bases, attrs, **kwargs)
136
- return klass
137
-
138
-
139
- class Resource(BaseModel, Generic[AnyExtension], metaclass=ResourceMetaclass):
140
- schemas: Annotated[list[str], Required.true]
141
- """The "schemas" attribute is a REQUIRED attribute and is an array of
142
- Strings containing URIs that are used to indicate the namespaces of the
143
- SCIM schemas that define the attributes present in the current JSON
144
- structure."""
145
-
127
+ class Resource(ScimObject, Generic[AnyExtension]):
146
128
  # Common attributes as defined by
147
129
  # https://www.rfc-editor.org/rfc/rfc7643#section-3.1
148
130
 
@@ -165,13 +147,74 @@ class Resource(BaseModel, Generic[AnyExtension], metaclass=ResourceMetaclass):
165
147
  meta: Annotated[Optional[Meta], Mutability.read_only, Returned.default] = None
166
148
  """A complex attribute containing resource metadata."""
167
149
 
168
- def __getitem__(self, item: Any):
150
+ @classmethod
151
+ def __class_getitem__(cls, item: Any) -> type["Resource"]:
152
+ """Create a Resource class with extension fields dynamically added."""
153
+ if hasattr(cls, "__scim_extension_metadata__"):
154
+ return cls
155
+
156
+ extensions = get_args(item) if get_origin(item) in UNION_TYPES else [item]
157
+
158
+ # Skip TypeVar parameters and Any (used for generic class definitions)
159
+ valid_extensions = [
160
+ extension
161
+ for extension in extensions
162
+ if not isinstance(extension, TypeVar) and extension is not Any
163
+ ]
164
+
165
+ if not valid_extensions:
166
+ return cls
167
+
168
+ cache_key = (cls, tuple(valid_extensions))
169
+ if cache_key in _PARAMETERIZED_CLASSES:
170
+ return _PARAMETERIZED_CLASSES[cache_key]
171
+
172
+ for extension in valid_extensions:
173
+ if not (isinstance(extension, type) and issubclass(extension, Extension)):
174
+ raise TypeError(f"{extension} is not a valid Extension type")
175
+
176
+ class_name = (
177
+ f"{cls.__name__}[{', '.join(ext.__name__ for ext in valid_extensions)}]"
178
+ )
179
+
180
+ class_attrs = {"__scim_extension_metadata__": valid_extensions}
181
+
182
+ for extension in valid_extensions:
183
+ schema = extension.model_fields["schemas"].default[0]
184
+ class_attrs[extension.__name__] = Field(
185
+ default=None, # type: ignore[arg-type]
186
+ serialization_alias=schema,
187
+ validation_alias=normalize_attribute_name(schema),
188
+ )
189
+
190
+ new_annotations = {
191
+ extension.__name__: Annotated[
192
+ Optional[extension],
193
+ WrapSerializer(extension_serializer),
194
+ ]
195
+ for extension in valid_extensions
196
+ }
197
+
198
+ new_class = type(
199
+ class_name,
200
+ (cls,),
201
+ {
202
+ "__annotations__": new_annotations,
203
+ **class_attrs,
204
+ },
205
+ )
206
+
207
+ _PARAMETERIZED_CLASSES[cache_key] = new_class
208
+
209
+ return new_class
210
+
211
+ def __getitem__(self, item: Any) -> Optional[Extension]:
169
212
  if not isinstance(item, type) or not issubclass(item, Extension):
170
213
  raise KeyError(f"{item} is not a valid extension type")
171
214
 
172
- return getattr(self, item.__name__)
215
+ return cast(Optional[Extension], getattr(self, item.__name__))
173
216
 
174
- def __setitem__(self, item: Any, value: "Resource"):
217
+ def __setitem__(self, item: Any, value: "Extension") -> None:
175
218
  if not isinstance(item, type) or not issubclass(item, Extension):
176
219
  raise KeyError(f"{item} is not a valid extension type")
177
220
 
@@ -180,20 +223,16 @@ class Resource(BaseModel, Generic[AnyExtension], metaclass=ResourceMetaclass):
180
223
  @classmethod
181
224
  def get_extension_models(cls) -> dict[str, type[Extension]]:
182
225
  """Return extension a dict associating extension models with their schemas."""
183
- extension_models = cls.__pydantic_generic_metadata__.get("args", [])
184
- extension_models = (
185
- get_args(extension_models[0])
186
- if len(extension_models) == 1 and get_origin(extension_models[0]) == Union
187
- else extension_models
188
- )
189
-
226
+ extension_models = getattr(cls, "__scim_extension_metadata__", [])
190
227
  by_schema = {
191
228
  ext.model_fields["schemas"].default[0]: ext for ext in extension_models
192
229
  }
193
230
  return by_schema
194
231
 
195
232
  @classmethod
196
- def get_extension_model(cls, name_or_schema) -> Optional[type[Extension]]:
233
+ def get_extension_model(
234
+ cls, name_or_schema: Union[str, "Schema"]
235
+ ) -> Optional[type[Extension]]:
197
236
  """Return an extension by its name or schema."""
198
237
  for schema, extension in cls.get_extension_models().items():
199
238
  if schema == name_or_schema or extension.__name__ == name_or_schema:
@@ -202,15 +241,17 @@ class Resource(BaseModel, Generic[AnyExtension], metaclass=ResourceMetaclass):
202
241
 
203
242
  @staticmethod
204
243
  def get_by_schema(
205
- resource_types: list[type[BaseModel]], schema: str, with_extensions=True
206
- ) -> Optional[type]:
244
+ resource_types: list[type["Resource"]],
245
+ schema: str,
246
+ with_extensions: bool = True,
247
+ ) -> Optional[Union[type["Resource"], type["Extension"]]]:
207
248
  """Given a resource type list and a schema, find the matching resource type."""
208
- by_schema = {
249
+ by_schema: dict[str, Union[type[Resource], type[Extension]]] = {
209
250
  resource_type.model_fields["schemas"].default[0].lower(): resource_type
210
251
  for resource_type in (resource_types or [])
211
252
  }
212
253
  if with_extensions:
213
- for resource_type in list(by_schema.values()):
254
+ for resource_type in resource_types:
214
255
  by_schema.update(
215
256
  {
216
257
  schema.lower(): extension
@@ -221,7 +262,11 @@ class Resource(BaseModel, Generic[AnyExtension], metaclass=ResourceMetaclass):
221
262
  return by_schema.get(schema.lower())
222
263
 
223
264
  @staticmethod
224
- def get_by_payload(resource_types: list[type], payload: dict, **kwargs):
265
+ def get_by_payload(
266
+ resource_types: list[type["Resource"]],
267
+ payload: dict[str, Any],
268
+ **kwargs: Any,
269
+ ) -> Optional[type]:
225
270
  """Given a resource type list and a payload, find the matching resource type."""
226
271
  if not payload or not payload.get("schemas"):
227
272
  return None
@@ -230,7 +275,9 @@ class Resource(BaseModel, Generic[AnyExtension], metaclass=ResourceMetaclass):
230
275
  return Resource.get_by_schema(resource_types, schema, **kwargs)
231
276
 
232
277
  @field_serializer("schemas")
233
- def set_extension_schemas(self, schemas: Annotated[list[str], Required.true]):
278
+ def set_extension_schemas(
279
+ self, schemas: Annotated[list[str], Required.true]
280
+ ) -> list[str]:
234
281
  """Add model extension ids to the 'schemas' attribute."""
235
282
  extension_schemas = self.get_extension_models().keys()
236
283
  schemas = self.schemas + [
@@ -239,25 +286,95 @@ class Resource(BaseModel, Generic[AnyExtension], metaclass=ResourceMetaclass):
239
286
  return schemas
240
287
 
241
288
  @classmethod
242
- def to_schema(cls):
289
+ def to_schema(cls) -> "Schema":
243
290
  """Build a :class:`~scim2_models.Schema` from the current resource class."""
244
291
  return model_to_schema(cls)
245
292
 
246
293
  @classmethod
247
- def from_schema(cls, schema) -> "Resource":
294
+ def from_schema(cls, schema: "Schema") -> type["Resource"]:
248
295
  """Build a :class:`scim2_models.Resource` subclass from the schema definition."""
249
296
  from .schema import make_python_model
250
297
 
251
298
  return make_python_model(schema, cls)
252
299
 
300
+ def _prepare_model_dump(
301
+ self,
302
+ scim_ctx: Optional[Context] = Context.DEFAULT,
303
+ attributes: Optional[list[str]] = None,
304
+ excluded_attributes: Optional[list[str]] = None,
305
+ **kwargs: Any,
306
+ ) -> dict[str, Any]:
307
+ kwargs = super()._prepare_model_dump(scim_ctx, **kwargs)
308
+ kwargs["context"]["scim_attributes"] = [
309
+ validate_attribute_urn(attribute, self.__class__)
310
+ for attribute in (attributes or [])
311
+ ]
312
+ kwargs["context"]["scim_excluded_attributes"] = [
313
+ validate_attribute_urn(attribute, self.__class__)
314
+ for attribute in (excluded_attributes or [])
315
+ ]
316
+ return kwargs
317
+
318
+ def model_dump(
319
+ self,
320
+ *args: Any,
321
+ scim_ctx: Optional[Context] = Context.DEFAULT,
322
+ attributes: Optional[list[str]] = None,
323
+ excluded_attributes: Optional[list[str]] = None,
324
+ **kwargs: Any,
325
+ ) -> dict[str, Any]:
326
+ """Create a model representation that can be included in SCIM messages by using Pydantic :code:`BaseModel.model_dump`.
327
+
328
+ :param scim_ctx: If a SCIM context is passed, some default values of
329
+ Pydantic :code:`BaseModel.model_dump` are tuned to generate valid SCIM
330
+ messages. Pass :data:`None` to get the default Pydantic behavior.
331
+ :param attributes: A multi-valued list of strings indicating the names of resource
332
+ attributes to return in the response, overriding the set of attributes that
333
+ would be returned by default.
334
+ :param excluded_attributes: A multi-valued list of strings indicating the names of resource
335
+ attributes to be removed from the default set of attributes to return.
336
+ """
337
+ dump_kwargs = self._prepare_model_dump(
338
+ scim_ctx, attributes, excluded_attributes, **kwargs
339
+ )
340
+ if scim_ctx:
341
+ dump_kwargs.setdefault("mode", "json")
342
+ return super(ScimObject, self).model_dump(*args, **dump_kwargs)
343
+
344
+ def model_dump_json(
345
+ self,
346
+ *args: Any,
347
+ scim_ctx: Optional[Context] = Context.DEFAULT,
348
+ attributes: Optional[list[str]] = None,
349
+ excluded_attributes: Optional[list[str]] = None,
350
+ **kwargs: Any,
351
+ ) -> str:
352
+ """Create a JSON model representation that can be included in SCIM messages by using Pydantic :code:`BaseModel.model_dump_json`.
353
+
354
+ :param scim_ctx: If a SCIM context is passed, some default values of
355
+ Pydantic :code:`BaseModel.model_dump` are tuned to generate valid SCIM
356
+ messages. Pass :data:`None` to get the default Pydantic behavior.
357
+ :param attributes: A multi-valued list of strings indicating the names of resource
358
+ attributes to return in the response, overriding the set of attributes that
359
+ would be returned by default.
360
+ :param excluded_attributes: A multi-valued list of strings indicating the names of resource
361
+ attributes to be removed from the default set of attributes to return.
362
+ """
363
+ dump_kwargs = self._prepare_model_dump(
364
+ scim_ctx, attributes, excluded_attributes, **kwargs
365
+ )
366
+ return super(ScimObject, self).model_dump_json(*args, **dump_kwargs)
367
+
253
368
 
254
369
  AnyResource = TypeVar("AnyResource", bound="Resource")
255
370
 
256
371
 
257
- def dedicated_attributes(model, excluded_models):
372
+ def dedicated_attributes(
373
+ model: type[BaseModel], excluded_models: list[type[BaseModel]]
374
+ ) -> dict[str, Any]:
258
375
  """Return attributes that are not members the parent 'excluded_models'."""
259
376
 
260
- def compare_field_infos(fi1, fi2):
377
+ def compare_field_infos(fi1: Any, fi2: Any) -> bool:
261
378
  return (
262
379
  fi1
263
380
  and fi2
@@ -280,13 +397,13 @@ def dedicated_attributes(model, excluded_models):
280
397
  return field_infos
281
398
 
282
399
 
283
- def model_to_schema(model: type[BaseModel]):
400
+ def model_to_schema(model: type[BaseModel]) -> "Schema":
284
401
  from scim2_models.rfc7643.schema import Schema
285
402
 
286
403
  schema_urn = model.model_fields["schemas"].default[0]
287
404
  field_infos = dedicated_attributes(model, [Resource])
288
405
  attributes = [
289
- model_attribute_to_attribute(model, attribute_name)
406
+ model_attribute_to_scim_attribute(model, attribute_name)
290
407
  for attribute_name in field_infos
291
408
  if attribute_name != "schemas"
292
409
  ]
@@ -299,46 +416,37 @@ def model_to_schema(model: type[BaseModel]):
299
416
  return schema
300
417
 
301
418
 
302
- def get_reference_types(type) -> list[str]:
303
- first_arg = get_args(type)[0]
304
- types = get_args(first_arg) if get_origin(first_arg) == Union else [first_arg]
305
-
306
- def serialize_ref_type(ref_type):
307
- if ref_type == URIReference:
308
- return "uri"
309
-
310
- elif ref_type == ExternalReference:
311
- return "external"
312
-
313
- return get_args(ref_type)[0]
314
-
315
- return list(map(serialize_ref_type, types))
316
-
317
-
318
- def model_attribute_to_attribute(model, attribute_name):
419
+ def model_attribute_to_scim_attribute(
420
+ model: type[BaseModel], attribute_name: str
421
+ ) -> "Attribute":
319
422
  from scim2_models.rfc7643.schema import Attribute
320
423
 
321
424
  field_info = model.model_fields[attribute_name]
322
425
  root_type = model.get_field_root_type(attribute_name)
426
+ if root_type is None:
427
+ raise ValueError(
428
+ f"Could not determine root type for attribute {attribute_name}"
429
+ )
323
430
  attribute_type = Attribute.Type.from_python(root_type)
324
431
  sub_attributes = (
325
432
  [
326
- model_attribute_to_attribute(root_type, sub_attribute_name)
433
+ model_attribute_to_scim_attribute(root_type, sub_attribute_name)
327
434
  for sub_attribute_name in dedicated_attributes(
328
- root_type, [MultiValuedComplexAttribute]
435
+ root_type,
436
+ [MultiValuedComplexAttribute],
329
437
  )
330
438
  if (
331
439
  attribute_name != "sub_attributes"
332
440
  or sub_attribute_name != "sub_attributes"
333
441
  )
334
442
  ]
335
- if is_complex_attribute(root_type)
443
+ if root_type and is_complex_attribute(root_type)
336
444
  else None
337
445
  )
338
446
 
339
447
  return Attribute(
340
448
  name=field_info.serialization_alias or attribute_name,
341
- type=attribute_type,
449
+ type=Attribute.Type(attribute_type),
342
450
  multi_valued=model.get_field_multiplicity(attribute_name),
343
451
  description=field_info.description,
344
452
  canonical_values=field_info.examples,
@@ -348,7 +456,7 @@ def model_attribute_to_attribute(model, attribute_name):
348
456
  returned=model.get_field_annotation(attribute_name, Returned),
349
457
  uniqueness=model.get_field_annotation(attribute_name, Uniqueness),
350
458
  sub_attributes=sub_attributes,
351
- reference_types=get_reference_types(root_type)
459
+ reference_types=Reference.get_types(root_type)
352
460
  if attribute_type == Attribute.Type.reference
353
461
  else None,
354
462
  )
@@ -4,13 +4,13 @@ from typing import Optional
4
4
  from pydantic import Field
5
5
  from typing_extensions import Self
6
6
 
7
- from ..base import CaseExact
8
- from ..base import ComplexAttribute
9
- from ..base import Mutability
10
- from ..base import Reference
11
- from ..base import Required
12
- from ..base import Returned
13
- from ..base import URIReference
7
+ from ..annotations import CaseExact
8
+ from ..annotations import Mutability
9
+ from ..annotations import Required
10
+ from ..annotations import Returned
11
+ from ..attributes import ComplexAttribute
12
+ from ..reference import Reference
13
+ from ..reference import URIReference
14
14
  from .resource import Resource
15
15
 
16
16
 
@@ -82,12 +82,15 @@ class ResourceType(Resource):
82
82
  """Build a naive ResourceType from a resource model."""
83
83
  schema = resource_model.model_fields["schemas"].default[0]
84
84
  name = schema.split(":")[-1]
85
- extensions = resource_model.__pydantic_generic_metadata__["args"]
86
- return ResourceType(
85
+
86
+ # Get extensions from the metadata system
87
+ extensions = getattr(resource_model, "__scim_extension_metadata__", [])
88
+
89
+ return cls(
87
90
  id=name,
88
91
  name=name,
89
92
  description=name,
90
- endpoint=f"/{name}s",
93
+ endpoint=Reference[URIReference](f"/{name}s"),
91
94
  schema_=schema,
92
95
  schema_extensions=[
93
96
  SchemaExtension(
@@ -6,6 +6,7 @@ from typing import Any
6
6
  from typing import List # noqa : UP005,UP035
7
7
  from typing import Literal
8
8
  from typing import Optional
9
+ from typing import TypeVar
9
10
  from typing import Union
10
11
  from typing import get_origin
11
12
 
@@ -16,24 +17,25 @@ from pydantic.alias_generators import to_pascal
16
17
  from pydantic.alias_generators import to_snake
17
18
  from pydantic_core import Url
18
19
 
20
+ from ..annotations import CaseExact
21
+ from ..annotations import Mutability
22
+ from ..annotations import Required
23
+ from ..annotations import Returned
24
+ from ..annotations import Uniqueness
25
+ from ..attributes import ComplexAttribute
26
+ from ..attributes import MultiValuedComplexAttribute
27
+ from ..attributes import is_complex_attribute
19
28
  from ..base import BaseModel
20
- from ..base import CaseExact
21
- from ..base import ComplexAttribute
22
- from ..base import ExternalReference
23
- from ..base import MultiValuedComplexAttribute
24
- from ..base import Mutability
25
- from ..base import Reference
26
- from ..base import Required
27
- from ..base import Returned
28
- from ..base import Uniqueness
29
- from ..base import URIReference
30
- from ..base import is_complex_attribute
31
29
  from ..constants import RESERVED_WORDS
30
+ from ..reference import ExternalReference
31
+ from ..reference import Reference
32
+ from ..reference import URIReference
32
33
  from ..utils import Base64Bytes
33
34
  from ..utils import normalize_attribute_name
34
- from .resource import Extension
35
35
  from .resource import Resource
36
36
 
37
+ T = TypeVar("T", bound=BaseModel)
38
+
37
39
 
38
40
  def make_python_identifier(identifier: str) -> str:
39
41
  """Sanitize string to be a suitable Python/Pydantic class attribute name."""
@@ -46,9 +48,9 @@ def make_python_identifier(identifier: str) -> str:
46
48
 
47
49
  def make_python_model(
48
50
  obj: Union["Schema", "Attribute"],
49
- base: Optional[type[BaseModel]] = None,
50
- multiple=False,
51
- ) -> Union[Resource, Extension]:
51
+ base: type[T],
52
+ multiple: bool = False,
53
+ ) -> type[T]:
52
54
  """Build a Python model from a Schema or an Attribute object."""
53
55
  if isinstance(obj, Attribute):
54
56
  pydantic_attributes = {
@@ -56,7 +58,6 @@ def make_python_model(
56
58
  for attr in (obj.sub_attributes or [])
57
59
  if attr.name
58
60
  }
59
- base = MultiValuedComplexAttribute if multiple else ComplexAttribute
60
61
 
61
62
  else:
62
63
  pydantic_attributes = {
@@ -69,14 +70,17 @@ def make_python_model(
69
70
  Field(default=[obj.id]),
70
71
  )
71
72
 
73
+ if not obj.name:
74
+ raise ValueError("Schema or Attribute 'name' must be defined")
75
+
72
76
  model_name = to_pascal(to_snake(obj.name))
73
- model = create_model(model_name, __base__=base, **pydantic_attributes)
77
+ model: type[T] = create_model(model_name, __base__=base, **pydantic_attributes) # type: ignore[call-overload]
74
78
 
75
79
  # Set the ComplexType class as a member of the model
76
80
  # e.g. make Member an attribute of Group
77
81
  for attr_name in model.model_fields:
78
82
  attr_type = model.get_field_root_type(attr_name)
79
- if is_complex_attribute(attr_type):
83
+ if attr_type and is_complex_attribute(attr_type):
80
84
  setattr(model, attr_type.__name__, attr_type)
81
85
 
82
86
  return model
@@ -95,7 +99,7 @@ class Attribute(ComplexAttribute):
95
99
 
96
100
  def to_python(
97
101
  self,
98
- multiple=False,
102
+ multiple: bool = False,
99
103
  reference_types: Optional[list[str]] = None,
100
104
  ) -> type:
101
105
  if self.value == self.reference and reference_types is not None:
@@ -122,32 +126,32 @@ class Attribute(ComplexAttribute):
122
126
  return attr_types[self.value]
123
127
 
124
128
  @classmethod
125
- def from_python(cls, pytype) -> str:
129
+ def from_python(cls, pytype: type) -> "Attribute.Type":
126
130
  if get_origin(pytype) == Reference:
127
- return cls.reference.value
131
+ return cls.reference
128
132
 
129
- if is_complex_attribute(pytype):
130
- return cls.complex.value
133
+ if pytype and is_complex_attribute(pytype):
134
+ return cls.complex
131
135
 
132
136
  if pytype in (Required, CaseExact):
133
- return cls.boolean.value
137
+ return cls.boolean
134
138
 
135
139
  attr_types = {
136
- str: cls.string.value,
137
- bool: cls.boolean.value,
138
- float: cls.decimal.value,
139
- int: cls.integer.value,
140
- datetime: cls.date_time.value,
141
- Base64Bytes: cls.binary.value,
140
+ str: cls.string,
141
+ bool: cls.boolean,
142
+ float: cls.decimal,
143
+ int: cls.integer,
144
+ datetime: cls.date_time,
145
+ Base64Bytes: cls.binary,
142
146
  }
143
- return attr_types.get(pytype, cls.string.value)
147
+ return attr_types.get(pytype, cls.string)
144
148
 
145
149
  name: Annotated[
146
150
  Optional[str], Mutability.read_only, Required.true, CaseExact.true
147
151
  ] = None
148
152
  """The attribute's name."""
149
153
 
150
- type: Annotated[Type, Mutability.read_only, Required.true] = Field(
154
+ type: Annotated[Optional[Type], Mutability.read_only, Required.true] = Field(
151
155
  None, examples=[item.value for item in Type]
152
156
  )
153
157
  """The attribute's data type."""
@@ -206,15 +210,17 @@ class Attribute(ComplexAttribute):
206
210
  """When an attribute is of type "complex", "subAttributes" defines a set of
207
211
  sub-attributes."""
208
212
 
209
- def to_python(self) -> Optional[tuple[Any, Field]]:
213
+ def to_python(self) -> Optional[tuple[Any, Any]]:
210
214
  """Build tuple suited to be passed to pydantic 'create_model'."""
211
- if not self.name:
215
+ if not self.name or not self.type:
212
216
  return None
213
217
 
214
- attr_type = self.type.to_python(self.multi_valued, self.reference_types)
218
+ attr_type = self.type.to_python(bool(self.multi_valued), self.reference_types)
215
219
 
216
220
  if attr_type in (ComplexAttribute, MultiValuedComplexAttribute):
217
- attr_type = make_python_model(obj=self, multiple=self.multi_valued)
221
+ attr_type = make_python_model(
222
+ obj=self, base=attr_type, multiple=bool(self.multi_valued)
223
+ )
218
224
 
219
225
  if self.multi_valued:
220
226
  attr_type = list[attr_type] # type: ignore
@@ -245,7 +251,7 @@ class Attribute(ComplexAttribute):
245
251
  return sub_attribute
246
252
  return None
247
253
 
248
- def __getitem__(self, name):
254
+ def __getitem__(self, name: str) -> "Attribute":
249
255
  """Find an attribute by its name."""
250
256
  if attribute := self.get_attribute(name):
251
257
  return attribute
@@ -287,7 +293,7 @@ class Schema(Resource):
287
293
  return attribute
288
294
  return None
289
295
 
290
- def __getitem__(self, name):
296
+ def __getitem__(self, name: str) -> "Attribute": # type: ignore[override]
291
297
  """Find an attribute by its name."""
292
298
  if attribute := self.get_attribute(name):
293
299
  return attribute