scim2-models 0.3.7__tar.gz → 0.4.0__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.
- {scim2_models-0.3.7 → scim2_models-0.4.0}/.pre-commit-config.yaml +1 -1
- {scim2_models-0.3.7 → scim2_models-0.4.0}/PKG-INFO +1 -1
- {scim2_models-0.3.7 → scim2_models-0.4.0}/doc/changelog.rst +14 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/doc/tutorial.rst +45 -5
- {scim2_models-0.3.7 → scim2_models-0.4.0}/pyproject.toml +1 -1
- scim2_models-0.4.0/scim2_models/__init__.py +109 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/scim2_models/attributes.py +2 -2
- {scim2_models-0.3.7 → scim2_models-0.4.0}/scim2_models/base.py +140 -42
- {scim2_models-0.3.7 → scim2_models-0.4.0}/scim2_models/context.py +1 -1
- {scim2_models-0.3.7/scim2_models/rfc7644 → scim2_models-0.4.0/scim2_models/messages}/bulk.py +2 -2
- {scim2_models-0.3.7/scim2_models/rfc7644 → scim2_models-0.4.0/scim2_models/messages}/error.py +2 -2
- {scim2_models-0.3.7/scim2_models/rfc7644 → scim2_models-0.4.0/scim2_models/messages}/list_response.py +3 -3
- {scim2_models-0.3.7/scim2_models/rfc7644 → scim2_models-0.4.0/scim2_models/messages}/message.py +16 -7
- scim2_models-0.4.0/scim2_models/messages/patch_op.py +478 -0
- {scim2_models-0.3.7/scim2_models/rfc7644 → scim2_models-0.4.0/scim2_models/messages}/search_request.py +51 -2
- {scim2_models-0.3.7/scim2_models/rfc7643 → scim2_models-0.4.0/scim2_models/resources}/resource.py +32 -26
- {scim2_models-0.3.7/scim2_models/rfc7643 → scim2_models-0.4.0/scim2_models/resources}/schema.py +10 -10
- scim2_models-0.4.0/scim2_models/scim_object.py +66 -0
- scim2_models-0.4.0/scim2_models/urn.py +109 -0
- scim2_models-0.4.0/scim2_models/utils.py +201 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/tests/test_dynamic_resources.py +5 -5
- {scim2_models-0.3.7 → scim2_models-0.4.0}/tests/test_dynamic_schemas.py +11 -11
- {scim2_models-0.3.7 → scim2_models-0.4.0}/tests/test_errors.py +1 -1
- {scim2_models-0.3.7 → scim2_models-0.4.0}/tests/test_model_attributes.py +68 -56
- {scim2_models-0.3.7 → scim2_models-0.4.0}/tests/test_model_serialization.py +80 -1
- {scim2_models-0.3.7 → scim2_models-0.4.0}/tests/test_model_validation.py +1 -1
- {scim2_models-0.3.7 → scim2_models-0.4.0}/tests/test_models.py +2 -2
- scim2_models-0.4.0/tests/test_patch_op_add.py +278 -0
- scim2_models-0.4.0/tests/test_patch_op_extensions.py +335 -0
- scim2_models-0.4.0/tests/test_patch_op_remove.py +289 -0
- scim2_models-0.4.0/tests/test_patch_op_replace.py +219 -0
- scim2_models-0.4.0/tests/test_patch_op_validation.py +464 -0
- scim2_models-0.4.0/tests/test_path_validation.py +112 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/tests/test_resource_extension.py +2 -2
- scim2_models-0.4.0/tests/test_search_request.py +243 -0
- scim2_models-0.4.0/tests/test_utils.py +29 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/uv.lock +34 -34
- scim2_models-0.3.7/scim2_models/__init__.py +0 -109
- scim2_models-0.3.7/scim2_models/rfc7644/patch_op.py +0 -84
- scim2_models-0.3.7/scim2_models/scim_object.py +0 -143
- scim2_models-0.3.7/scim2_models/utils.py +0 -99
- scim2_models-0.3.7/tests/test_patch_op.py +0 -63
- scim2_models-0.3.7/tests/test_search_request.py +0 -79
- scim2_models-0.3.7/tests/test_utils.py +0 -15
- {scim2_models-0.3.7 → scim2_models-0.4.0}/.github/FUNDING.yml +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/.github/workflows/release.yml +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/.github/workflows/tests.yaml +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/.gitignore +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/.readthedocs.yml +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/LICENSE +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/README.md +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/conftest.py +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/doc/__init__.py +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/doc/conf.py +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/doc/contributing.rst +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/doc/index.rst +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/doc/reference.rst +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7643-8.1-user-minimal.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7643-8.2-user-full.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7643-8.3-enterprise_user.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7643-8.4-group.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7643-8.5-service_provider_configuration.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7643-8.6-resource_type-group.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7643-8.6-resource_type-user.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7643-8.7.1-schema-enterprise_user.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7643-8.7.1-schema-group.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7643-8.7.1-schema-user.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7643-8.7.2-schema-resource_type.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7643-8.7.2-schema-schema.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7643-8.7.2-schema-service_provider_configuration.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.12-error-bad_request.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.12-error-not_found.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.14-user-post_request.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.14-user-post_response.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.3-user-post_request.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.3-user-post_response.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.4.1-user-known-resource.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.4.2-list_response-partial_attributes.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.4.3-list_response-post_query.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.4.3-search_request.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.5.1-user-put_request.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.5.1-user-put_response.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.5.2.1-patch_op-add_emails.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.5.2.1-patch_op-add_members.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.5.2.2-patch_op-remove_all_members.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.5.2.2-patch_op-remove_and_add_one_member.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.5.2.2-patch_op-remove_multi_complex_value.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.5.2.2-patch_op-remove_one_member.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.5.2.3-patch_op-replace_all_email_values.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.5.2.3-patch_op-replace_all_members.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.5.2.3-patch_op-replace_street_address.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.5.2.3-patch_op-replace_user_work_address.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.6-error-not_found.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.7.1-bulk_request-circular_conflict.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.7.1-list_response-circular_reference.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.7.2-bulk_request-enterprise_user.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.7.2-bulk_request-temporary_identifier.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.7.2-bulk_response-temporary_identifier.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.7.3-bulk_request-multiple_operations.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.7.3-bulk_response-error_invalid_syntax.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.7.3-bulk_response-multiple_errors.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.7.3-bulk_response-multiple_operations.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.7.3-error-invalid_syntax.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.7.4-error-payload_too_large.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-3.9-user-partial_response.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/samples/rfc7644-4-list_response-resource_types.json +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/scim2_models/annotations.py +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/scim2_models/constants.py +0 -0
- {scim2_models-0.3.7/scim2_models/rfc7643 → scim2_models-0.4.0/scim2_models/messages}/__init__.py +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/scim2_models/py.typed +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/scim2_models/reference.py +0 -0
- {scim2_models-0.3.7/scim2_models/rfc7644 → scim2_models-0.4.0/scim2_models/resources}/__init__.py +0 -0
- {scim2_models-0.3.7/scim2_models/rfc7643 → scim2_models-0.4.0/scim2_models/resources}/enterprise_user.py +0 -0
- {scim2_models-0.3.7/scim2_models/rfc7643 → scim2_models-0.4.0/scim2_models/resources}/group.py +0 -0
- {scim2_models-0.3.7/scim2_models/rfc7643 → scim2_models-0.4.0/scim2_models/resources}/resource_type.py +0 -0
- {scim2_models-0.3.7/scim2_models/rfc7643 → scim2_models-0.4.0/scim2_models/resources}/service_provider_config.py +0 -0
- {scim2_models-0.3.7/scim2_models/rfc7643 → scim2_models-0.4.0/scim2_models/resources}/user.py +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/tests/__init__.py +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/tests/conftest.py +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/tests/test_enterprise_user.py +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/tests/test_group.py +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/tests/test_list_response.py +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/tests/test_reference.py +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/tests/test_resource_type.py +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/tests/test_schema.py +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/tests/test_service_provider_configuration.py +0 -0
- {scim2_models-0.3.7 → scim2_models-0.4.0}/tests/test_user.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scim2-models
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
Changelog
|
|
2
2
|
=========
|
|
3
3
|
|
|
4
|
+
[0.4.0] - 2025-07-23
|
|
5
|
+
--------------------
|
|
6
|
+
|
|
7
|
+
Added
|
|
8
|
+
^^^^^
|
|
9
|
+
- Proper path validation for :attr:`~scim2_models.SearchRequest.attributes`, :attr:`~scim2_models.SearchRequest.excluded_attributes` and :attr:`~scim2_models.SearchRequest.sort_by`.
|
|
10
|
+
- Implement :meth:`~scim2_models.PatchOp.patch`
|
|
11
|
+
|
|
12
|
+
Fixed
|
|
13
|
+
^^^^^
|
|
14
|
+
- When using ``model_dump``, ignore invalid ``attributes`` and ``excluded_attributes``
|
|
15
|
+
as suggested by RFC7644.
|
|
16
|
+
- Don't normalize attributes typed with :data:`Any`. :issue:`20`
|
|
17
|
+
|
|
4
18
|
[0.3.7] - 2025-07-17
|
|
5
19
|
--------------------
|
|
6
20
|
|
|
@@ -204,9 +204,9 @@ If a response resource type cannot be found, a ``pydantic.ValidationError`` will
|
|
|
204
204
|
>>> response = ListResponse[Union[User, Group]].model_validate(payload)
|
|
205
205
|
>>> user, group = response.resources
|
|
206
206
|
>>> type(user)
|
|
207
|
-
<class 'scim2_models.
|
|
207
|
+
<class 'scim2_models.resources.user.User'>
|
|
208
208
|
>>> type(group)
|
|
209
|
-
<class 'scim2_models.
|
|
209
|
+
<class 'scim2_models.resources.group.Group'>
|
|
210
210
|
|
|
211
211
|
|
|
212
212
|
Schema extensions
|
|
@@ -403,9 +403,49 @@ This can be used by client applications that intends to dynamically discover ser
|
|
|
403
403
|
:language: json
|
|
404
404
|
:caption: schema-group.json
|
|
405
405
|
|
|
406
|
-
|
|
407
|
-
|
|
406
|
+
Patch operations
|
|
407
|
+
================
|
|
408
|
+
|
|
409
|
+
:class:`~scim2_models.PatchOp` allows you to apply patch operations to modify SCIM resources.
|
|
410
|
+
The :meth:`~scim2_models.PatchOp.patch` method applies operations in sequence and returns whether the resource was modified. The return code is a boolean indicating whether the object have been modified by the operations.
|
|
411
|
+
|
|
412
|
+
.. note::
|
|
413
|
+
:class:`~scim2_models.PatchOp` takes a type parameter that should be the class of the resource
|
|
414
|
+
that is expected to be patched.
|
|
415
|
+
|
|
416
|
+
.. code-block:: python
|
|
417
|
+
|
|
418
|
+
>>> from scim2_models import User, PatchOp, PatchOperation
|
|
419
|
+
>>> user = User(user_name="john.doe", nick_name="Johnny")
|
|
420
|
+
|
|
421
|
+
>>> payload = {
|
|
422
|
+
... "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
|
423
|
+
... "Operations": [
|
|
424
|
+
... {"op": "replace", "path": "nickName", "value": "John" },
|
|
425
|
+
... {"op": "add", "path": "emails", "value": [{"value": "john@example.com"}]},
|
|
426
|
+
... ]
|
|
427
|
+
... }
|
|
428
|
+
>>> patch = PatchOp[User].model_validate(
|
|
429
|
+
... payload, scim_ctx=Context.RESOURCE_PATCH_REQUEST
|
|
430
|
+
... )
|
|
431
|
+
|
|
432
|
+
>>> modified = patch.patch(user)
|
|
433
|
+
>>> print(modified)
|
|
434
|
+
True
|
|
435
|
+
>>> print(user.nick_name)
|
|
436
|
+
John
|
|
437
|
+
>>> print(user.emails[0].value)
|
|
438
|
+
john@example.com
|
|
439
|
+
|
|
440
|
+
.. warning::
|
|
441
|
+
|
|
442
|
+
Patch operations are validated in the :attr:`~scim2_models.Context.RESOURCE_PATCH_REQUEST`
|
|
443
|
+
context. Make sure to validate patch operations with the correct context to
|
|
444
|
+
ensure proper validation of mutability and required constraints.
|
|
445
|
+
|
|
446
|
+
Bulk operations
|
|
447
|
+
===============
|
|
408
448
|
|
|
409
449
|
.. todo::
|
|
410
450
|
|
|
411
|
-
Bulk
|
|
451
|
+
Bulk operations are not implemented yet, but any help is welcome!
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scim2-models"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
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"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from .annotations import CaseExact
|
|
2
|
+
from .annotations import Mutability
|
|
3
|
+
from .annotations import Required
|
|
4
|
+
from .annotations import Returned
|
|
5
|
+
from .annotations import Uniqueness
|
|
6
|
+
from .attributes import ComplexAttribute
|
|
7
|
+
from .attributes import MultiValuedComplexAttribute
|
|
8
|
+
from .base import BaseModel
|
|
9
|
+
from .context import Context
|
|
10
|
+
from .messages.bulk import BulkOperation
|
|
11
|
+
from .messages.bulk import BulkRequest
|
|
12
|
+
from .messages.bulk import BulkResponse
|
|
13
|
+
from .messages.error import Error
|
|
14
|
+
from .messages.list_response import ListResponse
|
|
15
|
+
from .messages.message import Message
|
|
16
|
+
from .messages.patch_op import PatchOp
|
|
17
|
+
from .messages.patch_op import PatchOperation
|
|
18
|
+
from .messages.search_request import SearchRequest
|
|
19
|
+
from .reference import ExternalReference
|
|
20
|
+
from .reference import Reference
|
|
21
|
+
from .reference import URIReference
|
|
22
|
+
from .resources.enterprise_user import EnterpriseUser
|
|
23
|
+
from .resources.enterprise_user import Manager
|
|
24
|
+
from .resources.group import Group
|
|
25
|
+
from .resources.group import GroupMember
|
|
26
|
+
from .resources.resource import AnyExtension
|
|
27
|
+
from .resources.resource import AnyResource
|
|
28
|
+
from .resources.resource import Extension
|
|
29
|
+
from .resources.resource import Meta
|
|
30
|
+
from .resources.resource import Resource
|
|
31
|
+
from .resources.resource_type import ResourceType
|
|
32
|
+
from .resources.resource_type import SchemaExtension
|
|
33
|
+
from .resources.schema import Attribute
|
|
34
|
+
from .resources.schema import Schema
|
|
35
|
+
from .resources.service_provider_config import AuthenticationScheme
|
|
36
|
+
from .resources.service_provider_config import Bulk
|
|
37
|
+
from .resources.service_provider_config import ChangePassword
|
|
38
|
+
from .resources.service_provider_config import ETag
|
|
39
|
+
from .resources.service_provider_config import Filter
|
|
40
|
+
from .resources.service_provider_config import Patch
|
|
41
|
+
from .resources.service_provider_config import ServiceProviderConfig
|
|
42
|
+
from .resources.service_provider_config import Sort
|
|
43
|
+
from .resources.user import Address
|
|
44
|
+
from .resources.user import Email
|
|
45
|
+
from .resources.user import Entitlement
|
|
46
|
+
from .resources.user import GroupMembership
|
|
47
|
+
from .resources.user import Im
|
|
48
|
+
from .resources.user import Name
|
|
49
|
+
from .resources.user import PhoneNumber
|
|
50
|
+
from .resources.user import Photo
|
|
51
|
+
from .resources.user import Role
|
|
52
|
+
from .resources.user import User
|
|
53
|
+
from .resources.user import X509Certificate
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"Address",
|
|
57
|
+
"AnyResource",
|
|
58
|
+
"AnyExtension",
|
|
59
|
+
"Attribute",
|
|
60
|
+
"AuthenticationScheme",
|
|
61
|
+
"BaseModel",
|
|
62
|
+
"Bulk",
|
|
63
|
+
"BulkOperation",
|
|
64
|
+
"BulkRequest",
|
|
65
|
+
"BulkResponse",
|
|
66
|
+
"CaseExact",
|
|
67
|
+
"ChangePassword",
|
|
68
|
+
"ComplexAttribute",
|
|
69
|
+
"Context",
|
|
70
|
+
"ETag",
|
|
71
|
+
"Email",
|
|
72
|
+
"EnterpriseUser",
|
|
73
|
+
"Entitlement",
|
|
74
|
+
"Error",
|
|
75
|
+
"ExternalReference",
|
|
76
|
+
"Extension",
|
|
77
|
+
"Filter",
|
|
78
|
+
"Group",
|
|
79
|
+
"GroupMember",
|
|
80
|
+
"GroupMembership",
|
|
81
|
+
"Im",
|
|
82
|
+
"ListResponse",
|
|
83
|
+
"Manager",
|
|
84
|
+
"Message",
|
|
85
|
+
"Meta",
|
|
86
|
+
"Mutability",
|
|
87
|
+
"MultiValuedComplexAttribute",
|
|
88
|
+
"Name",
|
|
89
|
+
"Patch",
|
|
90
|
+
"PatchOp",
|
|
91
|
+
"PatchOperation",
|
|
92
|
+
"PhoneNumber",
|
|
93
|
+
"Photo",
|
|
94
|
+
"Reference",
|
|
95
|
+
"Required",
|
|
96
|
+
"Resource",
|
|
97
|
+
"ResourceType",
|
|
98
|
+
"Returned",
|
|
99
|
+
"Role",
|
|
100
|
+
"Schema",
|
|
101
|
+
"SchemaExtension",
|
|
102
|
+
"SearchRequest",
|
|
103
|
+
"ServiceProviderConfig",
|
|
104
|
+
"Sort",
|
|
105
|
+
"Uniqueness",
|
|
106
|
+
"URIReference",
|
|
107
|
+
"User",
|
|
108
|
+
"X509Certificate",
|
|
109
|
+
]
|
|
@@ -16,7 +16,7 @@ from .reference import Reference
|
|
|
16
16
|
class ComplexAttribute(BaseModel):
|
|
17
17
|
"""A complex attribute as defined in :rfc:`RFC7643 §2.3.8 <7643#section-2.3.8>`."""
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
_attribute_urn: Optional[str] = None
|
|
20
20
|
|
|
21
21
|
def get_attribute_urn(self, field_name: str) -> str:
|
|
22
22
|
"""Build the full URN of the attribute.
|
|
@@ -26,7 +26,7 @@ class ComplexAttribute(BaseModel):
|
|
|
26
26
|
alias = (
|
|
27
27
|
self.__class__.model_fields[field_name].serialization_alias or field_name
|
|
28
28
|
)
|
|
29
|
-
return f"{self.
|
|
29
|
+
return f"{self._attribute_urn}.{alias}"
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class MultiValuedComplexAttribute(ComplexAttribute):
|
|
@@ -23,13 +23,13 @@ from scim2_models.annotations import Mutability
|
|
|
23
23
|
from scim2_models.annotations import Required
|
|
24
24
|
from scim2_models.annotations import Returned
|
|
25
25
|
from scim2_models.context import Context
|
|
26
|
-
from scim2_models.utils import
|
|
27
|
-
from scim2_models.utils import
|
|
26
|
+
from scim2_models.utils import UNION_TYPES
|
|
27
|
+
from scim2_models.utils import _find_field_name
|
|
28
|
+
from scim2_models.utils import _normalize_attribute_name
|
|
29
|
+
from scim2_models.utils import _to_camel
|
|
28
30
|
|
|
29
|
-
from .utils import UNION_TYPES
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
def contains_attribute_or_subattributes(
|
|
32
|
+
def _contains_attribute_or_subattributes(
|
|
33
33
|
attribute_urns: list[str], attribute_urn: str
|
|
34
34
|
) -> bool:
|
|
35
35
|
return attribute_urn in attribute_urns or any(
|
|
@@ -43,8 +43,8 @@ class BaseModel(PydanticBaseModel):
|
|
|
43
43
|
|
|
44
44
|
model_config = ConfigDict(
|
|
45
45
|
alias_generator=AliasGenerator(
|
|
46
|
-
validation_alias=
|
|
47
|
-
serialization_alias=
|
|
46
|
+
validation_alias=_normalize_attribute_name,
|
|
47
|
+
serialization_alias=_to_camel,
|
|
48
48
|
),
|
|
49
49
|
validate_assignment=True,
|
|
50
50
|
populate_by_name=True,
|
|
@@ -54,7 +54,35 @@ class BaseModel(PydanticBaseModel):
|
|
|
54
54
|
|
|
55
55
|
@classmethod
|
|
56
56
|
def get_field_annotation(cls, field_name: str, annotation_type: type) -> Any:
|
|
57
|
-
"""Return the annotation of type 'annotation_type' of the field 'field_name'.
|
|
57
|
+
"""Return the annotation of type 'annotation_type' of the field 'field_name'.
|
|
58
|
+
|
|
59
|
+
This method extracts SCIM-specific annotations from a field's metadata,
|
|
60
|
+
such as :class:`~scim2_models.Mutability`, :class:`~scim2_models.Required`,
|
|
61
|
+
or :class:`~scim2_models.Returned` annotations.
|
|
62
|
+
|
|
63
|
+
:return: The annotation instance if found, otherwise the annotation type's default value
|
|
64
|
+
|
|
65
|
+
>>> from scim2_models.resources.user import User
|
|
66
|
+
>>> from scim2_models.annotations import Mutability, Required
|
|
67
|
+
|
|
68
|
+
Get the mutability annotation of the 'id' field:
|
|
69
|
+
|
|
70
|
+
>>> mutability = User.get_field_annotation("id", Mutability)
|
|
71
|
+
>>> mutability
|
|
72
|
+
<Mutability.read_only: 'readOnly'>
|
|
73
|
+
|
|
74
|
+
Get the required annotation of the 'user_name' field:
|
|
75
|
+
|
|
76
|
+
>>> required = User.get_field_annotation("user_name", Required)
|
|
77
|
+
>>> required
|
|
78
|
+
<Required.true: True>
|
|
79
|
+
|
|
80
|
+
If no annotation is found, returns the default value:
|
|
81
|
+
|
|
82
|
+
>>> missing = User.get_field_annotation("display_name", Required)
|
|
83
|
+
>>> missing
|
|
84
|
+
<Required.false: False>
|
|
85
|
+
"""
|
|
58
86
|
field_metadata = cls.model_fields[field_name].metadata
|
|
59
87
|
|
|
60
88
|
default_value = getattr(annotation_type, "_default", None)
|
|
@@ -71,8 +99,34 @@ class BaseModel(PydanticBaseModel):
|
|
|
71
99
|
def get_field_root_type(cls, attribute_name: str) -> Optional[type]:
|
|
72
100
|
"""Extract the root type from a model field.
|
|
73
101
|
|
|
74
|
-
|
|
75
|
-
|
|
102
|
+
This method unwraps complex type annotations to find the underlying
|
|
103
|
+
type, removing Optional and List wrappers to get to the actual type
|
|
104
|
+
of the field's content.
|
|
105
|
+
|
|
106
|
+
:return: The root type of the field, or None if not found
|
|
107
|
+
|
|
108
|
+
>>> from scim2_models.resources.user import User
|
|
109
|
+
>>> from scim2_models.resources.group import Group
|
|
110
|
+
|
|
111
|
+
Simple type:
|
|
112
|
+
|
|
113
|
+
>>> User.get_field_root_type("user_name")
|
|
114
|
+
<class 'str'>
|
|
115
|
+
|
|
116
|
+
``Optional`` type unwraps to the underlying type:
|
|
117
|
+
|
|
118
|
+
>>> User.get_field_root_type("display_name")
|
|
119
|
+
<class 'str'>
|
|
120
|
+
|
|
121
|
+
``List`` type unwraps to the element type:
|
|
122
|
+
|
|
123
|
+
>>> User.get_field_root_type("emails") # doctest: +ELLIPSIS
|
|
124
|
+
<class 'scim2_models.resources.user.Email'>
|
|
125
|
+
|
|
126
|
+
``Optional[List[T]]`` unwraps to ``T``:
|
|
127
|
+
|
|
128
|
+
>>> Group.get_field_root_type("members") # doctest: +ELLIPSIS
|
|
129
|
+
<class 'scim2_models.resources.group.GroupMember'>
|
|
76
130
|
"""
|
|
77
131
|
attribute_type = cls.model_fields[attribute_name].annotation
|
|
78
132
|
|
|
@@ -89,7 +143,20 @@ class BaseModel(PydanticBaseModel):
|
|
|
89
143
|
|
|
90
144
|
@classmethod
|
|
91
145
|
def get_field_multiplicity(cls, attribute_name: str) -> bool:
|
|
92
|
-
"""Indicate whether a field holds multiple values.
|
|
146
|
+
"""Indicate whether a field holds multiple values.
|
|
147
|
+
|
|
148
|
+
This method determines if a field is defined as a list type,
|
|
149
|
+
which indicates it can contain multiple values. It handles
|
|
150
|
+
Optional wrappers correctly.
|
|
151
|
+
|
|
152
|
+
:return: True if the field holds multiple values (is a list), False otherwise
|
|
153
|
+
|
|
154
|
+
>>> from scim2_models.resources.user import User
|
|
155
|
+
>>> User.get_field_multiplicity("user_name")
|
|
156
|
+
False
|
|
157
|
+
>>> User.get_field_multiplicity("emails")
|
|
158
|
+
True
|
|
159
|
+
"""
|
|
93
160
|
attribute_type = cls.model_fields[attribute_name].annotation
|
|
94
161
|
|
|
95
162
|
# extract 'x' from 'Optional[x]'
|
|
@@ -104,7 +171,7 @@ class BaseModel(PydanticBaseModel):
|
|
|
104
171
|
def check_request_attributes_mutability(
|
|
105
172
|
cls, value: Any, info: ValidationInfo
|
|
106
173
|
) -> Any:
|
|
107
|
-
"""Check and fix that the field mutability is expected according to the requests validation context, as defined in :rfc:`RFC7643 §7 <
|
|
174
|
+
"""Check and fix that the field mutability is expected according to the requests validation context, as defined in :rfc:`RFC7643 §7 <7643#section-7>`."""
|
|
108
175
|
if (
|
|
109
176
|
not info.context
|
|
110
177
|
or not info.field_name
|
|
@@ -147,20 +214,49 @@ class BaseModel(PydanticBaseModel):
|
|
|
147
214
|
) -> Self:
|
|
148
215
|
"""Normalize payload attribute names.
|
|
149
216
|
|
|
150
|
-
:rfc:`RFC7643 §2.1 <
|
|
217
|
+
:rfc:`RFC7643 §2.1 <7643#section-2.1>` indicate that attribute
|
|
151
218
|
names should be case-insensitive. Any attribute name is
|
|
152
219
|
transformed in lowercase so any case is handled the same way.
|
|
153
220
|
"""
|
|
154
221
|
|
|
155
|
-
def
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
return value
|
|
222
|
+
def normalize_dict_keys(
|
|
223
|
+
input_dict: dict, model_class: type["BaseModel"]
|
|
224
|
+
) -> dict:
|
|
225
|
+
"""Normalize dictionary keys, preserving case for Any fields."""
|
|
226
|
+
result = {}
|
|
162
227
|
|
|
163
|
-
|
|
228
|
+
for key, val in input_dict.items():
|
|
229
|
+
field_name = _find_field_name(model_class, key)
|
|
230
|
+
field_type = (
|
|
231
|
+
model_class.get_field_root_type(field_name) if field_name else None
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Don't normalize keys for attributes typed with Any
|
|
235
|
+
# This way, agnostic dicts such as PatchOp.operations.value
|
|
236
|
+
# are preserved
|
|
237
|
+
if field_name and field_type == Any:
|
|
238
|
+
result[key] = normalize_value(val)
|
|
239
|
+
else:
|
|
240
|
+
result[_normalize_attribute_name(key)] = normalize_value(
|
|
241
|
+
val, field_type
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return result
|
|
245
|
+
|
|
246
|
+
def normalize_value(
|
|
247
|
+
val: Any, model_class: Optional[type["BaseModel"]] = None
|
|
248
|
+
) -> Any:
|
|
249
|
+
"""Normalize input value based on model class."""
|
|
250
|
+
if not isinstance(val, dict):
|
|
251
|
+
return val
|
|
252
|
+
|
|
253
|
+
# If no model_class, preserve original keys
|
|
254
|
+
if not model_class:
|
|
255
|
+
return {k: normalize_value(v) for k, v in val.items()}
|
|
256
|
+
|
|
257
|
+
return normalize_dict_keys(val, model_class)
|
|
258
|
+
|
|
259
|
+
normalized_value = normalize_value(value, cls)
|
|
164
260
|
obj = handler(normalized_value)
|
|
165
261
|
assert isinstance(obj, cls)
|
|
166
262
|
return obj
|
|
@@ -170,7 +266,7 @@ class BaseModel(PydanticBaseModel):
|
|
|
170
266
|
def check_response_attributes_returnability(
|
|
171
267
|
cls, value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
|
|
172
268
|
) -> Self:
|
|
173
|
-
"""Check that the fields returnability is expected according to the responses validation context, as defined in :rfc:`RFC7643 §7 <
|
|
269
|
+
"""Check that the fields returnability is expected according to the responses validation context, as defined in :rfc:`RFC7643 §7 <7643#section-7>`."""
|
|
174
270
|
obj = handler(value)
|
|
175
271
|
assert isinstance(obj, cls)
|
|
176
272
|
|
|
@@ -244,7 +340,7 @@ class BaseModel(PydanticBaseModel):
|
|
|
244
340
|
cls, value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
|
|
245
341
|
) -> Self:
|
|
246
342
|
"""Check if 'immutable' attributes have been mutated in replacement requests."""
|
|
247
|
-
from scim2_models.
|
|
343
|
+
from scim2_models.resources.resource import Resource
|
|
248
344
|
|
|
249
345
|
obj = handler(value)
|
|
250
346
|
assert isinstance(obj, cls)
|
|
@@ -256,11 +352,11 @@ class BaseModel(PydanticBaseModel):
|
|
|
256
352
|
and issubclass(cls, Resource)
|
|
257
353
|
and original is not None
|
|
258
354
|
):
|
|
259
|
-
cls.
|
|
355
|
+
cls._check_mutability_issues(original, obj)
|
|
260
356
|
return obj
|
|
261
357
|
|
|
262
358
|
@classmethod
|
|
263
|
-
def
|
|
359
|
+
def _check_mutability_issues(
|
|
264
360
|
cls, original: "BaseModel", replacement: "BaseModel"
|
|
265
361
|
) -> None:
|
|
266
362
|
"""Compare two instances, and check for differences of values on the fields marked as immutable."""
|
|
@@ -287,18 +383,18 @@ class BaseModel(PydanticBaseModel):
|
|
|
287
383
|
original_val = getattr(original, field_name)
|
|
288
384
|
replacement_value = getattr(replacement, field_name)
|
|
289
385
|
if original_val is not None and replacement_value is not None:
|
|
290
|
-
cls.
|
|
386
|
+
cls._check_mutability_issues(original_val, replacement_value)
|
|
291
387
|
|
|
292
|
-
def
|
|
293
|
-
"""Navigate through attributes and sub-attributes of type ComplexAttribute, and mark them with a '
|
|
388
|
+
def _set_complex_attribute_urns(self) -> None:
|
|
389
|
+
"""Navigate through attributes and sub-attributes of type ComplexAttribute, and mark them with a '_attribute_urn' attribute.
|
|
294
390
|
|
|
295
|
-
'
|
|
391
|
+
'_attribute_urn' will later be used by 'get_attribute_urn'.
|
|
296
392
|
"""
|
|
297
393
|
from .attributes import ComplexAttribute
|
|
298
394
|
from .attributes import is_complex_attribute
|
|
299
395
|
|
|
300
396
|
if isinstance(self, ComplexAttribute):
|
|
301
|
-
main_schema = self.
|
|
397
|
+
main_schema = self._attribute_urn
|
|
302
398
|
separator = "."
|
|
303
399
|
else:
|
|
304
400
|
main_schema = self.__class__.model_fields["schemas"].default[0]
|
|
@@ -314,9 +410,9 @@ class BaseModel(PydanticBaseModel):
|
|
|
314
410
|
if attr_value := getattr(self, field_name):
|
|
315
411
|
if isinstance(attr_value, list):
|
|
316
412
|
for item in attr_value:
|
|
317
|
-
item.
|
|
413
|
+
item._attribute_urn = schema
|
|
318
414
|
else:
|
|
319
|
-
attr_value.
|
|
415
|
+
attr_value._attribute_urn = schema
|
|
320
416
|
|
|
321
417
|
@field_serializer("*", mode="wrap")
|
|
322
418
|
def scim_serializer(
|
|
@@ -330,14 +426,14 @@ class BaseModel(PydanticBaseModel):
|
|
|
330
426
|
scim_ctx = info.context.get("scim") if info.context else None
|
|
331
427
|
|
|
332
428
|
if scim_ctx and Context.is_request(scim_ctx):
|
|
333
|
-
value = self.
|
|
429
|
+
value = self._scim_request_serializer(value, info)
|
|
334
430
|
|
|
335
431
|
if scim_ctx and Context.is_response(scim_ctx):
|
|
336
|
-
value = self.
|
|
432
|
+
value = self._scim_response_serializer(value, info)
|
|
337
433
|
|
|
338
434
|
return value
|
|
339
435
|
|
|
340
|
-
def
|
|
436
|
+
def _scim_request_serializer(self, value: Any, info: FieldSerializationInfo) -> Any:
|
|
341
437
|
"""Serialize the fields according to mutability indications passed in the serialization context."""
|
|
342
438
|
mutability = self.get_field_annotation(info.field_name, Mutability)
|
|
343
439
|
scim_ctx = info.context.get("scim") if info.context else None
|
|
@@ -361,7 +457,9 @@ class BaseModel(PydanticBaseModel):
|
|
|
361
457
|
|
|
362
458
|
return value
|
|
363
459
|
|
|
364
|
-
def
|
|
460
|
+
def _scim_response_serializer(
|
|
461
|
+
self, value: Any, info: FieldSerializationInfo
|
|
462
|
+
) -> Any:
|
|
365
463
|
"""Serialize the fields according to returnability indications passed in the serialization context."""
|
|
366
464
|
returnability = self.get_field_annotation(info.field_name, Returned)
|
|
367
465
|
attribute_urn = self.get_attribute_urn(info.field_name)
|
|
@@ -370,9 +468,9 @@ class BaseModel(PydanticBaseModel):
|
|
|
370
468
|
info.context.get("scim_excluded_attributes", []) if info.context else []
|
|
371
469
|
)
|
|
372
470
|
|
|
373
|
-
attribute_urn =
|
|
374
|
-
included_urns = [
|
|
375
|
-
excluded_urns = [
|
|
471
|
+
attribute_urn = _normalize_attribute_name(attribute_urn)
|
|
472
|
+
included_urns = [_normalize_attribute_name(urn) for urn in included_urns]
|
|
473
|
+
excluded_urns = [_normalize_attribute_name(urn) for urn in excluded_urns]
|
|
376
474
|
|
|
377
475
|
if returnability == Returned.never:
|
|
378
476
|
return None
|
|
@@ -380,7 +478,7 @@ class BaseModel(PydanticBaseModel):
|
|
|
380
478
|
if returnability == Returned.default and (
|
|
381
479
|
(
|
|
382
480
|
included_urns
|
|
383
|
-
and not
|
|
481
|
+
and not _contains_attribute_or_subattributes(
|
|
384
482
|
included_urns, attribute_urn
|
|
385
483
|
)
|
|
386
484
|
)
|
|
@@ -398,7 +496,7 @@ class BaseModel(PydanticBaseModel):
|
|
|
398
496
|
self, handler: SerializerFunctionWrapHandler, info: SerializationInfo
|
|
399
497
|
) -> dict[str, Any]:
|
|
400
498
|
"""Remove `None` values inserted by the :meth:`~scim2_models.base.BaseModel.scim_serializer`."""
|
|
401
|
-
self.
|
|
499
|
+
self._set_complex_attribute_urns()
|
|
402
500
|
result = handler(self)
|
|
403
501
|
return {key: value for key, value in result.items() if value is not None}
|
|
404
502
|
|
|
@@ -433,7 +531,7 @@ class BaseModel(PydanticBaseModel):
|
|
|
433
531
|
|
|
434
532
|
See :rfc:`RFC7644 §3.10 <7644#section-3.10>`.
|
|
435
533
|
"""
|
|
436
|
-
from scim2_models.
|
|
534
|
+
from scim2_models.resources.resource import Extension
|
|
437
535
|
|
|
438
536
|
main_schema = self.__class__.model_fields["schemas"].default[0]
|
|
439
537
|
field = self.__class__.model_fields[field_name]
|
|
@@ -78,7 +78,7 @@ class Context(Enum):
|
|
|
78
78
|
|
|
79
79
|
- When used for serialization, it will not dump attributes annotated with :attr:`~scim2_models.Mutability.read_only`.
|
|
80
80
|
- When used for validation, it will ignore attributes annotated with :attr:`scim2_models.Mutability.read_only` and raise a :class:`~pydantic.ValidationError`:
|
|
81
|
-
- when finding attributes annotated with :attr:`~scim2_models.Mutability.immutable` different than :
|
|
81
|
+
- when finding attributes annotated with :attr:`~scim2_models.Mutability.immutable` different than the ``original`` parameter passed to :meth:`~scim2_models.BaseModel.model_validate`;
|
|
82
82
|
- when attributes annotated with :attr:`Required.true <scim2_models.Required.true>` are missing on null.
|
|
83
83
|
"""
|
|
84
84
|
|
{scim2_models-0.3.7/scim2_models/rfc7644 → scim2_models-0.4.0/scim2_models/messages}/bulk.py
RENAMED
|
@@ -8,7 +8,7 @@ from pydantic import PlainSerializer
|
|
|
8
8
|
|
|
9
9
|
from ..annotations import Required
|
|
10
10
|
from ..attributes import ComplexAttribute
|
|
11
|
-
from ..utils import
|
|
11
|
+
from ..utils import _int_to_str
|
|
12
12
|
from .message import Message
|
|
13
13
|
|
|
14
14
|
|
|
@@ -42,7 +42,7 @@ class BulkOperation(ComplexAttribute):
|
|
|
42
42
|
response: Optional[Any] = None
|
|
43
43
|
"""The HTTP response body for the specified request operation."""
|
|
44
44
|
|
|
45
|
-
status: Annotated[Optional[int], PlainSerializer(
|
|
45
|
+
status: Annotated[Optional[int], PlainSerializer(_int_to_str)] = None
|
|
46
46
|
"""The HTTP response status code for the requested operation."""
|
|
47
47
|
|
|
48
48
|
|
{scim2_models-0.3.7/scim2_models/rfc7644 → scim2_models-0.4.0/scim2_models/messages}/error.py
RENAMED
|
@@ -4,7 +4,7 @@ from typing import Optional
|
|
|
4
4
|
from pydantic import PlainSerializer
|
|
5
5
|
|
|
6
6
|
from ..annotations import Required
|
|
7
|
-
from ..utils import
|
|
7
|
+
from ..utils import _int_to_str
|
|
8
8
|
from .message import Message
|
|
9
9
|
|
|
10
10
|
|
|
@@ -15,7 +15,7 @@ class Error(Message):
|
|
|
15
15
|
"urn:ietf:params:scim:api:messages:2.0:Error"
|
|
16
16
|
]
|
|
17
17
|
|
|
18
|
-
status: Annotated[Optional[int], PlainSerializer(
|
|
18
|
+
status: Annotated[Optional[int], PlainSerializer(_int_to_str)] = None
|
|
19
19
|
"""The HTTP status code (see Section 6 of [RFC7231]) expressed as a JSON
|
|
20
20
|
string."""
|
|
21
21
|
|
|
@@ -12,12 +12,12 @@ from typing_extensions import Self
|
|
|
12
12
|
|
|
13
13
|
from ..annotations import Required
|
|
14
14
|
from ..context import Context
|
|
15
|
-
from ..
|
|
16
|
-
from .message import GenericMessageMetaclass
|
|
15
|
+
from ..resources.resource import AnyResource
|
|
17
16
|
from .message import Message
|
|
17
|
+
from .message import _GenericMessageMetaclass
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
class ListResponse(Message, Generic[AnyResource], metaclass=
|
|
20
|
+
class ListResponse(Message, Generic[AnyResource], metaclass=_GenericMessageMetaclass):
|
|
21
21
|
schemas: Annotated[list[str], Required.true] = [
|
|
22
22
|
"urn:ietf:params:scim:api:messages:2.0:ListResponse"
|
|
23
23
|
]
|