Skip to content
Snippets Groups Projects
Commit 1d38066a authored by Vincent Texier's avatar Vincent Texier Committed by Vincent Texier
Browse files

issue #59 WIP: New Client.query method to request GraphQL API

parent b2faf741
No related branches found
No related tags found
No related merge requests found
......@@ -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
......
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())
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment