scim2-models 0.3.6__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.
@@ -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
@@ -4,13 +4,13 @@ from typing import Optional
4
4
 
5
5
  from pydantic import Field
6
6
 
7
- from ..base import ComplexAttribute
8
- from ..base import ExternalReference
9
- from ..base import Mutability
10
- from ..base import Reference
11
- from ..base import Required
12
- from ..base import Returned
13
- from ..base import Uniqueness
7
+ from ..annotations import Mutability
8
+ from ..annotations import Required
9
+ from ..annotations import Returned
10
+ from ..annotations import Uniqueness
11
+ from ..attributes import ComplexAttribute
12
+ from ..reference import ExternalReference
13
+ from ..reference import Reference
14
14
  from .resource import Resource
15
15
 
16
16
 
@@ -8,15 +8,15 @@ from typing import Union
8
8
  from pydantic import EmailStr
9
9
  from pydantic import Field
10
10
 
11
- from ..base import CaseExact
12
- from ..base import ComplexAttribute
13
- from ..base import ExternalReference
14
- from ..base import MultiValuedComplexAttribute
15
- from ..base import Mutability
16
- from ..base import Reference
17
- from ..base import Required
18
- from ..base import Returned
19
- from ..base import Uniqueness
11
+ from ..annotations import CaseExact
12
+ from ..annotations import Mutability
13
+ from ..annotations import Required
14
+ from ..annotations import Returned
15
+ from ..annotations import Uniqueness
16
+ from ..attributes import ComplexAttribute
17
+ from ..attributes import MultiValuedComplexAttribute
18
+ from ..reference import ExternalReference
19
+ from ..reference import Reference
20
20
  from ..utils import Base64Bytes
21
21
  from .resource import AnyExtension
22
22
  from .resource import Resource
@@ -6,8 +6,8 @@ from typing import Optional
6
6
  from pydantic import Field
7
7
  from pydantic import PlainSerializer
8
8
 
9
- from ..base import ComplexAttribute
10
- from ..base import Required
9
+ from ..annotations import Required
10
+ from ..attributes import ComplexAttribute
11
11
  from ..utils import int_to_str
12
12
  from .message import Message
13
13
 
@@ -3,7 +3,7 @@ from typing import Optional
3
3
 
4
4
  from pydantic import PlainSerializer
5
5
 
6
- from ..base import Required
6
+ from ..annotations import Required
7
7
  from ..utils import int_to_str
8
8
  from .message import Message
9
9
 
@@ -26,7 +26,7 @@ class Error(Message):
26
26
  """A detailed human-readable message."""
27
27
 
28
28
  @classmethod
29
- def make_invalid_filter_error(cls):
29
+ def make_invalid_filter_error(cls) -> "Error":
30
30
  """Pre-defined error intended to be raised when the specified filter syntax was invalid (does not comply with :rfc:`Figure 1 of RFC7644 <7644#section-3.4.2.2>`), or the specified attribute and filter comparison combination is not supported."""
31
31
  return Error(
32
32
  status=400,
@@ -35,7 +35,7 @@ class Error(Message):
35
35
  )
36
36
 
37
37
  @classmethod
38
- def make_too_many_error(cls):
38
+ def make_too_many_error(cls) -> "Error":
39
39
  """Pre-defined error intended to be raised when the specified filter yields many more results than the server is willing to calculate or process. For example, a filter such as ``(userName pr)`` by itself would return all entries with a ``userName`` and MAY not be acceptable to the service provider."""
40
40
  return Error(
41
41
  status=400,
@@ -44,7 +44,7 @@ class Error(Message):
44
44
  )
45
45
 
46
46
  @classmethod
47
- def make_uniqueness_error(cls):
47
+ def make_uniqueness_error(cls) -> "Error":
48
48
  """Pre-defined error intended to be raised when One or more of the attribute values are already in use or are reserved."""
49
49
  return Error(
50
50
  status=409,
@@ -53,7 +53,7 @@ class Error(Message):
53
53
  )
54
54
 
55
55
  @classmethod
56
- def make_mutability_error(cls):
56
+ def make_mutability_error(cls) -> "Error":
57
57
  """Pre-defined error intended to be raised when the attempted modification is not compatible with the target attribute's mutability or current state (e.g., modification of an "immutable" attribute with an existing value)."""
58
58
  return Error(
59
59
  status=400,
@@ -62,7 +62,7 @@ class Error(Message):
62
62
  )
63
63
 
64
64
  @classmethod
65
- def make_invalid_syntax_error(cls):
65
+ def make_invalid_syntax_error(cls) -> "Error":
66
66
  """Pre-defined error intended to be raised when the request body message structure was invalid or did not conform to the request schema."""
67
67
  return Error(
68
68
  status=400,
@@ -71,7 +71,7 @@ class Error(Message):
71
71
  )
72
72
 
73
73
  @classmethod
74
- def make_invalid_path_error(cls):
74
+ def make_invalid_path_error(cls) -> "Error":
75
75
  """Pre-defined error intended to be raised when the "path" attribute was invalid or malformed (see :rfc:`Figure 7 of RFC7644 <7644#section-3.5.2>`)."""
76
76
  return Error(
77
77
  status=400,
@@ -80,7 +80,7 @@ class Error(Message):
80
80
  )
81
81
 
82
82
  @classmethod
83
- def make_no_target_error(cls):
83
+ def make_no_target_error(cls) -> "Error":
84
84
  """Pre-defined error intended to be raised when the specified "path" did not yield an attribute or attribute value that could be operated on. This occurs when the specified "path" value contains a filter that yields no match."""
85
85
  return Error(
86
86
  status=400,
@@ -89,7 +89,7 @@ class Error(Message):
89
89
  )
90
90
 
91
91
  @classmethod
92
- def make_invalid_value_error(cls):
92
+ def make_invalid_value_error(cls) -> "Error":
93
93
  """Pre-defined error intended to be raised when a required value was missing, or the value specified was not compatible with the operation or attribute type (see :rfc:`Section 2.2 of RFC7643 <7643#section-2.2>`), or resource schema (see :rfc:`Section 4 of RFC7643 <7643#section-4>`)."""
94
94
  return Error(
95
95
  status=400,
@@ -98,7 +98,7 @@ class Error(Message):
98
98
  )
99
99
 
100
100
  @classmethod
101
- def make_invalid_version_error(cls):
101
+ def make_invalid_version_error(cls) -> "Error":
102
102
  """Pre-defined error intended to be raised when the specified SCIM protocol version is not supported (see :rfc:`Section 3.13 of RFC7644 <7644#section-3.13>`)."""
103
103
  return Error(
104
104
  status=400,
@@ -107,7 +107,7 @@ class Error(Message):
107
107
  )
108
108
 
109
109
  @classmethod
110
- def make_sensitive_error(cls):
110
+ def make_sensitive_error(cls) -> "Error":
111
111
  """Pre-defined error intended to be raised when the specified request cannot be completed, due to the passing of sensitive (e.g., personal) information in a request URI. For example, personal information SHALL NOT be transmitted over request URIs. See :rfc:`Section 7.5.2 of RFC7644 <7644#section-7.5.2>`."""
112
112
  return Error(
113
113
  status=400,
@@ -2,84 +2,22 @@ from typing import Annotated
2
2
  from typing import Any
3
3
  from typing import Generic
4
4
  from typing import Optional
5
- from typing import Union
6
- from typing import get_args
7
- from typing import get_origin
8
5
 
9
- from pydantic import Discriminator
10
6
  from pydantic import Field
11
- from pydantic import Tag
12
7
  from pydantic import ValidationInfo
13
8
  from pydantic import ValidatorFunctionWrapHandler
14
9
  from pydantic import model_validator
15
10
  from pydantic_core import PydanticCustomError
16
11
  from typing_extensions import Self
17
12
 
18
- from ..base import BaseModel
19
- from ..base import BaseModelType
20
- from ..base import Context
21
- from ..base import Required
13
+ from ..annotations import Required
14
+ from ..context import Context
22
15
  from ..rfc7643.resource import AnyResource
23
- from ..utils import UNION_TYPES
16
+ from .message import GenericMessageMetaclass
24
17
  from .message import Message
25
18
 
26
19
 
27
- class ListResponseMetaclass(BaseModelType):
28
- def tagged_resource_union(resource_union):
29
- """Build Discriminated Unions, so pydantic can guess which class are needed to instantiate by inspecting a payload.
30
-
31
- https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions
32
- """
33
- if get_origin(resource_union) not in UNION_TYPES:
34
- return resource_union
35
-
36
- resource_types = get_args(resource_union)
37
-
38
- def get_schema_from_payload(payload: Any) -> Optional[str]:
39
- if not payload:
40
- return None
41
-
42
- payload_schemas = (
43
- payload.get("schemas", [])
44
- if isinstance(payload, dict)
45
- else payload.schemas
46
- )
47
-
48
- resource_types_schemas = [
49
- resource_type.model_fields["schemas"].default[0]
50
- for resource_type in resource_types
51
- ]
52
- common_schemas = [
53
- schema for schema in payload_schemas if schema in resource_types_schemas
54
- ]
55
- return common_schemas[0] if common_schemas else None
56
-
57
- discriminator = Discriminator(get_schema_from_payload)
58
-
59
- def get_tag(resource_type: type[BaseModel]) -> Tag:
60
- return Tag(resource_type.model_fields["schemas"].default[0])
61
-
62
- tagged_resources = [
63
- Annotated[resource_type, get_tag(resource_type)]
64
- for resource_type in resource_types
65
- ]
66
- union = Union[tuple(tagged_resources)]
67
- return Annotated[union, discriminator]
68
-
69
- def __new__(cls, name, bases, attrs, **kwargs):
70
- if kwargs.get("__pydantic_generic_metadata__") and kwargs[
71
- "__pydantic_generic_metadata__"
72
- ].get("args"):
73
- tagged_union = cls.tagged_resource_union(
74
- kwargs["__pydantic_generic_metadata__"]["args"][0]
75
- )
76
- kwargs["__pydantic_generic_metadata__"]["args"] = (tagged_union,)
77
-
78
- klass = super().__new__(cls, name, bases, attrs, **kwargs)
79
- return klass
80
-
81
-
82
- class ListResponse(Message, Generic[AnyResource], metaclass=ListResponseMetaclass):
20
+ class ListResponse(Message, Generic[AnyResource], metaclass=GenericMessageMetaclass):
83
21
  schemas: Annotated[list[str], Required.true] = [
84
22
  "urn:ietf:params:scim:api:messages:2.0:ListResponse"
85
23
  ]
@@ -113,6 +51,7 @@ class ListResponse(Message, Generic[AnyResource], metaclass=ListResponseMetaclas
113
51
  - 'resources' must be set if 'totalResults' is non-zero.
114
52
  """
115
53
  obj = handler(value)
54
+ assert isinstance(obj, cls)
116
55
 
117
56
  if (
118
57
  not info.context
@@ -1,10 +1,110 @@
1
1
  from typing import Annotated
2
+ from typing import Any
3
+ from typing import Callable
4
+ from typing import Optional
5
+ from typing import Union
6
+ from typing import get_args
7
+ from typing import get_origin
8
+
9
+ from pydantic import Discriminator
10
+ from pydantic import Tag
11
+ from pydantic._internal._model_construction import ModelMetaclass
2
12
 
3
13
  from ..base import BaseModel
4
- from ..base import Required
14
+ from ..scim_object import ScimObject
15
+ from ..utils import UNION_TYPES
5
16
 
6
17
 
7
- class Message(BaseModel):
18
+ class Message(ScimObject):
8
19
  """SCIM protocol messages as defined by :rfc:`RFC7644 §3.1 <7644#section-3.1>`."""
9
20
 
10
- schemas: Annotated[list[str], Required.true]
21
+
22
+ def create_schema_discriminator(
23
+ resource_types_schemas: list[str],
24
+ ) -> Callable[[Any], Optional[str]]:
25
+ """Create a schema discriminator function for the given resource schemas.
26
+
27
+ :param resource_types_schemas: List of valid resource schemas
28
+ :return: Discriminator function for Pydantic
29
+ """
30
+
31
+ def get_schema_from_payload(payload: Any) -> Optional[str]:
32
+ """Extract schema from SCIM payload for discrimination.
33
+
34
+ :param payload: SCIM payload dict or object
35
+ :return: First matching schema or None
36
+ """
37
+ if not payload:
38
+ return None
39
+
40
+ payload_schemas = (
41
+ payload.get("schemas", []) if isinstance(payload, dict) else payload.schemas
42
+ )
43
+
44
+ common_schemas = [
45
+ schema for schema in payload_schemas if schema in resource_types_schemas
46
+ ]
47
+ return common_schemas[0] if common_schemas else None
48
+
49
+ return get_schema_from_payload
50
+
51
+
52
+ def get_tag(resource_type: type[BaseModel]) -> Tag:
53
+ """Create Pydantic tag from resource type schema.
54
+
55
+ :param resource_type: SCIM resource type
56
+ :return: Pydantic Tag for discrimination
57
+ """
58
+ return Tag(resource_type.model_fields["schemas"].default[0])
59
+
60
+
61
+ def create_tagged_resource_union(resource_union: Any) -> Any:
62
+ """Build Discriminated Unions for SCIM resources.
63
+
64
+ Creates discriminated unions so Pydantic can determine which class to instantiate
65
+ by inspecting the payload's schemas field.
66
+
67
+ :param resource_union: Union type of SCIM resources
68
+ :return: Annotated discriminated union or original type
69
+ """
70
+ if get_origin(resource_union) not in UNION_TYPES:
71
+ return resource_union
72
+
73
+ resource_types = get_args(resource_union)
74
+
75
+ # Set up schemas for the discriminator function
76
+ resource_types_schemas = [
77
+ resource_type.model_fields["schemas"].default[0]
78
+ for resource_type in resource_types
79
+ ]
80
+
81
+ # Create discriminator function with schemas captured in closure
82
+ schema_discriminator = create_schema_discriminator(resource_types_schemas)
83
+ discriminator = Discriminator(schema_discriminator)
84
+
85
+ tagged_resources = [
86
+ Annotated[resource_type, get_tag(resource_type)]
87
+ for resource_type in resource_types
88
+ ]
89
+ # Dynamic union construction from tuple - MyPy can't validate this at compile time
90
+ union = Union[tuple(tagged_resources)] # type: ignore
91
+ return Annotated[union, discriminator]
92
+
93
+
94
+ class GenericMessageMetaclass(ModelMetaclass):
95
+ """Metaclass for SCIM generic types with discriminated unions."""
96
+
97
+ def __new__(
98
+ cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any], **kwargs: Any
99
+ ) -> type:
100
+ """Create class with tagged resource unions for generic parameters."""
101
+ if kwargs.get("__pydantic_generic_metadata__") and kwargs[
102
+ "__pydantic_generic_metadata__"
103
+ ].get("args"):
104
+ tagged_union = create_tagged_resource_union(
105
+ kwargs["__pydantic_generic_metadata__"]["args"][0]
106
+ )
107
+ kwargs["__pydantic_generic_metadata__"]["args"] = (tagged_union,)
108
+
109
+ klass = super().__new__(cls, name, bases, attrs, **kwargs)
110
+ return klass
@@ -5,9 +5,11 @@ from typing import Optional
5
5
 
6
6
  from pydantic import Field
7
7
  from pydantic import field_validator
8
+ from pydantic import model_validator
9
+ from typing_extensions import Self
8
10
 
9
- from ..base import ComplexAttribute
10
- from ..base import Required
11
+ from ..annotations import Required
12
+ from ..attributes import ComplexAttribute
11
13
  from .message import Message
12
14
 
13
15
 
@@ -17,7 +19,7 @@ class PatchOperation(ComplexAttribute):
17
19
  remove = "remove"
18
20
  add = "add"
19
21
 
20
- op: Optional[Optional[Op]] = None
22
+ op: Op
21
23
  """Each PATCH operation object MUST have exactly one "op" member, whose
22
24
  value indicates the operation to perform and MAY be one of "add", "remove",
23
25
  or "replace".
@@ -32,11 +34,23 @@ class PatchOperation(ComplexAttribute):
32
34
  """The "path" attribute value is a String containing an attribute path
33
35
  describing the target of the operation."""
34
36
 
37
+ @model_validator(mode="after")
38
+ def validate_path(self) -> Self:
39
+ # The "path" attribute value is a String containing an attribute path
40
+ # describing the target of the operation. The "path" attribute is
41
+ # OPTIONAL for "add" and "replace" and is REQUIRED for "remove"
42
+ # operations. See relevant operation sections below for details.
43
+
44
+ if self.path is None and self.op == PatchOperation.Op.remove:
45
+ raise ValueError("Op.path is required for remove operations")
46
+
47
+ return self
48
+
35
49
  value: Optional[Any] = None
36
50
 
37
51
  @field_validator("op", mode="before")
38
52
  @classmethod
39
- def normalize_op(cls, v):
53
+ def normalize_op(cls, v: Any) -> Any:
40
54
  """Ignorecase for op.
41
55
 
42
56
  This brings
@@ -63,8 +77,8 @@ class PatchOp(Message):
63
77
  "urn:ietf:params:scim:api:messages:2.0:PatchOp"
64
78
  ]
65
79
 
66
- operations: Optional[list[PatchOperation]] = Field(
67
- None, serialization_alias="Operations"
80
+ operations: Annotated[Optional[list[PatchOperation]], Required.true] = Field(
81
+ None, serialization_alias="Operations", min_length=1
68
82
  )
69
83
  """The body of an HTTP PATCH request MUST contain the attribute
70
84
  "Operations", whose value is an array of one or more PATCH operations."""
@@ -5,7 +5,7 @@ from typing import Optional
5
5
  from pydantic import field_validator
6
6
  from pydantic import model_validator
7
7
 
8
- from ..base import Required
8
+ from ..annotations import Required
9
9
  from .message import Message
10
10
 
11
11
 
@@ -69,7 +69,7 @@ class SearchRequest(Message):
69
69
  return None if value is None else max(0, value)
70
70
 
71
71
  @model_validator(mode="after")
72
- def attributes_validator(self):
72
+ def attributes_validator(self) -> "SearchRequest":
73
73
  if self.attributes and self.excluded_attributes:
74
74
  raise ValueError(
75
75
  "'attributes' and 'excluded_attributes' are mutually exclusive"
@@ -78,12 +78,12 @@ class SearchRequest(Message):
78
78
  return self
79
79
 
80
80
  @property
81
- def start_index_0(self):
81
+ def start_index_0(self) -> Optional[int]:
82
82
  """The 0 indexed start index."""
83
83
  return self.start_index - 1 if self.start_index is not None else None
84
84
 
85
85
  @property
86
- def stop_index_0(self):
86
+ def stop_index_0(self) -> Optional[int]:
87
87
  """The 0 indexed stop index."""
88
88
  return (
89
89
  self.start_index_0 + self.count