scim2-models 0.4.0__tar.gz → 0.4.2__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.4.0 → scim2_models-0.4.2}/.pre-commit-config.yaml +3 -2
- {scim2_models-0.4.0 → scim2_models-0.4.2}/PKG-INFO +1 -1
- {scim2_models-0.4.0 → scim2_models-0.4.2}/doc/changelog.rst +14 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/doc/conf.py +8 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/pyproject.toml +2 -1
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/attributes.py +1 -1
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/base.py +2 -2
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/messages/message.py +2 -2
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/messages/patch_op.py +86 -41
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/messages/search_request.py +0 -2
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/reference.py +0 -2
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/resources/group.py +2 -1
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/resources/resource.py +7 -7
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/resources/resource_type.py +3 -2
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/resources/schema.py +1 -1
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/resources/service_provider_config.py +2 -1
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/urn.py +7 -5
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/utils.py +9 -13
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_patch_op_validation.py +35 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/uv.lock +167 -139
- {scim2_models-0.4.0 → scim2_models-0.4.2}/.github/FUNDING.yml +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/.github/workflows/release.yml +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/.github/workflows/tests.yaml +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/.gitignore +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/.readthedocs.yml +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/LICENSE +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/README.md +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/conftest.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/doc/__init__.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/doc/contributing.rst +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/doc/index.rst +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/doc/reference.rst +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/doc/tutorial.rst +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7643-8.1-user-minimal.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7643-8.2-user-full.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7643-8.3-enterprise_user.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7643-8.4-group.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7643-8.5-service_provider_configuration.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7643-8.6-resource_type-group.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7643-8.6-resource_type-user.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7643-8.7.1-schema-enterprise_user.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7643-8.7.1-schema-group.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7643-8.7.1-schema-user.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7643-8.7.2-schema-resource_type.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7643-8.7.2-schema-schema.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7643-8.7.2-schema-service_provider_configuration.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.12-error-bad_request.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.12-error-not_found.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.14-user-post_request.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.14-user-post_response.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.3-user-post_request.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.3-user-post_response.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.4.1-user-known-resource.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.4.2-list_response-partial_attributes.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.4.3-list_response-post_query.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.4.3-search_request.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.5.1-user-put_request.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.5.1-user-put_response.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.5.2.1-patch_op-add_emails.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.5.2.1-patch_op-add_members.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.5.2.2-patch_op-remove_all_members.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.5.2.2-patch_op-remove_and_add_one_member.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.5.2.2-patch_op-remove_multi_complex_value.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.5.2.2-patch_op-remove_one_member.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.5.2.3-patch_op-replace_all_email_values.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.5.2.3-patch_op-replace_all_members.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.5.2.3-patch_op-replace_street_address.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.5.2.3-patch_op-replace_user_work_address.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.6-error-not_found.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.7.1-bulk_request-circular_conflict.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.7.1-list_response-circular_reference.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.7.2-bulk_request-enterprise_user.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.7.2-bulk_request-temporary_identifier.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.7.2-bulk_response-temporary_identifier.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.7.3-bulk_request-multiple_operations.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.7.3-bulk_response-error_invalid_syntax.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.7.3-bulk_response-multiple_errors.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.7.3-bulk_response-multiple_operations.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.7.3-error-invalid_syntax.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.7.4-error-payload_too_large.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-3.9-user-partial_response.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/samples/rfc7644-4-list_response-resource_types.json +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/__init__.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/annotations.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/constants.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/context.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/messages/__init__.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/messages/bulk.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/messages/error.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/messages/list_response.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/py.typed +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/resources/__init__.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/resources/enterprise_user.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/resources/user.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/scim2_models/scim_object.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/__init__.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/conftest.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_dynamic_resources.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_dynamic_schemas.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_enterprise_user.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_errors.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_group.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_list_response.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_model_attributes.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_model_serialization.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_model_validation.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_models.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_patch_op_add.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_patch_op_extensions.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_patch_op_remove.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_patch_op_replace.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_path_validation.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_reference.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_resource_extension.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_resource_type.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_schema.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_search_request.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_service_provider_configuration.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_user.py +0 -0
- {scim2_models-0.4.0 → scim2_models-0.4.2}/tests/test_utils.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
repos:
|
|
3
3
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
4
|
-
rev: 'v0.12.
|
|
4
|
+
rev: 'v0.12.7'
|
|
5
5
|
hooks:
|
|
6
6
|
- id: ruff
|
|
7
7
|
args: [--fix, --exit-non-zero-on-fix]
|
|
@@ -16,9 +16,10 @@ repos:
|
|
|
16
16
|
exclude: "\\.svg$|\\.map$|\\.min\\.css$|\\.min\\.js$|\\.po$|\\.pot$"
|
|
17
17
|
- id: check-toml
|
|
18
18
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
19
|
-
rev: v1.17.
|
|
19
|
+
rev: v1.17.1
|
|
20
20
|
hooks:
|
|
21
21
|
- id: mypy
|
|
22
|
+
exclude: ^(tests/|conftest.py)
|
|
22
23
|
additional_dependencies:
|
|
23
24
|
- pydantic[email]>=2.7.0
|
|
24
25
|
- repo: https://github.com/codespell-project/codespell
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scim2-models
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
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.2] - 2025-08-05
|
|
5
|
+
--------------------
|
|
6
|
+
|
|
7
|
+
Fixed
|
|
8
|
+
^^^^^
|
|
9
|
+
- The library is 100% typed with mypy strict.
|
|
10
|
+
|
|
11
|
+
[0.4.1] - 2025-07-23
|
|
12
|
+
--------------------
|
|
13
|
+
|
|
14
|
+
Fixed
|
|
15
|
+
^^^^^
|
|
16
|
+
- Allow ``TypeVar`` as type parameters for :class:`~scim2_models.PatchOp`.
|
|
17
|
+
|
|
4
18
|
[0.4.0] - 2025-07-23
|
|
5
19
|
--------------------
|
|
6
20
|
|
|
@@ -62,6 +62,14 @@ html_theme_options = {
|
|
|
62
62
|
"title": "scim2-cli",
|
|
63
63
|
"url": "https://scim2-cli.readthedocs.io",
|
|
64
64
|
},
|
|
65
|
+
{
|
|
66
|
+
"title": "scim2-server",
|
|
67
|
+
"url": "https://github.com/python-scim/scim2-server",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"title": "pytest-scim2-server",
|
|
71
|
+
"url": "https://github.com/pytest-dev/pytest-scim2-server",
|
|
72
|
+
},
|
|
65
73
|
],
|
|
66
74
|
}
|
|
67
75
|
html_context = {
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scim2-models"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.2"
|
|
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"}
|
|
@@ -114,6 +114,7 @@ exclude = [
|
|
|
114
114
|
"tests/",
|
|
115
115
|
"conftest.py",
|
|
116
116
|
]
|
|
117
|
+
strict=true
|
|
117
118
|
|
|
118
119
|
[tool.pydantic-mypy]
|
|
119
120
|
init_forbid_extra = true
|
|
@@ -43,7 +43,7 @@ class MultiValuedComplexAttribute(ComplexAttribute):
|
|
|
43
43
|
value: Optional[Any] = None
|
|
44
44
|
"""The value of an entitlement."""
|
|
45
45
|
|
|
46
|
-
ref: Optional[Reference] = Field(None, serialization_alias="$ref")
|
|
46
|
+
ref: Optional[Reference[Any]] = Field(None, serialization_alias="$ref")
|
|
47
47
|
"""The reference URI of a target resource, if the attribute is a
|
|
48
48
|
reference."""
|
|
49
49
|
|
|
@@ -220,8 +220,8 @@ class BaseModel(PydanticBaseModel):
|
|
|
220
220
|
"""
|
|
221
221
|
|
|
222
222
|
def normalize_dict_keys(
|
|
223
|
-
input_dict: dict, model_class: type["BaseModel"]
|
|
224
|
-
) -> dict:
|
|
223
|
+
input_dict: dict[str, Any], model_class: type["BaseModel"]
|
|
224
|
+
) -> dict[str, Any]:
|
|
225
225
|
"""Normalize dictionary keys, preserving case for Any fields."""
|
|
226
226
|
result = {}
|
|
227
227
|
|
|
@@ -112,8 +112,8 @@ class _GenericMessageMetaclass(ModelMetaclass):
|
|
|
112
112
|
return klass
|
|
113
113
|
|
|
114
114
|
|
|
115
|
-
def _get_resource_class(obj) ->
|
|
115
|
+
def _get_resource_class(obj: BaseModel) -> type[Resource[Any]]:
|
|
116
116
|
"""Extract the resource class from generic type parameter."""
|
|
117
117
|
metadata = getattr(obj.__class__, "__pydantic_generic_metadata__", {"args": [None]})
|
|
118
118
|
resource_class = metadata["args"][0]
|
|
119
|
-
return resource_class
|
|
119
|
+
return resource_class # type: ignore[no-any-return]
|
|
@@ -5,6 +5,7 @@ from typing import Any
|
|
|
5
5
|
from typing import Generic
|
|
6
6
|
from typing import Optional
|
|
7
7
|
from typing import TypeVar
|
|
8
|
+
from typing import Union
|
|
8
9
|
|
|
9
10
|
from pydantic import Field
|
|
10
11
|
from pydantic import ValidationInfo
|
|
@@ -26,7 +27,7 @@ from .error import Error
|
|
|
26
27
|
from .message import Message
|
|
27
28
|
from .message import _get_resource_class
|
|
28
29
|
|
|
29
|
-
T = TypeVar("T", bound=Resource)
|
|
30
|
+
T = TypeVar("T", bound=Resource[Any])
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
class PatchOperation(ComplexAttribute):
|
|
@@ -51,7 +52,7 @@ class PatchOperation(ComplexAttribute):
|
|
|
51
52
|
describing the target of the operation."""
|
|
52
53
|
|
|
53
54
|
def _validate_mutability(
|
|
54
|
-
self, resource_class: type[
|
|
55
|
+
self, resource_class: type[Resource[Any]], field_name: str
|
|
55
56
|
) -> None:
|
|
56
57
|
"""Validate mutability constraints."""
|
|
57
58
|
# RFC 7644 Section 3.5.2: "Servers should be tolerant of schema extensions"
|
|
@@ -72,7 +73,7 @@ class PatchOperation(ComplexAttribute):
|
|
|
72
73
|
raise ValueError(Error.make_mutability_error().detail)
|
|
73
74
|
|
|
74
75
|
def _validate_required_attribute(
|
|
75
|
-
self, resource_class: type[
|
|
76
|
+
self, resource_class: type[Resource[Any]], field_name: str
|
|
76
77
|
) -> None:
|
|
77
78
|
"""Validate required attribute constraints for remove operations."""
|
|
78
79
|
# RFC 7644 Section 3.5.2.3: Only validate for remove operations
|
|
@@ -143,7 +144,7 @@ class PatchOp(Message, Generic[T]):
|
|
|
143
144
|
- Using PatchOp without a type parameter raises TypeError
|
|
144
145
|
"""
|
|
145
146
|
|
|
146
|
-
def __new__(cls, *args, **kwargs):
|
|
147
|
+
def __new__(cls, *args: Any, **kwargs: Any) -> Self:
|
|
147
148
|
"""Create new PatchOp instance with type parameter validation.
|
|
148
149
|
|
|
149
150
|
Only handles the case of direct instantiation without type parameter (PatchOp()).
|
|
@@ -162,25 +163,48 @@ class PatchOp(Message, Generic[T]):
|
|
|
162
163
|
|
|
163
164
|
return super().__new__(cls)
|
|
164
165
|
|
|
165
|
-
def __class_getitem__(
|
|
166
|
+
def __class_getitem__(
|
|
167
|
+
cls, typevar_values: Union[type[Resource[Any]], tuple[type[Resource[Any]], ...]]
|
|
168
|
+
) -> Any:
|
|
166
169
|
"""Validate type parameter when creating parameterized type.
|
|
167
170
|
|
|
168
|
-
Ensures the type parameter is a concrete Resource subclass (not Resource itself)
|
|
169
|
-
Rejects invalid types (str, int, etc.) and Union types.
|
|
171
|
+
Ensures the type parameter is a concrete Resource subclass (not Resource itself)
|
|
172
|
+
or a TypeVar bound to Resource. Rejects invalid types (str, int, etc.) and Union types.
|
|
170
173
|
"""
|
|
174
|
+
if isinstance(typevar_values, TypeVar):
|
|
175
|
+
# Check if TypeVar is bound to Resource or its subclass
|
|
176
|
+
if typevar_values.__bound__ is not None and (
|
|
177
|
+
typevar_values.__bound__ is Resource
|
|
178
|
+
or (
|
|
179
|
+
isclass(typevar_values.__bound__)
|
|
180
|
+
and issubclass(typevar_values.__bound__, Resource)
|
|
181
|
+
)
|
|
182
|
+
):
|
|
183
|
+
return super().__class_getitem__(typevar_values)
|
|
184
|
+
else:
|
|
185
|
+
raise TypeError(
|
|
186
|
+
f"PatchOp TypeVar must be bound to Resource or its subclass, got {typevar_values}. "
|
|
187
|
+
"Example: T = TypeVar('T', bound=Resource)"
|
|
188
|
+
)
|
|
189
|
+
|
|
171
190
|
# Check if type parameter is a concrete Resource subclass (not Resource itself)
|
|
172
|
-
if
|
|
191
|
+
if typevar_values is Resource:
|
|
173
192
|
raise TypeError(
|
|
174
193
|
"PatchOp requires a concrete Resource subclass, not Resource itself. "
|
|
175
194
|
"Use PatchOp[User], PatchOp[Group], etc. instead of PatchOp[Resource]."
|
|
176
195
|
)
|
|
177
|
-
|
|
196
|
+
|
|
197
|
+
if not (
|
|
198
|
+
isclass(typevar_values)
|
|
199
|
+
and issubclass(typevar_values, Resource)
|
|
200
|
+
and typevar_values is not Resource
|
|
201
|
+
):
|
|
178
202
|
raise TypeError(
|
|
179
|
-
f"PatchOp type parameter must be a concrete Resource subclass, got {
|
|
203
|
+
f"PatchOp type parameter must be a concrete Resource subclass or TypeVar, got {typevar_values}. "
|
|
180
204
|
"Use PatchOp[User], PatchOp[Group], etc."
|
|
181
205
|
)
|
|
182
206
|
|
|
183
|
-
return super().__class_getitem__(
|
|
207
|
+
return super().__class_getitem__(typevar_values)
|
|
184
208
|
|
|
185
209
|
schemas: Annotated[list[str], Required.true] = [
|
|
186
210
|
"urn:ietf:params:scim:api:messages:2.0:PatchOp"
|
|
@@ -240,7 +264,9 @@ class PatchOp(Message, Generic[T]):
|
|
|
240
264
|
|
|
241
265
|
return modified
|
|
242
266
|
|
|
243
|
-
def _apply_operation(
|
|
267
|
+
def _apply_operation(
|
|
268
|
+
self, resource: Resource[Any], operation: PatchOperation
|
|
269
|
+
) -> bool:
|
|
244
270
|
"""Apply a single patch operation to a resource.
|
|
245
271
|
|
|
246
272
|
:return: :data:`True` if the resource was modified, else :data:`False`.
|
|
@@ -252,7 +278,9 @@ class PatchOp(Message, Generic[T]):
|
|
|
252
278
|
|
|
253
279
|
raise ValueError(Error.make_invalid_value_error().detail)
|
|
254
280
|
|
|
255
|
-
def _apply_add_replace(
|
|
281
|
+
def _apply_add_replace(
|
|
282
|
+
self, resource: Resource[Any], operation: PatchOperation
|
|
283
|
+
) -> bool:
|
|
256
284
|
"""Apply an add or replace operation."""
|
|
257
285
|
# RFC 7644 Section 3.5.2.1: "If path is specified, add/replace at that path"
|
|
258
286
|
if operation.path is not None:
|
|
@@ -266,7 +294,7 @@ class PatchOp(Message, Generic[T]):
|
|
|
266
294
|
# RFC 7644 Section 3.5.2.1: "If no path specified, add/replace at root level"
|
|
267
295
|
return self._apply_root_attributes(resource, operation.value)
|
|
268
296
|
|
|
269
|
-
def _apply_remove(self, resource: Resource, operation: PatchOperation) -> bool:
|
|
297
|
+
def _apply_remove(self, resource: Resource[Any], operation: PatchOperation) -> bool:
|
|
270
298
|
"""Apply a remove operation."""
|
|
271
299
|
# RFC 7644 Section 3.5.2.3: "Path is required for remove operations"
|
|
272
300
|
if operation.path is None:
|
|
@@ -280,7 +308,8 @@ class PatchOp(Message, Generic[T]):
|
|
|
280
308
|
|
|
281
309
|
return self._remove_value_at_path(resource, operation.path)
|
|
282
310
|
|
|
283
|
-
|
|
311
|
+
@classmethod
|
|
312
|
+
def _apply_root_attributes(cls, resource: BaseModel, value: Any) -> bool:
|
|
284
313
|
"""Apply attributes to the resource root."""
|
|
285
314
|
if not isinstance(value, dict):
|
|
286
315
|
return False
|
|
@@ -298,8 +327,9 @@ class PatchOp(Message, Generic[T]):
|
|
|
298
327
|
|
|
299
328
|
return modified
|
|
300
329
|
|
|
330
|
+
@classmethod
|
|
301
331
|
def _set_value_at_path(
|
|
302
|
-
|
|
332
|
+
cls, resource: Resource[Any], path: str, value: Any, is_add: bool
|
|
303
333
|
) -> bool:
|
|
304
334
|
"""Set a value at a specific path."""
|
|
305
335
|
target, attr_path = _resolve_path_to_target(resource, path)
|
|
@@ -309,12 +339,13 @@ class PatchOp(Message, Generic[T]):
|
|
|
309
339
|
|
|
310
340
|
path_parts = attr_path.split(".")
|
|
311
341
|
if len(path_parts) == 1:
|
|
312
|
-
return
|
|
342
|
+
return cls._set_simple_attribute(target, path_parts[0], value, is_add)
|
|
313
343
|
|
|
314
|
-
return
|
|
344
|
+
return cls._set_complex_attribute(target, path_parts, value, is_add)
|
|
315
345
|
|
|
346
|
+
@classmethod
|
|
316
347
|
def _set_simple_attribute(
|
|
317
|
-
|
|
348
|
+
cls, resource: BaseModel, attr_name: str, value: Any, is_add: bool
|
|
318
349
|
) -> bool:
|
|
319
350
|
"""Set a value on a simple (non-nested) attribute."""
|
|
320
351
|
field_name = _find_field_name(type(resource), attr_name)
|
|
@@ -322,8 +353,8 @@ class PatchOp(Message, Generic[T]):
|
|
|
322
353
|
raise ValueError(Error.make_no_target_error().detail)
|
|
323
354
|
|
|
324
355
|
# RFC 7644 Section 3.5.2.1: "For multi-valued attributes, add operation appends values"
|
|
325
|
-
if is_add and
|
|
326
|
-
return
|
|
356
|
+
if is_add and cls._is_multivalued_field(resource, field_name):
|
|
357
|
+
return cls._handle_multivalued_add(resource, field_name, value)
|
|
327
358
|
|
|
328
359
|
old_value = getattr(resource, field_name)
|
|
329
360
|
if old_value == value:
|
|
@@ -332,8 +363,9 @@ class PatchOp(Message, Generic[T]):
|
|
|
332
363
|
setattr(resource, field_name, value)
|
|
333
364
|
return True
|
|
334
365
|
|
|
366
|
+
@classmethod
|
|
335
367
|
def _set_complex_attribute(
|
|
336
|
-
|
|
368
|
+
cls, resource: BaseModel, path_parts: list[str], value: Any, is_add: bool
|
|
337
369
|
) -> bool:
|
|
338
370
|
"""Set a value on a complex (nested) attribute."""
|
|
339
371
|
parent_attr = path_parts[0]
|
|
@@ -345,38 +377,45 @@ class PatchOp(Message, Generic[T]):
|
|
|
345
377
|
|
|
346
378
|
parent_obj = getattr(resource, parent_field_name)
|
|
347
379
|
if parent_obj is None:
|
|
348
|
-
parent_obj =
|
|
380
|
+
parent_obj = cls._create_parent_object(resource, parent_field_name)
|
|
349
381
|
if parent_obj is None:
|
|
350
382
|
return False
|
|
351
383
|
|
|
352
|
-
return
|
|
384
|
+
return cls._set_value_at_path(parent_obj, sub_path, value, is_add)
|
|
353
385
|
|
|
354
|
-
|
|
386
|
+
@classmethod
|
|
387
|
+
def _is_multivalued_field(cls, resource: BaseModel, field_name: str) -> bool:
|
|
355
388
|
"""Check if a field is multi-valued."""
|
|
356
389
|
return hasattr(resource, field_name) and type(resource).get_field_multiplicity(
|
|
357
390
|
field_name
|
|
358
391
|
)
|
|
359
392
|
|
|
393
|
+
@classmethod
|
|
360
394
|
def _handle_multivalued_add(
|
|
361
|
-
|
|
395
|
+
cls, resource: BaseModel, field_name: str, value: Any
|
|
362
396
|
) -> bool:
|
|
363
397
|
"""Handle adding values to a multi-valued attribute."""
|
|
364
398
|
current_list = getattr(resource, field_name) or []
|
|
365
399
|
|
|
366
400
|
# RFC 7644 Section 3.5.2.1: "Add operation appends values to multi-valued attributes"
|
|
367
401
|
if isinstance(value, list):
|
|
368
|
-
return
|
|
402
|
+
return cls._add_multiple_values(resource, field_name, current_list, value)
|
|
369
403
|
|
|
370
|
-
return
|
|
404
|
+
return cls._add_single_value(resource, field_name, current_list, value)
|
|
371
405
|
|
|
406
|
+
@classmethod
|
|
372
407
|
def _add_multiple_values(
|
|
373
|
-
|
|
408
|
+
cls,
|
|
409
|
+
resource: BaseModel,
|
|
410
|
+
field_name: str,
|
|
411
|
+
current_list: list[Any],
|
|
412
|
+
values: list[Any],
|
|
374
413
|
) -> bool:
|
|
375
414
|
"""Add multiple values to a multi-valued attribute."""
|
|
376
415
|
new_values = []
|
|
377
416
|
# RFC 7644 Section 3.5.2.1: "Do not add duplicate values"
|
|
378
417
|
for new_val in values:
|
|
379
|
-
if not
|
|
418
|
+
if not cls._value_exists_in_list(current_list, new_val):
|
|
380
419
|
new_values.append(new_val)
|
|
381
420
|
|
|
382
421
|
if not new_values:
|
|
@@ -385,23 +424,26 @@ class PatchOp(Message, Generic[T]):
|
|
|
385
424
|
setattr(resource, field_name, current_list + new_values)
|
|
386
425
|
return True
|
|
387
426
|
|
|
427
|
+
@classmethod
|
|
388
428
|
def _add_single_value(
|
|
389
|
-
|
|
429
|
+
cls, resource: BaseModel, field_name: str, current_list: list[Any], value: Any
|
|
390
430
|
) -> bool:
|
|
391
431
|
"""Add a single value to a multi-valued attribute."""
|
|
392
432
|
# RFC 7644 Section 3.5.2.1: "Do not add duplicate values"
|
|
393
|
-
if
|
|
433
|
+
if cls._value_exists_in_list(current_list, value):
|
|
394
434
|
return False
|
|
395
435
|
|
|
396
436
|
current_list.append(value)
|
|
397
437
|
setattr(resource, field_name, current_list)
|
|
398
438
|
return True
|
|
399
439
|
|
|
400
|
-
|
|
440
|
+
@classmethod
|
|
441
|
+
def _value_exists_in_list(cls, current_list: list[Any], new_value: Any) -> bool:
|
|
401
442
|
"""Check if a value already exists in a list."""
|
|
402
|
-
return any(
|
|
443
|
+
return any(cls._values_match(item, new_value) for item in current_list)
|
|
403
444
|
|
|
404
|
-
|
|
445
|
+
@classmethod
|
|
446
|
+
def _create_parent_object(cls, resource: BaseModel, parent_field_name: str) -> Any:
|
|
405
447
|
"""Create a parent object if it doesn't exist."""
|
|
406
448
|
parent_class = type(resource).get_field_root_type(parent_field_name)
|
|
407
449
|
if not parent_class or not isclass(parent_class):
|
|
@@ -411,7 +453,8 @@ class PatchOp(Message, Generic[T]):
|
|
|
411
453
|
setattr(resource, parent_field_name, parent_obj)
|
|
412
454
|
return parent_obj
|
|
413
455
|
|
|
414
|
-
|
|
456
|
+
@classmethod
|
|
457
|
+
def _remove_value_at_path(cls, resource: Resource[Any], path: str) -> bool:
|
|
415
458
|
"""Remove a value at a specific path."""
|
|
416
459
|
target, attr_path = _resolve_path_to_target(resource, path)
|
|
417
460
|
|
|
@@ -434,10 +477,11 @@ class PatchOp(Message, Generic[T]):
|
|
|
434
477
|
return True
|
|
435
478
|
|
|
436
479
|
sub_path = ".".join(path_parts)
|
|
437
|
-
return
|
|
480
|
+
return cls._remove_value_at_path(parent_obj, sub_path)
|
|
438
481
|
|
|
482
|
+
@classmethod
|
|
439
483
|
def _remove_specific_value(
|
|
440
|
-
|
|
484
|
+
cls, resource: Resource[Any], path: str, value_to_remove: Any
|
|
441
485
|
) -> bool:
|
|
442
486
|
"""Remove a specific value from a multi-valued attribute."""
|
|
443
487
|
target, attr_path = _resolve_path_to_target(resource, path)
|
|
@@ -458,7 +502,7 @@ class PatchOp(Message, Generic[T]):
|
|
|
458
502
|
modified = False
|
|
459
503
|
# RFC 7644 Section 3.5.2.3: "Remove matching values from multi-valued attributes"
|
|
460
504
|
for item in current_list:
|
|
461
|
-
if not
|
|
505
|
+
if not cls._values_match(item, value_to_remove):
|
|
462
506
|
new_list.append(item)
|
|
463
507
|
else:
|
|
464
508
|
modified = True
|
|
@@ -469,10 +513,11 @@ class PatchOp(Message, Generic[T]):
|
|
|
469
513
|
|
|
470
514
|
return False
|
|
471
515
|
|
|
472
|
-
|
|
516
|
+
@classmethod
|
|
517
|
+
def _values_match(cls, value1: Any, value2: Any) -> bool:
|
|
473
518
|
"""Check if two values match, converting BaseModel to dict for comparison."""
|
|
474
519
|
|
|
475
|
-
def to_dict(value):
|
|
520
|
+
def to_dict(value: Any) -> dict[str, Any]:
|
|
476
521
|
return value.model_dump() if isinstance(value, BaseModel) else value
|
|
477
522
|
|
|
478
523
|
return to_dict(value1) == to_dict(value2)
|
|
@@ -71,9 +71,7 @@ class SearchRequest(Message):
|
|
|
71
71
|
"""Validate syntax of sort_by attribute path.
|
|
72
72
|
|
|
73
73
|
:param v: The sort_by attribute path to validate
|
|
74
|
-
:type v: Optional[str]
|
|
75
74
|
:return: The validated sort_by attribute path
|
|
76
|
-
:rtype: Optional[str]
|
|
77
75
|
:raises ValueError: If sort_by attribute path has invalid syntax
|
|
78
76
|
"""
|
|
79
77
|
if v is None:
|
|
@@ -61,9 +61,7 @@ class Reference(UserString, Generic[ReferenceTypes]):
|
|
|
61
61
|
"""Get reference types from a type annotation.
|
|
62
62
|
|
|
63
63
|
:param type_annotation: Type annotation to extract reference types from
|
|
64
|
-
:type type_annotation: Any
|
|
65
64
|
:return: List of reference type strings
|
|
66
|
-
:rtype: list[str]
|
|
67
65
|
"""
|
|
68
66
|
first_arg = get_args(type_annotation)[0]
|
|
69
67
|
types = (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from typing import Annotated
|
|
2
|
+
from typing import Any
|
|
2
3
|
from typing import ClassVar
|
|
3
4
|
from typing import Literal
|
|
4
5
|
from typing import Optional
|
|
@@ -33,7 +34,7 @@ class GroupMember(MultiValuedComplexAttribute):
|
|
|
33
34
|
display: Annotated[Optional[str], Mutability.read_only] = None
|
|
34
35
|
|
|
35
36
|
|
|
36
|
-
class Group(Resource):
|
|
37
|
+
class Group(Resource[Any]):
|
|
37
38
|
schemas: Annotated[list[str], Required.true] = [
|
|
38
39
|
"urn:ietf:params:scim:schemas:core:2.0:Group"
|
|
39
40
|
]
|
|
@@ -148,7 +148,7 @@ class Resource(ScimObject, Generic[AnyExtension]):
|
|
|
148
148
|
"""A complex attribute containing resource metadata."""
|
|
149
149
|
|
|
150
150
|
@classmethod
|
|
151
|
-
def __class_getitem__(cls, item: Any) -> type["Resource"]:
|
|
151
|
+
def __class_getitem__(cls, item: Any) -> type["Resource[Any]"]:
|
|
152
152
|
"""Create a Resource class with extension fields dynamically added."""
|
|
153
153
|
if hasattr(cls, "__scim_extension_metadata__"):
|
|
154
154
|
return cls
|
|
@@ -241,12 +241,12 @@ class Resource(ScimObject, Generic[AnyExtension]):
|
|
|
241
241
|
|
|
242
242
|
@staticmethod
|
|
243
243
|
def get_by_schema(
|
|
244
|
-
resource_types: list[type["Resource"]],
|
|
244
|
+
resource_types: list[type["Resource[Any]"]],
|
|
245
245
|
schema: str,
|
|
246
246
|
with_extensions: bool = True,
|
|
247
|
-
) -> Optional[Union[type["Resource"], type["Extension"]]]:
|
|
247
|
+
) -> Optional[Union[type["Resource[Any]"], type["Extension"]]]:
|
|
248
248
|
"""Given a resource type list and a schema, find the matching resource type."""
|
|
249
|
-
by_schema: dict[str, Union[type[Resource], type[Extension]]] = {
|
|
249
|
+
by_schema: dict[str, Union[type[Resource[Any]], type[Extension]]] = {
|
|
250
250
|
resource_type.model_fields["schemas"].default[0].lower(): resource_type
|
|
251
251
|
for resource_type in (resource_types or [])
|
|
252
252
|
}
|
|
@@ -263,7 +263,7 @@ class Resource(ScimObject, Generic[AnyExtension]):
|
|
|
263
263
|
|
|
264
264
|
@staticmethod
|
|
265
265
|
def get_by_payload(
|
|
266
|
-
resource_types: list[type["Resource"]],
|
|
266
|
+
resource_types: list[type["Resource[Any]"]],
|
|
267
267
|
payload: dict[str, Any],
|
|
268
268
|
**kwargs: Any,
|
|
269
269
|
) -> Optional[type]:
|
|
@@ -291,7 +291,7 @@ class Resource(ScimObject, Generic[AnyExtension]):
|
|
|
291
291
|
return _model_to_schema(cls)
|
|
292
292
|
|
|
293
293
|
@classmethod
|
|
294
|
-
def from_schema(cls, schema: "Schema") -> type["Resource"]:
|
|
294
|
+
def from_schema(cls, schema: "Schema") -> type["Resource[Any]"]:
|
|
295
295
|
"""Build a :class:`scim2_models.Resource` subclass from the schema definition."""
|
|
296
296
|
from .schema import _make_python_model
|
|
297
297
|
|
|
@@ -372,7 +372,7 @@ class Resource(ScimObject, Generic[AnyExtension]):
|
|
|
372
372
|
return super(ScimObject, self).model_dump_json(*args, **dump_kwargs)
|
|
373
373
|
|
|
374
374
|
|
|
375
|
-
AnyResource = TypeVar("AnyResource", bound="Resource")
|
|
375
|
+
AnyResource = TypeVar("AnyResource", bound="Resource[Any]")
|
|
376
376
|
|
|
377
377
|
|
|
378
378
|
def _dedicated_attributes(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from typing import Annotated
|
|
2
|
+
from typing import Any
|
|
2
3
|
from typing import Optional
|
|
3
4
|
|
|
4
5
|
from pydantic import Field
|
|
@@ -34,7 +35,7 @@ class SchemaExtension(ComplexAttribute):
|
|
|
34
35
|
"""
|
|
35
36
|
|
|
36
37
|
|
|
37
|
-
class ResourceType(Resource):
|
|
38
|
+
class ResourceType(Resource[Any]):
|
|
38
39
|
schemas: Annotated[list[str], Required.true] = [
|
|
39
40
|
"urn:ietf:params:scim:schemas:core:2.0:ResourceType"
|
|
40
41
|
]
|
|
@@ -78,7 +79,7 @@ class ResourceType(Resource):
|
|
|
78
79
|
"""A list of URIs of the resource type's schema extensions."""
|
|
79
80
|
|
|
80
81
|
@classmethod
|
|
81
|
-
def from_resource(cls, resource_model: type[Resource]) -> Self:
|
|
82
|
+
def from_resource(cls, resource_model: type[Resource[Any]]) -> Self:
|
|
82
83
|
"""Build a naive ResourceType from a resource model."""
|
|
83
84
|
schema = resource_model.model_fields["schemas"].default[0]
|
|
84
85
|
name = schema.split(":")[-1]
|
|
@@ -258,7 +258,7 @@ class Attribute(ComplexAttribute):
|
|
|
258
258
|
raise KeyError(f"This attribute has no '{name}' sub-attribute")
|
|
259
259
|
|
|
260
260
|
|
|
261
|
-
class Schema(Resource):
|
|
261
|
+
class Schema(Resource[Any]):
|
|
262
262
|
schemas: Annotated[list[str], Required.true] = [
|
|
263
263
|
"urn:ietf:params:scim:schemas:core:2.0:Schema"
|
|
264
264
|
]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
from typing import Annotated
|
|
3
|
+
from typing import Any
|
|
3
4
|
from typing import Optional
|
|
4
5
|
|
|
5
6
|
from pydantic import Field
|
|
@@ -93,7 +94,7 @@ class AuthenticationScheme(ComplexAttribute):
|
|
|
93
94
|
address."""
|
|
94
95
|
|
|
95
96
|
|
|
96
|
-
class ServiceProviderConfig(Resource):
|
|
97
|
+
class ServiceProviderConfig(Resource[Any]):
|
|
97
98
|
schemas: Annotated[list[str], Required.true] = [
|
|
98
99
|
"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"
|
|
99
100
|
]
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING
|
|
2
2
|
from typing import Any
|
|
3
3
|
from typing import Optional
|
|
4
|
+
from typing import Union
|
|
4
5
|
|
|
5
6
|
from .base import BaseModel
|
|
6
7
|
from .utils import _normalize_attribute_name
|
|
7
8
|
|
|
8
9
|
if TYPE_CHECKING:
|
|
9
10
|
from .base import BaseModel
|
|
11
|
+
from .resources.resource import Extension
|
|
10
12
|
from .resources.resource import Resource
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
def _get_or_create_extension_instance(
|
|
14
|
-
model: "Resource", extension_class: type
|
|
15
|
-
) -> "
|
|
16
|
+
model: "Resource[Any]", extension_class: type
|
|
17
|
+
) -> "Extension":
|
|
16
18
|
"""Get existing extension instance or create a new one."""
|
|
17
19
|
extension_instance = model[extension_class]
|
|
18
20
|
if extension_instance is None:
|
|
@@ -62,7 +64,7 @@ def _validate_model_attribute(model: type["BaseModel"], attribute_base: str) ->
|
|
|
62
64
|
|
|
63
65
|
|
|
64
66
|
def _validate_attribute_urn(
|
|
65
|
-
attribute_name: str, resource: type["Resource"]
|
|
67
|
+
attribute_name: str, resource: type["Resource[Any]"]
|
|
66
68
|
) -> Optional[str]:
|
|
67
69
|
"""Validate that an attribute urn is valid or not.
|
|
68
70
|
|
|
@@ -87,8 +89,8 @@ def _validate_attribute_urn(
|
|
|
87
89
|
|
|
88
90
|
|
|
89
91
|
def _resolve_path_to_target(
|
|
90
|
-
resource: "Resource", path: str
|
|
91
|
-
) -> tuple[Optional["
|
|
92
|
+
resource: "Resource[Any]", path: str
|
|
93
|
+
) -> tuple[Optional[Union["Resource[Any]", "Extension"]], str]:
|
|
92
94
|
"""Resolve a path to a target and an attribute_path.
|
|
93
95
|
|
|
94
96
|
The target can be the resource itself, or an extension object.
|