scim2-models 0.3.7__py3-none-any.whl → 0.4.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.
@@ -28,9 +28,9 @@ from ..base import BaseModel
28
28
  from ..context import Context
29
29
  from ..reference import Reference
30
30
  from ..scim_object import ScimObject
31
- from ..scim_object import validate_attribute_urn
31
+ from ..urn import _validate_attribute_urn
32
32
  from ..utils import UNION_TYPES
33
- from ..utils import normalize_attribute_name
33
+ from ..utils import _normalize_attribute_name
34
34
 
35
35
  if TYPE_CHECKING:
36
36
  from .schema import Attribute
@@ -92,14 +92,14 @@ class Extension(ScimObject):
92
92
  @classmethod
93
93
  def to_schema(cls) -> "Schema":
94
94
  """Build a :class:`~scim2_models.Schema` from the current extension class."""
95
- return model_to_schema(cls)
95
+ return _model_to_schema(cls)
96
96
 
97
97
  @classmethod
98
98
  def from_schema(cls, schema: "Schema") -> type["Extension"]:
99
99
  """Build a :class:`~scim2_models.Extension` subclass from the schema definition."""
100
- from .schema import make_python_model
100
+ from .schema import _make_python_model
101
101
 
102
- return make_python_model(schema, cls)
102
+ return _make_python_model(schema, cls)
103
103
 
104
104
 
105
105
  AnyExtension = TypeVar("AnyExtension", bound="Extension")
@@ -107,7 +107,7 @@ AnyExtension = TypeVar("AnyExtension", bound="Extension")
107
107
  _PARAMETERIZED_CLASSES: dict[tuple[type, tuple[Any, ...]], type] = {}
108
108
 
109
109
 
110
- def extension_serializer(
110
+ def _extension_serializer(
111
111
  value: Any, handler: SerializerFunctionWrapHandler, info: SerializationInfo
112
112
  ) -> Optional[dict[str, Any]]:
113
113
  """Exclude the Resource attributes from the extension dump.
@@ -184,13 +184,13 @@ class Resource(ScimObject, Generic[AnyExtension]):
184
184
  class_attrs[extension.__name__] = Field(
185
185
  default=None, # type: ignore[arg-type]
186
186
  serialization_alias=schema,
187
- validation_alias=normalize_attribute_name(schema),
187
+ validation_alias=_normalize_attribute_name(schema),
188
188
  )
189
189
 
190
190
  new_annotations = {
191
191
  extension.__name__: Annotated[
192
192
  Optional[extension],
193
- WrapSerializer(extension_serializer),
193
+ WrapSerializer(_extension_serializer),
194
194
  ]
195
195
  for extension in valid_extensions
196
196
  }
@@ -288,14 +288,14 @@ class Resource(ScimObject, Generic[AnyExtension]):
288
288
  @classmethod
289
289
  def to_schema(cls) -> "Schema":
290
290
  """Build a :class:`~scim2_models.Schema` from the current resource class."""
291
- return model_to_schema(cls)
291
+ return _model_to_schema(cls)
292
292
 
293
293
  @classmethod
294
294
  def from_schema(cls, schema: "Schema") -> type["Resource"]:
295
295
  """Build a :class:`scim2_models.Resource` subclass from the schema definition."""
296
- from .schema import make_python_model
296
+ from .schema import _make_python_model
297
297
 
298
- return make_python_model(schema, cls)
298
+ return _make_python_model(schema, cls)
299
299
 
300
300
  def _prepare_model_dump(
301
301
  self,
@@ -305,13 +305,19 @@ class Resource(ScimObject, Generic[AnyExtension]):
305
305
  **kwargs: Any,
306
306
  ) -> dict[str, Any]:
307
307
  kwargs = super()._prepare_model_dump(scim_ctx, **kwargs)
308
+
309
+ # RFC 7644: "SHOULD ignore any query parameters they do not recognize"
308
310
  kwargs["context"]["scim_attributes"] = [
309
- validate_attribute_urn(attribute, self.__class__)
311
+ valid_attr
310
312
  for attribute in (attributes or [])
313
+ if (valid_attr := _validate_attribute_urn(attribute, self.__class__))
314
+ is not None
311
315
  ]
312
316
  kwargs["context"]["scim_excluded_attributes"] = [
313
- validate_attribute_urn(attribute, self.__class__)
317
+ valid_attr
314
318
  for attribute in (excluded_attributes or [])
319
+ if (valid_attr := _validate_attribute_urn(attribute, self.__class__))
320
+ is not None
315
321
  ]
316
322
  return kwargs
317
323
 
@@ -330,9 +336,9 @@ class Resource(ScimObject, Generic[AnyExtension]):
330
336
  messages. Pass :data:`None` to get the default Pydantic behavior.
331
337
  :param attributes: A multi-valued list of strings indicating the names of resource
332
338
  attributes to return in the response, overriding the set of attributes that
333
- would be returned by default.
339
+ would be returned by default. Invalid values are ignored.
334
340
  :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.
341
+ attributes to be removed from the default set of attributes to return. Invalid values are ignored.
336
342
  """
337
343
  dump_kwargs = self._prepare_model_dump(
338
344
  scim_ctx, attributes, excluded_attributes, **kwargs
@@ -356,9 +362,9 @@ class Resource(ScimObject, Generic[AnyExtension]):
356
362
  messages. Pass :data:`None` to get the default Pydantic behavior.
357
363
  :param attributes: A multi-valued list of strings indicating the names of resource
358
364
  attributes to return in the response, overriding the set of attributes that
359
- would be returned by default.
365
+ would be returned by default. Invalid values are ignored.
360
366
  :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.
367
+ attributes to be removed from the default set of attributes to return. Invalid values are ignored.
362
368
  """
363
369
  dump_kwargs = self._prepare_model_dump(
364
370
  scim_ctx, attributes, excluded_attributes, **kwargs
@@ -369,7 +375,7 @@ class Resource(ScimObject, Generic[AnyExtension]):
369
375
  AnyResource = TypeVar("AnyResource", bound="Resource")
370
376
 
371
377
 
372
- def dedicated_attributes(
378
+ def _dedicated_attributes(
373
379
  model: type[BaseModel], excluded_models: list[type[BaseModel]]
374
380
  ) -> dict[str, Any]:
375
381
  """Return attributes that are not members the parent 'excluded_models'."""
@@ -397,13 +403,13 @@ def dedicated_attributes(
397
403
  return field_infos
398
404
 
399
405
 
400
- def model_to_schema(model: type[BaseModel]) -> "Schema":
401
- from scim2_models.rfc7643.schema import Schema
406
+ def _model_to_schema(model: type[BaseModel]) -> "Schema":
407
+ from scim2_models.resources.schema import Schema
402
408
 
403
409
  schema_urn = model.model_fields["schemas"].default[0]
404
- field_infos = dedicated_attributes(model, [Resource])
410
+ field_infos = _dedicated_attributes(model, [Resource])
405
411
  attributes = [
406
- model_attribute_to_scim_attribute(model, attribute_name)
412
+ _model_attribute_to_scim_attribute(model, attribute_name)
407
413
  for attribute_name in field_infos
408
414
  if attribute_name != "schemas"
409
415
  ]
@@ -416,10 +422,10 @@ def model_to_schema(model: type[BaseModel]) -> "Schema":
416
422
  return schema
417
423
 
418
424
 
419
- def model_attribute_to_scim_attribute(
425
+ def _model_attribute_to_scim_attribute(
420
426
  model: type[BaseModel], attribute_name: str
421
427
  ) -> "Attribute":
422
- from scim2_models.rfc7643.schema import Attribute
428
+ from scim2_models.resources.schema import Attribute
423
429
 
424
430
  field_info = model.model_fields[attribute_name]
425
431
  root_type = model.get_field_root_type(attribute_name)
@@ -430,8 +436,8 @@ def model_attribute_to_scim_attribute(
430
436
  attribute_type = Attribute.Type.from_python(root_type)
431
437
  sub_attributes = (
432
438
  [
433
- model_attribute_to_scim_attribute(root_type, sub_attribute_name)
434
- for sub_attribute_name in dedicated_attributes(
439
+ _model_attribute_to_scim_attribute(root_type, sub_attribute_name)
440
+ for sub_attribute_name in _dedicated_attributes(
435
441
  root_type,
436
442
  [MultiValuedComplexAttribute],
437
443
  )
@@ -31,13 +31,13 @@ from ..reference import ExternalReference
31
31
  from ..reference import Reference
32
32
  from ..reference import URIReference
33
33
  from ..utils import Base64Bytes
34
- from ..utils import normalize_attribute_name
34
+ from ..utils import _normalize_attribute_name
35
35
  from .resource import Resource
36
36
 
37
37
  T = TypeVar("T", bound=BaseModel)
38
38
 
39
39
 
40
- def make_python_identifier(identifier: str) -> str:
40
+ def _make_python_identifier(identifier: str) -> str:
41
41
  """Sanitize string to be a suitable Python/Pydantic class attribute name."""
42
42
  sanitized = re.sub(r"\W|^(?=\d)", "", identifier)
43
43
  if sanitized in RESERVED_WORDS:
@@ -46,7 +46,7 @@ def make_python_identifier(identifier: str) -> str:
46
46
  return sanitized
47
47
 
48
48
 
49
- def make_python_model(
49
+ def _make_python_model(
50
50
  obj: Union["Schema", "Attribute"],
51
51
  base: type[T],
52
52
  multiple: bool = False,
@@ -54,14 +54,14 @@ def make_python_model(
54
54
  """Build a Python model from a Schema or an Attribute object."""
55
55
  if isinstance(obj, Attribute):
56
56
  pydantic_attributes = {
57
- to_snake(make_python_identifier(attr.name)): attr.to_python()
57
+ to_snake(_make_python_identifier(attr.name)): attr._to_python()
58
58
  for attr in (obj.sub_attributes or [])
59
59
  if attr.name
60
60
  }
61
61
 
62
62
  else:
63
63
  pydantic_attributes = {
64
- to_snake(make_python_identifier(attr.name)): attr.to_python()
64
+ to_snake(_make_python_identifier(attr.name)): attr._to_python()
65
65
  for attr in (obj.attributes or [])
66
66
  if attr.name
67
67
  }
@@ -97,7 +97,7 @@ class Attribute(ComplexAttribute):
97
97
  reference = "reference"
98
98
  binary = "binary"
99
99
 
100
- def to_python(
100
+ def _to_python(
101
101
  self,
102
102
  multiple: bool = False,
103
103
  reference_types: Optional[list[str]] = None,
@@ -210,15 +210,15 @@ class Attribute(ComplexAttribute):
210
210
  """When an attribute is of type "complex", "subAttributes" defines a set of
211
211
  sub-attributes."""
212
212
 
213
- def to_python(self) -> Optional[tuple[Any, Any]]:
213
+ def _to_python(self) -> Optional[tuple[Any, Any]]:
214
214
  """Build tuple suited to be passed to pydantic 'create_model'."""
215
215
  if not self.name or not self.type:
216
216
  return None
217
217
 
218
- attr_type = self.type.to_python(bool(self.multi_valued), self.reference_types)
218
+ attr_type = self.type._to_python(bool(self.multi_valued), self.reference_types)
219
219
 
220
220
  if attr_type in (ComplexAttribute, MultiValuedComplexAttribute):
221
- attr_type = make_python_model(
221
+ attr_type = _make_python_model(
222
222
  obj=self, base=attr_type, multiple=bool(self.multi_valued)
223
223
  )
224
224
 
@@ -238,7 +238,7 @@ class Attribute(ComplexAttribute):
238
238
  description=self.description,
239
239
  examples=self.canonical_values,
240
240
  serialization_alias=self.name,
241
- validation_alias=normalize_attribute_name(self.name),
241
+ validation_alias=_normalize_attribute_name(self.name),
242
242
  default=None,
243
243
  )
244
244
 
@@ -8,86 +8,9 @@ from typing import Optional
8
8
  from .annotations import Required
9
9
  from .base import BaseModel
10
10
  from .context import Context
11
- from .utils import normalize_attribute_name
12
11
 
13
12
  if TYPE_CHECKING:
14
- from .rfc7643.resource import Resource
15
-
16
-
17
- def validate_model_attribute(model: type["BaseModel"], attribute_base: str) -> None:
18
- """Validate that an attribute name or a sub-attribute path exist for a given model."""
19
- attribute_name, *sub_attribute_blocks = attribute_base.split(".")
20
- sub_attribute_base = ".".join(sub_attribute_blocks)
21
-
22
- aliases = {field.validation_alias for field in model.model_fields.values()}
23
-
24
- if normalize_attribute_name(attribute_name) not in aliases:
25
- raise ValueError(
26
- f"Model '{model.__name__}' has no attribute named '{attribute_name}'"
27
- )
28
-
29
- if sub_attribute_base:
30
- attribute_type = model.get_field_root_type(attribute_name)
31
-
32
- if not attribute_type or not issubclass(attribute_type, BaseModel):
33
- raise ValueError(
34
- f"Attribute '{attribute_name}' is not a complex attribute, and cannot have a '{sub_attribute_base}' sub-attribute"
35
- )
36
-
37
- validate_model_attribute(attribute_type, sub_attribute_base)
38
-
39
-
40
- def extract_schema_and_attribute_base(attribute_urn: str) -> tuple[str, str]:
41
- """Extract the schema urn part and the attribute name part from attribute name.
42
-
43
- As defined in :rfc:`RFC7644 §3.10 <7644#section-3.10>`.
44
- """
45
- *urn_blocks, attribute_base = attribute_urn.split(":")
46
- schema = ":".join(urn_blocks)
47
- return schema, attribute_base
48
-
49
-
50
- def validate_attribute_urn(
51
- attribute_name: str,
52
- default_resource: Optional[type["Resource"]] = None,
53
- resource_types: Optional[list[type["Resource"]]] = None,
54
- ) -> str:
55
- """Validate that an attribute urn is valid or not.
56
-
57
- :param attribute_name: The attribute urn to check.
58
- :default_resource: The default resource if `attribute_name` is not an absolute urn.
59
- :resource_types: The available resources in which to look for the attribute.
60
- :return: The normalized attribute URN.
61
- """
62
- from .rfc7643.resource import Resource
63
-
64
- if not resource_types:
65
- resource_types = []
66
-
67
- if default_resource and default_resource not in resource_types:
68
- resource_types.append(default_resource)
69
-
70
- default_schema = (
71
- default_resource.model_fields["schemas"].default[0]
72
- if default_resource
73
- else None
74
- )
75
-
76
- schema: Optional[Any]
77
- schema, attribute_base = extract_schema_and_attribute_base(attribute_name)
78
- if not schema:
79
- schema = default_schema
80
-
81
- if not schema:
82
- raise ValueError("No default schema and relative URN")
83
-
84
- resource = Resource.get_by_schema(resource_types, schema)
85
- if not resource:
86
- raise ValueError(f"No resource matching schema '{schema}'")
87
-
88
- validate_model_attribute(resource, attribute_base)
89
-
90
- return f"{schema}:{attribute_base}"
13
+ pass
91
14
 
92
15
 
93
16
  class ScimObject(BaseModel):
scim2_models/urn.py ADDED
@@ -0,0 +1,109 @@
1
+ from typing import TYPE_CHECKING
2
+ from typing import Any
3
+ from typing import Optional
4
+
5
+ from .base import BaseModel
6
+ from .utils import _normalize_attribute_name
7
+
8
+ if TYPE_CHECKING:
9
+ from .base import BaseModel
10
+ from .resources.resource import Resource
11
+
12
+
13
+ def _get_or_create_extension_instance(
14
+ model: "Resource", extension_class: type
15
+ ) -> "BaseModel":
16
+ """Get existing extension instance or create a new one."""
17
+ extension_instance = model[extension_class]
18
+ if extension_instance is None:
19
+ extension_instance = extension_class()
20
+ model[extension_class] = extension_instance
21
+ return extension_instance
22
+
23
+
24
+ def _normalize_path(model: Optional[type["BaseModel"]], path: str) -> tuple[str, str]:
25
+ """Resolve a path to (schema_urn, attribute_path)."""
26
+ from .resources.resource import Resource
27
+
28
+ # Absolute URN
29
+ if ":" in path:
30
+ parts = path.rsplit(":", 1)
31
+ return parts[0], parts[1]
32
+
33
+ # Relative URN with a schema
34
+ elif model and issubclass(model, Resource) and hasattr(model, "model_fields"):
35
+ schemas_field = model.model_fields.get("schemas")
36
+ return schemas_field.default[0], path # type: ignore
37
+
38
+ return "", path
39
+
40
+
41
+ def _validate_model_attribute(model: type["BaseModel"], attribute_base: str) -> None:
42
+ """Validate that an attribute name or a sub-attribute path exist for a given model."""
43
+ attribute_name, *sub_attribute_blocks = attribute_base.split(".")
44
+ sub_attribute_base = ".".join(sub_attribute_blocks)
45
+
46
+ aliases = {field.validation_alias for field in model.model_fields.values()}
47
+
48
+ if _normalize_attribute_name(attribute_name) not in aliases:
49
+ raise ValueError(
50
+ f"Model '{model.__name__}' has no attribute named '{attribute_name}'"
51
+ )
52
+
53
+ if sub_attribute_base:
54
+ attribute_type = model.get_field_root_type(attribute_name)
55
+
56
+ if not attribute_type or not issubclass(attribute_type, BaseModel):
57
+ raise ValueError(
58
+ f"Attribute '{attribute_name}' is not a complex attribute, and cannot have a '{sub_attribute_base}' sub-attribute"
59
+ )
60
+
61
+ _validate_model_attribute(attribute_type, sub_attribute_base)
62
+
63
+
64
+ def _validate_attribute_urn(
65
+ attribute_name: str, resource: type["Resource"]
66
+ ) -> Optional[str]:
67
+ """Validate that an attribute urn is valid or not.
68
+
69
+ :param attribute_name: The attribute urn to check.
70
+ :return: The normalized attribute URN.
71
+ """
72
+ from .resources.resource import Resource
73
+
74
+ schema: Optional[Any]
75
+ schema, attribute_base = _normalize_path(resource, attribute_name)
76
+
77
+ validated_resource = Resource.get_by_schema([resource], schema)
78
+ if not validated_resource:
79
+ return None
80
+
81
+ try:
82
+ _validate_model_attribute(validated_resource, attribute_base)
83
+ except ValueError:
84
+ return None
85
+
86
+ return f"{schema}:{attribute_base}"
87
+
88
+
89
+ def _resolve_path_to_target(
90
+ resource: "Resource", path: str
91
+ ) -> tuple[Optional["BaseModel"], str]:
92
+ """Resolve a path to a target and an attribute_path.
93
+
94
+ The target can be the resource itself, or an extension object.
95
+ """
96
+ schema_urn, attr_path = _normalize_path(type(resource), path)
97
+
98
+ if not schema_urn:
99
+ return resource, attr_path
100
+
101
+ if schema_urn in resource.schemas:
102
+ return resource, attr_path
103
+
104
+ extension_class = resource.get_extension_model(schema_urn)
105
+ if not extension_class:
106
+ return (None, "")
107
+
108
+ extension_instance = _get_or_create_extension_instance(resource, extension_class)
109
+ return extension_instance, attr_path
scim2_models/utils.py CHANGED
@@ -19,12 +19,12 @@ except ImportError:
19
19
  UNION_TYPES = [Union]
20
20
 
21
21
 
22
- def int_to_str(status: Optional[int]) -> Optional[str]:
22
+ def _int_to_str(status: Optional[int]) -> Optional[str]:
23
23
  return None if status is None else str(status)
24
24
 
25
25
 
26
26
  # Copied from Pydantic 2.10 repository
27
- class Base64Encoder(EncoderProtocol): # pragma: no cover
27
+ class _Base64Encoder(EncoderProtocol): # pragma: no cover
28
28
  """Standard (non-URL-safe) Base64 encoder."""
29
29
 
30
30
  @classmethod
@@ -71,10 +71,10 @@ class Base64Encoder(EncoderProtocol): # pragma: no cover
71
71
 
72
72
  # Compatibility with Pydantic <2.10
73
73
  # https://pydantic.dev/articles/pydantic-v2-10-release#use-b64decode-and-b64encode-for-base64bytes-and-base64str-types
74
- Base64Bytes = Annotated[bytes, EncodedBytes(encoder=Base64Encoder)]
74
+ Base64Bytes = Annotated[bytes, EncodedBytes(encoder=_Base64Encoder)]
75
75
 
76
76
 
77
- def to_camel(string: str) -> str:
77
+ def _to_camel(string: str) -> str:
78
78
  """Transform strings to camelCase.
79
79
 
80
80
  This method is used for attribute name serialization. This is more
@@ -87,7 +87,7 @@ def to_camel(string: str) -> str:
87
87
  return camel
88
88
 
89
89
 
90
- def normalize_attribute_name(attribute_name: str) -> str:
90
+ def _normalize_attribute_name(attribute_name: str) -> str:
91
91
  """Remove all non-alphabetical characters and lowerise a string.
92
92
 
93
93
  This method is used for attribute name validation.
@@ -97,3 +97,105 @@ def normalize_attribute_name(attribute_name: str) -> str:
97
97
  attribute_name = re.sub(r"[\W_]+", "", attribute_name)
98
98
 
99
99
  return attribute_name.lower()
100
+
101
+
102
+ def _validate_scim_path_syntax(path: str) -> bool:
103
+ """Check if path syntax is valid according to RFC 7644 simplified rules.
104
+
105
+ :param path: The path to validate
106
+ :type path: str
107
+ :return: True if path syntax is valid, False otherwise
108
+ :rtype: bool
109
+ """
110
+ if not path or not path.strip():
111
+ return False
112
+
113
+ # Cannot start with a digit
114
+ if path[0].isdigit():
115
+ return False
116
+
117
+ # Cannot contain double dots
118
+ if ".." in path:
119
+ return False
120
+
121
+ # Cannot contain invalid characters (basic check)
122
+ # Allow alphanumeric, dots, underscores, hyphens, colons (for URNs), brackets
123
+ if not re.match(r'^[a-zA-Z][a-zA-Z0-9._:\-\[\]"=\s]*$', path):
124
+ return False
125
+
126
+ # If it contains a colon, validate it's a proper URN format
127
+ if ":" in path:
128
+ if not _validate_scim_urn_syntax(path):
129
+ return False
130
+
131
+ return True
132
+
133
+
134
+ def _validate_scim_urn_syntax(path: str) -> bool:
135
+ """Validate URN-based path format.
136
+
137
+ :param path: The URN path to validate
138
+ :type path: str
139
+ :return: True if URN path format is valid, False otherwise
140
+ :rtype: bool
141
+ """
142
+ # Basic URN validation: should start with urn:
143
+ if not path.startswith("urn:"):
144
+ return False
145
+
146
+ # Split on the last colon to separate URN from attribute
147
+ urn_part, attr_part = path.rsplit(":", 1)
148
+
149
+ # URN part should have at least 4 parts (urn:namespace:specific:resource)
150
+ urn_segments = urn_part.split(":")
151
+ if len(urn_segments) < 4:
152
+ return False
153
+
154
+ # Attribute part should be valid
155
+ if not attr_part or attr_part[0].isdigit():
156
+ return False
157
+
158
+ return True
159
+
160
+
161
+ def _extract_field_name(path: str) -> Optional[str]:
162
+ """Extract the field name from a path.
163
+
164
+ For now, only handle simple paths (no filters, no complex expressions).
165
+ Returns None for complex paths that require filter parsing.
166
+
167
+ """
168
+ # Handle URN paths
169
+ if path.startswith("urn:"):
170
+ # First validate it's a proper URN
171
+ if not _validate_scim_urn_syntax(path):
172
+ return None
173
+ parts = path.rsplit(":", 1)
174
+ return parts[1]
175
+
176
+ # Simple attribute path (may have dots for sub-attributes)
177
+ # For now, just take the first part before any dot
178
+ if "." in path:
179
+ return path.split(".")[0]
180
+
181
+ return path
182
+
183
+
184
+ def _find_field_name(resource_class, attr_name: str) -> Optional[str]:
185
+ """Find the actual field name in a resource class from an attribute name.
186
+
187
+ Args:
188
+ resource_class: The resource class to search in
189
+ attr_name: The attribute name to find (e.g., "nickName")
190
+
191
+ Returns:
192
+ The actual field name if found (e.g., "nick_name"), None otherwise
193
+
194
+ """
195
+ normalized_attr_name = _normalize_attribute_name(attr_name)
196
+
197
+ for field_key in resource_class.model_fields:
198
+ if _normalize_attribute_name(field_key) == normalized_attr_name:
199
+ return field_key
200
+
201
+ return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scim2-models
3
- Version: 0.3.7
3
+ Version: 0.4.1
4
4
  Summary: SCIM2 models serialization and validation with pydantic
5
5
  Project-URL: documentation, https://scim2-models.readthedocs.io
6
6
  Project-URL: repository, https://github.com/python-scim/scim2-models
@@ -0,0 +1,30 @@
1
+ scim2_models/__init__.py,sha256=IACL_c94UhKq0ZEXOb3LMZgeEQMfQhdOVpWGDnHSpXo,3201
2
+ scim2_models/annotations.py,sha256=oRjlKL1fqrYfa9UtaMdxF5fOT8CUUN3m-rdzvf7aiSA,3304
3
+ scim2_models/attributes.py,sha256=REp_WMTxs02NDcJJrSCbplCqnhVtExvfzozp_JJ_BdY,1785
4
+ scim2_models/base.py,sha256=8Nh-R8vN_Gd9_NU9Oq-heD5h2MzjT_X9djidG3OeO0g,20389
5
+ scim2_models/constants.py,sha256=9egq8JW0dFAqPng85CiHoH5T6pRtYL87-gC0C-IMGsk,573
6
+ scim2_models/context.py,sha256=RjgMIvWPr8f41qbVL1sjaDnm9GRKyrCrgfC4npwwcMg,9149
7
+ scim2_models/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ scim2_models/reference.py,sha256=EQM8bbSr_kxbFMNlWYf_4sAJlSsOS5wUrn-9_eF0Ykc,2483
9
+ scim2_models/scim_object.py,sha256=6a-lf8iIQhlMY7lCM7RcSde4f1kECncE0Ae8uJ4RaaA,2404
10
+ scim2_models/urn.py,sha256=n94gnOBMJguXfyczcFvkn8ipvZuRXoxYPC7QeXe9HoY,3559
11
+ scim2_models/utils.py,sha256=3yvU261wl5IR-zPcNhcNcQFwCd0h--k07mfTSgUY1nE,5681
12
+ scim2_models/messages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ scim2_models/messages/bulk.py,sha256=snPB722V_Msg1JBFcaTeEW_sa-jt-KHv6fh071NoKCg,2592
14
+ scim2_models/messages/error.py,sha256=_IHUoFwl-QJiICIcRkhaXf_9C7RXgiX7TL_26QXDo2c,6304
15
+ scim2_models/messages/list_response.py,sha256=B-XELBTks6I45pKheAS5Go9x5IDKhrLJWsqC8y3G00s,2400
16
+ scim2_models/messages/message.py,sha256=sjo_iFMJelZkssvA4O5xTqIt-urDxUbSP6N_WsnZ63Y,4081
17
+ scim2_models/messages/patch_op.py,sha256=yT_5gDcXWeSOdxvpYrdHh5nD2uD-B9kdfQ5qjC-JUkY,19726
18
+ scim2_models/messages/search_request.py,sha256=BDTDzvtfCuFZDFsZBHl8ys8Te02jLuvGaTNipBQByrM,4632
19
+ scim2_models/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ scim2_models/resources/enterprise_user.py,sha256=TVa5aS-eLHcDkwyr58hZYsRKk0AwjUaUaSFhU51mn5E,1806
21
+ scim2_models/resources/group.py,sha256=lJXKopa__LoWhkaNqu0JFVyQaOMe-AF4vISaq0Gg7cE,1458
22
+ scim2_models/resources/resource.py,sha256=GZaV_bi6r2A_UfPSQgEb3jGEeEqMhw2YGImRIhfKOY0,17615
23
+ scim2_models/resources/resource_type.py,sha256=scLqbD3HX3fXT2JOXY9OUVnZz7i5ty2lx4VuiVxt6DE,3314
24
+ scim2_models/resources/schema.py,sha256=KBqDRr2sgLrqL8UYWon-uxBzIA0118srJ8tXobs950E,10659
25
+ scim2_models/resources/service_provider_config.py,sha256=6xJ182T-1szEQnN5Zb1cTdQCgTYIFi4XKygbvDlTKTM,5446
26
+ scim2_models/resources/user.py,sha256=ErOghhilUF7fipwDRqARyLwJhbntQx4GJG3u2sZNJXs,10664
27
+ scim2_models-0.4.1.dist-info/METADATA,sha256=zu1Txl8FZcnVF6WYYkBl5g7O1rvfcnaG1Dk987DtIRM,16288
28
+ scim2_models-0.4.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
29
+ scim2_models-0.4.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
30
+ scim2_models-0.4.1.dist-info/RECORD,,