Skip to content
Snippets Groups Projects
Commit 865a9eb8 authored by Vincent Texier's avatar Vincent Texier
Browse files

issue #56 WIP - Add web socket support to Client() + example

parent ca039879
No related branches found
No related tags found
No related merge requests found
...@@ -17,8 +17,9 @@ ...@@ -17,8 +17,9 @@
# #
# vit # vit
import logging import logging
from duniterpy.api.client import Client
from duniterpy.api.bma.blockchain import BLOCK_SCHEMA from duniterpy.api.bma.blockchain import BLOCK_SCHEMA
from duniterpy.api.client import Client
logger = logging.getLogger("duniter/ws") logger = logging.getLogger("duniter/ws")
...@@ -52,23 +53,21 @@ WS_PEER_SCHEMA = { ...@@ -52,23 +53,21 @@ WS_PEER_SCHEMA = {
} }
# def block(client: Client): def block(client: Client):
# """ """
# Connect to block websocket Connect to block websocket
#
# :param client: Client to connect to the api
# :rtype: aiohttp.ClientWebSocketResponse
# """
# client = API(connection, MODULE)
# return client.connect_ws('/block')
:param client: Client to connect to the api
:rtype: aiohttp.ClientWebSocketResponse
"""
return client.connect_ws(MODULE + '/block')
# def peer(client: Client):
# """ def peer(client: Client):
# Connect to peer websocket """
# Connect to peer websocket
# :param duniterpy.api.bma.ConnectionHandler connection: Connection handler instance
# :rtype: aiohttp.ClientWebSocketResponse :param client: Client to connect to the api
# """ :rtype: aiohttp.ClientWebSocketResponse
# client = API(connection, MODULE) """
# return client.connect_ws('/peer') return client.connect_ws(MODULE + '/peer')
...@@ -34,43 +34,45 @@ ERROR_SCHEMA = { ...@@ -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 Validate and parse the BMA answer from websocket
:param str text: the bma answer :param text: the bma answer
:param dict schema: dict for jsonschema :param schema: dict for jsonschema
:return: the json data :return: the json data
""" """
try: try:
data = json.loads(text) data = json.loads(text)
jsonschema.validate(data, schema) jsonschema.validate(data, schema)
return data
except (TypeError, json.decoder.JSONDecodeError): except (TypeError, json.decoder.JSONDecodeError):
raise jsonschema.ValidationError("Could not parse json") 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 Validate and parse the BMA answer from websocket
:param str text: the bma error :param text: the bma error
:return: the json data :return: the json data
""" """
try: try:
data = json.loads(text) data = json.loads(text)
jsonschema.validate(data, ERROR_SCHEMA) jsonschema.validate(data, ERROR_SCHEMA)
return data
except (TypeError, json.decoder.JSONDecodeError) as e: except (TypeError, json.decoder.JSONDecodeError) as e:
raise jsonschema.ValidationError("Could not parse json : {0}".format(str(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 Validate and parse the BMA answer
:param aiohttp.ClientResponse response: Response of aiohttp request :param response: Response of aiohttp request
:param dict schema: The expected response structure :param schema: The expected response structure
:return: the json data :return: the json data
""" """
try: try:
...@@ -84,28 +86,28 @@ async def parse_response(response, schema): ...@@ -84,28 +86,28 @@ async def parse_response(response, schema):
class API(object): 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 = {} 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. Asks a module in order to create the url used then by derivated classes.
:param ConnectionHandler connection_handler: Connection handler :param connection_handler: Connection handler
:param str module: Module path :param module: Module path
""" """
self.module = module self.module = module
self.connection_handler = connection_handler self.connection_handler = connection_handler
self.headers = {} 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. Reverses the url using scheme and path given in parameter.
:param str scheme: Scheme of the url :param scheme: Scheme of the url
:param str path: Path of the url :param path: Path of the url
:return: str :return: str
""" """
...@@ -124,11 +126,11 @@ class API(object): ...@@ -124,11 +126,11 @@ class API(object):
return url + path 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. Requests GET wrapper in order to use API parameters.
:param str path: the request path :param path: the request path
:rtype: aiohttp.ClientResponse :rtype: aiohttp.ClientResponse
""" """
logging.debug("Request : {0}".format(self.reverse_url(self.connection_handler.http_scheme, path))) logging.debug("Request : {0}".format(self.reverse_url(self.connection_handler.http_scheme, path)))
...@@ -145,11 +147,11 @@ class API(object): ...@@ -145,11 +147,11 @@ class API(object):
return response 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. Requests POST wrapper in order to use API parameters.
:param str path: the request path :param path: the request path
:rtype: aiohttp.ClientResponse :rtype: aiohttp.ClientResponse
""" """
if 'self_' in kwargs: if 'self_' in kwargs:
...@@ -165,11 +167,11 @@ class API(object): ...@@ -165,11 +167,11 @@ class API(object):
) )
return response 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 Connect to a websocket in order to use API parameters
:param str path: the url path :param path: the url path
:rtype: aiohttp.ClientWebSocketResponse :rtype: aiohttp.ClientWebSocketResponse
""" """
url = self.reverse_url(self.connection_handler.ws_scheme, path) url = self.reverse_url(self.connection_handler.ws_scheme, path)
...@@ -266,6 +268,16 @@ class Client: ...@@ -266,6 +268,16 @@ class Client:
elif rtype == RESPONSE_JSON: elif rtype == RESPONSE_JSON:
return await 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): async def close(self):
""" """
Close aiohttp session Close aiohttp session
...@@ -274,14 +286,14 @@ class Client: ...@@ -274,14 +286,14 @@ class Client:
""" """
await self.session.close() 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 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 _function: The function to call
:param args: The parameters :param args: The parameters
:param kwargs: The key/value parameters :param kwargs: The key/value parameters
:return: :return:
""" """
return await _function(self, *args, **kwargs) return _function(self, *args, **kwargs)
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())
import unittest 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 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): def test_block(self):
json_sample = """ json_sample = """
...@@ -101,4 +102,5 @@ class Test_BMA_Websocket(WebFunctionalSetupMixin, unittest.TestCase): ...@@ -101,4 +102,5 @@ class Test_BMA_Websocket(WebFunctionalSetupMixin, unittest.TestCase):
self.assertEqual(data["currency"], "beta_brouzouf") self.assertEqual(data["currency"], "beta_brouzouf")
self.assertEqual(data["pubkey"], "HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY") self.assertEqual(data["pubkey"], "HsLShAtzXTVxeUtQd7yi5Z5Zh4zNvbu8sTEZ53nfKcqY")
self.assertEqual(len(data["endpoints"]), 3) self.assertEqual(len(data["endpoints"]), 3)
self.assertEqual(data["signature"], "42yQm4hGTJYWkPg39hQAUgP6S6EQ4vTfXdJuxKEHL1ih6YHiDL2hcwrFgBHjXLRgxRhj2VNVqqc6b4JayKqTE14r") self.assertEqual(data["signature"],
"42yQm4hGTJYWkPg39hQAUgP6S6EQ4vTfXdJuxKEHL1ih6YHiDL2hcwrFgBHjXLRgxRhj2VNVqqc6b4JayKqTE14r")
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment