Skip to content

Commit be0c26a

Browse files
committed
feat: implement discoverability methods
1 parent dbf963e commit be0c26a

File tree

5 files changed

+104
-27
lines changed

5 files changed

+104
-27
lines changed

doc/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Changelog
1313
Added
1414
^^^^^
1515
- Implement :meth:`~scim2_client.BaseSCIMClient.register_naive_resource_types`.
16+
- Implement :meth:`~scim2_client.BaseSyncSCIMClient.discover` methods.
1617

1718
[0.3.3] - 2024-11-29
1819
--------------------

scim2_client/client.py

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import sys
23
from collections.abc import Collection
34
from dataclasses import dataclass
@@ -29,6 +30,7 @@
2930
"Accept": "application/scim+json",
3031
"Content-Type": "application/scim+json",
3132
}
33+
CONFIG_RESOURCES = (ResourceType, Schema, ServiceProviderConfig)
3234

3335

3436
@dataclass
@@ -153,18 +155,27 @@ def __init__(
153155
check_response_payload: bool = True,
154156
raise_scim_errors: bool = True,
155157
):
156-
self.resource_models = tuple(
157-
set(resource_models or []) | {ResourceType, Schema, ServiceProviderConfig}
158-
)
158+
self.resource_models = tuple(set(resource_models or []) | set(CONFIG_RESOURCES))
159159
self.resource_types = resource_types
160160
self.check_request_payload = check_request_payload
161161
self.check_response_payload = check_response_payload
162162
self.raise_scim_errors = raise_scim_errors
163163

164+
def get_resource_model(self, name: str) -> Optional[type[Resource]]:
165+
"""Get a registered model by its name or its schema."""
166+
for resource_model in self.resource_models:
167+
schema = resource_model.model_fields["schemas"].default[0]
168+
if schema == name or schema.split(":")[-1] == name:
169+
return resource_model
170+
return None
171+
164172
def check_resource_model(
165173
self, resource_model: type[Resource], payload=None
166174
) -> None:
167-
if resource_model not in self.resource_models:
175+
if (
176+
resource_model not in self.resource_models
177+
and resource_model not in CONFIG_RESOURCES
178+
):
168179
raise SCIMRequestError(
169180
f"Unknown resource type: '{resource_model}'", source=payload
170181
)
@@ -202,7 +213,7 @@ def register_naive_resource_types(self):
202213
self.resource_types = [
203214
ResourceType.from_resource(model)
204215
for model in self.resource_models
205-
if model not in (ResourceType, Schema, ServiceProviderConfig)
216+
if model not in CONFIG_RESOURCES
206217
]
207218

