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.
@@ -4,12 +4,11 @@ from enum import Enum
4
4
  from typing import Annotated
5
5
  from typing import Any
6
6
  from typing import List # noqa : UP005,UP035
7
- from typing import Literal
8
7
  from typing import Optional
9
8
  from typing import TypeVar
10
9
  from typing import Union
11
- from typing import get_origin
12
10
 
11
+ from pydantic import Base64Bytes
13
12
  from pydantic import Field
14
13
  from pydantic import create_model
15
14
  from pydantic import field_validator
@@ -26,10 +25,10 @@ from ..attributes import ComplexAttribute
26
25
  from ..attributes import is_complex_attribute
27
26
  from ..base import BaseModel
28
27
  from ..constants import RESERVED_WORDS
29
- from ..reference import ExternalReference
28
+ from ..path import URN
29
+ from ..reference import URI
30
+ from ..reference import External
30
31
  from ..reference import Reference
31
- from ..reference import URIReference
32
- from ..utils import Base64Bytes
33
32
  from ..utils import _normalize_attribute_name
34
33
  from .resource import Resource
35
34
 
@@ -65,10 +64,6 @@ def _make_python_model(
65
64
  for attr in (obj.attributes or [])
66
65
  if attr.name
67
66
  }
68
- pydantic_attributes["schemas"] = (
69
- Annotated[list[str], Required.true],
70
- Field(default=[obj.id]),
71
- )
72
67
 
73
68
  if not obj.name:
74
69
  raise ValueError("Schema or Attribute 'name' must be defined")
@@ -76,8 +71,9 @@ def _make_python_model(
76
71
  model_name = to_pascal(to_snake(obj.name))
77
72
  model: type[T] = create_model(model_name, __base__=base, **pydantic_attributes) # type: ignore[call-overload]
78
73
 
79
- # Set the ComplexType class as a member of the model
80
- # e.g. make Member an attribute of Group
74
+ if isinstance(obj, Schema) and obj.id:
75
+ model.__schema__ = URN(obj.id) # type: ignore[attr-defined]
76
+
81
77
  for attr_name in model.model_fields:
82
78
  attr_type = model.get_field_root_type(attr_name)
83
79
  if attr_type and is_complex_attribute(attr_type):
@@ -103,13 +99,14 @@ class Attribute(ComplexAttribute):
103
99
  ) -> type:
104
100
  if self.value == self.reference and reference_types is not None:
105
101
  if reference_types == ["external"]:
106
- return Reference[ExternalReference]
102
+ return Reference[External]
107
103
 
108
104
  if reference_types == ["uri"]:
109
- return Reference[URIReference]
105
+ return Reference[URI]
110
106
 
111
- types = tuple(Literal[t] for t in reference_types)
112
- return Reference[Union[types]] # type: ignore # noqa: UP007
107
+ if len(reference_types) == 1:
108
+ return Reference[reference_types[0]] # type: ignore[valid-type]
109
+ return Reference[Union[tuple(reference_types)]] # type: ignore[misc,return-value] # noqa: UP007
113
110
 
