diff --git a/mpt_api_client/http/async_client.py b/mpt_api_client/http/async_client.py index adc12066..acd62401 100644 --- a/mpt_api_client/http/async_client.py +++ b/mpt_api_client/http/async_client.py @@ -11,7 +11,8 @@ from mpt_api_client.constants import APPLICATION_JSON from mpt_api_client.exceptions import MPTError, transform_http_status_exception from mpt_api_client.http.client import json_to_file_payload -from mpt_api_client.http.client_utils import validate_base_url +from mpt_api_client.http.client_utils import get_query_params, validate_base_url +from mpt_api_client.http.query_options import QueryOptions from mpt_api_client.http.types import ( HeaderTypes, QueryParam, @@ -63,6 +64,7 @@ async def request( # noqa: WPS211 headers: HeaderTypes | None = None, json_file_key: str = "_attachment_data", force_multipart: bool = False, + options: QueryOptions | None = None, ) -> Response: """Perform an HTTP request. @@ -75,6 +77,7 @@ async def request( # noqa: WPS211 headers: Request headers. json_file_key: json file name for data when sending a multipart request. force_multipart: force multipart request even if file is not provided. + options: Additional options for the request. Returns: Response object. @@ -88,13 +91,14 @@ async def request( # noqa: WPS211 if force_multipart or (files and json): files[json_file_key] = (None, json_to_file_payload(json), APPLICATION_JSON) json = None + params_str = get_query_params(query_params, options) try: response = await self.httpx_client.request( method, url, files=files, json=json, - params=query_params, + params=params_str or None, headers=headers, ) except HTTPError as err: diff --git a/mpt_api_client/http/client.py b/mpt_api_client/http/client.py index fab0dfda..60421056 100644 --- a/mpt_api_client/http/client.py +++ b/mpt_api_client/http/client.py @@ -14,7 +14,8 @@ MPTError, transform_http_status_exception, ) -from mpt_api_client.http.client_utils import validate_base_url +from mpt_api_client.http.client_utils import get_query_params, validate_base_url +from mpt_api_client.http.query_options import QueryOptions from mpt_api_client.http.types import ( HeaderTypes, QueryParam, @@ -76,6 +77,7 @@ def request( # noqa: WPS211 headers: HeaderTypes | None = None, json_file_key: str = "_attachment_data", force_multipart: bool = False, + options: QueryOptions | None = None, ) -> Response: """Perform an HTTP request. @@ -88,6 +90,7 @@ def request( # noqa: WPS211 headers: Request headers. json_file_key: json file name for data when sending a multipart request. force_multipart: force multipart request even if file is not provided. + options: Additional options for the request. Returns: Response object. @@ -101,13 +104,14 @@ def request( # noqa: WPS211 if force_multipart or (files and json): files[json_file_key] = (None, json_to_file_payload(json), APPLICATION_JSON) json = None + params_str = get_query_params(query_params, options) try: response = self.httpx_client.request( method, url, files=files, json=json, - params=query_params, + params=params_str or None, headers=headers, ) except HTTPError as err: diff --git a/mpt_api_client/http/client_utils.py b/mpt_api_client/http/client_utils.py index ed64c2c4..979437a2 100644 --- a/mpt_api_client/http/client_utils.py +++ b/mpt_api_client/http/client_utils.py @@ -1,5 +1,8 @@ import re -from urllib.parse import SplitResult, urlsplit, urlunparse +from typing import Any +from urllib.parse import SplitResult, urlencode, urlsplit, urlunparse + +from mpt_api_client.http.query_options import QueryOptions INVALID_ENV_URL_MESSAGE = ( "Base URL is required. " @@ -45,3 +48,20 @@ def validate_base_url(base_url: str | None) -> str: raise ValueError(INVALID_ENV_URL_MESSAGE) return _build_sanitized_base_url(split_result) + + +def get_query_params( + query_params: dict[str, Any] | None, options: QueryOptions | None = None +) -> str: + """Get query params string from dict.""" + filtered_params = { + query_param: query_value + for query_param, query_value in (query_params or {}).items() + if query_value is not None + } + + query_params_str = urlencode(filtered_params) if filtered_params else "" + if options and options.render: + query_params_str += "&render()" if query_params_str else "render()" + + return query_params_str diff --git a/mpt_api_client/http/mixins/get_mixin.py b/mpt_api_client/http/mixins/get_mixin.py index 3bdc16ca..d3e4d585 100644 --- a/mpt_api_client/http/mixins/get_mixin.py +++ b/mpt_api_client/http/mixins/get_mixin.py @@ -13,8 +13,10 @@ def get(self, resource_id: str, select: list[str] | str | None = None) -> Model: """ if isinstance(select, list): select = ",".join(select) if select else None - - return self._resource(resource_id).get(query_params={"select": select}) # type: ignore[attr-defined, no-any-return] + return self._resource(resource_id).get( # type: ignore[attr-defined, no-any-return] + query_params={"select": select}, + options=self.query_state.options, # type: ignore[attr-defined] + ) class AsyncGetMixin[Model]: @@ -32,4 +34,7 @@ async def get(self, resource_id: str, select: list[str] | str | None = None) -> """ if isinstance(select, list): select = ",".join(select) if select else None - return await self._resource(resource_id).get(query_params={"select": select}) # type: ignore[attr-defined, no-any-return] + return await self._resource(resource_id).get( # type: ignore[attr-defined, no-any-return] + query_params={"select": select}, + options=self.query_state.options, # type: ignore[attr-defined] + ) diff --git a/mpt_api_client/http/mixins/queryable_mixin.py b/mpt_api_client/http/mixins/queryable_mixin.py index 8b8f894c..b2fa4c87 100644 --- a/mpt_api_client/http/mixins/queryable_mixin.py +++ b/mpt_api_client/http/mixins/queryable_mixin.py @@ -1,5 +1,6 @@ from typing import Self +from mpt_api_client.http.query_options import QueryOptions from mpt_api_client.http.query_state import QueryState from mpt_api_client.rql import RQLQuery @@ -23,7 +24,7 @@ def order_by(self, *fields: str) -> Self: rql=self.query_state.filter, # type: ignore[attr-defined] order_by=list(fields), select=self.query_state.select, # type: ignore[attr-defined] - render=self.query_state.render, # type: ignore[attr-defined] + options=self.query_state.options, # type: ignore[attr-defined] ) ) @@ -40,7 +41,7 @@ def filter(self, rql: RQLQuery) -> Self: rql=combined_filter, order_by=self.query_state.order_by, # type: ignore[attr-defined] select=self.query_state.select, # type: ignore[attr-defined] - render=self.query_state.render, # type: ignore[attr-defined] + options=self.query_state.options, # type: ignore[attr-defined] ) ) @@ -62,7 +63,7 @@ def select(self, *fields: str) -> Self: rql=self.query_state.filter, # type: ignore[attr-defined] order_by=self.query_state.order_by, # type: ignore[attr-defined] select=list(fields), - render=self.query_state.render, # type: ignore[attr-defined] + options=self.query_state.options, # type: ignore[attr-defined] ), ) @@ -77,7 +78,7 @@ def options(self, *, render: bool = False) -> Self: rql=self.query_state.filter, # type: ignore[attr-defined] order_by=self.query_state.order_by, # type: ignore[attr-defined] select=self.query_state.select, # type: ignore[attr-defined] - render=render, + options=QueryOptions(render=render), ), ) diff --git a/mpt_api_client/http/query_options.py b/mpt_api_client/http/query_options.py new file mode 100644 index 00000000..19406581 --- /dev/null +++ b/mpt_api_client/http/query_options.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + + +@dataclass +class QueryOptions: + """Options for query state.""" + + render: bool = False diff --git a/mpt_api_client/http/query_state.py b/mpt_api_client/http/query_state.py index 80a47f81..238df2c3 100644 --- a/mpt_api_client/http/query_state.py +++ b/mpt_api_client/http/query_state.py @@ -1,5 +1,6 @@ from typing import Any +from mpt_api_client.http.query_options import QueryOptions from mpt_api_client.rql import RQLQuery @@ -18,7 +19,7 @@ def __init__( order_by: list[str] | None = None, select: list[str] | None = None, *, - render: bool = False, + options: QueryOptions | None = None, ) -> None: """Initialize the query state with optional filter, ordering, and selection criteria. @@ -26,12 +27,12 @@ def __init__( rql: RQL query for filtering data. order_by: List of fields to order by (prefix with '-' for descending). select: List of fields to select in the response. - render: Whether to include the render() parameter in the query string. + options: Query options for the request. """ self._filter = rql self._order_by = order_by self._select = select - self._render = render + self._options = options or QueryOptions() @property def filter(self) -> RQLQuery | None: @@ -49,9 +50,9 @@ def select(self) -> list[str] | None: return self._select @property - def render(self) -> bool: - """Get the current render state.""" - return self._render + def options(self) -> QueryOptions: + """Get the current query options.""" + return self._options def build(self, query_params: dict[str, Any] | None = None) -> str: """Build a query string from the current state and additional parameters. @@ -75,7 +76,7 @@ def build(self, query_params: dict[str, Any] | None = None) -> str: if self._filter: query_parts.append(str(self._filter)) - if self._render: + if self._options.render: query_parts.append("render()") if query_parts: diff --git a/mpt_api_client/http/resource_accessor.py b/mpt_api_client/http/resource_accessor.py index 4d007d6a..a7247645 100644 --- a/mpt_api_client/http/resource_accessor.py +++ b/mpt_api_client/http/resource_accessor.py @@ -1,6 +1,7 @@ from mpt_api_client.constants import APPLICATION_JSON from mpt_api_client.http.async_client import AsyncHTTPClient from mpt_api_client.http.client import HTTPClient +from mpt_api_client.http.query_options import QueryOptions from mpt_api_client.http.types import QueryParam, Response from mpt_api_client.http.url_utils import join_url_path from mpt_api_client.models.collection import ResourceList @@ -37,6 +38,7 @@ def do_request( # noqa: WPS211 json: _JsonPayload = None, query_params: QueryParam | None = None, headers: dict[str, str] | None = None, + options: QueryOptions | None = None, ) -> Response: """Perform an HTTP request and return the raw ``Response``. @@ -46,10 +48,11 @@ def do_request( # noqa: WPS211 json: JSON body payload. query_params: Query-string parameters. headers: Extra HTTP headers. + options: Query options. """ url = join_url_path(self._resource_url, action) if action else self._resource_url return self._http_client.request( - method, url, json=json, query_params=query_params, headers=headers + method, url, json=json, query_params=query_params, headers=headers, options=options ) # -- model-returning helpers --------------------------------------------- @@ -59,9 +62,10 @@ def get( action: str | None = None, *, query_params: QueryParam | None = None, + options: QueryOptions | None = None, ) -> ResourceModel: """``GET`` the resource (optionally with a sub-action).""" - return self._action("GET", action, query_params=query_params) + return self._action("GET", action, query_params=query_params, options=options) def post( self, @@ -94,6 +98,7 @@ def _action( *, json: _JsonPayload = None, query_params: QueryParam | None = None, + options: QueryOptions | None = None, ) -> ResourceModel: response = self.do_request( method, @@ -101,6 +106,7 @@ def _action( json=json, query_params=query_params, headers={"Accept": APPLICATION_JSON}, + options=options, ) return self._model_class.from_response(response) @@ -131,6 +137,7 @@ async def do_request( # noqa: WPS211 json: _JsonPayload = None, query_params: QueryParam | None = None, headers: dict[str, str] | None = None, + options: QueryOptions | None = None, ) -> Response: """Perform an HTTP request and return the raw ``Response``. @@ -140,10 +147,11 @@ async def do_request( # noqa: WPS211 json: JSON body payload. query_params: Query-string parameters. headers: Extra HTTP headers. + options: Additional options for the request. """ url = join_url_path(self._resource_url, action) if action else self._resource_url return await self._http_client.request( - method, url, json=json, query_params=query_params, headers=headers + method, url, json=json, query_params=query_params, headers=headers, options=options ) # -- model-returning helpers --------------------------------------------- @@ -153,9 +161,10 @@ async def get( action: str | None = None, *, query_params: QueryParam | None = None, + options: QueryOptions | None = None, ) -> ResourceModel: """``GET`` the resource (optionally with a sub-action).""" - return await self._action("GET", action, query_params=query_params) + return await self._action("GET", action, query_params=query_params, options=options) async def post( self, @@ -188,6 +197,7 @@ async def _action( *, json: _JsonPayload = None, query_params: QueryParam | None = None, + options: QueryOptions | None = None, ) -> ResourceModel: response = await self.do_request( method, @@ -195,5 +205,6 @@ async def _action( json=json, query_params=query_params, headers={"Accept": APPLICATION_JSON}, + options=options, ) return self._model_class.from_response(response) diff --git a/tests/e2e/audit/records/test_async_records.py b/tests/e2e/audit/records/test_async_records.py index f87baba2..0a714a59 100644 --- a/tests/e2e/audit/records/test_async_records.py +++ b/tests/e2e/audit/records/test_async_records.py @@ -54,3 +54,40 @@ async def test_get_records_with_render(async_mpt_vendor: AsyncMPTClient, product for record in records: assert record.object.id == product_id assert not any(char in record.details for char in template_chars) + + +async def test_get_record_with_render( + async_mpt_vendor: AsyncMPTClient, audit_record_id: str +) -> None: + template_chars = ["{{", "}}"] + service = async_mpt_vendor.audit.records.options(render=True) + + result = await service.get(audit_record_id) + + assert result.id == audit_record_id + assert not any(char in result.details for char in template_chars) + + +async def test_get_record_with_select( + async_mpt_vendor: AsyncMPTClient, audit_record_id: str +) -> None: + service = async_mpt_vendor.audit.records + + result = await service.get(audit_record_id, select=["object", "actor"]) + + assert result.id == audit_record_id + assert result.object is not None + assert result.actor is not None + + +async def test_get_record_with_render_and_select( + async_mpt_vendor: AsyncMPTClient, audit_record_id: str +) -> None: + template_chars = ["{{", "}}"] + service = async_mpt_vendor.audit.records.options(render=True) + + result = await service.get(audit_record_id, select=["object", "actor", "details"]) + + assert result.id == audit_record_id + assert result.object is not None + assert not any(char in result.details for char in template_chars) diff --git a/tests/e2e/audit/records/test_sync_records.py b/tests/e2e/audit/records/test_sync_records.py index 5b40027d..3438827c 100644 --- a/tests/e2e/audit/records/test_sync_records.py +++ b/tests/e2e/audit/records/test_sync_records.py @@ -56,3 +56,34 @@ def test_get_records_with_render(mpt_vendor: MPTClient, product_id) -> None: for record in result: assert record.object.id == product_id assert not any(char in record.details for char in template_chars) + + +def test_get_record_with_render(mpt_vendor: MPTClient, audit_record_id) -> None: + template_chars = ["{{", "}}"] + service = mpt_vendor.audit.records.options(render=True) + + result = service.get(audit_record_id) + + assert result.id == audit_record_id + assert not any(char in result.details for char in template_chars) + + +def test_get_record_with_select(mpt_vendor: MPTClient, audit_record_id) -> None: + service = mpt_vendor.audit.records + + result = service.get(audit_record_id, select=["object", "actor"]) + + assert result.id == audit_record_id + assert result.object is not None + assert result.actor is not None + + +def test_get_record_with_render_and_select(mpt_vendor: MPTClient, audit_record_id) -> None: + template_chars = ["{{", "}}"] + service = mpt_vendor.audit.records.options(render=True) + + result = service.get(audit_record_id, select=["object", "actor", "details"]) + + assert result.id == audit_record_id + assert result.object is not None + assert not any(char in result.details for char in template_chars) diff --git a/tests/unit/http/mixins/test_get_mixin.py b/tests/unit/http/mixins/test_get_mixin.py index f7ac27e5..a7f031dd 100644 --- a/tests/unit/http/mixins/test_get_mixin.py +++ b/tests/unit/http/mixins/test_get_mixin.py @@ -2,6 +2,8 @@ import pytest import respx +from mpt_api_client.http.query_options import QueryOptions +from mpt_api_client.http.query_state import QueryState from tests.unit.http.conftest import AsyncDummyService, DummyService @@ -58,3 +60,39 @@ async def test_async_get_select_str(async_dummy_service: AsyncDummyService) -> N accept_header = (b"Accept", b"application/json") assert accept_header in request.headers.raw assert result.to_dict() == resource_data + + +def test_sync_get_with_render(http_client) -> None: + """Test getting a resource synchronously with render=True.""" + resource_data = {"id": "RES-123", "name": "Test Resource"} + service = DummyService( + http_client=http_client, + query_state=QueryState(options=QueryOptions(render=True)), + ) + with respx.mock: + mock_route = respx.get("https://api.example.com/api/v1/test/RES-123?render()").mock( + return_value=httpx.Response(httpx.codes.OK, json=resource_data) + ) + + result = service.get("RES-123") + + assert result.to_dict() == resource_data + assert mock_route.call_count == 1 + + +async def test_async_get_with_render(async_http_client) -> None: + """Test getting a resource asynchronously with render=True.""" + resource_data = {"id": "RES-123", "name": "Test Resource"} + service = AsyncDummyService( + http_client=async_http_client, + query_state=QueryState(options=QueryOptions(render=True)), + ) + with respx.mock: + mock_route = respx.get("https://api.example.com/api/v1/test/RES-123?render()").mock( + return_value=httpx.Response(httpx.codes.OK, json=resource_data) + ) + + result = await service.get("RES-123") + + assert result.to_dict() == resource_data + assert mock_route.call_count == 1 diff --git a/tests/unit/http/mixins/test_queryable_mixin.py b/tests/unit/http/mixins/test_queryable_mixin.py index bf66b1b4..221a3033 100644 --- a/tests/unit/http/mixins/test_queryable_mixin.py +++ b/tests/unit/http/mixins/test_queryable_mixin.py @@ -84,6 +84,6 @@ def test_queryable_mixin_options_render(dummy_service: DummyService) -> None: result = dummy_service.options(render=True) assert result != dummy_service - assert not dummy_service.query_state.render - assert result.query_state.render - assert result.select("id").query_state.render + assert not dummy_service.query_state.options.render + assert result.query_state.options.render + assert result.select("id").query_state.options.render diff --git a/tests/unit/http/test_async_client.py b/tests/unit/http/test_async_client.py index d176d046..414bacab 100644 --- a/tests/unit/http/test_async_client.py +++ b/tests/unit/http/test_async_client.py @@ -7,6 +7,7 @@ from mpt_api_client.exceptions import MPTError from mpt_api_client.http.async_client import AsyncHTTPClient +from mpt_api_client.http.query_options import QueryOptions from tests.unit.conftest import API_TOKEN, API_URL @@ -118,3 +119,27 @@ async def test_http_call_force_multipart(mocker, async_http_client, mock_httpx_r payload_tuple = sent_files["_attachment_data"] assert payload_tuple[2] == "application/json" assert payload_tuple[1].decode() == '{"foo":"bar"}' + + +async def test_request_with_render(mocker, async_http_client, mock_httpx_response): + parent_request = mocker.patch.object( + async_http_client.httpx_client, "request", autospec=True, return_value=mock_httpx_response + ) + + await async_http_client.request("GET", "/", options=QueryOptions(render=True)) + + called_kwargs = parent_request.call_args[1] + assert called_kwargs["params"] == "render()" + + +async def test_request_with_render_and_query_params(mocker, async_http_client, mock_httpx_response): + parent_request = mocker.patch.object( + async_http_client.httpx_client, "request", autospec=True, return_value=mock_httpx_response + ) + + await async_http_client.request( + "GET", "/", query_params={"select": "id,name"}, options=QueryOptions(render=True) + ) + + called_kwargs = parent_request.call_args[1] + assert called_kwargs["params"] == "select=id%2Cname&render()" diff --git a/tests/unit/http/test_client.py b/tests/unit/http/test_client.py index 3dc263ca..50a50fdb 100644 --- a/tests/unit/http/test_client.py +++ b/tests/unit/http/test_client.py @@ -7,6 +7,7 @@ from mpt_api_client.exceptions import MPTError from mpt_api_client.http.client import HTTPClient +from mpt_api_client.http.query_options import QueryOptions from tests.unit.conftest import API_TOKEN, API_URL @@ -107,3 +108,27 @@ def test_http_call_force_multipart(mocker, http_client): payload_tuple = sent_files["_attachment_data"] assert payload_tuple[2] == "application/json" assert payload_tuple[1].decode() == '{"foo":"bar"}' + + +def test_request_with_render(mocker, http_client, mock_httpx_response): + parent_request = mocker.patch.object( + http_client.httpx_client, "request", autospec=True, return_value=mock_httpx_response + ) + http_client.request("GET", "/", options=QueryOptions(render=True)) + + result = parent_request.call_args[1] + + assert result["params"] == "render()" + + +def test_request_with_render_and_query_params(mocker, http_client, mock_httpx_response): + parent_request = mocker.patch.object( + http_client.httpx_client, "request", autospec=True, return_value=mock_httpx_response + ) + http_client.request( + "GET", "/", query_params={"select": "id,name"}, options=QueryOptions(render=True) + ) + + result = parent_request.call_args[1] + + assert result["params"] == "select=id%2Cname&render()" diff --git a/tests/unit/http/test_client_utils.py b/tests/unit/http/test_client_utils.py index 92ac018b..21aa0d79 100644 --- a/tests/unit/http/test_client_utils.py +++ b/tests/unit/http/test_client_utils.py @@ -1,6 +1,28 @@ import pytest -from mpt_api_client.http.client_utils import validate_base_url +from mpt_api_client.http.client_utils import get_query_params, validate_base_url +from mpt_api_client.http.query_options import QueryOptions + + +@pytest.mark.parametrize( + ("query_params", "options", "expected"), + [ + (None, None, ""), + ({}, None, ""), + ({"select": None}, None, ""), + ({"select": "id,name"}, None, "select=id%2Cname"), + ({"select": "id", "order": "asc"}, None, "select=id&order=asc"), + (None, QueryOptions(render=True), "render()"), + ({}, QueryOptions(render=True), "render()"), + ({"select": "id"}, QueryOptions(render=True), "select=id&render()"), + ({"select": None}, QueryOptions(render=True), "render()"), + (None, QueryOptions(render=False), ""), + ], +) +def test_get_query_params(query_params, options, expected): + result = get_query_params(query_params, options) + + assert result == expected @pytest.mark.parametrize( diff --git a/tests/unit/http/test_query_options.py b/tests/unit/http/test_query_options.py new file mode 100644 index 00000000..3f3ca0fc --- /dev/null +++ b/tests/unit/http/test_query_options.py @@ -0,0 +1,13 @@ +from mpt_api_client.http.query_options import QueryOptions + + +def test_default_options() -> None: + result = QueryOptions() + + assert result.render is False + + +def test_render_true() -> None: + result = QueryOptions(render=True) + + assert result.render is True diff --git a/tests/unit/http/test_query_state.py b/tests/unit/http/test_query_state.py index 6c922971..16fb8035 100644 --- a/tests/unit/http/test_query_state.py +++ b/tests/unit/http/test_query_state.py @@ -1,5 +1,6 @@ import pytest +from mpt_api_client.http.query_options import QueryOptions from mpt_api_client.http.query_state import QueryState @@ -23,7 +24,7 @@ def test_build_url(filter_status_active): rql=filter_status_active, select=["-audit", "product.agreements", "-product.agreements.product"], order_by=["-created", "name"], - render=False, + options=QueryOptions(render=False), ) result = query_state.build() @@ -40,7 +41,7 @@ def test_build_url_with_render(filter_status_active): rql=filter_status_active, select=["-audit", "product.agreements", "-product.agreements.product"], order_by=["-created", "name"], - render=True, + options=QueryOptions(render=True), ) result = query_state.build() diff --git a/tests/unit/http/test_resource_accessor.py b/tests/unit/http/test_resource_accessor.py index 383eecd6..f8edf08b 100644 --- a/tests/unit/http/test_resource_accessor.py +++ b/tests/unit/http/test_resource_accessor.py @@ -4,6 +4,7 @@ import pytest import respx +from mpt_api_client.http.query_options import QueryOptions from mpt_api_client.http.resource_accessor import AsyncResourceAccessor, ResourceAccessor from tests.unit.conftest import API_URL, DummyModel @@ -240,3 +241,31 @@ async def test_async_delete(async_http_client): # noqa: AAA01 assert mock_route.call_count == 1 assert mock_route.calls[0].request.method == "DELETE" + + +async def test_async_get_with_render(async_http_client): + response_data = {"id": "RES-123", "name": "Test Resource"} + with respx.mock: + mock_route = respx.get(f"{FULL_URL}?render()").mock( + return_value=httpx.Response(httpx.codes.OK, json=response_data) + ) + accessor = AsyncResourceAccessor(async_http_client, RESOURCE_URL, DummyModel) + + result = await accessor.get(options=QueryOptions(render=True)) + + assert result.to_dict() == response_data + assert mock_route.call_count == 1 + + +def test_get_with_render(http_client): + response_data = {"id": "RES-123", "name": "Test Resource"} + with respx.mock: + mock_route = respx.get(f"{FULL_URL}?render()").mock( + return_value=httpx.Response(httpx.codes.OK, json=response_data) + ) + accessor = ResourceAccessor(http_client, RESOURCE_URL, DummyModel) + + result = accessor.get(options=QueryOptions(render=True)) + + assert result.to_dict() == response_data + assert mock_route.call_count == 1