scim2-models 0.5.0__py3-none-any.whl → 0.5.2__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,7 +1,6 @@
1
1
  from inspect import isclass
2
2
  from typing import Annotated
3
3
  from typing import Any
4
- from typing import Optional
5
4
  from typing import get_origin
6
5
 
7
6
  from pydantic import Field
@@ -16,7 +15,7 @@ from .reference import Reference
16
15
  class ComplexAttribute(BaseModel):
17
16
  """A complex attribute as defined in :rfc:`RFC7643 §2.3.8 <7643#section-2.3.8>`."""
18
17
 
19
- _attribute_urn: Optional[str] = None
18
+ _attribute_urn: str | None = None
20
19
 
21
20
  def get_attribute_urn(self, field_name: str) -> str:
22
21
  """Build the full URN of the attribute.
@@ -30,20 +29,20 @@ class ComplexAttribute(BaseModel):
30
29
 
31
30
 
32
31
  class MultiValuedComplexAttribute(ComplexAttribute):
33
- type: Optional[str] = None
32
+ type: str | None = None
34
33
  """A label indicating the attribute's function."""
35
34
 
36
- primary: Optional[bool] = None
35
+ primary: bool | None = None
37
36
  """A Boolean value indicating the 'primary' or preferred attribute value
38
37
  for this attribute."""
39
38
 
40
- display: Annotated[Optional[str], Mutability.immutable] = None
39
+ display: Annotated[str | None, Mutability.immutable] = None
41
40
  """A human-readable name, primarily used for display purposes."""
42
41
 
43
- value: Optional[Any] = None
42
+ value: Any | None = None
44
43
  """The value of an entitlement."""
45
44
 
46
- ref: Optional[Reference[Any]] = Field(None, serialization_alias="$ref")
45
+ ref: Reference[Any] | None = Field(None, serialization_alias="$ref")
47
46
  """The reference URI of a target resource, if the attribute is a
48
47
  reference."""
49
48
 
scim2_models/base.py CHANGED
@@ -29,12 +29,21 @@ from scim2_models.utils import _normalize_attribute_name
29
29
  from scim2_models.utils import _to_camel
30
30
 
31
31
 
