scim2-models 0.6.1__tar.gz → 0.6.3__tar.gz

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.
Files changed (31) hide show
  1. {scim2_models-0.6.1 → scim2_models-0.6.3}/PKG-INFO +12 -1
  2. {scim2_models-0.6.1 → scim2_models-0.6.3}/README.md +11 -0
  3. {scim2_models-0.6.1 → scim2_models-0.6.3}/pyproject.toml +1 -1
  4. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/exceptions.py +29 -0
  5. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/path.py +10 -0
  6. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/reference.py +10 -0
  7. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/resources/enterprise_user.py +2 -1
  8. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/resources/group.py +2 -1
  9. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/resources/resource.py +18 -16
  10. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/resources/resource_type.py +11 -4
  11. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/resources/user.py +3 -4
  12. {scim2_models-0.6.1 → scim2_models-0.6.3}/LICENSE +0 -0
  13. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/__init__.py +0 -0
  14. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/annotations.py +0 -0
  15. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/attributes.py +0 -0
  16. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/base.py +0 -0
  17. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/constants.py +0 -0
  18. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/context.py +0 -0
  19. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/messages/__init__.py +0 -0
  20. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/messages/bulk.py +0 -0
  21. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/messages/error.py +0 -0
  22. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/messages/list_response.py +0 -0
  23. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/messages/message.py +0 -0
  24. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/messages/patch_op.py +0 -0
  25. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/messages/search_request.py +0 -0
  26. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/py.typed +0 -0
  27. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/resources/__init__.py +0 -0
  28. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/resources/schema.py +0 -0
  29. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/resources/service_provider_config.py +0 -0
  30. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/scim_object.py +0 -0
  31. {scim2_models-0.6.1 → scim2_models-0.6.3}/scim2_models/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: scim2-models
3
- Version: 0.6.1
3
+ Version: 0.6.3
4
4
  Summary: SCIM2 models serialization and validation with pydantic
5
5
  Keywords: scim,scim2,provisioning,pydantic,rfc7643,rfc7644
6
6
  Author: Yaal Coop
@@ -240,6 +240,17 @@ Provisioning is the action of managing a set of resources across different servi
240
240
  SCIM is often used between Identity Providers and applications in completion of standards like OAuth2 and OpenID Connect.
241
241
  It allows users and groups creations, modifications and deletions to be synchronized between applications.
242
242
 
