diff --git a/duniterpy/api/client.py b/duniterpy/api/client.py index 4960d961fcb218c26c49708898873088ce6a68d9..92518245c7a2d4e103367a44916a2146112e5b5a 100644 --- a/duniterpy/api/client.py +++ b/duniterpy/api/client.py @@ -4,7 +4,7 @@ # vit 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 @@ -44,7 +44,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 @@ -120,7 +120,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. @@ -150,7 +150,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. @@ -162,12 +162,53 @@ class API: logging.debug("POST : %s", kwargs) response = await self.connection_handler.session.post( - self.reverse_url(self.connection_handler.http_scheme, path), + url, data=kwargs, headers=self.headers, 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): + raise ValueError('status code != 200 => %d (%s)' % (response.status, (await response.text()))) + + return response + + async def requests(self, method: str = 'GET', path: str = '', data: Optional[dict] = None, + _json: Optional[dict] = None) -> aiohttp.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("{0} : {1}, data={2}".format(method, url, data)) + elif _json is not None: + logging.debug("{0} : {1}, json={2}".format(method, url, _json)) + # http header to send json body + self.headers['Content-Type'] = 'application/json; charset=utf-8' + else: + logging.debug("{0} : {1}".format(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 def connect_ws(self, path: str) -> _WSRequestContextManager: @@ -193,18 +234,14 @@ class Client: Main class to create an API client """ - def __init__( - self, - _endpoint: Union[str, endpoint.Endpoint], - session: ClientSession = None, - proxy: str = None, - ) -> None: + def __init__(self, _endpoint: Union[str, endpoint.Endpoint], 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 @@ -225,19 +262,14 @@ class Client: self.session = session self.proxy = proxy - async def get( - self, - url_path: str, - params: dict = None, - rtype: str = RESPONSE_JSON, - schema: dict = None, - ) -> Any: + async def get(self, url_path: str, params: Optional[dict] = None, rtype: str = RESPONSE_JSON, + 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: """ @@ -263,19 +295,14 @@ class Client: return result - async def post( - self, - url_path: str, - params: dict = None, - rtype: str = RESPONSE_JSON, - schema: dict = None, - ) -> Any: + async def post(self, url_path: str, params: Optional[dict] = None, rtype: str = RESPONSE_JSON, + 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: """ @@ -287,6 +314,42 @@ class Client: # get aiohttp response response = await client.requests_post(url_path, **params) + # if schema supplied... + if schema is not None: + # validate response + await parse_response(response, schema) + + # return the chosen type + if rtype == RESPONSE_AIOHTTP: + return response + elif rtype == RESPONSE_TEXT: + return await response.text() + elif rtype == RESPONSE_JSON: + return await response.json() + + 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 diff --git a/examples/request_swapi.py b/examples/request_swapi.py new file mode 100644 index 0000000000000000000000000000000000000000..4322ac6e23358514af41665a9f2cf9cefdeb19f4 --- /dev/null +++ b/examples/request_swapi.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())