Skip to content

Commit 588c2d4

Browse files
committed
refactor: the query method can use ResponseParameters
1 parent bd06456 commit 588c2d4

File tree

7 files changed

+154
-22
lines changed

7 files changed

+154
-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: 58 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
@@ -396,11 +398,32 @@ def _prepare_create_request(
396398

397399
return req
398400

401+
@staticmethod
402+
def _resolve_query_parameters(
403+
query_parameters: ResponseParameters | dict | None,
404+
search_request: ResponseParameters | dict | None,
405+
) -> ResponseParameters | dict | None:
406+
if search_request is not None:
407+
if query_parameters is not None:
408+
raise TypeError(
409+
"Cannot pass both 'query_parameters' and "
410+
"deprecated 'search_request'"
411+
)
412+
warnings.warn(
413+
"The 'search_request' parameter of 'query' is deprecated, "
414+
"use 'query_parameters' instead. "
415+
"Will be removed in 0.9.",
416+
DeprecationWarning,
417+
stacklevel=3,
418+
)
419+
return search_request
420+
return query_parameters
421+
399422
def _prepare_query_request(
400423
self,
401424
resource_model: type[Resource] | None = None,
402425
id: str | None = None,
403-
search_request: SearchRequest | dict | None = None,
426+
query_parameters: ResponseParameters | dict | None = None,
404427
check_request_payload: bool | None = None,
405428
expected_status_codes: list[int] | None = None,
406429
**kwargs,
@@ -416,17 +439,23 @@ def _prepare_query_request(
416439
if resource_model and check_request_payload:
417440
self._check_resource_model(resource_model)
418441

419-
payload: SearchRequest | None
442+
payload: ResponseParameters | None
420443
if not check_request_payload:
421-
payload = search_request
444+
payload = query_parameters
422445

423-
elif isinstance(search_request, SearchRequest):
424-
payload = search_request.model_dump(
446+
elif isinstance(query_parameters, SearchRequest):
447+
payload = query_parameters.model_dump(
425448
exclude_unset=True,
426449
exclude={"schemas"},
427450
scim_ctx=Context.RESOURCE_QUERY_REQUEST,
428451
)
429452

453+
elif isinstance(query_parameters, ResponseParameters):
454+
payload = query_parameters.model_dump(
455+
exclude_unset=True,
456+
by_alias=True,
457+
)
458+
430459
else:
431460
payload = None
432461

@@ -703,12 +732,13 @@ def query(
703732
self,
704733
resource_model: type[Resource] | None = None,
705734
id: str | None = None,
706-
search_request: SearchRequest | dict | None = None,
735+
query_parameters: ResponseParameters | dict | None = None,
707736
check_request_payload: bool | None = None,
708737
check_response_payload: bool | None = None,
709738
expected_status_codes: list[int]
710739
| None = SCIMClient.QUERY_RESPONSE_STATUS_CODES,
711740
raise_scim_errors: bool | None = None,
741+
search_request: ResponseParameters | dict | None = None,
712742
**kwargs,
713743
) -> Resource | ListResponse[Resource] | Error | dict:
714744
"""Perform a GET request to read resources, as defined in :rfc:`RFC7644 §3.4.2 <7644#section-3.4.2>`.
@@ -718,7 +748,14 @@ def query(
718748
719749
:param resource_model: A :class:`~scim2_models.Resource` subtype or :data:`None`
720750
:param id: The SCIM id of an object to get, or :data:`None`
721-
:param search_request: An object detailing the search query parameters.
751+
:param query_parameters: A :class:`~scim2_models.ResponseParameters` or
752+
:class:`~scim2_models.SearchRequest` detailing the query parameters.
753+
Use :class:`~scim2_models.ResponseParameters` when querying a single
754+
resource by id, where only ``attributes`` and ``excludedAttributes``
755+
are meaningful (:rfc:`RFC 7644 §3.4.1 <7644#section-3.4.1>`).
756+
Use :class:`~scim2_models.SearchRequest` when listing resources, to
757+
also pass ``filter``, ``sortBy``, ``sortOrder``, ``startIndex`` and
758+
``count`` (:rfc:`RFC 7644 §3.4.2 <7644#section-3.4.2>`).
722759
:param check_request_payload: If set, overwrites :paramref:`scim2_client.SCIMClient.check_request_payload`.
723760
:param check_response_payload: If set, overwrites :paramref:`scim2_client.SCIMClient.check_response_payload`.
724761
:param expected_status_codes: The list of expected status codes form the response.
@@ -752,13 +789,13 @@ def query(
752789
from scim2_models import User, SearchRequest
753790
754791
req = SearchRequest(filter='userName sw "john"')
755-
response = scim.query(User, search_request=search_request)
792+
response = scim.query(User, query_parameters=req)
756793
# 'response' may be a ListResponse[User] or an Error object
757794
758795
.. code-block:: python
759796
:caption: Query of all the available resources
760797
761-
from scim2_models import User, SearchRequest
798+
from scim2_models import User
762799
763800
response = scim.query()
764801
# 'response' may be a ListResponse[Union[User, Group, ...]] or an Error object
@@ -1030,12 +1067,13 @@ async def query(
10301067
self,
10311068
resource_model: type[Resource] | None = None,
10321069
id: str | None = None,
1033-
search_request: SearchRequest | dict | None = None,
1070+
query_parameters: ResponseParameters | dict | None = None,
10341071
check_request_payload: bool | None = None,
10351072
check_response_payload: bool | None = None,
10361073
expected_status_codes: list[int]
10371074
| None = SCIMClient.QUERY_RESPONSE_STATUS_CODES,
10381075
raise_scim_errors: bool | None = None,
1076+
search_request: ResponseParameters | dict | None = None,
10391077
**kwargs,
10401078
) -> Resource | ListResponse[Resource] | Error | dict:
10411079
"""Perform a GET request to read resources, as defined in :rfc:`RFC7644 §3.4.2 <7644#section-3.4.2>`.
@@ -1045,7 +1083,14 @@ async def query(
10451083
10461084
:param resource_model: A :class:`~scim2_models.Resource` subtype or :data:`None`
10471085
:param id: The SCIM id of an object to get, or :data:`None`
1048-
:param search_request: An object detailing the search query parameters.
1086+
:param query_parameters: A :class:`~scim2_models.ResponseParameters` or
1087+
:class:`~scim2_models.SearchRequest` detailing the query parameters.
1088+
Use :class:`~scim2_models.ResponseParameters` when querying a single
1089+
resource by id, where only ``attributes`` and ``excludedAttributes``
1090+
are meaningful (:rfc:`RFC 7644 §3.4.1 <7644#section-3.4.1>`).
1091+
Use :class:`~scim2_models.SearchRequest` when listing resources, to
1092+
also pass ``filter``, ``sortBy``, ``sortOrder``, ``startIndex`` and
1093+
``count`` (:rfc:`RFC 7644 §3.4.2 <7644#section-3.4.2>`).
10491094
:param check_request_payload: If set, overwrites :paramref:`scim2_client.SCIMClient.check_request_payload`.
10501095
:param check_response_payload: If set, overwrites :paramref:`scim2_client.SCIMClient.check_response_payload`.
10511096
:param expected_status_codes: The list of expected status codes form the response.
@@ -1079,13 +1124,13 @@ async def query(
10791124
from scim2_models import User, SearchRequest
10801125
10811126
req = SearchRequest(filter='userName sw "john"')
1082-
response = scim.query(User, search_request=search_request)
1127+
response = scim.query(User, query_parameters=req)
10831128
# 'response' may be a ListResponse[User] or an Error object
10841129
10851130
.. code-block:: python
10861131
:caption: Query of all the available resources
10871132
1088-
from scim2_models import User, SearchRequest
1133+
from scim2_models import User
10891134
10901135
response = scim.query()
10911136
# 'response' may be a ListResponse[Union[User, Group, ...]] or an Error object

scim2_client/engines/httpx.py

Lines changed: 13 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,18 +106,22 @@ 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]
112113
| None = BaseSyncSCIMClient.QUERY_RESPONSE_STATUS_CODES,
113114
raise_scim_errors: bool | None = None,
115+
search_request: ResponseParameters | dict | None = None,
114116
**kwargs,
115117
) -> Resource | ListResponse[Resource] | Error | dict:
118+
query_parameters = self._resolve_query_parameters(
119+
query_parameters, search_request
120+
)
116121
req = self._prepare_query_request(
117122
resource_model=resource_model,
118123
id=id,
119-
search_request=search_request,
124+
query_parameters=query_parameters,
120125
check_request_payload=check_request_payload,
121126
expected_status_codes=expected_status_codes,
122127
**kwargs,
@@ -331,18 +336,22 @@ async def query(
331336
self,
332337
resource_model: type[Resource] | None = None,
333338
id: str | None = None,
334-
search_request: SearchRequest | dict | None = None,
339+
query_parameters: ResponseParameters | dict | None = None,
335340
check_request_payload: bool | None = None,
336341
check_response_payload: bool | None = None,
337342
expected_status_codes: list[int]
338343
| None = BaseAsyncSCIMClient.QUERY_RESPONSE_STATUS_CODES,
339344
raise_scim_errors: bool | None = None,
345+
search_request: ResponseParameters | dict | None = None,
340346
**kwargs,
341347
) -> Resource | ListResponse[Resource] | Error | dict:
348+
query_parameters = self._resolve_query_parameters(
349+
query_parameters, search_request
350+
)
342351
req = self._prepare_query_request(
343352
resource_model=resource_model,
344353
id=id,
345-
search_request=search_request,
354+
query_parameters=query_parameters,
346355
check_request_payload=check_request_payload,
347356
expected_status_codes=expected_status_codes,
348357
**kwargs,

scim2_client/engines/werkzeug.py

Lines changed: 7 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,18 +140,22 @@ 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]
146147
| None = BaseSyncSCIMClient.QUERY_RESPONSE_STATUS_CODES,
147148
raise_scim_errors: bool | None = None,
149+
search_request: ResponseParameters | dict | None = None,
148150
**kwargs,
149151
) -> Resource | ListResponse[Resource] | Error | dict:
152+
query_parameters = self._resolve_query_parameters(
153+
query_parameters, search_request
154+
)
150155
req = self._prepare_query_request(
151156
resource_model=resource_model,
152157
id=id,
153-
search_request=search_request,
158+
query_parameters=query_parameters,
154159
check_request_payload=check_request_payload,
155160
expected_status_codes=expected_status_codes,
156161
**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)