243
+ ## Features
244
+
245
+ - **SCIM Resources**: Native Python classes for `User`, `Group`, `EnterpriseUser`, `ServiceProviderConfig`, `ResourceType` and `Schema`
246
+ - **Pydantic Integration**: Full Pydantic v2 support with validation, serialization and IDE autocompletion
247
+ - **RFC Compliance**: Implements [RFC7643](https://datatracker.ietf.org/doc/html/rfc7643) (schema) and [RFC7644](https://datatracker.ietf.org/doc/html/rfc7644) (protocol) specifications
248
+ - **Attribute Metadata**: Support for `Mutability`, `Returned`, `Uniqueness` and `Required` field annotations
249
+ - **Context-Aware Validation**: Automatic field inclusion/exclusion based on HTTP context (creation, query, replacement, etc.)
250
+ - **Schema Extensions**: Dynamic schema extensions and custom resource types, dynamic conversion between schemas and Python models.
251
+ - **SCIM Messages**: `ListResponse`, `SearchRequest`, `PatchOp`, and `Error` messages
252
+ - **Error Handling**: SCIM-compliant exceptions with automatic conversion to `Error` responses
253
+
243
254
  ## Installation
244
255
 
245
256
  ```shell
@@ -12,6 +12,17 @@ Provisioning is the action of managing a set of resources across different servi
12
12
  SCIM is often used between Identity Providers and applications in completion of standards like OAuth2 and OpenID Connect.
13
13
  It allows users and groups creations, modifications and deletions to be synchronized between applications.
14
14
 
15
+ ## Features
16
+
17
+ - **SCIM Resources**: Native Python classes for `User`, `Group`, `EnterpriseUser`, `ServiceProviderConfig`, `ResourceType` and `Schema`
18
+ - **Pydantic Integration**: Full Pydantic v2 support with validation, serialization and IDE autocompletion
19
+ - **RFC Compliance**: Implements [RFC7643](https://datatracker.ietf.org/doc/html/rfc7643) (schema) and [RFC7644](https://datatracker.ietf.org/doc/html/rfc7644) (protocol) specifications
20
+ - **Attribute Metadata**: Support for `Mutability`, `Returned`, `Uniqueness` and `Required` field annotations
21
+ - **Context-Aware Validation**: Automatic field inclusion/exclusion based on HTTP context (creation, query, replacement, etc.)
22
+ - **Schema Extensions**: Dynamic schema extensions and custom resource types, dynamic conversion between schemas and Python models.
23
+ - **SCIM Messages**: `ListResponse`, `SearchRequest`, `PatchOp`, and `Error` messages
24
+ - **Error Handling**: SCIM-compliant exceptions with automatic conversion to `Error` responses
25
+
15
26
  ## Installation
16
27
 
17
28
  ```shell
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "scim2-models"
7
- version = "0.6.1"
7
+ version = "0.6.3"
8
8
  description = "SCIM2 models serialization and validation with pydantic"
9
9
  authors = [{name="Yaal Coop", email="contact@yaal.coop"}]
10
10
  license = {file = "LICENSE"}
@@ -52,6 +52,21 @@ class SCIMException(Exception):
52
52
  {"scim_type": self.scim_type, "status": self.status, **self.context},
53
53
  )
54
54
 
55
+ @classmethod
56
+ def from_error(cls, error: "Error") -> "SCIMException":
57
+ """Create an exception from a SCIM Error object.
58
+
59
+ :param error: The SCIM Error object to convert.
60
+ :return: The appropriate SCIMException subclass instance.
61
+ """
62
+ from .messages.error import Error
63
+
64
+ if not isinstance(error, Error):
65
+ raise TypeError(f"Expected Error, got {type(error).__name__}")
66
+
67
+ exception_class = _SCIM_TYPE_TO_EXCEPTION.get(error.scim_type or "", cls)
68
+ return exception_class(detail=error.detail)
69
+
55
70
 
56
71
  class InvalidFilterException(SCIMException):