32
- def _contains_attribute_or_subattributes(
33
- attribute_urns: list[str], attribute_urn: str
34
- ) -> bool:
35
- return attribute_urn in attribute_urns or any(
36
- item.startswith(f"{attribute_urn}.") or item.startswith(f"{attribute_urn}:")
37
- for item in attribute_urns
32
+ def _is_attribute_requested(requested_urns: list[str], current_urn: str) -> bool:
33
+ """Check if an attribute should be included based on the requested URNs.
34
+
35
+ Returns True if:
36
+ - The current attribute is explicitly requested
37
+ - A sub-attribute of the current attribute is requested
38
+ - The current attribute is a sub-attribute of a requested attribute
39
+ """
40
+ return (
41
+ current_urn in requested_urns
42
+ or any(
43
+ item.startswith(f"{current_urn}.") or item.startswith(f"{current_urn}:")
44
+ for item in requested_urns
45
+ )
46
+ or any(current_urn.startswith(f"{item}.") for item in requested_urns)
38
47
  )
39
48
 
40
49
 
@@ -96,7 +105,7 @@ class BaseModel(PydanticBaseModel):
96
105
  return field_annotation
97
106
 
98
107
  @classmethod
99
- def get_field_root_type(cls, attribute_name: str) -> Optional[type]:
108
+ def get_field_root_type(cls, attribute_name: str) -> type | None:
100
109
  """Extract the root type from a model field.
101
110
 
102
111
  This method unwraps complex type annotations to find the underlying
@@ -244,7 +253,7 @@ class BaseModel(PydanticBaseModel):
244
253
  return result
245
254
 
246
255
  def normalize_value(
247
- val: Any, model_class: Optional[type["BaseModel"]] = None
256
+ val: Any, model_class: type["BaseModel"] | None = None
248
257
  ) -> Any:
249
258
  """Normalize input value based on model class."""
250
259
  if not isinstance(val, dict):
@@ -478,9 +487,7 @@ class BaseModel(PydanticBaseModel):
478
487
  if returnability == Returned.default and (
479
488
  (
480
489
  included_urns
481
- and not _contains_attribute_or_subattributes(
482
- included_urns, attribute_urn
483
- )
490
+ and not _is_attribute_requested(included_urns, attribute_urn)
484
491
  )
485
492
  or attribute_urn in excluded_urns
486
493
  ):
@@ -504,7 +511,7 @@ class BaseModel(PydanticBaseModel):
504
511
  def model_validate(
505
512
  cls,
506
513
  *args: Any,
507
- scim_ctx: Optional[Context] = Context.DEFAULT,
514
+ scim_ctx: Context | None = Context.DEFAULT,
508
515
  original: Optional["BaseModel"] = None,
509
516
  **kwargs: Any,
510
517
  ) -> Self:
@@ -1,7 +1,6 @@
1
1
  from enum import Enum
2
2
  from typing import Annotated
3
3
  from typing import Any
4
- from typing import Optional
5
4
 
6
5
  from pydantic import Field
7
6
  from pydantic import PlainSerializer
@@ -19,30 +18,30 @@ class BulkOperation(ComplexAttribute):
19
18
  patch = "PATCH"
20
19
  delete = "DELETE"
21
20
 
22
- method: Optional[Method] = None
21
+ method: Method | None = None
23
22
  """The HTTP method of the current operation."""
24
23
 
25
- bulk_id: Optional[str] = None
24
+ bulk_id: str | None = None
26
25
  """The transient identifier of a newly created resource, unique within a
27
26
  bulk request and created by the client."""
28
27
 
29
- version: Optional[str] = None
28
+ version: str | None = None
30
29
  """The current resource version."""
31
30
 
32
- path: Optional[str] = None
31
+ path: str | None = None
33
32
  """The resource's relative path to the SCIM service provider's root."""
34
33
 
35
- data: Optional[Any] = None
34
+ data: Any | None = None
36
35
  """The resource data as it would appear for a single SCIM POST, PUT, or
37
36
  PATCH operation."""
38
37
 
39
- location: Optional[str] = None
38
+ location: str | None = None
40
39
  """The resource endpoint URL."""
41
40
 
42
- response: Optional[Any] = None
41
+ response: Any | None = None
43
42
  """The HTTP response body for the specified request operation."""
44
43
 
45
- status: Annotated[Optional[int], PlainSerializer(_int_to_str)] = None
44
+ status: Annotated[int | None, PlainSerializer(_int_to_str)] = None
46
45
  """The HTTP response status code for the requested operation."""
47
46
 
48
47
 
@@ -58,12 +57,12 @@ class BulkRequest(Message):
58
57
  "urn:ietf:params:scim:api:messages:2.0:BulkRequest"
59
58
  ]
60
59
 
61
- fail_on_errors: Optional[int] = None
60
+ fail_on_errors: int | None = None
62
61
  """An integer specifying the number of errors that the service provider
63
62
  will accept before the operation is terminated and an error response is
64
63
  returned."""
65
64
 
66
- operations: Optional[list[BulkOperation]] = Field(
65
+ operations: list[BulkOperation] | None = Field(
67
66
  None, serialization_alias="Operations"
68
67
  )
69
68
  """Defines operations within a bulk job."""
@@ -81,7 +80,7 @@ class BulkResponse(Message):
81
80
  "urn:ietf:params:scim:api:messages:2.0:BulkResponse"
82
81
  ]
83
82
 
84
- operations: Optional[list[BulkOperation]] = Field(
83
+ operations: list[BulkOperation] | None = Field(
85
84
  None, serialization_alias="Operations"
86
85
  )
87
86
  """Defines operations within a bulk job."""
@@ -1,5 +1,4 @@
1
1
  from typing import Annotated
2
- from typing import Optional
3
2
 
4
3
  from pydantic import PlainSerializer
5
4
 
@@ -15,14 +14,14 @@ class Error(Message):
15
14
  "urn:ietf:params:scim:api:messages:2.0:Error"
16
15
  ]
17
16
 
18
- status: Annotated[Optional[int], PlainSerializer(_int_to_str)] = None
17
+ status: Annotated[int | None, PlainSerializer(_int_to_str)] = None
19
18
  """The HTTP status code (see Section 6 of [RFC7231]) expressed as a JSON
20
19
  string."""
21
20
 
22
- scim_type: Optional[str] = None
21
+ scim_type: str | None = None
23
22
  """A SCIM detail error keyword."""
24
23
 
25
- detail: Optional[str] = None
24
+ detail: str | None = None
26
25
  """A detailed human-readable message."""
27
26
 
28
27
  @classmethod
@@ -1,7 +1,6 @@
1
1
  from typing import Annotated
2
2
  from typing import Any
3
3
  from typing import Generic
4
- from typing import Optional
5
4
 
6
5
  from pydantic import Field
7
6
  from pydantic import ValidationInfo
@@ -22,19 +21,17 @@ class ListResponse(Message, Generic[AnyResource], metaclass=_GenericMessageMetac
22
21
  "urn:ietf:params:scim:api:messages:2.0:ListResponse"
23
22
  ]
24
23
 
25
- total_results: Optional[int] = None
24
+ total_results: int | None = None
26
25
  """The total number of results returned by the list or query operation."""
27
26
 
28
- start_index: Optional[int] = None
27
+ start_index: int | None = None
29
28
  """The 1-based index of the first result in the current set of list
30
29
  results."""
31
30
 
32
- items_per_page: Optional[int] = None
31
+ items_per_page: int | None = None
33
32
  """The number of resources returned in a list response page."""
34
33
 
35
- resources: Optional[list[AnyResource]] = Field(
36
- None, serialization_alias="Resources"
37
- )
34
+ resources: list[AnyResource] | None = Field(None, serialization_alias="Resources")
38
35
  """A multi-valued list of complex objects containing the requested
39
36
  resources."""
40
37
 
@@ -1,7 +1,6 @@
1
+ from collections.abc import Callable
1
2
  from typing import Annotated
2
3
  from typing import Any
3
- from typing import Callable
4
- from typing import Optional
5
4
  from typing import Union
6
5
  from typing import get_args
7
6
  from typing import get_origin
@@ -23,14 +22,14 @@ class Message(ScimObject):
23
22
 
24
23
  def _create_schema_discriminator(
25
24
  resource_types_schemas: list[str],
26
- ) -> Callable[[Any], Optional[str]]:
25
+ ) -> Callable[[Any], str | None]:
27
26
  """Create a schema discriminator function for the given resource schemas.
28
27
 
29
28
  :param resource_types_schemas: List of valid resource schemas
30
29
  :return: Discriminator function for Pydantic
31
30
  """
32
31
 
33
- def get_schema_from_payload(payload: Any) -> Optional[str]:
32
+ def get_schema_from_payload(payload: Any) -> str | None:
34
33
  """Extract schema from SCIM payload for discrimination.
35
34
 
36
35
  :param payload: SCIM payload dict or object
@@ -89,7 +88,7 @@ def _create_tagged_resource_union(resource_union: Any) -> Any:
89
88
  for resource_type in resource_types
90
89
  ]
