Skip to content

Commit 7572307

Browse files
committed
chore: bump to scim2-client 0.9.0
1 parent 1b12ea0 commit 7572307

24 files changed

Lines changed: 349 additions & 171 deletions

‎doc/changelog.rst‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,39 @@ Changelog
44
[Unreleased]
55
------------
66

7+
Added
8+
^^^^^
9+
- A check reporting a service description that cannot be composed, such as a
10+
:class:`~scim2_models.ResourceType` naming a schema ``/Schemas`` does not publish.
11+
The remaining checks used to be silently skipped.
12+
13+
Changed
14+
^^^^^^^
15+
- scim2-client 0.9.0 and scim2-models 0.8.0 are now the minimum supported versions.
16+
- What the discovery endpoints publish is registered on the client as a
17+
:class:`~scim2_models.ScimProvider`, in place of its resource models, its resource types
18+
and its service provider configuration.
19+
20+
Removed
21+
^^^^^^^
22+
- **Breaking:** the ``httpx`` packaging extra, as scim2-client dropped its own.
23+
The engines require `httpx2 <https://github.com/pydantic/httpx2>`_, pulled by the
24+
``httpx2`` extra. An application that cannot migrate all its dependencies at once can
25+
call :code:`httpx2.alias_httpx()` at the very top of its entrypoint, so that
26+
:code:`import httpx` resolves to httpx2 process-wide.
27+
728
Fixed
829
^^^^^
930
- The :attr:`~scim2_tester.CheckResult.data` of a failed check holds the decoded response
1031
payload instead of the response object, whose representation only carries a status code.
32+
- Two resource types built upon a same schema are told apart, where the model of the first
33+
one declaring the schema used to answer for both.
34+
- The :attr:`~scim2_tester.CheckResult.data` of a check failed against the
35+
:class:`~scim2_client.engines.werkzeug.TestSCIMClient` engine holds the response payload,
36+
where reading it raised a :class:`TypeError`.
37+
- :meth:`~scim2_tester.check_server` takes any synchronous client, as it always did, instead
38+
of declaring the httpx2 one. Importing ``scim2_tester`` no longer requires the ``httpx2``
39+
packaging extra, which only the command line needs.
1140

1241
[0.3.0] - 2026-09-20
1342
--------------------

‎doc/tutorial.rst‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ scim2-tester is integrated in :doc:`scim2-cli <scim2_cli:index>`:
1212
export SCIM_CLI_URL="https://auth.example"
1313
scim test
1414
15-
You can check the :ref:`scim2-cli test command reference <scim2_cli:test>` for more details.
15+
You can check the :ref:`scim2-cli test command reference <scim2_cli:reference:test>` for more details.
1616

1717
Code integration
1818
================
1919

20-
If you need to integrate the tester in your code, you can initialize a :ref:`scim2-client engine <scim2_client:tutorial:engines>`
20+
If you need to integrate the tester in your code, you can initialize a :ref:`scim2-client engine <scim2_client:reference:engines>`
2121
and pass it to the :meth:`~scim2_tester.check_server` method:
2222

2323
.. code-block:: python

