From 3eb1154f59686ce260f256212eb5a52e668b7e6c Mon Sep 17 00:00:00 2001 From: Vincent Texier <vit@free.fr> Date: Sat, 21 Jul 2018 19:16:28 +0200 Subject: [PATCH] [enh] #59 new Client.query method to request GraphQL API --- duniterpy/api/client.py | 123 +++++++++++++++++++++++++++++++----- examples/request_graphql.py | 38 +++++++++++ 2 files changed, 144 insertions(+), 17 deletions(-) create mode 100644 examples/request_graphql.py diff --git a/duniterpy/api/client.py b/duniterpy/api/client.py index 23a3dc17..1df2880a 100644 --- a/duniterpy/api/client.py +++ b/duniterpy/api/client.py @@ -17,7 +17,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. import json import logging -from typing import Callable, Union, Any, Optional +from typing import Callable, Union, Any, Optional, Dict import jsonschema from aiohttp import ClientResponse, ClientSession, ClientWebSocketResponse @@ -60,7 +60,7 @@ def parse_text(text: str, schema: dict) -> Any: return data -def parse_error(text: str) -> Any: +def parse_error(text: str) -> dict: """ Validate and parse the BMA answer from websocket @@ -237,7 +237,7 @@ class API: return url + path - async def requests_get(self, path: str, **kwargs) -> ClientResponse: + async def requests_get(self, path: str, **kwargs: Any) -> ClientResponse: """ Requests GET wrapper in order to use API parameters. @@ -267,7 +267,7 @@ class API: return response - async def requests_post(self, path: str, **kwargs) -> ClientResponse: + async def requests_post(self, path: str, **kwargs: Any) -> ClientResponse: """ Requests POST wrapper in order to use API parameters. @@ -285,6 +285,55 @@ class API: proxy=self.connection_handler.proxy, timeout=15, ) + + if response.status != 200: + try: + error_data = parse_error(await response.text()) + raise DuniterError(error_data) + except (TypeError, jsonschema.ValidationError) as e: + raise ValueError( + "status code != 200 => %d (%s)" + % (response.status, (await response.text())) + ) from e + + return response + + async def requests( + self, + method: str = "GET", + path: str = "", + data: Optional[dict] = None, + _json: Optional[dict] = None, + ) -> ClientResponse: + """ + Generic requests wrapper on aiohttp + + :param method: the request http method + :param path: the path added to endpoint + :param data: data for form POST request + :param _json: json for json POST request + :rtype: aiohttp.ClientResponse + """ + url = self.reverse_url(self.connection_handler.http_scheme, path) + + if data is not None: + logging.debug("%s : %s, data=%s", method, url, data) + elif _json is not None: + logging.debug("%s : %s, json=%s", method, url, _json) + # http header to send json body + self.headers["Content-Type"] = "application/json; charset=utf-8" + else: + logging.debug("%s : %s", method, url) + + response = await self.connection_handler.session.request( + method, + url, + data=data, + json=_json, + headers=self.headers, + proxy=self.connection_handler.proxy, + timeout=15, + ) return response async def connect_ws(self, path: str) -> WSConnection: @@ -321,15 +370,15 @@ class Client: def __init__( self, _endpoint: Union[str, endpoint.Endpoint], - session: ClientSession = None, - proxy: str = None, + session: Optional[ClientSession] = None, + proxy: Optional[str] = None, ) -> None: """ Init Client instance :param _endpoint: Endpoint string in duniter format :param session: Aiohttp client session (optional, default None) - :param proxy: Proxy server as hostname:port + :param proxy: Proxy server as hostname:port (optional, default None) """ if isinstance(_endpoint, str): # Endpoint Protocol detection @@ -353,16 +402,16 @@ class Client: async def get( self, url_path: str, - params: dict = None, + params: Optional[dict] = None, rtype: str = RESPONSE_JSON, - schema: dict = None, + schema: Optional[dict] = None, ) -> Any: """ - GET request on self.endpoint + url_path + GET request on endpoint host + url_path :param url_path: Url encoded path following the endpoint - :param params: Url query string parameters dictionary - :param rtype: Response type + :param params: Url query string parameters dictionary (optional, default None) + :param rtype: Response type (optional, default RESPONSE_JSON) :param schema: Json Schema to validate response (optional, default None) :return: """ @@ -391,16 +440,16 @@ class Client: async def post( self, url_path: str, - params: dict = None, + params: Optional[dict] = None, rtype: str = RESPONSE_JSON, - schema: dict = None, + schema: Optional[dict] = None, ) -> Any: """ - POST request on self.endpoint + url_path + POST request on endpoint host + url_path :param url_path: Url encoded path following the endpoint - :param params: Url query string parameters dictionary - :param rtype: Response type + :param params: Url query string parameters dictionary (optional, default None) + :param rtype: Response type (optional, default RESPONSE_JSON) :param schema: Json Schema to validate response (optional, default None) :return: """ @@ -426,6 +475,46 @@ class Client: return result + async def query( + self, + query: str, + variables: Optional[dict] = None, + rtype: str = RESPONSE_JSON, + schema: Optional[dict] = None, + ) -> Any: + """ + GraphQL query or mutation request on endpoint + + :param query: GraphQL query string + :param variables: Variables for the query (optional, default None) + :param rtype: Response type (optional, default RESPONSE_JSON) + :param schema: Json Schema to validate response (optional, default None) + :return: + """ + payload = {"query": query} # type: Dict[str, Union[str, dict]] + + if variables is not None: + payload["variables"] = variables + + client = API(self.endpoint.conn_handler(self.session, self.proxy)) + + # get aiohttp response + response = await client.requests("POST", _json=payload) + + # if schema supplied... + if schema is not None: + # validate response + await parse_response(response, schema) + + # return the chosen type + result = response # type: Any + if rtype == RESPONSE_TEXT: + result = await response.text() + elif rtype == RESPONSE_JSON: + result = await response.json() + + return result + async def connect_ws(self, path: str = "") -> WSConnection: """ Connect to a websocket in order to use API parameters diff --git a/examples/request_graphql.py b/examples/request_graphql.py new file mode 100644 index 00000000..4322ac6e --- /dev/null +++ b/examples/request_graphql.py @@ -0,0 +1,38 @@ +import asyncio + +from duniterpy.api.client import Client + +# CONFIG ####################################### + +# You can either use a complete defined endpoint : [NAME_OF_THE_API] [DOMAIN] [IPv4] [IPv6] [PORT] +# or the simple definition : [NAME_OF_THE_API] [DOMAIN] [PORT] +# Here we use the secure BASIC_MERKLED_API (BMAS) +SWAPI_ENDPOINT = "BMAS swapi.graph.cool 443" + + +################################################ + + +async def main(): + client = Client(SWAPI_ENDPOINT) + + query = """query { + allFilms { + title, + characters { + name + } + } + } + """ + + response = await client.query(query) + print(response) + + # Close client aiohttp session + await client.close() + + +# Latest duniter-python-api is asynchronous and you have to use asyncio, an asyncio loop and a "as" on the data. +# ( https://docs.python.org/3/library/asyncio.html ) +asyncio.get_event_loop().run_until_complete(main()) -- GitLab