91
90
  # Dynamic union construction from tuple - MyPy can't validate this at compile time
92
- union = Union[tuple(tagged_resources)] # type: ignore
91
+ union = Union[tuple(tagged_resources)] # type: ignore # noqa: UP007
93
92
  return Annotated[union, discriminator]
94
93
 
95
94
 
@@ -3,9 +3,7 @@ from inspect import isclass
3
3
  from typing import Annotated
4
4
  from typing import Any
5
5
  from typing import Generic
6
- from typing import Optional
7
6
  from typing import TypeVar
8
- from typing import Union
9
7
 
10
8
  from pydantic import Field
11
9
  from pydantic import ValidationInfo
@@ -48,7 +46,7 @@ class PatchOperation(ComplexAttribute):
48
46
  despite :rfc:`RFC7644 §3.5.2 <7644#section-3.5.2>`, op is case-insensitive.
49
47
  """
50
48
 
51
- path: Optional[str] = None
49
+ path: str | None = None
52
50
  """The "path" attribute value is a String containing an attribute path
53
51
  describing the target of the operation."""
54
52
 
@@ -113,7 +111,7 @@ class PatchOperation(ComplexAttribute):
113
111
 
114
112
  return self
115
113
 
116
- value: Optional[Any] = None
114
+ value: Any | None = None
117
115
 
118
116
  @field_validator("op", mode="before")
119
117
  @classmethod
@@ -165,7 +163,7 @@ class PatchOp(Message, Generic[T]):
165
163
  return super().__new__(cls)
166
164
 
167
165
  def __class_getitem__(
168
- cls, typevar_values: Union[type[Resource[Any]], tuple[type[Resource[Any]], ...]]
166
+ cls, typevar_values: type[Resource[Any]] | tuple[type[Resource[Any]], ...]
169
167
  ) -> Any:
170
168
  """Validate type parameter when creating parameterized type.