‎pyproject.toml‎

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ classifiers = [
2626

2727
requires-python = ">= 3.10"
2828
dependencies = [
29-
"scim2-client>=0.8.0",
30-
"scim2-models>=0.7.0",
29+
"scim2-client>=0.9.0",
30+
"scim2-models>=0.8.0",
3131
]
3232

3333
[project.urls]
@@ -41,12 +41,7 @@ module-root = ""
4141

4242
[project.optional-dependencies]
4343
httpx2 = [
44-
"scim2-client[httpx2]>=0.8.0",
45-
]
46-
47-
# Deprecated, will be removed in 0.9.
48-
httpx = [
49-
"scim2-client[httpx]>=0.8.0",
44+
"scim2-client[httpx2]>=0.9.0",
5045
]
5146

5247
[dependency-groups]
@@ -138,7 +133,6 @@ env_list = [
138133
"py312",
139134
"py313",
140135
"py314",
141-
"legacy",
142136
"minversions",
143137
"doc",
144138
"coverage",
@@ -158,9 +152,6 @@ commands = [
158152
["prek", "run", "--all-files", "--show-diff-on-failure"],
159153
]
160154

161-
[tool.tox.env.legacy]
162-
extras = ["httpx"]
163-
164155
[tool.tox.env.minversions]
165156
base_python = ["python3.10"]
166157
runner = "uv-venv-runner"

‎scim2_tester/checker.py‎

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from typing import Any
22

3-
from scim2_client.engines.httpx2 import SyncSCIMClient
3+
from scim2_client import BaseSyncSCIMClient
4+
from scim2_models import ScimProvider
5+
from scim2_models import ScimProviderError
46

57
from scim2_tester.checkers import random_url
68
from scim2_tester.checkers import resource_type_tests
@@ -11,6 +13,7 @@
1113
from scim2_tester.utils import CheckContext
1214
from scim2_tester.utils import CheckResult
1315
from scim2_tester.utils import Status
16+
from scim2_tester.utils import check_result
1417

1518

1619
def _discovered_objects(results: list[CheckResult]) -> Any:
@@ -23,8 +26,56 @@ def _discovered_objects(results: list[CheckResult]) -> Any:
2326
return results[0].data if results[0].status == Status.SUCCESS else None
2427

2528

29+
def _describe_service(
30+
context: CheckContext,
31+
results_spc: list[CheckResult],
32+
results_resource_types: list[CheckResult],
33+
results_schemas: list[CheckResult],
34+
) -> tuple[ScimProvider | None, list[CheckResult]]:
35+
"""Compose the description of the server from what its discovery endpoints published.
36+
37+
What the client already describes wins over what the server publishes, as
38+
:meth:`~scim2_client.BaseSyncSCIMClient.discover` does. The description is
39+
registered on the client so the remaining checks are run against it.
40+
"""
41+
provider = context.client.provider
42+
config = provider.config or _discovered_objects(results_spc)
43+
44+
if provider.models:
45+
described = ScimProvider(
46+
provider.models, provider.resource_types, config, provider.policy
47+
)
48+
context.client.provider = described
49+
return described, []
50+
51+
resource_types = _discovered_objects(results_resource_types)
52+
schemas = _discovered_objects(results_schemas)
53+
if not resource_types or not schemas:
54+
return None, []
55+
56+
try:
57+
described = ScimProvider.from_discovery(
58+
schemas, resource_types, config, provider.policy
59+
)
60+
except ScimProviderError as exc:
61+
return None, [
62+
check_result(
63+
context,
64+
status=Status.ERROR,
65+
title="service_description",
66+
description="Compose the resource models the server serves from "
67+
"the schemas and the resource types it publishes.",
68+
reason=str(exc),
69+
tags={"discovery"},
70+
)
71+
]
72+
73+
context.client.provider = described
74+
return described, []
75+
76+
2677
def check_server(
27-
client: SyncSCIMClient,
78+
client: BaseSyncSCIMClient,
2879
raise_exceptions: bool = False,
2980
include_tags: set[str] | None = None,
3081
exclude_tags: set[str] | None = None,
@@ -34,7 +85,8 @@ def check_server(
3485
3586
It starts by retrieving the standard :class:`~scim2_models.ServiceProviderConfig`,
3687
:class:`~scim2_models.Schema` and :class:`~scim2_models.ResourceType` endpoints.
37-
Those configuration resources will be registered to the client if no other have been registered yet.
88+
What they publish describes the server as a :class:`~scim2_models.ScimProvider`
89+
registered on the client, leaving untouched whatever the client already described.
3890
3991
Then for all available resources (whether they have been manually configured in the client,
4092
or dynamically discovered by the checker), it perform a series of creation, query, replacement and deletion.
@@ -86,33 +138,25 @@ def check_server(
86138

87139
results_spc = service_provider_config_endpoint(context)
88140
results.extend(results_spc)
89-
if not client.service_provider_config:
90-
client.service_provider_config = _discovered_objects(results_spc)
91141

92142
results_resource_types = _resource_types_endpoint(context)
93143
results.extend(results_resource_types)
94-
if not client.resource_types:
95-
client.resource_types = _discovered_objects(results_resource_types)
96144

97145
results_schemas = _schemas_endpoint(context)
98146
results.extend(results_schemas)
99-
schemas = _discovered_objects(results_schemas)
100-
if not client.resource_models and schemas:
101-
client.resource_models = client.build_resource_models(
102-
client.resource_types or [], schemas
103-
)
104147

105-
if (
106-
not client.service_provider_config
107-
or not client.resource_types
108-
or not client.resource_models
109-
):
148+
provider, results_description = _describe_service(
149+
context, results_spc, results_resource_types, results_schemas
150+
)
151+
results.extend(results_description)
152+
153+
if provider is None or not provider.config:
110154
return results
111155

112156
result_random = random_url(context)
113157
results.extend(result_random)
114158

115-
for resource_type in client.resource_types or []:
159+
for resource_type in provider.resource_types:
116160
if conf.resource_types and resource_type.name not in conf.resource_types:
117161
continue
118162

‎scim2_tester/checkers/patch_add.py‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
from scim2_models import Mutability
77
from scim2_models import PatchOp
88
from scim2_models import PatchOperation
9+
from scim2_models import Path
910
from scim2_models import Required
1011
from scim2_models import Resource
1112
from scim2_models import SCIMException
12-
from scim2_models.path import Path
1313

1414
from ..filling import generate_random_value
1515
from ..utils import CheckContext
@@ -46,8 +46,8 @@ def check_add_attribute(
4646
an existing resource."
4747
"""
4848
if (
49-
context.client.service_provider_config
50-
and not context.client.service_provider_config.patch.supported
49+
context.client.provider.config
50+
and not context.client.provider.config.patch.supported
5151
):
5252
return [
5353
check_result(
@@ -86,7 +86,8 @@ def check_add_attribute(
8686
for path in all_paths:
8787
urn = str(path)
8888
patch_value = generate_random_value(context, path=path)
89-
mutability = path.get_annotation(Mutability)
89+
binding = path.resolve()
90+
mutability = binding.get_annotation(Mutability) if binding else None
9091

9192
patch_op = PatchOp[type(base_resource)](
9293
operations=[

‎scim2_tester/checkers/patch_remove.py‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
from scim2_models import Mutability
77
from scim2_models import PatchOp
88
from scim2_models import PatchOperation
9+
from scim2_models import Path
910
from scim2_models import Required
1011
from scim2_models import Resource
1112
from scim2_models import SCIMException
12-
from scim2_models.path import Path
1313

1414
from ..utils import CheckContext
1515
from ..utils import CheckResult
@@ -45,8 +45,8 @@ def check_remove_attribute(
4545
functions, depending on the target location specified by 'path'."
4646
"""
4747
if (
48-
context.client.service_provider_config
49-
and not context.client.service_provider_config.patch.supported
48+
context.client.provider.config
49+
and not context.client.provider.config.patch.supported
5050
):
5151
return [
5252
check_result(
@@ -81,7 +81,8 @@ def check_remove_attribute(
8181
for path in all_paths:
8282
urn = str(path)
8383
initial_value = path.get(full_resource)
84-
mutability = path.get_annotation(Mutability)
84+
binding = path.resolve()
85+
mutability = binding.get_annotation(Mutability) if binding else None
8586
if initial_value is None:
8687
continue
8788

‎scim2_tester/checkers/patch_replace.py‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
from scim2_models import Mutability
77
from scim2_models import PatchOp
88
from scim2_models import PatchOperation
9+
from scim2_models import Path
910
from scim2_models import Resource
1011
from scim2_models import SCIMException
11-
from scim2_models.path import Path
1212

1313
from ..filling import generate_random_value
1414
from ..utils import CheckContext
@@ -45,8 +45,8 @@ def check_replace_attribute(
4545
specified by the 'path'."
4646
"""
4747
if (
48-
context.client.service_provider_config
49-
and not context.client.service_provider_config.patch.supported
48+
context.client.provider.config
49+
and not context.client.provider.config.patch.supported
5050
):
5151
return [
5252
check_result(
@@ -80,7 +80,8 @@ def check_replace_attribute(
8080
for path in all_paths:
8181
urn = str(path)
8282
patch_value = generate_random_value(context, path=path)
83-
mutability = path.get_annotation(Mutability)
83+
binding = path.resolve()
84+
mutability = binding.get_annotation(Mutability) if binding else None
8485

8586
patch_op = PatchOp[type(base_resource)](
8687
operations=[

‎scim2_tester/checkers/resource_get.py‎

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import Any
2+
from typing import cast
23

34
from scim2_models import Resource
45
from scim2_models import ResourceType
@@ -13,16 +14,14 @@
1314
def _model_from_resource_type(
1415
context: CheckContext, resource_type: ResourceType
1516
) -> type[Resource[Any]] | None:
16-
"""Resolve Resource model class from ResourceType metadata.
17+
"""Resolve the model a :class:`~scim2_models.ResourceType` designates.
1718
18-
Maps a :class:`~scim2_models.ResourceType` object (containing schema URIs and metadata) to the
19-
corresponding Python model class registered in the SCIM client.
19+
The description of the server binds each resource type name to the model an
20+
endpoint serves, extensions included, so two resource types built upon a
21+
same schema are told apart.
2022
"""
21-
for resource_model in context.client.resource_models:
22-
if resource_model.__schema__ == resource_type.schema_:
23-
return resource_model
24-
25-
return None
23+
model = context.client.provider.model_for(resource_type)
24+
return cast("type[Resource[Any]]", model) if model else None
2625

2726

2827
@checker("crud:read")

‎scim2_tester/cli.py‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import argparse
22

3-
try:
4-
from httpx2 import Client
5-
except ImportError:
6-
from httpx import Client
7-
3+
from httpx2 import Client
84
from scim2_client.engines.httpx2 import SyncSCIMClient
95

106
from scim2_tester.checker import check_server

0 commit comments

Comments
 (0)