208219
def check_response(
@@ -759,6 +770,31 @@ def replace(
759770
"""
760771
raise NotImplementedError()
761772

773+
def discover(self):
774+
"""Dynamically discover the server models :class:`~scim2_models.Schema` and :class:`~scim2_models.ResourceType`.
775+
776+
This is a shortcut for :meth:`BaseSyncSCIMClient.discover_models`
777+
and :meth:`BaseSyncSCIMClient.discover_resource_types`.
778+
"""
779+
self.discover_models()
780+
self.discover_resource_types()
781+
782+
def discover_models(self):
783+
"""Dynamically register resource models by reading the server :class:`~scim2_models.Schema` endpoint.
784+
785+
Internally it performs a request to the SCIM server to get all the schemas,
786+
generate classes from those schemas, and register them in the client.
787+
"""
788+
schemas = self.query(Schema)
789+
self.resource_models = tuple(
790+
Resource.from_schema(schema) for schema in schemas.resources
791+
)
792+
793+
def discover_resource_types(self):
794+
"""Dynamically register resource types by reading the server :class:`~scim2_models.ResourceType` endpoint."""
795+
schemas = self.query(ResourceType)
796+
self.resource_types = schemas.resources
797+
762798

763799
class BaseAsyncSCIMClient(BaseSCIMClient):
764800
"""Base class for asynchronous request clients."""
@@ -1015,3 +1051,30 @@ async def replace(
10151051
the response payload.
10161052
"""
10171053
raise NotImplementedError()
1054+
1055+
async def discover(self):
1056+
"""Dynamically discover the server models :class:`~scim2_models.Schema` and :class:`~scim2_models.ResourceType`.
1057+
1058+
This is a shortcut for the parallel execution of :meth:`BaseAsyncSCIMClient.discover_models`
1059+
and :meth:`BaseAsyncSCIMClient.discover_resource_types`.
1060+
"""
1061+
models_task = asyncio.create_task(self.discover_models())
1062+
resources_task = asyncio.create_task(self.discover_resource_types())
1063+
await models_task
1064+
await resources_task
1065+
1066+
async def discover_models(self):
1067+
"""Dynamically register resource models by reading the server :class:`~scim2_models.Schema` endpoint.
1068+
1069+
Internally it performs a request to the SCIM server to get all the schemas,
1070+
generate classes from those schemas, and register them in the client.
1071+
"""
1072+
schemas = await self.query(Schema)
1073+
self.resource_models = tuple(
1074+
Resource.from_schema(schema) for schema in schemas.resources
1075+
)
1076+
1077+
async def discover_resource_types(self):
1078+
"""Dynamically register resource types by reading the server :class:`~scim2_models.ResourceType` endpoint."""
1079+
schemas = await self.query(ResourceType)
1080+
self.resource_types = schemas.resources

tests/engines/test_httpx.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55
import pytest
66
from httpx import AsyncClient
77
from httpx import Client
8-
from scim2_models import ResourceType
98
from scim2_models import SearchRequest
10-
from scim2_models import User
119

1210
from scim2_client.engines.httpx import AsyncSCIMClient
1311
from scim2_client.engines.httpx import SyncSCIMClient
@@ -16,14 +14,18 @@
1614
scim2_server = pytest.importorskip("scim2_server")
1715
from scim2_server.backend import InMemoryBackend # noqa: E402
1816
from scim2_server.provider import SCIMProvider # noqa: E402
17+
from scim2_server.utils import load_default_resource_types # noqa: E402
18+
from scim2_server.utils import load_default_schemas # noqa: E402
1919

2020

2121
@pytest.fixture(scope="session")
2222
def server():
2323
backend = InMemoryBackend()
2424
provider = SCIMProvider(backend)
25-
provider.register_schema(User.to_schema())
26-
provider.register_resource_type(ResourceType.from_resource(User))
25+
for schema in load_default_schemas().values():
26+
provider.register_schema(schema)
27+
for resource_type in load_default_resource_types().values():
28+
provider.register_resource_type(resource_type)
2729
host = "localhost"
2830
port = portpicker.pick_unused_port()
2931
httpd = wsgiref.simple_server.make_server(host, port, provider)
@@ -40,11 +42,9 @@ def server():
4042
def test_sync_engine(server):
4143
host, port = server
4244
client = Client(base_url=f"http://{host}:{port}")
43-
scim_client = SyncSCIMClient(
44-
client,
45-
resource_models=[User],
46-
resource_types=[ResourceType.from_resource(User)],
47-
)
45+
scim_client = SyncSCIMClient(client)
46+
scim_client.discover()
47+
User = scim_client.get_resource_model("User")
4848

4949
request_user = User(user_name="foo", display_name="bar")
5050
response_user = scim_client.create(request_user)
@@ -77,11 +77,9 @@ def test_sync_engine(server):
7777
async def test_async_engine(server):
7878
host, port = server
7979
client = AsyncClient(base_url=f"http://{host}:{port}")
80-
scim_client = AsyncSCIMClient(
81-
client,
82-
resource_models=(User,),
83-
resource_types=[ResourceType.from_resource(User)],
84-
)
80+
scim_client = AsyncSCIMClient(client)
81+
await scim_client.discover()
82+
User = scim_client.get_resource_model("User")
8583

8684
request_user = User(user_name="foo", display_name="bar")
8785
response_user = await scim_client.create(request_user)

tests/engines/test_werkzeug.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import pytest
2-
from scim2_models import ResourceType
32
from scim2_models import SearchRequest
43
from scim2_models import User
54
from werkzeug.wrappers import Request
@@ -12,26 +11,29 @@
1211
scim2_server = pytest.importorskip("scim2_server")
1312
from scim2_server.backend import InMemoryBackend # noqa: E402
1413
from scim2_server.provider import SCIMProvider # noqa: E402
14+
from scim2_server.utils import load_default_resource_types # noqa: E402
15+
from scim2_server.utils import load_default_schemas # noqa: E402
1516

1617

1718
@pytest.fixture
1819
def scim_provider():
1920
provider = SCIMProvider(InMemoryBackend())
20-
provider.register_schema(User.to_schema())
21-
provider.register_resource_type(ResourceType.from_resource(User))
21+
for schema in load_default_schemas().values():
22+
provider.register_schema(schema)
23+
for resource_type in load_default_resource_types().values():
24+
provider.register_resource_type(resource_type)
2225
return provider
2326

2427

2528
@pytest.fixture
2629
def scim_client(scim_provider):
27-
return TestSCIMClient(
28-
app=scim_provider,
29-
resource_models=(User,),
30-
resource_types=[ResourceType.from_resource(User)],
31-
)
30+
client = TestSCIMClient(app=scim_provider)
31+
client.discover()
32+
return client
3233

3334

3435
def test_werkzeug_engine(scim_client):
36+
User = scim_client.get_resource_model("User")
3537
request_user = User(user_name="foo", display_name="bar")
3638
response_user = scim_client.create(request_user)
3739
assert response_user.user_name == "foo"

tests/test_utils.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,16 @@ class Foobar(Resource):
3434

3535
with pytest.raises(SCIMRequestError):
3636
client.resource_endpoint(Foobar)
37+
38+
39+
def test_get_resource_model():
40+
client = SyncSCIMClient(
41+
None,
42+
resource_models=[User[EnterpriseUser]],
43+
)
44+
assert client.get_resource_model("User") == User[EnterpriseUser]
45+
assert (
46+
client.get_resource_model("urn:ietf:params:scim:schemas:core:2.0:User")
47+
== User[EnterpriseUser]
48+
)
49+
assert client.get_resource_model("Group") is None

0 commit comments

Comments
 (0)