Skip to content

Commit 9ff32ce

Browse files
committed
refactor: the query method can use ResponseParameters
1 parent bd06456 commit 9ff32ce

File tree

7 files changed

+134
-22
lines changed

7 files changed

+134
-22
lines changed

doc/changelog.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
Changelog
22
=========
33

4+
[0.7.4] - Unreleased
5+
--------------------
6+
7+
Changed
8+
^^^^^^^
9+
- The ``query`` method now accepts :class:`~scim2_models.ResponseParameters` in addition
10+
to :class:`~scim2_models.SearchRequest`.
11+
- The ``search_request`` parameter of ``query`` is renamed to ``query_parameters``.
12+
The old name is deprecated and will be removed in 0.9.
13+
414
[0.7.3] - 2026-02-04
515
--------------------
616

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ classifiers = [
2727

2828
requires-python = ">= 3.10"
2929
dependencies = [
30-
"scim2-models>=0.6.4",
30+
"scim2-models>=0.6.7",
3131
]
3232

3333
[project.optional-dependencies]

scim2_client/client.py

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import sys
3+
import warnings
34
from collections.abc import Collection
45
from dataclasses import dataclass
56
from typing import TypeVar
@@ -14,6 +15,7 @@
1415
from scim2_models import PatchOp
1516
from scim2_models import Resource
1617
from scim2_models import ResourceType
18+
from scim2_models import ResponseParameters
1719
from scim2_models import Schema
1820
from scim2_models import SearchRequest
1921
from scim2_models import ServiceProviderConfig
@@ -400,11 +402,26 @@ def _prepare_query_request(
400402
self,
401403
resource_model: type[Resource] | None = None,
402404
id: str | None = None,
403-
search_request: SearchRequest | dict | None = None,
405+
query_parameters: ResponseParameters | dict | None = None,
404406
check_request_payload: bool | None = None,
405407
expected_status_codes: list[int] | None = None,
406408
**kwargs,
407409
) -> RequestPayload:
410+
if "search_request" in kwargs:
411+
if query_parameters is not None:
412+
raise TypeError(
413+
"Cannot pass both 'query_parameters' and "
414+
"deprecated 'search_request'"
415+
)
416+
warnings.warn(
417+
"The 'search_request' parameter of 'query' is deprecated, "
418+
"use 'query_parameters' instead. "
419+
"Will be removed in 0.9.",
420+
DeprecationWarning,
421+
stacklevel=3,
422+
)
423+
query_parameters = kwargs.pop("search_request")
424+
408425
req = RequestPayload(
409426
expected_status_codes=expected_status_codes,
410427
request_kwargs=kwargs,
@@ -416,17 +433,23 @@ def _prepare_query_request(
416433
if resource_model and check_request_payload:
417434
self._check_resource_model(resource_model)
418435

419-
payload: SearchRequest | None
436+
payload: ResponseParameters | None
420437
if not check_request_payload:
421-
payload = search_request
438+
payload = query_parameters
422439

423-
elif isinstance(search_request, SearchRequest):
424-
payload = search_request.model_dump(
440+
elif isinstance(query_parameters, SearchRequest):
441+
payload = query_parameters.model_dump(
425442
exclude_unset=True,
426443
exclude={"schemas"},
427444
scim_ctx=Context.RESOURCE_QUERY_REQUEST,
428445
)
429446

447+
elif isinstance(query_parameters, ResponseParameters):
448+
payload = query_parameters.model_dump(
449+
exclude_unset=True,
450+
by_alias=True,
451+
)
452+
430453
else:
431454
payload = None
432455

@@ -703,7 +726,7 @@ def query(
703726
self,
704727
resource_model: type[Resource] | None = None,
705728
id: str | None = None,
706-
search_request: SearchRequest | dict | None = None,
729+
query_parameters: ResponseParameters | dict | None = None,
707730
check_request_payload: bool | None = None,
708731
check_response_payload: bool | None = None,
709732
expected_status_codes: list[int]
@@ -718,7 +741,14 @@ def query(
718741
719742
:param resource_model: A :class:`~scim2_models.Resource` subtype or :data:`None`
720743
:param id: The SCIM id of an object to get, or :data:`None`
721-
:param search_request: An object detailing the search query parameters.
744+
:param query_parameters: A :class:`~scim2_models.ResponseParameters` or
745+
:class:`~scim2_models.SearchRequest` detailing the query parameters.
746+
Use :class:`~scim2_models.ResponseParameters` when querying a single
747+
resource by id, where only ``attributes`` and ``excludedAttributes``
748+
are meaningful (:rfc:`RFC 7644 §3.4.1 <7644#section-3.4.1>`).
749+
Use :class:`~scim2_models.SearchRequest` when listing resources, to
750+
also pass ``filter``, ``sortBy``, ``sortOrder``, ``startIndex`` and
751+
``count`` (:rfc:`RFC 7644 §3.4.2 <7644#section-3.4.2>`).
722752
:param check_request_payload: If set, overwrites :paramref:`scim2_client.SCIMClient.check_request_payload`.
723753
:param check_response_payload: If set, overwrites :paramref:`scim2_client.SCIMClient.check_response_payload`.
724754
:param expected_status_codes: The list of expected status codes form the response.
@@ -752,13 +782,13 @@ def query(
752782
from scim2_models import User, SearchRequest
753783
754784
req = SearchRequest(filter='userName sw "john"')
755-
response = scim.query(User, search_request=search_request)
785+
response = scim.query(User, query_parameters=req)
756786
# 'response' may be a ListResponse[User] or an Error object
757787
758788
.. code-block:: python
759789
:caption: Query of all the available resources
760790
761-
from scim2_models import User, SearchRequest
791+
from scim2_models import User
762792
763793
response = scim.query()
764794
# 'response' may be a ListResponse[Union[User, Group, ...]] or an Error object
@@ -1030,7 +1060,7 @@ async def query(
10301060
self,
10311061
resource_model: type[Resource] | None = None,
10321062
id: str | None = None,
1033-
search_request: SearchRequest | dict | None = None,
1063+
query_parameters: ResponseParameters | dict | None = None,
10341064
check_request_payload: bool | None = None,
10351065
check_response_payload: bool | None = None,
10361066
expected_status_codes: list[int]
@@ -1045,7 +1075,14 @@ async def query(
10451075
10461076
:param resource_model: A :class:`~scim2_models.Resource` subtype or :data:`None`
10471077
:param id: The SCIM id of an object to get, or :data:`None`
1048-
:param search_request: An object detailing the search query parameters.
1078+
:param query_parameters: A :class:`~scim2_models.ResponseParameters` or
1079+
:class:`~scim2_models.SearchRequest` detailing the query parameters.
1080+
Use :class:`~scim2_models.ResponseParameters` when querying a single
1081+
resource by id, where only ``attributes`` and ``excludedAttributes``
1082+
are meaningful (:rfc:`RFC 7644 §3.4.1 <7644#section-3.4.1>`).
1083+
Use :class:`~scim2_models.SearchRequest` when listing resources, to
1084+
also pass ``filter``, ``sortBy``, ``sortOrder``, ``startIndex`` and
1085+
``count`` (:rfc:`RFC 7644 §3.4.2 <7644#section-3.4.2>`).
10491086
:param check_request_payload: If set, overwrites :paramref:`scim2_client.SCIMClient.check_request_payload`.
10501087
:param check_response_payload: If set, overwrites :paramref:`scim2_client.SCIMClient.check_response_payload`.
10511088
:param expected_status_codes: The list of expected status codes form the response.
@@ -1079,13 +1116,13 @@ async def query(
10791116
from scim2_models import User, SearchRequest
10801117
10811118
req = SearchRequest(filter='userName sw "john"')
1082-
response = scim.query(User, search_request=search_request)
1119+
response = scim.query(User, query_parameters=req)
10831120
# 'response' may be a ListResponse[User] or an Error object
10841121
10851122
.. code-block:: python
10861123
:caption: Query of all the available resources
10871124
1088-
from scim2_models import User, SearchRequest
1125+
from scim2_models import User
10891126
10901127
response = scim.query()
10911128
# 'response' may be a ListResponse[Union[User, Group, ...]] or an Error object

scim2_client/engines/httpx.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from scim2_models import ListResponse
1414
from scim2_models import PatchOp
1515
from scim2_models import Resource
16+
from scim2_models import ResponseParameters
1617
from scim2_models import SearchRequest
1718

1819
from scim2_client.client import BaseAsyncSCIMClient
@@ -105,7 +106,7 @@ def query(
105106
self,
106107
resource_model: type[Resource] | None = None,
107108
id: str | None = None,
108-
search_request: SearchRequest | dict | None = None,
109+
query_parameters: ResponseParameters | dict | None = None,
109110
check_request_payload: bool | None = None,
110111
check_response_payload: bool | None = None,
111112
expected_status_codes: list[int]
@@ -116,7 +117,7 @@ def query(
116117
req = self._prepare_query_request(
117118
resource_model=resource_model,
118119
id=id,
119-
search_request=search_request,
120+
query_parameters=query_parameters,
120121
check_request_payload=check_request_payload,
121122
expected_status_codes=expected_status_codes,
122123
**kwargs,
@@ -331,7 +332,7 @@ async def query(
331332
self,
332333
resource_model: type[Resource] | None = None,
333334
id: str | None = None,
334-
search_request: SearchRequest | dict | None = None,
335+
query_parameters: ResponseParameters | dict | None = None,
335336
check_request_payload: bool | None = None,
336337
check_response_payload: bool | None = None,
337338
expected_status_codes: list[int]
@@ -342,7 +343,7 @@ async def query(
342343
req = self._prepare_query_request(
343344
resource_model=resource_model,
344345
id=id,
345-
search_request=search_request,
346+
query_parameters=query_parameters,
346347
check_request_payload=check_request_payload,
347348
expected_status_codes=expected_status_codes,
348349
**kwargs,

scim2_client/engines/werkzeug.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from scim2_models import ListResponse
1010
from scim2_models import PatchOp
1111
from scim2_models import Resource
12+
from scim2_models import ResponseParameters
1213
from scim2_models import SearchRequest
1314
from werkzeug.test import Client
1415

@@ -139,7 +140,7 @@ def query(
139140
self,
140141
resource_model: type[Resource] | None = None,
141142
id: str | None = None,
142-
search_request: SearchRequest | dict | None = None,
143+
query_parameters: ResponseParameters | dict | None = None,
143144
check_request_payload: bool | None = None,
144145
check_response_payload: bool | None = None,
145146
expected_status_codes: list[int]
@@ -150,7 +151,7 @@ def query(
150151
req = self._prepare_query_request(
151152
resource_model=resource_model,
152153
id=id,
153-
search_request=search_request,
154+
query_parameters=query_parameters,
154155
check_request_payload=check_request_payload,
155156
expected_status_codes=expected_status_codes,
156157
**kwargs,

tests/test_query.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from scim2_models import Meta
88
from scim2_models import Resource
99
from scim2_models import ResourceType
10+
from scim2_models import ResponseParameters
1011
from scim2_models import SearchRequest
1112
from scim2_models import ServiceProviderConfig
1213
from scim2_models import User
@@ -555,8 +556,35 @@ def test_search_request(httpserver, sync_client):
555556
assert response.id == "with-qs"
556557

557558

559+
def test_query_parameters(httpserver, sync_client):
560+
"""ResponseParameters can be used instead of SearchRequest for single-resource queries."""
561+
query_string = "attributes=userName&attributes=displayName"
562+
563+
httpserver.expect_request(
564+
"/Users/with-rp", query_string=query_string
565+
).respond_with_json(
566+
{
567+
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
568+
"id": "with-rp",
569+
"userName": "bjensen@example.com",
570+
"meta": {
571+
"resourceType": "User",
572+
"created": "2010-01-23T04:56:22Z",
573+
"lastModified": "2011-05-13T04:42:34Z",
574+
"version": 'W\\/"3694e05e9dff590"',
575+
"location": "https://example.com/v2/Users/with-rp",
576+
},
577+
},
578+
status=200,
579+
)
580+
params = ResponseParameters(attributes=["userName", "displayName"])
581+
response = sync_client.query(User, "with-rp", params)
582+
assert isinstance(response, User)
583+
assert response.id == "with-rp"
584+
585+
558586
def test_query_dont_check_request_payload(httpserver, sync_client):
559-
"""Test the check_request_payload attribute on query."""
587+
"""Raw dict payloads are forwarded as-is when check_request_payload is False."""
560588
query_string = "attributes=userName&attributes=displayName&excluded_attributes=timezone&excluded_attributes=phoneNumbers&filter=userName+Eq+%22john%22&sort_by=userName&sort_order=ascending&start_index=1&count=10"
561589

562590
httpserver.expect_request(
@@ -591,6 +619,41 @@ def test_query_dont_check_request_payload(httpserver, sync_client):
591619
assert response.id == "with-qs"
592620

593621

622+
def test_deprecated_search_request_keyword(httpserver, sync_client):
623+
"""Passing search_request as keyword argument emits a DeprecationWarning."""
624+
query_string = "attributes=userName"
625+
626+
httpserver.expect_request(
627+
"/Users/with-dep", query_string=query_string
628+
).respond_with_json(
629+
{
630+
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
631+
"id": "with-dep",
632+
"userName": "bjensen@example.com",
633+
"meta": {
634+
"resourceType": "User",
635+
"created": "2010-01-23T04:56:22Z",
636+
"lastModified": "2011-05-13T04:42:34Z",
637+
"version": 'W\\/"3694e05e9dff590"',
638+
"location": "https://example.com/v2/Users/with-dep",
639+
},
640+
},
641+
status=200,
642+
)
643+
params = ResponseParameters(attributes=["userName"])
644+
with pytest.warns(DeprecationWarning, match="search_request.*deprecated"):
645+
response = sync_client.query(User, "with-dep", search_request=params)
646+
assert isinstance(response, User)
647+
assert response.id == "with-dep"
648+
649+
650+
def test_both_search_request_and_query_parameters_raises(sync_client):
651+
"""Passing both search_request and query_parameters raises TypeError."""
652+
params = ResponseParameters(attributes=["userName"])
653+
with pytest.raises(TypeError, match="Cannot pass both"):
654+
sync_client.query(User, "some-id", params, search_request=params)
655+
656+
594657
def test_invalid_resource_model(sync_client):
595658
"""Test that resource_models passed to the method must be part of SCIMClient.resource_models."""
596659
sync_client.resource_models = (User,)

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)