171
169
 
@@ -211,7 +209,7 @@ class PatchOp(Message, Generic[T]):
211
209
  "urn:ietf:params:scim:api:messages:2.0:PatchOp"
212
210
  ]
213
211
 
214
- operations: Annotated[Optional[list[PatchOperation]], Required.true] = Field(
212
+ operations: Annotated[list[PatchOperation] | None, Required.true] = Field(
215
213
  None, serialization_alias="Operations", min_length=1
216
214
  )
217
215
  """The body of an HTTP PATCH request MUST contain the attribute
@@ -1,6 +1,5 @@
1
1
  from enum import Enum
2
2
  from typing import Annotated
3
- from typing import Optional
4
3
 
5
4
  from pydantic import field_validator
6
5
  from pydantic import model_validator
@@ -21,14 +20,14 @@ class SearchRequest(Message):
21
20
  "urn:ietf:params:scim:api:messages:2.0:SearchRequest"
22
21
  ]
23
22
 
24
- attributes: Optional[list[str]] = None
23
+ attributes: list[str] | None = None
25
24
  """A multi-valued list of strings indicating the names of resource
26
25
  attributes to return in the response, overriding the set of attributes that
27
26
  would be returned by default."""
28
27
 
29
28
  @field_validator("attributes")
30
29
  @classmethod
31
- def validate_attributes_syntax(cls, v: Optional[list[str]]) -> Optional[list[str]]:
30
+ def validate_attributes_syntax(cls, v: list[str] | None) -> list[str] | None:
32
31
  """Validate syntax of attribute paths."""
33
32
  if v is None:
34
33
  return v
@@ -39,15 +38,15 @@ class SearchRequest(Message):
39
38
 
40
39
  return v
41
40
 
42
- excluded_attributes: Optional[list[str]] = None
41
+ excluded_attributes: list[str] | None = None
43
42
  """A multi-valued list of strings indicating the names of resource
44
43
  attributes to be removed from the default set of attributes to return."""
45
44
 
46
45
  @field_validator("excluded_attributes")
47
46
  @classmethod
48
47
  def validate_excluded_attributes_syntax(
49
- cls, v: Optional[list[str]]
50
- ) -> Optional[list[str]]:
48
+ cls, v: list[str] | None
49
+ ) -> list[str] | None:
51
50
  """Validate syntax of excluded attribute paths."""
52
51
  if v is None:
53
52
  return v
@@ -58,16 +57,16 @@ class SearchRequest(Message):
58
57
 
59
58
  return v
60
59
 
61
- filter: Optional[str] = None
60
+ filter: str | None = None
62
61
  """The filter string used to request a subset of resources."""
63
62
 
64
- sort_by: Optional[str] = None
63
+ sort_by: str | None = None
65
64
  """A string indicating the attribute whose value SHALL be used to order the
66
65
  returned responses."""
67
66
 
68
67
  @field_validator("sort_by")
69
68
  @classmethod
70
- def validate_sort_by_syntax(cls, v: Optional[str]) -> Optional[str]:
69
+ def validate_sort_by_syntax(cls, v: str | None) -> str | None:
71
70
  """Validate syntax of sort_by attribute path.
72
71
 
73
72
  :param v: The sort_by attribute path to validate
@@ -86,29 +85,29 @@ class SearchRequest(Message):
86
85
  ascending = "ascending"
87
86
  descending = "descending"
88
87
 
89
- sort_order: Optional[SortOrder] = None
88
+ sort_order: SortOrder | None = None
90
89
  """A string indicating the order in which the "sortBy" parameter is
91
90
  applied."""
92
91
 
93
- start_index: Optional[int] = None
92
+ start_index: int | None = None
94
93
  """An integer indicating the 1-based index of the first query result."""
95
94
 
96
95
  @field_validator("start_index")
97
96
  @classmethod
98
- def start_index_floor(cls, value: Optional[int]) -> Optional[int]:
97
+ def start_index_floor(cls, value: int | None) -> int | None:
99
98
  """According to :rfc:`RFC7644 §3.4.2 <7644#section-3.4.2.4>, start_index values less than 1 are interpreted as 1.
100
99
 
101
100
  A value less than 1 SHALL be interpreted as 1.
102
101
  """
103
102
  return None if value is None else max(1, value)
104
103
 
105
- count: Optional[int] = None
104
+ count: int | None = None
106
105
  """An integer indicating the desired maximum number of query results per
107
106
  page."""
108
107
 
109
108
  @field_validator("count")
110
109
  @classmethod
111
- def count_floor(cls, value: Optional[int]) -> Optional[int]:
110
+ def count_floor(cls, value: int | None) -> int | None:
112
111
  """According to :rfc:`RFC7644 §3.4.2 <7644#section-3.4.2.4>, count values less than 0 are interpreted as 0.
113
112
 
114
113
  A negative value SHALL be interpreted as 0.
@@ -125,12 +124,12 @@ class SearchRequest(Message):
125
124
  return self
126
125
 
127
126
  @property
128
- def start_index_0(self) -> Optional[int]:
127
+ def start_index_0(self) -> int | None:
129
128
  """The 0 indexed start index."""
130
129
  return self.start_index - 1 if self.start_index is not None else None
131
130
 
132
131
  @property
133
- def stop_index_0(self) -> Optional[int]:
132
+ def stop_index_0(self) -> int | None:
134
133
  """The 0 indexed stop index."""
135
134
  return (
136
135
  self.start_index_0 + self.count
scim2_models/reference.py CHANGED
@@ -1,13 +1,13 @@
1
1
  from collections import UserString
2
2
  from typing import Any
3
3
  from typing import Generic
4
+ from typing import NewType
4
5
  from typing import TypeVar
5
6
  from typing import get_args
6
7
  from typing import get_origin
7
8
 
8
9
  from pydantic import GetCoreSchemaHandler
9
10
  from pydantic_core import core_schema
10
- from typing_extensions import NewType
11
11
 
12
12
  from .utils import UNION_TYPES
13
13
 
@@ -1,6 +1,5 @@
1
1
  from typing import Annotated
2
2
  from typing import Literal
3
- from typing import Optional
4
3
 
5
4
  from pydantic import Field
6
5
 
@@ -12,16 +11,16 @@ from .resource import Extension
12
11
 
13
12
 
14
13
  class Manager(ComplexAttribute):
15
- value: Annotated[Optional[str], Required.true] = None
14
+ value: Annotated[str | None, Required.true] = None
16
15
  """The id of the SCIM resource representing the User's manager."""
17
16
 
18
- ref: Annotated[Optional[Reference[Literal["User"]]], Required.true] = Field(
17
+ ref: Annotated[Reference[Literal["User"]] | None, Required.true] = Field(
19
18
  None,
20
19
  serialization_alias="$ref",
21
20
  )
22
21
  """The URI of the SCIM resource representing the User's manager."""
23
22
 
24
- display_name: Annotated[Optional[str], Mutability.read_only] = None
23
+ display_name: Annotated[str | None, Mutability.read_only] = None
25
24
  """The displayName of the User's manager."""
26
25
 
27
26
 
@@ -30,24 +29,24 @@ class EnterpriseUser(Extension):
30
29
  "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
31
30
  ]
32
31
 
33
- employee_number: Optional[str] = None
32
+ employee_number: str | None = None
34
33
  """Numeric or alphanumeric identifier assigned to a person, typically based
35
34
  on order of hire or association with an organization."""
36
35
 
37
- cost_center: Optional[str] = None
36
+ cost_center: str | None = None
38
37
  """"Identifies the name of a cost center."""
39
38
 
40
- organization: Optional[str] = None
39
+ organization: str | None = None
41
40
  """Identifies the name of an organization."""
42
41
 
43
- division: Optional[str] = None
42
+ division: str | None = None
44
43
  """Identifies the name of a division."""
45
44
 
46
- department: Optional[str] = None
45
+ department: str | None = None
47
46
  """Numeric or alphanumeric identifier assigned to a person, typically based
48
47
  on order of hire or association with an organization."""
49
48
 
50
- manager: Optional[Manager] = None
49
+ manager: Manager | None = None
51
50
  """The User's manager.
52
51
 
53
52
  A complex type that optionally allows service providers to represent
@@ -2,8 +2,6 @@ from typing import Annotated
2
2
  from typing import Any
3
3
  from typing import ClassVar
4
4
  from typing import Literal
5
- from typing import Optional
6
- from typing import Union
7
5
 
8
6
  from pydantic import Field
9
7
 
@@ -15,22 +13,22 @@ from .resource import Resource
15
13
 
16
14
 
17
15
  class GroupMember(ComplexAttribute):
18
- value: Annotated[Optional[str], Mutability.immutable] = None
16
+ value: Annotated[str | None, Mutability.immutable] = None
19
17
  """Identifier of the member of this Group."""
20
18
 
21
19
  ref: Annotated[
22
- Optional[Reference[Union[Literal["User"], Literal["Group"]]]],
20
+ Reference[Literal["User"] | Literal["Group"]] | None,
23
21
  Mutability.immutable,
24
22
  ] = Field(None, serialization_alias="$ref")
25
23
  """The reference URI of a target resource, if the attribute is a
26
24
  reference."""
27
25
 
28
- type: Annotated[Optional[str], Mutability.immutable] = Field(
26
+ type: Annotated[str | None, Mutability.immutable] = Field(
29
27
  None, examples=["User", "Group"]
30
28
  )
31
29
  """A label indicating the attribute's function, e.g., "work" or "home"."""
32
30
 
33
- display: Annotated[Optional[str], Mutability.read_only] = None
31
+ display: Annotated[str | None, Mutability.read_only] = None
34
32
 
35
33
 
36
34
  class Group(Resource[Any]):
@@ -38,10 +36,10 @@ class Group(Resource[Any]):
38
36
  "urn:ietf:params:scim:schemas:core:2.0:Group"
39
37
  ]
40
38
 
41
- display_name: Optional[str] = None
39
+ display_name: str | None = None
42
40
  """A human-readable name for the Group."""
43
41
 
44
- members: Optional[list[GroupMember]] = None
42
+ members: list[GroupMember] | None = None
45
43
  """A list of members of the Group."""
46
44
 
47
45
  Members: ClassVar[type[ComplexAttribute]] = GroupMember