@@ -45,6 +45,7 @@ class RequestPayload:
4545 payload : dict | None = None
4646 expected_types : list [type [Resource ]] | None = None
4747 expected_status_codes : list [int ] | None = None
48+ target : Resource | None = None
4849
4950
5051class SCIMClient :
@@ -92,10 +93,21 @@ class SCIMClient:
9293 :rfc:`RFC7644 §3.12 <7644#section-3.12>`.
9394 """
9495
95- QUERY_RESPONSE_STATUS_CODES : list [int ] = [200 , 400 , 307 , 308 , 401 , 403 , 404 , 500 ]
96+ QUERY_RESPONSE_STATUS_CODES : list [int ] = [
97+ 200 ,
98+ 304 ,
99+ 400 ,
100+ 307 ,
101+ 308 ,
102+ 401 ,
103+ 403 ,
104+ 404 ,
105+ 500 ,
106+ ]
96107 """Resource querying HTTP codes.
97108
98- As defined at :rfc:`RFC7644 §3.4.2 <7644#section-3.4.2>` and
109+ As defined at :rfc:`RFC7644 §3.4.2 <7644#section-3.4.2>`,
110+ :rfc:`RFC7644 §3.14 <7644#section-3.14>` and
99111 :rfc:`RFC7644 §3.12 <7644#section-3.12>`.
100112 """
101113
@@ -208,6 +220,14 @@ def _set_if_match(self, req: RequestPayload, resource: Resource) -> None:
208220 headers = req .request_kwargs .setdefault ("headers" , {})
209221 headers .setdefault ("If-Match" , resource .meta .version )
210222
223+ def _set_if_none_match (self , req : RequestPayload , resource : Resource ) -> None :
224+ """Add ``If-None-Match`` header to the request if the server supports ETags."""
225+ if not self ._etag_supported :
226+ return
227+ if resource .meta and resource .meta .version :
228+ headers = req .request_kwargs .setdefault ("headers" , {})
229+ headers .setdefault ("If-None-Match" , resource .meta .version )
230+
211231 def get_resource_model (self , name : str ) -> type [Resource ] | None :
212232 """Get a registered model by its name or its schema."""
213233 for resource_model in self .resource_models :
@@ -301,10 +321,14 @@ def check_response(
301321 check_response_payload : bool | None = None ,
302322 raise_scim_errors : bool | None = None ,
303323 scim_ctx : Context | None = None ,
324+ target : Resource | None = None ,
304325 ) -> Error | None | dict | type [Resource ]:
305326 if raise_scim_errors is None :
306327 raise_scim_errors = self .raise_scim_errors
307328
329+ if status_code == 304 and target :
330+ return target
331+
308332 # In addition to returning an HTTP response code, implementers MUST return
309333 # the errors in the body of the response in a JSON format
310334 # https://datatracker.ietf.org/doc/html/rfc7644.html#section-3.12
@@ -434,8 +458,7 @@ def _resolve_query_parameters(
434458
435459 def _prepare_query_request (
436460 self ,
437- resource_model : type [Resource ] | None = None ,
438- id : str | None = None ,
461+ target : type [Resource ] | Resource | None = None ,
439462 query_parameters : ResponseParameters | dict | None = None ,
440463 check_request_payload : bool | None = None ,
441464 expected_status_codes : list [int ] | None = None ,
@@ -446,6 +469,14 @@ def _prepare_query_request(
446469 request_kwargs = kwargs ,
447470 )
448471
472+ resource_model : type [Resource ] | None
473+ if isinstance (target , Resource ):
474+ resource_model = type (target )
475+ id = target .id
476+ else :
477+ resource_model = target
478+ id = None
479+
449480 if check_request_payload is None :
450481 check_request_payload = self .check_request_payload
451482
@@ -489,6 +520,9 @@ def _prepare_query_request(
489520 elif id :
490521 req .expected_types = [resource_model ]
491522 req .url = f"{ req .url } /{ id } "
523+ if isinstance (target , Resource ) and not payload :
524+ req .target = target
525+ self ._set_if_none_match (req , target )
492526
493527 else :
494528 req .expected_types = [ListResponse [resource_model ]]
@@ -733,8 +767,7 @@ def create(
733767
734768 def query (
735769 self ,
736- resource_model : type [Resource ] | None = None ,
737- id : str | None = None ,
770+ target : type [Resource ] | Resource | None = None ,
738771 query_parameters : ResponseParameters | dict | None = None ,
739772 check_request_payload : bool | None = None ,
740773 check_response_payload : bool | None = None ,
@@ -746,11 +779,15 @@ def query(
746779 ) -> Resource | ListResponse [Resource ] | Error | dict :
747780 """Perform a GET request to read resources, as defined in :rfc:`RFC7644 §3.4.2 <7644#section-3.4.2>`.
748781
749- - If `id` is not :data:`None`, the resource with the exact id will be reached.
750- - If `id` is :data:`None`, all the resources with the given type will be reached.
782+ - If ``target`` is a :class:`~scim2_models.Resource` instance, the resource
783+ with the same id will be queried. If the server supports ETags and the
784+ resource has ``meta.version``, an ``If-None-Match`` header is sent;
785+ on ``304 Not Modified`` the original instance is returned.
786+ - If ``target`` is a :class:`~scim2_models.Resource` subtype, all the
787+ resources with the given type will be reached.
788+ - If ``target`` is :data:`None`, all available resources will be reached.
751789
752- :param resource_model: A :class:`~scim2_models.Resource` subtype or :data:`None`
753- :param id: The SCIM id of an object to get, or :data:`None`
790+ :param target: A :class:`~scim2_models.Resource` instance, subtype, or :data:`None`.
754791 :param query_parameters: A :class:`~scim2_models.ResponseParameters` or
755792 :class:`~scim2_models.SearchRequest` detailing the query parameters.
756793 Use :class:`~scim2_models.ResponseParameters` when querying a single
@@ -768,8 +805,9 @@ def query(
768805
769806 :return:
770807 - A :class:`~scim2_models.Error` object in case of error.
771- - A `resource_model` object in case of success if `id` is not :data:`None`
772- - A :class:`~scim2_models.ListResponse[resource_model]` object in case of success if `id` is :data:`None`
808+ - The original ``target`` instance if the server responds with ``304 Not Modified``.
809+ - A ``target`` type object in case of success if ``target`` is a resource instance.
810+ - A :class:`~scim2_models.ListResponse` object in case of success if ``target`` is a resource type.
773811
774812 .. note::
775813
@@ -783,7 +821,7 @@ def query(
783821
784822 from scim2_models import User
785823
786- response = scim.query(User, "my-user-id)
824+ response = scim.query(User(id= "my-user-id") )
787825 # 'response' may be a User or an Error object
788826
789827 .. code-block:: python
@@ -1068,8 +1106,7 @@ async def create(
10681106
10691107 async def query (
10701108 self ,
1071- resource_model : type [Resource ] | None = None ,
1072- id : str | None = None ,
1109+ target : type [Resource ] | Resource | None = None ,
10731110 query_parameters : ResponseParameters | dict | None = None ,
10741111 check_request_payload : bool | None = None ,
10751112 check_response_payload : bool | None = None ,
@@ -1081,11 +1118,15 @@ async def query(
10811118 ) -> Resource | ListResponse [Resource ] | Error | dict :
10821119 """Perform a GET request to read resources, as defined in :rfc:`RFC7644 §3.4.2 <7644#section-3.4.2>`.
10831120
1084- - If `id` is not :data:`None`, the resource with the exact id will be reached.
1085- - If `id` is :data:`None`, all the resources with the given type will be reached.
1121+ - If ``target`` is a :class:`~scim2_models.Resource` instance, the resource
1122+ with the same id will be queried. If the server supports ETags and the
1123+ resource has ``meta.version``, an ``If-None-Match`` header is sent;
1124+ on ``304 Not Modified`` the original instance is returned.
1125+ - If ``target`` is a :class:`~scim2_models.Resource` subtype, all the
1126+ resources with the given type will be reached.
1127+ - If ``target`` is :data:`None`, all available resources will be reached.
10861128
1087- :param resource_model: A :class:`~scim2_models.Resource` subtype or :data:`None`
1088- :param id: The SCIM id of an object to get, or :data:`None`
1129+ :param target: A :class:`~scim2_models.Resource` instance, subtype, or :data:`None`.
10891130 :param query_parameters: A :class:`~scim2_models.ResponseParameters` or
10901131 :class:`~scim2_models.SearchRequest` detailing the query parameters.
10911132 Use :class:`~scim2_models.ResponseParameters` when querying a single
@@ -1103,8 +1144,9 @@ async def query(
11031144
11041145 :return:
11051146 - A :class:`~scim2_models.Error` object in case of error.
1106- - A `resource_model` object in case of success if `id` is not :data:`None`
1107- - A :class:`~scim2_models.ListResponse[resource_model]` object in case of success if `id` is :data:`None`
1147+ - The original ``target`` instance if the server responds with ``304 Not Modified``.
1148+ - A ``target`` type object in case of success if ``target`` is a resource instance.
1149+ - A :class:`~scim2_models.ListResponse` object in case of success if ``target`` is a resource type.
11081150
11091151 .. note::
11101152
@@ -1118,7 +1160,7 @@ async def query(
11181160
11191161 from scim2_models import User
11201162
1121- response = scim.query(User, "my-user-id)
1163+ response = await scim.query(User(id= "my-user-id") )
11221164 # 'response' may be a User or an Error object
11231165
11241166 .. code-block:: python
@@ -1127,15 +1169,15 @@ async def query(
11271169 from scim2_models import User, SearchRequest
11281170
11291171 req = SearchRequest(filter='userName sw "john"')
1130- response = scim.query(User, query_parameters=req)
1172+ response = await scim.query(User, query_parameters=req)
11311173 # 'response' may be a ListResponse[User] or an Error object
11321174
11331175 .. code-block:: python
11341176 :caption: Query of all the available resources
11351177
11361178 from scim2_models import User
11371179
1138- response = scim.query()
1180+ response = await scim.query()
11391181 # 'response' may be a ListResponse[Union[User, Group, ...]] or an Error object
11401182
11411183 .. tip::
0 commit comments