57
72
  """The specified filter syntax was invalid.
@@ -263,3 +278,17 @@ class SensitiveException(SCIMException):
263
278
  "The specified request cannot be completed, due to the passing of sensitive "
264
279
  "information in a request URI"
265
280
  )
281
+
282
+
283
+ _SCIM_TYPE_TO_EXCEPTION: dict[str, type[SCIMException]] = {
284
+ "invalidFilter": InvalidFilterException,
285
+ "tooMany": TooManyException,
286
+ "uniqueness": UniquenessException,
287
+ "mutability": MutabilityException,
288
+ "invalidSyntax": InvalidSyntaxException,
289
+ "invalidPath": InvalidPathException,
290
+ "noTarget": NoTargetException,
291
+ "invalidValue": InvalidValueException,
292
+ "invalidVers": InvalidVersionException,
293
+ "sensitive": SensitiveException,
294
+ }
@@ -9,6 +9,8 @@ from typing import NamedTuple
9
9
  from typing import TypeVar
10
10
 
11
11
  from pydantic import GetCoreSchemaHandler
12
+ from pydantic import GetJsonSchemaHandler
13
+ from pydantic.json_schema import JsonSchemaValue
12
14
  from pydantic_core import core_schema
13
15
 
14
16
  from .base import BaseModel
@@ -129,6 +131,14 @@ class Path(UserString, Generic[ResourceT]):
129
131
  serialization=core_schema.plain_serializer_function_ser_schema(str),
130
132
  )
131
133
 
134
+ @classmethod
135
+ def __get_pydantic_json_schema__(
136
+ cls,
137
+ _core_schema: core_schema.CoreSchema,
138
+ _handler: GetJsonSchemaHandler,
139
+ ) -> JsonSchemaValue:
140
+ return {"type": "string"}
141
+
132
142
  def __init__(self, path: "str | Path[Any]"):
133
143
  if isinstance(path, Path):
134
144
  path = str(path)
@@ -7,6 +7,8 @@ from typing import get_args
7
7
  from typing import get_origin
8
8
 
9
9
  from pydantic import GetCoreSchemaHandler
10
+ from pydantic import GetJsonSchemaHandler
11
+ from pydantic.json_schema import JsonSchemaValue
10
12
  from pydantic_core import Url
11
13
  from pydantic_core import ValidationError
12
14
  from pydantic_core import core_schema
@@ -112,6 +114,14 @@ class Reference(str, Generic[ReferenceTypes]):
112
114
  serialization=core_schema.plain_serializer_function_ser_schema(str),
113
115
  )
114
116
 
117
+ @classmethod
118
+ def __get_pydantic_json_schema__(
119
+ cls,
120
+ _core_schema: core_schema.CoreSchema,
121
+ _handler: GetJsonSchemaHandler,
122
+ ) -> JsonSchemaValue:
123
+ return {"type": "string", "format": "uri"}
124
+
115
125
  @classmethod
116
126
  def get_scim_reference_types(cls) -> list[str]:
117
127
  """Return referenceTypes for SCIM schema generation."""
@@ -3,6 +3,7 @@ from typing import Annotated
3
3
 
4
4
  from pydantic import Field
5
5
 
6
+ from ..annotations import CaseExact
6
7
  from ..annotations import Mutability
7
8
  from ..annotations import Required
8
9
  from ..attributes import ComplexAttribute
@@ -15,7 +16,7 @@ if TYPE_CHECKING:
15
16
 
16
17
 
17
18
  class Manager(ComplexAttribute):
18
- value: Annotated[str | None, Required.true] = None
19
+ value: Annotated[str | None, Required.true, CaseExact.true] = None
19
20
  """The id of the SCIM resource representing the User's manager."""
20
21
 
21
22
  ref: Annotated[ # type: ignore[type-arg]
@@ -7,6 +7,7 @@ from typing import Union
7
7
  from pydantic import Field
8
8
 
9
9
  from ..annotations import Mutability
10
+ from ..annotations import Required
10
11
  from ..attributes import ComplexAttribute
11
12
  from ..path import URN
12
13
  from ..reference import Reference
@@ -38,7 +39,7 @@ class GroupMember(ComplexAttribute):
38
39
  class Group(Resource[Any]):
39
40
  __schema__ = URN("urn:ietf:params:scim:schemas:core:2.0:Group")
40
41
 
41
- display_name: str | None = None
42
+ display_name: Annotated[str | None, Required.true] = None
42
43
  """A human-readable name for the Group."""
43
44
 
44
45
  members: list[GroupMember] | None = None
@@ -532,19 +532,21 @@ def _model_attribute_to_scim_attribute(
532
532
  else None
533
533
  )
534
534
 
535
- return Attribute(
536
- name=field_info.serialization_alias or attribute_name,
537
- type=Attribute.Type(attribute_type),
538
- multi_valued=model.get_field_multiplicity(attribute_name),
539
- description=field_info.description,
540
- canonical_values=field_info.examples,
541
- required=model.get_field_annotation(attribute_name, Required),
542
- case_exact=model.get_field_annotation(attribute_name, CaseExact),
543
- mutability=model.get_field_annotation(attribute_name, Mutability),
544
- returned=model.get_field_annotation(attribute_name, Returned),
545
- uniqueness=model.get_field_annotation(attribute_name, Uniqueness),
546
- sub_attributes=sub_attributes,
547
- reference_types=root_type.get_scim_reference_types() # type: ignore[attr-defined]
548
- if attribute_type == Attribute.Type.reference
549
- else None,
550
- )
535
+ kwargs: dict[str, Any] = {
536
+ "name": field_info.serialization_alias or attribute_name,
537
+ "type": Attribute.Type(attribute_type),
538
+ "multi_valued": model.get_field_multiplicity(attribute_name),
539
+ "description": field_info.description,
540
+ "canonical_values": field_info.examples,
541
+ "required": model.get_field_annotation(attribute_name, Required),
542
+ "case_exact": model.get_field_annotation(attribute_name, CaseExact),
543
+ "mutability": model.get_field_annotation(attribute_name, Mutability),
544
+ "returned": model.get_field_annotation(attribute_name, Returned),
545
+ "sub_attributes": sub_attributes,
546
+ }
547
+ if attribute_type != Attribute.Type.complex:
548
+ kwargs["uniqueness"] = model.get_field_annotation(attribute_name, Uniqueness)
549
+ if attribute_type == Attribute.Type.reference:
550
+ kwargs["reference_types"] = root_type.get_scim_reference_types() # type: ignore[attr-defined]
551
+
552
+ return Attribute(**kwargs)
@@ -8,6 +8,7 @@ from ..annotations import CaseExact
8
8
  from ..annotations import Mutability
9
9
  from ..annotations import Required
10
10
  from ..annotations import Returned
11
+ from ..annotations import Uniqueness
11
12
  from ..attributes import ComplexAttribute
12
13
  from ..path import URN
13
14
  from ..reference import URI
@@ -38,7 +39,13 @@ class SchemaExtension(ComplexAttribute):
38
39
  class ResourceType(Resource[Any]):
39
40
  __schema__ = URN("urn:ietf:params:scim:schemas:core:2.0:ResourceType")
40
41
 
41
- name: Annotated[str | None, Mutability.read_only, Required.true] = None
42
+ name: Annotated[
43
+ str | None,
44
+ Mutability.read_only,
45
+ Required.true,
46
+ CaseExact.true,
47
+ Uniqueness.server,
48
+ ] = None
42
49
  """The resource type name.
43
50
 
44
51
  When applicable, service providers MUST specify the name, e.g.,
@@ -57,9 +64,9 @@ class ResourceType(Resource[Any]):
57
64
  This is often the same value as the "name" attribute.
58
65
  """
59
66
 
60
- endpoint: Annotated[Reference[URI] | None, Mutability.read_only, Required.true] = (
61
- None
62
- )
67
+ endpoint: Annotated[
68
+ Reference[URI] | None, Mutability.read_only, Required.true, Uniqueness.server
69
+ ] = None
63
70
  """The resource type's HTTP-addressable endpoint relative to the Base URL,
64
71
  e.g., '/Users'."""
65
72
 
@@ -2,7 +2,6 @@ from enum import Enum
2
2
  from typing import TYPE_CHECKING
3
3
  from typing import Annotated
4
4
  from typing import ClassVar
5
- from typing import Union
6
5
 
7
6
  from pydantic import Base64Bytes
8
7
  from pydantic import EmailStr
@@ -180,7 +179,7 @@ class Address(ComplexAttribute):
180
179
 
181
180
  primary: bool | None = None
182
181
  """A Boolean value indicating the 'primary' or preferred attribute value
183
- for this attribute, e.g., the preferred photo or thumbnail."""
182
+ for this attribute, e.g., the preferred address."""
184
183
 
185
184
 
186
185
  class Entitlement(ComplexAttribute):
@@ -202,8 +201,8 @@ class GroupMembership(ComplexAttribute):
202
201
  value: Annotated[str | None, Mutability.read_only] = None
203
202
  """The identifier of the User's group."""
204
203
 
205
- ref: Annotated[ # type: ignore[type-arg]
206
- Reference[Union["User", "Group"]] | None,
204
+ ref: Annotated[
205
+ Reference["Group"] | None,
207
206
  Mutability.read_only,
208
207
  ] = Field(None, serialization_alias="$ref")
209
208
  """The reference URI of a target resource, if the attribute is a
File without changes