From 8364a569307dbf7987a3d944c3d37c844af250ae Mon Sep 17 00:00:00 2001 From: Leszek Hanusz Date: Thu, 8 Feb 2024 14:20:45 +0100 Subject: [PATCH 1/5] Adding json_unserialize parameter to AIOHTTP transport --- gql/transport/aiohttp.py | 6 ++++- tests/test_aiohttp.py | 50 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/gql/transport/aiohttp.py b/gql/transport/aiohttp.py index 60f42c94..f456aa3f 100644 --- a/gql/transport/aiohttp.py +++ b/gql/transport/aiohttp.py @@ -50,6 +50,7 @@ def __init__( timeout: Optional[int] = None, ssl_close_timeout: Optional[Union[int, float]] = 10, json_serialize: Callable = json.dumps, + json_unserialize: Callable = json.loads, client_session_args: Optional[Dict[str, Any]] = None, ) -> None: """Initialize the transport with the given aiohttp parameters. @@ -64,6 +65,8 @@ def __init__( to close properly :param json_serialize: Json serializer callable. By default json.dumps() function + :param json_unserialize: Json unserializer callable. + By default json.loads() function :param client_session_args: Dict of extra args passed to `aiohttp.ClientSession`_ @@ -81,6 +84,7 @@ def __init__( self.session: Optional[aiohttp.ClientSession] = None self.response_headers: Optional[CIMultiDictProxy[str]] self.json_serialize: Callable = json_serialize + self.json_unserialize: Callable = json_unserialize async def connect(self) -> None: """Coroutine which will create an aiohttp ClientSession() as self.session. @@ -328,7 +332,7 @@ async def raise_response_error(resp: aiohttp.ClientResponse, reason: str): ) try: - result = await resp.json(content_type=None) + result = await resp.json(loads=self.json_unserialize, content_type=None) if log.isEnabledFor(logging.INFO): result_text = await resp.text() diff --git a/tests/test_aiohttp.py b/tests/test_aiohttp.py index 09259e51..6a939966 100644 --- a/tests/test_aiohttp.py +++ b/tests/test_aiohttp.py @@ -1511,6 +1511,56 @@ async def handler(request): assert expected_log in caplog.text +query_float_str = """ + query getPi { + pi + } +""" + +query_float_server_answer_data = '{"pi": 3.141592653589793238462643383279502884197}' + +query_float_server_answer = f'{{"data":{query_float_server_answer_data}}}' + + +@pytest.mark.asyncio +async def test_aiohttp_json_unserializer(event_loop, aiohttp_server): + from aiohttp import web + from decimal import Decimal + from functools import partial + from gql.transport.aiohttp import AIOHTTPTransport + + async def handler(request): + return web.Response( + text=query_float_server_answer, + content_type="application/json", + ) + + app = web.Application() + app.router.add_route("POST", "/", handler) + server = await aiohttp_server(app) + + url = server.make_url("/") + + json_loads = partial(json.loads, parse_float=Decimal) + + transport = AIOHTTPTransport( + url=url, + timeout=10, + json_unserialize=json_loads, + ) + + async with Client(transport=transport) as session: + + query = gql(query_float_str) + + # Execute query asynchronously + result = await session.execute(query) + + pi = result["pi"] + + assert pi == Decimal("3.141592653589793238462643383279502884197") + + @pytest.mark.asyncio async def test_aiohttp_connector_owner_false(event_loop, aiohttp_server): from aiohttp import web, TCPConnector From b5ceeab13ebc3f75c994ff8f500a407849d3a7db Mon Sep 17 00:00:00 2001 From: Leszek Hanusz Date: Thu, 8 Feb 2024 14:43:59 +0100 Subject: [PATCH 2/5] Adding json_unserialize attribute the httpx transport --- gql/transport/httpx.py | 10 +++++++- tests/test_httpx_async.py | 50 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/gql/transport/httpx.py b/gql/transport/httpx.py index cfc25dc9..a328127f 100644 --- a/gql/transport/httpx.py +++ b/gql/transport/httpx.py @@ -38,6 +38,7 @@ def __init__( self, url: Union[str, httpx.URL], json_serialize: Callable = json.dumps, + json_unserialize: Callable = json.loads, **kwargs, ): """Initialize the transport with the given httpx parameters. @@ -45,10 +46,13 @@ def __init__( :param url: The GraphQL server URL. Example: 'https://server.com:PORT/path'. :param json_serialize: Json serializer callable. By default json.dumps() function. + :param json_unserialize: Json unserializer callable. + By default json.loads() function. :param kwargs: Extra args passed to the `httpx` client. """ self.url = url self.json_serialize = json_serialize + self.json_unserialize = json_unserialize self.kwargs = kwargs def _prepare_request( @@ -145,7 +149,11 @@ def _prepare_result(self, response: httpx.Response) -> ExecutionResult: log.debug("<<< %s", response.text) try: - result: Dict[str, Any] = response.json() + result: Dict[str, Any] + if self.json_unserialize == json.loads: + result = response.json() + else: + result = self.json_unserialize(response.content) except Exception: self._raise_response_error(response, "Not a JSON answer") diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py index e5be73ec..e8350ad1 100644 --- a/tests/test_httpx_async.py +++ b/tests/test_httpx_async.py @@ -1389,3 +1389,53 @@ async def handler(request): # Checking that there is no space after the colon in the log expected_log = '"query":"query getContinents' assert expected_log in caplog.text + + +query_float_str = """ + query getPi { + pi + } +""" + +query_float_server_answer_data = '{"pi": 3.141592653589793238462643383279502884197}' + +query_float_server_answer = f'{{"data":{query_float_server_answer_data}}}' + + +@pytest.mark.asyncio +async def test_httpx_json_unserializer(event_loop, aiohttp_server): + from aiohttp import web + from decimal import Decimal + from functools import partial + from gql.transport.httpx import HTTPXAsyncTransport + + async def handler(request): + return web.Response( + text=query_float_server_answer, + content_type="application/json", + ) + + app = web.Application() + app.router.add_route("POST", "/", handler) + server = await aiohttp_server(app) + + url = str(server.make_url("/")) + + json_loads = partial(json.loads, parse_float=Decimal) + + transport = HTTPXAsyncTransport( + url=url, + timeout=10, + json_unserialize=json_loads, + ) + + async with Client(transport=transport) as session: + + query = gql(query_float_str) + + # Execute query asynchronously + result = await session.execute(query) + + pi = result["pi"] + + assert pi == Decimal("3.141592653589793238462643383279502884197") From 311601b586b1434be00ec12889ce2e392be6b98e Mon Sep 17 00:00:00 2001 From: Leszek Hanusz Date: Thu, 8 Feb 2024 14:48:12 +0100 Subject: [PATCH 3/5] Add missing aiohttp marker on test --- tests/test_httpx_async.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py index e8350ad1..409d1d6c 100644 --- a/tests/test_httpx_async.py +++ b/tests/test_httpx_async.py @@ -1402,6 +1402,7 @@ async def handler(request): query_float_server_answer = f'{{"data":{query_float_server_answer_data}}}' +@pytest.mark.aiohttp @pytest.mark.asyncio async def test_httpx_json_unserializer(event_loop, aiohttp_server): from aiohttp import web From defef71e3963444ffa1faf583052e4595e9a2635 Mon Sep 17 00:00:00 2001 From: Leszek Hanusz Date: Thu, 8 Feb 2024 16:42:30 +0100 Subject: [PATCH 4/5] Replace unserialize by deserialize --- gql/client.py | 18 +++++++++--------- gql/transport/aiohttp.py | 8 ++++---- gql/transport/httpx.py | 10 +++++----- tests/test_aiohttp.py | 4 ++-- tests/test_httpx_async.py | 4 ++-- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/gql/client.py b/gql/client.py index a79d4b72..0d9e36c7 100644 --- a/gql/client.py +++ b/gql/client.py @@ -106,7 +106,7 @@ def __init__( :param serialize_variables: whether the variable values should be serialized. Used for custom scalars and/or enums. Default: False. :param parse_results: Whether gql will try to parse the serialized output - sent by the backend. Can be used to unserialize custom scalars or enums. + sent by the backend. Can be used to deserialize custom scalars or enums. :param batch_interval: Time to wait in seconds for batching requests together. Batching is disabled (by default) if 0. :param batch_max: Maximum number of requests in a single batch. @@ -892,7 +892,7 @@ def _execute( :param serialize_variables: whether the variable values should be serialized. Used for custom scalars and/or enums. By default use the serialize_variables argument of the client. - :param parse_result: Whether gql will unserialize the result. + :param parse_result: Whether gql will deserialize the result. By default use the parse_results argument of the client. The extra arguments are passed to the transport execute method.""" @@ -1006,7 +1006,7 @@ def execute( :param serialize_variables: whether the variable values should be serialized. Used for custom scalars and/or enums. By default use the serialize_variables argument of the client. - :param parse_result: Whether gql will unserialize the result. + :param parse_result: Whether gql will deserialize the result. By default use the parse_results argument of the client. :param get_execution_result: return the full ExecutionResult instance instead of only the "data" field. Necessary if you want to get the "extensions" field. @@ -1057,7 +1057,7 @@ def _execute_batch( :param serialize_variables: whether the variable values should be serialized. Used for custom scalars and/or enums. By default use the serialize_variables argument of the client. - :param parse_result: Whether gql will unserialize the result. + :param parse_result: Whether gql will deserialize the result. By default use the parse_results argument of the client. :param validate_document: Whether we still need to validate the document. @@ -1151,7 +1151,7 @@ def execute_batch( :param serialize_variables: whether the variable values should be serialized. Used for custom scalars and/or enums. By default use the serialize_variables argument of the client. - :param parse_result: Whether gql will unserialize the result. + :param parse_result: Whether gql will deserialize the result. By default use the parse_results argument of the client. :param get_execution_result: return the full ExecutionResult instance instead of only the "data" field. Necessary if you want to get the "extensions" field. @@ -1333,7 +1333,7 @@ async def _subscribe( :param serialize_variables: whether the variable values should be serialized. Used for custom scalars and/or enums. By default use the serialize_variables argument of the client. - :param parse_result: Whether gql will unserialize the result. + :param parse_result: Whether gql will deserialize the result. By default use the parse_results argument of the client. The extra arguments are passed to the transport subscribe method.""" @@ -1454,7 +1454,7 @@ async def subscribe( :param serialize_variables: whether the variable values should be serialized. Used for custom scalars and/or enums. By default use the serialize_variables argument of the client. - :param parse_result: Whether gql will unserialize the result. + :param parse_result: Whether gql will deserialize the result. By default use the parse_results argument of the client. :param get_execution_result: yield the full ExecutionResult instance instead of only the "data" field. Necessary if you want to get the "extensions" field. @@ -1511,7 +1511,7 @@ async def _execute( :param serialize_variables: whether the variable values should be serialized. Used for custom scalars and/or enums. By default use the serialize_variables argument of the client. - :param parse_result: Whether gql will unserialize the result. + :param parse_result: Whether gql will deserialize the result. By default use the parse_results argument of the client. The extra arguments are passed to the transport execute method.""" @@ -1617,7 +1617,7 @@ async def execute( :param serialize_variables: whether the variable values should be serialized. Used for custom scalars and/or enums. By default use the serialize_variables argument of the client. - :param parse_result: Whether gql will unserialize the result. + :param parse_result: Whether gql will deserialize the result. By default use the parse_results argument of the client. :param get_execution_result: return the full ExecutionResult instance instead of only the "data" field. Necessary if you want to get the "extensions" field. diff --git a/gql/transport/aiohttp.py b/gql/transport/aiohttp.py index f456aa3f..be22ce9c 100644 --- a/gql/transport/aiohttp.py +++ b/gql/transport/aiohttp.py @@ -50,7 +50,7 @@ def __init__( timeout: Optional[int] = None, ssl_close_timeout: Optional[Union[int, float]] = 10, json_serialize: Callable = json.dumps, - json_unserialize: Callable = json.loads, + json_deserialize: Callable = json.loads, client_session_args: Optional[Dict[str, Any]] = None, ) -> None: """Initialize the transport with the given aiohttp parameters. @@ -65,7 +65,7 @@ def __init__( to close properly :param json_serialize: Json serializer callable. By default json.dumps() function - :param json_unserialize: Json unserializer callable. + :param json_deserialize: Json deserializer callable. By default json.loads() function :param client_session_args: Dict of extra args passed to `aiohttp.ClientSession`_ @@ -84,7 +84,7 @@ def __init__( self.session: Optional[aiohttp.ClientSession] = None self.response_headers: Optional[CIMultiDictProxy[str]] self.json_serialize: Callable = json_serialize - self.json_unserialize: Callable = json_unserialize + self.json_deserialize: Callable = json_deserialize async def connect(self) -> None: """Coroutine which will create an aiohttp ClientSession() as self.session. @@ -332,7 +332,7 @@ async def raise_response_error(resp: aiohttp.ClientResponse, reason: str): ) try: - result = await resp.json(loads=self.json_unserialize, content_type=None) + result = await resp.json(loads=self.json_deserialize, content_type=None) if log.isEnabledFor(logging.INFO): result_text = await resp.text() diff --git a/gql/transport/httpx.py b/gql/transport/httpx.py index a328127f..0846b353 100644 --- a/gql/transport/httpx.py +++ b/gql/transport/httpx.py @@ -38,7 +38,7 @@ def __init__( self, url: Union[str, httpx.URL], json_serialize: Callable = json.dumps, - json_unserialize: Callable = json.loads, + json_deserialize: Callable = json.loads, **kwargs, ): """Initialize the transport with the given httpx parameters. @@ -46,13 +46,13 @@ def __init__( :param url: The GraphQL server URL. Example: 'https://server.com:PORT/path'. :param json_serialize: Json serializer callable. By default json.dumps() function. - :param json_unserialize: Json unserializer callable. + :param json_deserialize: Json deserializer callable. By default json.loads() function. :param kwargs: Extra args passed to the `httpx` client. """ self.url = url self.json_serialize = json_serialize - self.json_unserialize = json_unserialize + self.json_deserialize = json_deserialize self.kwargs = kwargs def _prepare_request( @@ -150,10 +150,10 @@ def _prepare_result(self, response: httpx.Response) -> ExecutionResult: try: result: Dict[str, Any] - if self.json_unserialize == json.loads: + if self.json_deserialize == json.loads: result = response.json() else: - result = self.json_unserialize(response.content) + result = self.json_deserialize(response.content) except Exception: self._raise_response_error(response, "Not a JSON answer") diff --git a/tests/test_aiohttp.py b/tests/test_aiohttp.py index 6a939966..b16964d0 100644 --- a/tests/test_aiohttp.py +++ b/tests/test_aiohttp.py @@ -1523,7 +1523,7 @@ async def handler(request): @pytest.mark.asyncio -async def test_aiohttp_json_unserializer(event_loop, aiohttp_server): +async def test_aiohttp_json_deserializer(event_loop, aiohttp_server): from aiohttp import web from decimal import Decimal from functools import partial @@ -1546,7 +1546,7 @@ async def handler(request): transport = AIOHTTPTransport( url=url, timeout=10, - json_unserialize=json_loads, + json_deserialize=json_loads, ) async with Client(transport=transport) as session: diff --git a/tests/test_httpx_async.py b/tests/test_httpx_async.py index 409d1d6c..3665f5d8 100644 --- a/tests/test_httpx_async.py +++ b/tests/test_httpx_async.py @@ -1404,7 +1404,7 @@ async def handler(request): @pytest.mark.aiohttp @pytest.mark.asyncio -async def test_httpx_json_unserializer(event_loop, aiohttp_server): +async def test_httpx_json_deserializer(event_loop, aiohttp_server): from aiohttp import web from decimal import Decimal from functools import partial @@ -1427,7 +1427,7 @@ async def handler(request): transport = HTTPXAsyncTransport( url=url, timeout=10, - json_unserialize=json_loads, + json_deserialize=json_loads, ) async with Client(transport=transport) as session: From faa40d7e04210f95f362e6a3d3eeddbaa24b7f25 Mon Sep 17 00:00:00 2001 From: Leszek Hanusz Date: Thu, 8 Feb 2024 16:52:26 +0100 Subject: [PATCH 5/5] Simplify httpx transport --- gql/transport/httpx.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/gql/transport/httpx.py b/gql/transport/httpx.py index 0846b353..811601b8 100644 --- a/gql/transport/httpx.py +++ b/gql/transport/httpx.py @@ -149,11 +149,7 @@ def _prepare_result(self, response: httpx.Response) -> ExecutionResult: log.debug("<<< %s", response.text) try: - result: Dict[str, Any] - if self.json_deserialize == json.loads: - result = response.json() - else: - result = self.json_deserialize(response.content) + result: Dict[str, Any] = self.json_deserialize(response.content) except Exception: self._raise_response_error(response, "Not a JSON answer")