diff --git a/duniterpy/api/bma/ws.py b/duniterpy/api/bma/ws.py index ecd857f08070e48a642e2685d85ecbd8803d84a0..1a0096cc59a96049c3f2f5efd302b25bc1ab9a38 100644 --- a/duniterpy/api/bma/ws.py +++ b/duniterpy/api/bma/ws.py @@ -17,8 +17,9 @@ # # vit import logging -from duniterpy.api.client import Client + from duniterpy.api.bma.blockchain import BLOCK_SCHEMA +from duniterpy.api.client import Client logger = logging.getLogger("duniter/ws") @@ -52,23 +53,21 @@ WS_PEER_SCHEMA = { } -# def block(client: Client): -# """ -# Connect to block websocket -# -# :param client: Client to connect to the api -# :rtype: aiohttp.ClientWebSocketResponse -# """ -# client = API(connection, MODULE) -# return client.connect_ws('/block') +def block(client: Client): + """ + Connect to block websocket + :param client: Client to connect to the api + :rtype: aiohttp.ClientWebSocketResponse + """ + return client.connect_ws(MODULE + '/block') -# def peer(client: Client): -# """ -# Connect to peer websocket -# -# :param duniterpy.api.bma.ConnectionHandler connection: Connection handler instance -# :rtype: aiohttp.ClientWebSocketResponse -# """ -# client = API(connection, MODULE) -# return client.connect_ws('/peer') + +def peer(client: Client): + """ + Connect to peer websocket + + :param client: Client to connect to the api + :rtype: aiohttp.ClientWebSocketResponse + """ + return client.connect_ws(MODULE + '/peer') diff --git a/duniterpy/api/client.py b/duniterpy/api/client.py index 9bbeecca92ec55972275eca060e2dae71fcb973b..4e7aea6d177071d4db1da619ca310e75803ffeb3 100644 --- a/duniterpy/api/client.py +++ b/duniterpy/api/client.py @@ -34,43 +34,45 @@ ERROR_SCHEMA = { } -def parse_text(text, schema): +def parse_text(text: str, schema: dict) -> any: """ Validate and parse the BMA answer from websocket - :param str text: the bma answer - :param dict schema: dict for jsonschema + :param text: the bma answer + :param schema: dict for jsonschema :return: the json data """ try: data = json.loads(text) jsonschema.validate(data, schema) - return data except (TypeError, json.decoder.JSONDecodeError): raise jsonschema.ValidationError("Could not parse json") + return data + -def parse_error(text): +def parse_error(text: str) -> any: """ Validate and parse the BMA answer from websocket - :param str text: the bma error + :param text: the bma error :return: the json data """ try: data = json.loads(text) jsonschema.validate(data, ERROR_SCHEMA) - return data except (TypeError, json.decoder.JSONDecodeError) as e: raise jsonschema.ValidationError("Could not parse json : {0}".format(str(e))) + return data + -async def parse_response(response, schema): +async def parse_response(response: aiohttp.ClientResponse, schema: dict) -> any: """ Validate and parse the BMA answer - :param aiohttp.ClientResponse response: Response of aiohttp request - :param dict schema: The expected response structure + :param response: Response of aiohttp request + :param schema: The expected response structure :return: the json data """ try: @@ -84,28 +86,28 @@ async def parse_response(response, schema): class API(object): - """APIRequest is a class used as an interface. The intermediate derivated classes are the modules and the leaf - classes are the API requests. """ - + """ + API is a class used as an abstraction layer over the request library (AIOHTTP). + """ schema = {} - def __init__(self, connection_handler, module): + def __init__(self, connection_handler: endpoint.ConnectionHandler, module: str): """ Asks a module in order to create the url used then by derivated classes. - :param ConnectionHandler connection_handler: Connection handler - :param str module: Module path + :param connection_handler: Connection handler + :param module: Module path """ self.module = module self.connection_handler = connection_handler self.headers = {} - def reverse_url(self, scheme, path): + def reverse_url(self, scheme: str, path: str) -> str: """ Reverses the url using scheme and path given in parameter. - :param str scheme: Scheme of the url - :param str path: Path of the url + :param scheme: Scheme of the url + :param path: Path of the url :return: str """ @@ -124,11 +126,11 @@ class API(object): return url + path - async def requests_get(self, path, **kwargs): + async def requests_get(self, path: str, **kwargs) -> aiohttp.ClientResponse: """ Requests GET wrapper in order to use API parameters. - :param str path: the request path + :param path: the request path :rtype: aiohttp.ClientResponse """ logging.debug("Request : {0}".format(self.reverse_url(self.connection_handler.http_scheme, path))) @@ -145,11 +147,11 @@ class API(object): return response - async def requests_post(self, path, **kwargs): + async def requests_post(self, path: str, **kwargs) -> aiohttp.ClientResponse: """ Requests POST wrapper in order to use API parameters. - :param str path: the request path + :param path: the request path :rtype: aiohttp.ClientResponse """ if 'self_' in kwargs: @@ -165,11 +167,11 @@ class API(object): ) return response - def connect_ws(self, path): + def connect_ws(self, path: str) -> aiohttp.ClientWebSocketResponse: """ Connect to a websocket in order to use API parameters - :param str path: the url path + :param path: the url path :rtype: aiohttp.ClientWebSocketResponse """ url = self.reverse_url(self.connection_handler.ws_scheme, path) @@ -266,6 +268,16 @@ class Client: elif rtype == RESPONSE_JSON: return await response.json() + def connect_ws(self, path: str) -> aiohttp.ClientWebSocketResponse: + """ + Connect to a websocket in order to use API parameters + + :param path: the url path + :rtype: aiohttp.ClientWebSocketResponse + """ + client = API(self.endpoint.conn_handler(self.session, self.proxy), '') + return client.connect_ws(path) + async def close(self): """ Close aiohttp session @@ -274,14 +286,14 @@ class Client: """ await self.session.close() - async def __call__(self, _function: Callable, *args: any, **kwargs: any) -> any: + def __call__(self, _function: Callable, *args: any, **kwargs: any) -> any: """ Call the _function given with the args given - So we can have many packages wrapping a REST API + So we can call many packages wrapping the REST API :param _function: The function to call :param args: The parameters :param kwargs: The key/value parameters :return: """ - return await _function(self, *args, **kwargs) + return _function(self, *args, **kwargs) diff --git a/examples/request_web_socket_block.py b/examples/request_web_socket_block.py new file mode 100644 index 0000000000000000000000000000000000000000..f52ffb57d2325854f22d3e19c02a8d8323e968e8 --- /dev/null +++ b/examples/request_web_socket_block.py @@ -0,0 +1,58 @@ +import asyncio +from _socket import gaierror + +import aiohttp +import jsonschema + +from duniterpy.api import bma +from duniterpy.api.client import Client, parse_text + +# 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) +BMAS_ENDPOINT = "BMAS g1-test.duniter.org 443" + + +################################################ + + +async def main(): + """ + Main code + """ + # Create Client from endpoint string in Duniter format + client = Client(BMAS_ENDPOINT) + + try: + # Create Web Socket connection on block path + ws_connection = client(bma.ws.block) + + # Mandatory to get the "for msg in ws" to work ! + # But it should work on the ws_connection which is a ClientWebSocketResponse object... + # https://docs.aiohttp.org/en/stable/client_quickstart.html#websockets + async with ws_connection as ws: + print("Connected successfully to block ws") + async for msg in ws: + if msg.type == aiohttp.WSMsgType.TEXT: + print("Received a block") + block_data = parse_text(msg.data, bma.ws.WS_BLOCK_SCHEMA) + print(block_data) + await client.close() + elif msg.type == aiohttp.WSMsgType.CLOSED: + print("Web socket connection closed !") + elif msg.type == aiohttp.WSMsgType.ERROR: + print("Web socket connection error !") + + except (aiohttp.WSServerHandshakeError, ValueError) as e: + print("Websocket block {0} : {1}".format(type(e).__name__, str(e))) + except (aiohttp.ClientError, gaierror, TimeoutError) as e: + print("{0} : {1}".format(str(e), BMAS_ENDPOINT)) + except jsonschema.ValidationError as e: + print("{:}:{:}".format(str(e.__class__.__name__), str(e))) + + +# 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()) diff --git a/tests/api/bma/test_ws.py b/tests/api/bma/test_ws.py index 511c3623eaa06000bf8c8bba94c69a79e973ac9c..9357eaf744aef920c7fcd1a86123ba5dc657f7e1 100644 --- a/tests/api/bma/test_ws.py +++ b/tests/api/bma/test_ws.py @@ -1,10 +1,11 @@ import unittest + +from duniterpy.api.bma.ws import WS_BLOCK_SCHEMA, WS_PEER_SCHEMA +from duniterpy.api.client import parse_text from tests.api.webserver import WebFunctionalSetupMixin -from duniterpy.api.bma.ws import block, peer, WS_BLOCK_SCHEMA, WS_PEER_SCHEMA -from duniterpy.api.bma import parse_text -class Test_BMA_Websocket(WebFunctionalSetupMixin, unittest.TestCase): +class TestBmaWebsocket(WebFunctionalSetupMixin, unittest.TestCase): def test_block(self): json_sample = """ @@ -101,4 +102,5 @@ class Test_BMA_Websocket(WebFunctionalSetupMixin, unittest.TestCase): self.assertEqual(data["currency"], "beta_brouzouf") self.assertEqual(data["pubkey"], "HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY") self.assertEqual(len(data["endpoints"]), 3) - self.assertEqual(data["signature"], "42yQm4hGTJYWkPg39hQAUgP6S6EQ4vTfXdJuxKEHL1ih6YHiDL2hcwrFgBHjXLRgxRhj2VNVqqc6b4JayKqTE14r") + self.assertEqual(data["signature"], + "42yQm4hGTJYWkPg39hQAUgP6S6EQ4vTfXdJuxKEHL1ih6YHiDL2hcwrFgBHjXLRgxRhj2VNVqqc6b4JayKqTE14r")