114
111
  attr_types = {
115
112
  self.string: str,
@@ -124,7 +121,7 @@ class Attribute(ComplexAttribute):
124
121
 
125
122
  @classmethod
126
123
  def from_python(cls, pytype: type) -> "Attribute.Type":
127
- if get_origin(pytype) == Reference:
124
+ if isinstance(pytype, type) and issubclass(pytype, Reference):
128
125
  return cls.reference
129
126
 
130
127
  if pytype and is_complex_attribute(pytype):
@@ -254,9 +251,7 @@ class Attribute(ComplexAttribute):
254
251
 
255
252
 
256
253
  class Schema(Resource[Any]):
257
- schemas: Annotated[list[str], Required.true] = [
258
- "urn:ietf:params:scim:schemas:core:2.0:Schema"
259
- ]
254
+ __schema__ = URN("urn:ietf:params:scim:schemas:core:2.0:Schema")
260
255
 
261
256
  id: Annotated[str | None, Mutability.read_only, Required.true] = None
262
257
  """The unique URI of the schema."""
@@ -288,7 +283,7 @@ class Schema(Resource[Any]):
288
283
  return attribute
289
284
  return None
290
285
 
291
- def __getitem__(self, name: str) -> "Attribute": # type: ignore[override]
286
+ def __getitem__(self, name: str) -> "Attribute":
292
287
  """Find an attribute by its name."""
293
288
  if attribute := self.get_attribute(name):
294
289
  return attribute
@@ -9,7 +9,8 @@ from ..annotations import Required
9
9
  from ..annotations import Returned
10
10
  from ..annotations import Uniqueness
11
11
  from ..attributes import ComplexAttribute
12
- from ..reference import ExternalReference
12
+ from ..path import URN
13
+ from ..reference import External
13
14
  from ..reference import Reference
14
15
  from .resource import Resource
15
16
 
@@ -73,15 +74,13 @@ class AuthenticationScheme(ComplexAttribute):
73
74
  description: Annotated[str | None, Mutability.read_only, Required.true] = None
74
75
  """A description of the authentication scheme."""
75
76
 
76
- spec_uri: Annotated[Reference[ExternalReference] | None, Mutability.read_only] = (
77
- None
78
- )
77
+ spec_uri: Annotated[Reference[External] | None, Mutability.read_only] = None
79
78
  """An HTTP-addressable URL pointing to the authentication scheme's
80
79
  specification."""
81
80
 
82
- documentation_uri: Annotated[
83
- Reference[ExternalReference] | None, Mutability.read_only
84
- ] = None
81
+ documentation_uri: Annotated[Reference[External] | None, Mutability.read_only] = (
82
+ None
83
+ )
85
84
  """An HTTP-addressable URL pointing to the authentication scheme's usage
86
85
  documentation."""
87
86
 
@@ -92,9 +91,7 @@ class AuthenticationScheme(ComplexAttribute):
92
91
 
93
92
 
94
93
  class ServiceProviderConfig(Resource[Any]):
95
- schemas: Annotated[list[str], Required.true] = [
96
- "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"
97
- ]
94
+ __schema__ = URN("urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig")
98
95
 
99
96
  id: Annotated[
100
97
  str | None, Mutability.read_only, Returned.default, Uniqueness.global_
@@ -106,9 +103,9 @@ class ServiceProviderConfig(Resource[Any]):
106
103
  # resources, the "id" attribute is not required for the service
107
104
  # provider configuration resource
108
105
 
109
- documentation_uri: Annotated[
110
- Reference[ExternalReference] | None, Mutability.read_only
111
- ] = None
106
+ documentation_uri: Annotated[Reference[External] | None, Mutability.read_only] = (
107
+ None
108
+ )
112
109
  """An HTTP-addressable URL pointing to the service provider's human-
113
110
  consumable help documentation."""
114
111
 
@@ -1,8 +1,10 @@
1
1
  from enum import Enum
2
+ from typing import TYPE_CHECKING
2
3
  from typing import Annotated
3
4
  from typing import ClassVar
4
- from typing import Literal
5
+ from typing import Union
5
6
 
7
+ from pydantic import Base64Bytes
6
8
  from pydantic import EmailStr
7
9
  from pydantic import Field
8
10
 
@@ -12,12 +14,15 @@ from ..annotations import Required
12
14
  from ..annotations import Returned
13
15
  from ..annotations import Uniqueness
14
16
  from ..attributes import ComplexAttribute
15
- from ..reference import ExternalReference
17
+ from ..path import URN
18
+ from ..reference import External
16
19
  from ..reference import Reference
17
- from ..utils import Base64Bytes
18
20
  from .resource import AnyExtension
19
21
  from .resource import Resource
20
22
 
23
+ if TYPE_CHECKING:
24
+ from .group import Group
25
+
21
26
 
22
27
  class Name(ComplexAttribute):
23
28
  formatted: str | None = None
@@ -126,7 +131,7 @@ class Photo(ComplexAttribute):
126
131
  photo = "photo"
127
132
  thumbnail = "thumbnail"
128
133
 
129
- value: Annotated[Reference[ExternalReference] | None, CaseExact.true] = None
134
+ value: Annotated[Reference[External] | None, CaseExact.true] = None
130
135
  """URL of a photo of the User."""
131
136
 
132
137
  display: str | None = None
@@ -197,8 +202,8 @@ class GroupMembership(ComplexAttribute):
197
202
  value: Annotated[str | None, Mutability.read_only] = None
198
203
  """The identifier of the User's group."""
199
204
 
200
- ref: Annotated[
201
- Reference[Literal["User"] | Literal["Group"]] | None,
205
+ ref: Annotated[ # type: ignore[type-arg]
206
+ Reference[Union["User", "Group"]] | None,
202
207
  Mutability.read_only,
203
208
  ] = Field(None, serialization_alias="$ref")
204
209
  """The reference URI of a target resource, if the attribute is a
@@ -245,9 +250,7 @@ class X509Certificate(ComplexAttribute):
245
250
 
246
251
 
247
252
  class User(Resource[AnyExtension]):
248
- schemas: Annotated[list[str], Required.true] = [
249
- "urn:ietf:params:scim:schemas:core:2.0:User"
250
- ]
253
+ __schema__ = URN("urn:ietf:params:scim:schemas:core:2.0:User")
251
254
 
252
255
  user_name: Annotated[str | None, Uniqueness.server, Required.true] = None
253
256
  """Unique identifier for the User, typically used by the user to directly
@@ -265,7 +268,7 @@ class User(Resource[AnyExtension]):
265
268
  """The casual way to address the user in real life, e.g., 'Bob' or 'Bobby'
266
269
  instead of 'Robert'."""
267
270
 
268
- profile_url: Reference[ExternalReference] | None = None
271
+ profile_url: Reference[External] | None = None
269
272
  """A fully qualified URL pointing to a page representing the User's online
270
273
  profile."""
271
274
 
@@ -1,24 +1,107 @@
1
1
  """Base SCIM object classes with schema identification."""
2
2
 
3
+ import warnings
3
4
  from typing import TYPE_CHECKING
4
5
  from typing import Annotated
5
6
  from typing import Any
7
+ from typing import ClassVar
8
+
9
+ from pydantic import ValidationInfo
10
+ from pydantic import ValidatorFunctionWrapHandler
11
+ from pydantic import model_validator
12
+ from pydantic._internal._model_construction import ModelMetaclass
13
+ from pydantic_core import PydanticCustomError
14
+ from typing_extensions import Self
6
15
 
7
16
  from .annotations import Required
8
17
  from .base import BaseModel
9
18
  from .context import Context
19
+ from .path import URN
10
20
 
11
21
  if TYPE_CHECKING:
12
22
  pass
13
23
 
14
24
 
15
- class ScimObject(BaseModel):
25
+ class ScimMetaclass(ModelMetaclass):
26
+ """Metaclass for SCIM objects that handles __schema__ backward compatibility."""
27
+
28
+ def __new__(
29
+ mcs,
30
+ name: str,
31
+ bases: tuple[type, ...],
32
+ namespace: dict[str, Any],
33
+ **kwargs: Any,
34
+ ) -> type:
35
+ cls = super().__new__(mcs, name, bases, namespace, **kwargs)
36
+
37
+ if name in ("ScimObject", "Resource", "Extension"):
38
+ return cls
39
+
40
+ if getattr(cls, "__schema__", None) is None:
41
+ schemas_field = cls.model_fields.get("schemas") # type: ignore[attr-defined]
42
+ if (
43
+ schemas_field
44
+ and schemas_field.default
45
+ and isinstance(schemas_field.default, list)
46
+ and schemas_field.default
47
+ ):
48
+ schema_value = schemas_field.default[0]
49
+ try:
50
+ cls.__schema__ = URN(schema_value) # type: ignore[attr-defined]
51
+ warnings.warn(
52
+ f"{name}: Defining schemas with a default value is deprecated "
53
+ f"and will be removed in version 0.7. "
54
+ f'Use __schema__ = URN("{schema_value}") instead.',
55
+ DeprecationWarning,
56
+ stacklevel=2,
57
+ )
58
+ except ValueError:
59
+ pass
60
+
61
+ return cls
62
+
63
+
64
+ class ScimObject(BaseModel, metaclass=ScimMetaclass):
65
+ __schema__: ClassVar[URN | None] = None
66
+
16
67
  schemas: Annotated[list[str], Required.true]
17
68
  """The "schemas" attribute is a REQUIRED attribute and is an array of
18
69
  Strings containing URIs that are used to indicate the namespaces of the
19
70
  SCIM schemas that define the attributes present in the current JSON
20
71
  structure."""
21
72
 
73
+ @model_validator(mode="before")
74
+ @classmethod
75
+ def _populate_schemas_default(cls, data: Any) -> Any:
76
+ """Auto-generate schemas from __schema__ if not provided."""
77
+ if isinstance(data, dict) and "schemas" not in data:
78
+ schema = getattr(cls, "__schema__", None)
79
+ if schema:
80
+ data = {**data, "schemas": [schema]}
81
+ return data
82
+
83
+ @model_validator(mode="wrap")
84
+ @classmethod
85
+ def _validate_schemas_attribute(
86
+ cls, value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
87
+ ) -> Self:
88
+ """Validate that the base schema is present in schemas attribute."""
89
+ obj: Self = handler(value)
90
+
91
+ scim_ctx = info.context.get("scim") if info.context else None
92
+ if scim_ctx is None or scim_ctx == Context.DEFAULT:
93
+ return obj
94
+
95
+ schema = getattr(cls, "__schema__", None)
96
+ if schema and schema not in obj.schemas:
97
+ raise PydanticCustomError(
98
+ "schema_error",
99
+ "schemas must contain the base schema '{schema}'",
100
+ {"schema": schema},
101
+ )
102
+
103
+ return obj
104
+
22
105
  def _prepare_model_dump(
23
106
  self,
24
107
  scim_ctx: Context | None = Context.DEFAULT,
scim2_models/utils.py CHANGED
@@ -1,14 +1,8 @@
1
- import base64
2
1
  import re
3
2
  from typing import TYPE_CHECKING
4
- from typing import Annotated
5
- from typing import Literal
6
3
  from typing import Union
7
4
 
8
- from pydantic import EncodedBytes
9
- from pydantic import EncoderProtocol
10
5
  from pydantic.alias_generators import to_snake
11
- from pydantic_core import PydanticCustomError
12
6
 
13
7
  if TYPE_CHECKING:
14
8
  from .base import BaseModel
@@ -23,64 +17,12 @@ except ImportError:
23
17
 
24
18
  _UNDERSCORE_ALPHANUMERIC = re.compile(r"_+([0-9A-Za-z]+)")
25
19
  _NON_WORD_UNDERSCORE = re.compile(r"[\W_]+")
26
- _VALID_PATH_PATTERN = re.compile(r'^[a-zA-Z][a-zA-Z0-9._:\-\[\]"=\s]*$')
27
20
 
28
21
 
29
22
  def _int_to_str(status: int | None) -> str | None:
30
23
  return None if status is None else str(status)
31
24
 
32
25
 
33
- # Copied from Pydantic 2.10 repository
34
- class _Base64Encoder(EncoderProtocol): # pragma: no cover
35
- """Standard (non-URL-safe) Base64 encoder."""
36
-
37
- @classmethod
38
- def decode(cls, data: bytes) -> bytes:
39
- """Decode the data from base64 encoded bytes to original bytes data.
40
-
41
- Args:
42
- data: The data to decode.
43
-
44
- Returns:
45
- The decoded data.
46
-
47
- """
48
- try:
49
- return base64.b64decode(data)
50
- except ValueError as e:
51
- raise PydanticCustomError(
52
- "base64_decode", "Base64 decoding error: '{error}'", {"error": str(e)}
53
- ) from e
54
-
55
- @classmethod
56
- def encode(cls, value: bytes) -> bytes:
57
- """Encode the data from bytes to a base64 encoded bytes.
58
-
59
- Args:
60
- value: The data to encode.
61
-
62
- Returns:
63
- The encoded data.
64
-
65
- """
66
- return base64.b64encode(value)
67
-
68
- @classmethod
69
- def get_json_format(cls) -> Literal["base64"]:
70
- """Get the JSON format for the encoded data.
71
-
72
- Returns:
73
- The JSON format for the encoded data.
74
-
75
- """
76
- return "base64"
77
-
78
-
79
- # Compatibility with Pydantic <2.10
80
- # https://pydantic.dev/articles/pydantic-v2-10-release#use-b64decode-and-b64encode-for-base64bytes-and-base64str-types
81
- Base64Bytes = Annotated[bytes, EncodedBytes(encoder=_Base64Encoder)]
82
-
83
-
84
26
  def _to_camel(string: str) -> str:
85
27
  """Transform strings to camelCase.
86
28
 
@@ -106,84 +48,6 @@ def _normalize_attribute_name(attribute_name: str) -> str:
106
48
  return attribute_name.lower()
107
49
 
108
50
 
109
- def _validate_scim_path_syntax(path: str) -> bool:
110
- """Check if path syntax is valid according to RFC 7644 simplified rules.
111
-
112
- :param path: The path to validate
113
- :return: True if path syntax is valid, False otherwise
114
- """
115
- if not path or not path.strip():
116
- return False
117
-
118
- # Cannot start with a digit
119
- if path[0].isdigit():
120
- return False
121
-
122
- # Cannot contain double dots
123
- if ".." in path:
124
- return False
125
-
126
- # Cannot contain invalid characters (basic check)
127
- # Allow alphanumeric, dots, underscores, hyphens, colons (for URNs), brackets
128
- if not _VALID_PATH_PATTERN.match(path):
129
- return False
130
-
131
- # If it contains a colon, validate it's a proper URN format
132
- if ":" in path:
133
- if not _validate_scim_urn_syntax(path):
134
- return False
135
-
136
- return True
137
-
138
-
139
- def _validate_scim_urn_syntax(path: str) -> bool:
140
- """Validate URN-based path format.
141
-
142
- :param path: The URN path to validate
143
- :return: True if URN path format is valid, False otherwise
144
- """
145
- # Basic URN validation: should start with urn:
146
- if not path.startswith("urn:"):
147
- return False
148
-
149
- # Split on the last colon to separate URN from attribute
150
- urn_part, attr_part = path.rsplit(":", 1)
151
-
152
- # URN part should have at least 4 parts (urn:namespace:specific:resource)
153
- urn_segments = urn_part.split(":")
154
- if len(urn_segments) < 4:
155
- return False
156
-
157
- # Attribute part should be valid
158
- if not attr_part or attr_part[0].isdigit():
159
- return False
160
-
161
- return True
162
-
163
-
164
- def _extract_field_name(path: str) -> str | None:
165
- """Extract the field name from a path.
166
-
167
- For now, only handle simple paths (no filters, no complex expressions).
168
- Returns None for complex paths that require filter parsing.
169
-
170
- """
171
- # Handle URN paths
172
- if path.startswith("urn:"):
173
- # First validate it's a proper URN
174
- if not _validate_scim_urn_syntax(path):
175
- return None
176
- parts = path.rsplit(":", 1)
177
- return parts[1]
178
-
179
- # Simple attribute path (may have dots for sub-attributes)
180
- # For now, just take the first part before any dot
181
- if "." in path:
182
- return path.split(".")[0]
183
-
184
- return path
185
-
186
-
187
51
  def _find_field_name(model_class: type["BaseModel"], attr_name: str) -> str | None:
188
52
  """Find the actual field name in a resource class from an attribute name.
189
53
 
@@ -198,7 +62,3 @@ def _find_field_name(model_class: type["BaseModel"], attr_name: str) -> str | No
198
62
  return field_key
199
63
 
200
64
  return None
201
-
202
-
203
- def _get_path_parts(path: str) -> list[str]:
204
- return path.split(".")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: scim2-models
3
- Version: 0.5.2
3
+ Version: 0.6.1
4
4
  Summary: SCIM2 models serialization and validation with pydantic
5
5
  Keywords: scim,scim2,provisioning,pydantic,rfc7643,rfc7644
6
6
  Author: Yaal Coop
@@ -218,7 +218,7 @@ Classifier: License :: OSI Approved :: Apache Software License
218
218
  Classifier: Environment :: Web Environment
219
219
  Classifier: Programming Language :: Python
220
220
  Classifier: Operating System :: OS Independent
221
- Requires-Dist: pydantic[email]>=2.7.0
221
+ Requires-Dist: pydantic[email]>=2.12.0
222
222
  Requires-Python: >=3.10
223
223
  Project-URL: changelog, https://scim2-models.readthedocs.io/en/latest/changelog.html
224
224
  Project-URL: documentation, https://scim2-models.readthedocs.io
@@ -0,0 +1,30 @@
1
+ scim2_models/__init__.py,sha256=mEaFa7STotG40wV9cy9aw00Klcx7mqpwJXFhir1BKmI,4213
2
+ scim2_models/annotations.py,sha256=oRjlKL1fqrYfa9UtaMdxF5fOT8CUUN3m-rdzvf7aiSA,3304
3
+ scim2_models/attributes.py,sha256=v_bjJSdBmPlGh20W2Y57PIZnqvMTYHQyJuJLfQjGmwc,1874
4
+ scim2_models/base.py,sha256=dB0idU8apEwPwJpax1EWYX-omxcIy9Z4DMjNhrwTJAg,22407
5
+ scim2_models/constants.py,sha256=9egq8JW0dFAqPng85CiHoH5T6pRtYL87-gC0C-IMGsk,573
6
+ scim2_models/context.py,sha256=RjgMIvWPr8f41qbVL1sjaDnm9GRKyrCrgfC4npwwcMg,9149
7
+ scim2_models/exceptions.py,sha256=0rvJTRM3CbaB2-T_PAhii5W-YEvTY31scPdrauGJi0M,7655
8
+ scim2_models/messages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ scim2_models/messages/bulk.py,sha256=fWm9s88FHa66Aa0XeLcN7HurHp98JhlqM1rkEnIKZ8A,2429
10
+ scim2_models/messages/error.py,sha256=gbHxf47ZIamnZAgD7cNAwHXxEZsf36PTdtcejesYIy0,12138
11
+ scim2_models/messages/list_response.py,sha256=ahAqdElGSNxt8LlOUeSsNbOykZdCLM4aJyirKcaOtoM,2260
12
+ scim2_models/messages/message.py,sha256=7svNN_xi8BSyWmRoMdYFTjQGuV1F9HUpQYHb6TQtf8A,4081
13
+ scim2_models/messages/patch_op.py,sha256=QwAkeHvzzUXFmS8Ut_ns99yhiSQehpuHAodI_eSc_hA,16236
14
+ scim2_models/messages/search_request.py,sha256=OXw4-oehLC5NyD7yVFEq0QgPsxAaQwXE7hZMKiNkvzY,3017
15
+ scim2_models/path.py,sha256=4u-Lqtq8lqSmJUi6thFDwhpzc-uzyVFulsTklIyIFL8,27053
16
+ scim2_models/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ scim2_models/reference.py,sha256=-E6VD-lFPEIXpSpwu0G_3MKLbWKgZhwk8ER3-JhCHig,5636
18
+ scim2_models/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ scim2_models/resources/enterprise_user.py,sha256=Wk8CzQrCXUE-roQYINpj4YhOlj2s_TD3Y3SnEXK6jKY,1798
20
+ scim2_models/resources/group.py,sha256=nmmPlbP7gQluNJEtFIFYtjDYJ2AR4Uml5LuxMcY_NIk,1379
21
+ scim2_models/resources/resource.py,sha256=0Y8xloQk6tWQ9L7gW__qlUkU55mpYAsxfpeuFuSX2Vk,20550
22
+ scim2_models/resources/resource_type.py,sha256=IDzUcv9_XiAHOpaaG9j24sa-nNKY6i0wPDTTrxNqJgQ,3297
23
+ scim2_models/resources/schema.py,sha256=wDh8G1oxCgyQcmChz30bVNPjyksYcgxD66k0mB1VIdI,10239
24
+ scim2_models/resources/service_provider_config.py,sha256=cmfPtLPOuwNrZl9hGpbrMsQhoOwIytlf2Q_LAJegAcQ,5303
25
+ scim2_models/resources/user.py,sha256=ZvyjxZiF52d_L_LUCEZpec6CXOe40_udZhOHpjzFpxs,11409
26
+ scim2_models/scim_object.py,sha256=suYhQ0iFQs1aKTlSerWZsi_sAVhFX_9N24U_DmZxCbw,5321
27
+ scim2_models/utils.py,sha256=Lb7mlP3I_IfAlqi_8_m4G-_1Rn7oKRqbNdpoDLjXXr0,1978
28
+ scim2_models-0.6.1.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
29
+ scim2_models-0.6.1.dist-info/METADATA,sha256=4_foQD9Bpu-ghYMS923_giVn1VC-e5UUaIZsgNO1nTY,16486
30
+ scim2_models-0.6.1.dist-info/RECORD,,
scim2_models/urn.py DELETED
@@ -1,126 +0,0 @@
1
- from typing import TYPE_CHECKING
2
- from typing import Any
3
- from typing import Union
4
-
5
- from .base import BaseModel
6
- from .utils import _get_path_parts
7
- from .utils import _normalize_attribute_name
8
-
9
- if TYPE_CHECKING:
10
- from .base import BaseModel
11
- from .resources.resource import Extension
12
- from .resources.resource import Resource
13
-
14
-
15
- def _get_or_create_extension_instance(
16
- model: "Resource[Any]", extension_class: type
17
- ) -> "Extension":
18
- """Get existing extension instance or create a new one."""
19
- extension_instance = model[extension_class]
20
- if extension_instance is None:
21
- extension_instance = extension_class()
22
- model[extension_class] = extension_instance
23
- return extension_instance
24
-
25
-
26
- def _normalize_path(model: type["BaseModel"] | None, path: str) -> tuple[str, str]:
27
- """Resolve a path to (schema_urn, attribute_path)."""
28
- from .resources.resource import Resource
29
-
30
- # Absolute URN
31
- if ":" in path:
32
- if (
33
- model
34
- and issubclass(model, Resource)
35
- and (
36
- path in model.get_extension_models()
37
- or path == model.model_fields["schemas"].default[0]
38
- )
39
- ):
40
- return path, ""
41
-
42
- parts = path.rsplit(":", 1)
43
- return parts[0], parts[1]
44
-
45
- # Relative URN with a schema
46
- elif model and issubclass(model, Resource) and hasattr(model, "model_fields"):
47
- schemas_field = model.model_fields.get("schemas")
48
- return schemas_field.default[0], path # type: ignore
49
-
50
- return "", path
51
-
52
-
53
- def _validate_model_attribute(model: type["BaseModel"], attribute_base: str) -> None:
54
- """Validate that an attribute name or a sub-attribute path exist for a given model."""
55
- attribute_name, *sub_attribute_blocks = _get_path_parts(attribute_base)
56
- sub_attribute_base = ".".join(sub_attribute_blocks)
57
-
58
- aliases = {field.validation_alias for field in model.model_fields.values()}
59
-
60
- if _normalize_attribute_name(attribute_name) not in aliases:
61
- raise ValueError(
62
- f"Model '{model.__name__}' has no attribute named '{attribute_name}'"
63
- )
64
-
65
- if sub_attribute_base:
66
- attribute_type = model.get_field_root_type(attribute_name)
67
-
68
- if not attribute_type or not issubclass(attribute_type, BaseModel):
69
- raise ValueError(
70
- f"Attribute '{attribute_name}' is not a complex attribute, and cannot have a '{sub_attribute_base}' sub-attribute"
71
- )
72
-
73
- _validate_model_attribute(attribute_type, sub_attribute_base)
74
-
75
-
76
- def _validate_attribute_urn(
77
- attribute_name: str, resource: type["Resource[Any]"]
78
- ) -> str | None:
79
- """Validate that an attribute urn is valid or not.
80
-
81
- :param attribute_name: The attribute urn to check.
82
- :return: The normalized attribute URN.
83
- """
84
- from .resources.resource import Resource
85
-
86
- schema: Any | None
87
- schema, attribute_base = _normalize_path(resource, attribute_name)
88
-
89
- validated_resource = Resource.get_by_schema([resource], schema)
90
- if not validated_resource:
91
- return None
92
-
93
- try:
94
- _validate_model_attribute(validated_resource, attribute_base)
95
- except ValueError:
96
- return None
97
-
98
- return f"{schema}:{attribute_base}"
99
-
100
-
101
- def _resolve_path_to_target(
102
- resource: "Resource[Any]", path: str
103
- ) -> tuple[Union["Resource[Any]", "Extension"] | None, str]:
104
- """Resolve a path to a target and an attribute_path.
105
-
106
- The target can be the resource itself, or an extension object.
107
- """
108
- schema_urn, attr_path = _normalize_path(type(resource), path)
109
-
110
- if not schema_urn:
111
- return resource, attr_path
112
-
113
- if extension_class := resource.get_extension_model(schema_urn):
114
- # Points to the extension root
115
- if not attr_path:
116
- return resource, extension_class.__name__
117
-
118
- extension_instance = _get_or_create_extension_instance(
119
- resource, extension_class
120
- )
121
- return extension_instance, attr_path
122
-
123
- if schema_urn in resource.schemas:
124
- return resource, attr_path
125
-
126
- return (None, "")