From 31d06c61308da1a16a2525a29ea810b02b69be0f Mon Sep 17 00:00:00 2001 From: Vincent Texier <vit@free.fr> Date: Sun, 16 Sep 2018 13:04:28 +0200 Subject: [PATCH] issue #52 add type hinting everywhere in the code (except on local variables) --- README.rst | 1 + duniterpy/api/bma/network.py | 2 +- duniterpy/api/bma/node.py | 1 + duniterpy/api/bma/tx.py | 2 +- duniterpy/api/bma/ud.py | 2 +- duniterpy/api/bma/ws.py | 10 +-- duniterpy/api/client.py | 24 +++---- duniterpy/api/endpoint.py | 121 +++++++++++++++++++++------------- duniterpy/api/ws2p/network.py | 1 + 9 files changed, 100 insertions(+), 64 deletions(-) diff --git a/README.rst b/README.rst index fb2df881..87cdb16e 100644 --- a/README.rst +++ b/README.rst @@ -18,6 +18,7 @@ Features * Support HTTP, HTTPS and Web Socket transport for BMA API * Support `Elasticsearch Duniter4j <https://git.duniter.org/clients/java/duniter4j/blob/master/src/site/markdown/ES.md#request-the-es-node>`_ API * Duniter signing key +* Sign/verify and encrypt/decrypt messages with the Duniter credentials Requirements ------------ diff --git a/duniterpy/api/bma/network.py b/duniterpy/api/bma/network.py index cf8a6c1a..bd91569b 100644 --- a/duniterpy/api/bma/network.py +++ b/duniterpy/api/bma/network.py @@ -114,7 +114,7 @@ async def peering(client: Client) -> dict: return await client.get(MODULE + '/peering', schema=PEERING_SCHEMA) -async def peers(client: Client, leaves: bool = False, leaf: str = ""): +async def peers(client: Client, leaves: bool = False, leaf: str = "") -> dict: """ GET peering entries of every node inside the currency network diff --git a/duniterpy/api/bma/node.py b/duniterpy/api/bma/node.py index b3a630f5..d42cd0a6 100644 --- a/duniterpy/api/bma/node.py +++ b/duniterpy/api/bma/node.py @@ -16,6 +16,7 @@ # Caner Candan <caner@candan.fr>, http://caner.candan.fr # vit import logging + from duniterpy.api.client import Client logger = logging.getLogger("duniter/node") diff --git a/duniterpy/api/bma/tx.py b/duniterpy/api/bma/tx.py index 74e30a49..2a358b4e 100644 --- a/duniterpy/api/bma/tx.py +++ b/duniterpy/api/bma/tx.py @@ -222,7 +222,7 @@ async def process(client: Client, transaction_signed_raw: str) -> ClientResponse return await client.post(MODULE + '/process', {'transaction': transaction_signed_raw}, rtype=RESPONSE_AIOHTTP) -async def sources(client: Client, pubkey: str): +async def sources(client: Client, pubkey: str) -> dict: """ GET transaction sources diff --git a/duniterpy/api/bma/ud.py b/duniterpy/api/bma/ud.py index 1a97ea35..6522fc5d 100644 --- a/duniterpy/api/bma/ud.py +++ b/duniterpy/api/bma/ud.py @@ -16,6 +16,7 @@ # Caner Candan <caner@candan.fr>, http://caner.candan.fr # vit import logging + from duniterpy.api.client import Client logger = logging.getLogger("duniter/ud") @@ -74,4 +75,3 @@ async def history(client: Client, pubkey: str) -> dict: :rtype: dict """ return await client.get(MODULE + '/history/%s' % pubkey, schema=UD_SCHEMA) - diff --git a/duniterpy/api/bma/ws.py b/duniterpy/api/bma/ws.py index 1a0096cc..884bcc33 100644 --- a/duniterpy/api/bma/ws.py +++ b/duniterpy/api/bma/ws.py @@ -18,6 +18,8 @@ # vit import logging +from aiohttp import ClientWebSocketResponse + from duniterpy.api.bma.blockchain import BLOCK_SCHEMA from duniterpy.api.client import Client @@ -53,21 +55,21 @@ WS_PEER_SCHEMA = { } -def block(client: Client): +def block(client: Client) -> ClientWebSocketResponse: """ Connect to block websocket :param client: Client to connect to the api - :rtype: aiohttp.ClientWebSocketResponse + :rtype: ClientWebSocketResponse """ return client.connect_ws(MODULE + '/block') -def peer(client: Client): +def peer(client: Client) -> ClientWebSocketResponse: """ Connect to peer websocket :param client: Client to connect to the api - :rtype: aiohttp.ClientWebSocketResponse + :rtype: ClientWebSocketResponse """ return client.connect_ws(MODULE + '/peer') diff --git a/duniterpy/api/client.py b/duniterpy/api/client.py index 820035c5..9f534dd0 100644 --- a/duniterpy/api/client.py +++ b/duniterpy/api/client.py @@ -6,8 +6,8 @@ import json import logging from typing import Callable, Union, Any, Optional -import aiohttp import jsonschema +from aiohttp import ClientResponse, ClientWebSocketResponse, ClientSession import duniterpy.api.endpoint as endpoint from .errors import DuniterError @@ -67,7 +67,7 @@ def parse_error(text: str) -> Any: return data -async def parse_response(response: aiohttp.ClientResponse, schema: dict) -> Any: +async def parse_response(response: ClientResponse, schema: dict) -> Any: """ Validate and parse the BMA answer @@ -124,12 +124,12 @@ class API(object): return url + path - async def requests_get(self, path: str, **kwargs) -> aiohttp.ClientResponse: + async def requests_get(self, path: str, **kwargs) -> ClientResponse: """ Requests GET wrapper in order to use API parameters. :param path: the request path - :rtype: aiohttp.ClientResponse + :rtype: ClientResponse """ logging.debug("Request : {0}".format(self.reverse_url(self.connection_handler.http_scheme, path))) url = self.reverse_url(self.connection_handler.http_scheme, path) @@ -145,12 +145,12 @@ class API(object): return response - async def requests_post(self, path: str, **kwargs) -> aiohttp.ClientResponse: + async def requests_post(self, path: str, **kwargs) -> ClientResponse: """ Requests POST wrapper in order to use API parameters. :param path: the request path - :rtype: aiohttp.ClientResponse + :rtype: ClientResponse """ if 'self_' in kwargs: kwargs['self'] = kwargs.pop('self_') @@ -165,7 +165,7 @@ class API(object): ) return response - def connect_ws(self, path: str) -> aiohttp.ClientWebSocketResponse: + def connect_ws(self, path: str) -> ClientWebSocketResponse: """ Connect to a websocket in order to use API parameters @@ -175,7 +175,7 @@ class API(object): and close the ClientWebSocketResponse in it. :param path: the url path - :rtype: aiohttp.ClientWebSocketResponse + :rtype: ClientWebSocketResponse """ url = self.reverse_url(self.connection_handler.ws_scheme, path) return self.connection_handler.session.ws_connect(url, proxy=self.connection_handler.proxy) @@ -186,7 +186,7 @@ class Client: Main class to create an API client """ - def __init__(self, _endpoint: Union[str, endpoint.Endpoint], session: aiohttp.ClientSession = None, + def __init__(self, _endpoint: Union[str, endpoint.Endpoint], session: ClientSession = None, proxy: str = None) -> None: """ Init Client instance @@ -207,7 +207,7 @@ class Client: # if no user session... if session is None: # open a session - self.session = aiohttp.ClientSession() + self.session = ClientSession() else: self.session = session self.proxy = proxy @@ -274,12 +274,12 @@ class Client: elif rtype == RESPONSE_JSON: return await response.json() - def connect_ws(self, path: str) -> aiohttp.ClientWebSocketResponse: + def connect_ws(self, path: str) -> ClientWebSocketResponse: """ Connect to a websocket in order to use API parameters :param path: the url path - :rtype: aiohttp.ClientWebSocketResponse + :rtype: ClientWebSocketResponse """ client = API(self.endpoint.conn_handler(self.session, self.proxy)) return client.connect_ws(path) diff --git a/duniterpy/api/endpoint.py b/duniterpy/api/endpoint.py index 42e96f5d..c83abbda 100644 --- a/duniterpy/api/endpoint.py +++ b/duniterpy/api/endpoint.py @@ -1,7 +1,7 @@ import re -from typing import Any, Optional +from typing import Any, Optional, TypeVar, Type, Dict -import aiohttp +from aiohttp import ClientSession from ..constants import * from ..documents import MalformedDocumentError @@ -11,7 +11,7 @@ class ConnectionHandler: """Helper class used by other API classes to ease passing server connection information.""" def __init__(self, http_scheme: str, ws_scheme: str, server: str, port: int, path: str, - session: aiohttp.ClientSession, proxy: Optional[str] = None) -> None: + session: ClientSession, proxy: Optional[str] = None) -> None: """ Init instance of connection handler @@ -35,15 +35,19 @@ class ConnectionHandler: return 'connection info: %s:%d' % (self.server, self.port) +# required to type hint cls in classmethod +EndpointType = TypeVar('EndpointType', bound='Endpoint') + + class Endpoint: @classmethod - def from_inline(cls, inline: str): + def from_inline(cls: Type[EndpointType], inline: str) -> EndpointType: raise NotImplementedError("from_inline(..) is not implemented") def inline(self) -> str: raise NotImplementedError("inline() is not implemented") - def conn_handler(self, session: aiohttp.ClientSession, proxy: str = None) -> ConnectionHandler: + def conn_handler(self, session: ClientSession, proxy: str = None) -> ConnectionHandler: raise NotImplementedError("conn_handler is not implemented") def __str__(self) -> str: @@ -53,6 +57,10 @@ class Endpoint: raise NotImplementedError("__eq__ is not implemented") +# required to type hint cls in classmethod +UnknownEndpointType = TypeVar('UnknownEndpointType', bound='UnknownEndpoint') + + class UnknownEndpoint(Endpoint): API = None @@ -61,7 +69,7 @@ class UnknownEndpoint(Endpoint): self.properties = properties @classmethod - def from_inline(cls, inline: str): + def from_inline(cls: Type[UnknownEndpointType], inline: str) -> UnknownEndpointType: """ Return UnknownEndpoint instance from endpoint string @@ -86,8 +94,8 @@ class UnknownEndpoint(Endpoint): doc += " {0}".format(p) return doc - def conn_handler(self, session: aiohttp.ClientSession, proxy: str = None) -> ConnectionHandler: - return ConnectionHandler("", "", "", 0, "", aiohttp.ClientSession()) + def conn_handler(self, session: ClientSession, proxy: str = None) -> ConnectionHandler: + return ConnectionHandler("", "", "", 0, "", ClientSession()) def __str__(self) -> str: return "{0} {1}".format(self.api, ' '.join(["{0}".format(p) for p in self.properties])) @@ -115,6 +123,9 @@ ERROR_SCHEMA = { "required": ["ucode", "message"] } +# required to type hint cls in classmethod +BMAEndpointType = TypeVar('BMAEndpointType', bound='BMAEndpoint') + class BMAEndpoint(Endpoint): API = "BASIC_MERKLED_API" @@ -139,7 +150,7 @@ class BMAEndpoint(Endpoint): self.port = port @classmethod - def from_inline(cls, inline: str): + def from_inline(cls: Type[BMAEndpointType], inline: str) -> BMAEndpointType: """ Return BMAEndpoint instance from endpoint string @@ -167,7 +178,7 @@ class BMAEndpoint(Endpoint): IPv6=(" {0}".format(self.ipv6) if self.ipv6 else ""), PORT=(" {0}".format(self.port) if self.port else "")) - def conn_handler(self, session: aiohttp.ClientSession, proxy: str = None) -> ConnectionHandler: + def conn_handler(self, session: ClientSession, proxy: str = None) -> ConnectionHandler: """ Return connection handler instance for the endpoint @@ -182,20 +193,24 @@ class BMAEndpoint(Endpoint): return ConnectionHandler("http", "ws", self.ipv4, self.port, "", session, proxy) - def __str__(self): + def __str__(self) -> str: return self.inline() - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: if isinstance(other, BMAEndpoint): return self.server == other.server and self.ipv4 == other.ipv4 \ and self.ipv6 == other.ipv6 and self.port == other.port else: return False - def __hash__(self): + def __hash__(self) -> int: return hash((self.server, self.ipv4, self.ipv6, self.port)) +# required to type hint cls in classmethod +SecuredBMAEndpointType = TypeVar('SecuredBMAEndpointType', bound='SecuredBMAEndpoint') + + class SecuredBMAEndpoint(BMAEndpoint): API = "BMAS" re_inline = re.compile( @@ -219,7 +234,7 @@ class SecuredBMAEndpoint(BMAEndpoint): self.path = path @classmethod - def from_inline(cls, inline: str): + def from_inline(cls: Type[SecuredBMAEndpointType], inline: str) -> SecuredBMAEndpointType: """ Return SecuredBMAEndpoint instance from endpoint string @@ -247,7 +262,7 @@ class SecuredBMAEndpoint(BMAEndpoint): inlined = [str(info) for info in (self.server, self.ipv4, self.ipv6, self.port, self.path) if info] return SecuredBMAEndpoint.API + " " + " ".join(inlined) - def conn_handler(self, session: aiohttp.ClientSession, proxy: str = None) -> ConnectionHandler: + def conn_handler(self, session: ClientSession, proxy: str = None) -> ConnectionHandler: """ Return connection handler instance for the endpoint @@ -263,6 +278,10 @@ class SecuredBMAEndpoint(BMAEndpoint): return ConnectionHandler("https", "wss", self.ipv4, self.port, self.path, session, proxy) +# required to type hint cls in classmethod +WS2PEndpointType = TypeVar('WS2PEndpointType', bound='WS2PEndpoint') + + class WS2PEndpoint(Endpoint): API = "WS2P" re_inline = re.compile( @@ -273,14 +292,14 @@ class WS2PEndpoint(Endpoint): ipv6_regex=IPV6_REGEX, path_regex=PATH_REGEX)) - def __init__(self, ws2pid, server, port, path): + def __init__(self, ws2pid: str, server: str, port: int, path: str) -> None: self.ws2pid = ws2pid self.server = server self.port = port self.path = path @classmethod - def from_inline(cls, inline): + def from_inline(cls: Type[WS2PEndpointType], inline: str) -> WS2PEndpointType: m = WS2PEndpoint.re_inline.match(inline) if m is None: raise MalformedDocumentError(WS2PEndpoint.API) @@ -292,11 +311,11 @@ class WS2PEndpoint(Endpoint): path = "" return cls(ws2pid, server, port, path) - def inline(self): + def inline(self) -> str: inlined = [str(info) for info in (self.ws2pid, self.server, self.port, self.path) if info] return WS2PEndpoint.API + " " + " ".join(inlined) - def conn_handler(self, session: aiohttp.ClientSession, proxy: str = None) -> ConnectionHandler: + def conn_handler(self, session: ClientSession, proxy: str = None) -> ConnectionHandler: """ Return connection handler instance for the endpoint @@ -306,32 +325,36 @@ class WS2PEndpoint(Endpoint): """ return ConnectionHandler("https", "wss", self.server, self.port, self.path, session, proxy) - def __str__(self): + def __str__(self) -> str: return self.inline() - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: if isinstance(other, WS2PEndpoint): return self.server == other.server and self.ws2pid == other.ws2pid \ and self.port == other.port and self.path == other.path else: return False - def __hash__(self): + def __hash__(self) -> int: return hash((self.ws2pid, self.server, self.port, self.path)) +# required to type hint cls in classmethod +ESCoreEndpointType = TypeVar('ESCoreEndpointType', bound='ESCoreEndpoint') + + class ESCoreEndpoint(Endpoint): API = "ES_CORE_API" re_inline = re.compile( '^ES_CORE_API ((?:{host_regex})|(?:{ipv4_regex})) ([0-9]+)$'.format(host_regex=HOST_REGEX, ipv4_regex=IPV4_REGEX)) - def __init__(self, server, port): + def __init__(self, server: str, port: int) -> None: self.server = server self.port = port @classmethod - def from_inline(cls, inline): + def from_inline(cls: Type[ESCoreEndpointType], inline: str) -> ESCoreEndpointType: m = ESCoreEndpoint.re_inline.match(inline) if m is None: raise MalformedDocumentError(ESCoreEndpoint.API) @@ -339,11 +362,11 @@ class ESCoreEndpoint(Endpoint): port = int(m.group(2)) return cls(server, port) - def inline(self): + def inline(self) -> str: inlined = [str(info) for info in (self.server, self.port) if info] return ESCoreEndpoint.API + " " + " ".join(inlined) - def conn_handler(self, session: aiohttp.ClientSession, proxy: str = None) -> ConnectionHandler: + def conn_handler(self, session: ClientSession, proxy: str = None) -> ConnectionHandler: """ Return connection handler instance for the endpoint @@ -353,31 +376,35 @@ class ESCoreEndpoint(Endpoint): """ return ConnectionHandler("https", "wss", self.server, self.port, "", session, proxy) - def __str__(self): + def __str__(self) -> str: return self.inline() - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: if isinstance(other, ESCoreEndpoint): return self.server == other.server and self.port == other.port else: return False - def __hash__(self): + def __hash__(self) -> int: return hash((self.server, self.port)) +# required to type hint cls in classmethod +ESUserEndpointType = TypeVar('ESUserEndpointType', bound='ESUserEndpoint') + + class ESUserEndpoint(Endpoint): API = "ES_USER_API" re_inline = re.compile( '^ES_USER_API ((?:{host_regex})|(?:{ipv4_regex})) ([0-9]+)$'.format(host_regex=HOST_REGEX, ipv4_regex=IPV4_REGEX)) - def __init__(self, server, port): + def __init__(self, server: str, port: int) -> None: self.server = server self.port = port @classmethod - def from_inline(cls, inline): + def from_inline(cls: Type[ESUserEndpointType], inline: str) -> ESUserEndpointType: m = ESUserEndpoint.re_inline.match(inline) if m is None: raise MalformedDocumentError(ESUserEndpoint.API) @@ -385,11 +412,11 @@ class ESUserEndpoint(Endpoint): port = int(m.group(2)) return cls(server, port) - def inline(self): + def inline(self) -> str: inlined = [str(info) for info in (self.server, self.port) if info] return ESUserEndpoint.API + " " + " ".join(inlined) - def conn_handler(self, session: aiohttp.ClientSession, proxy: str = None) -> ConnectionHandler: + def conn_handler(self, session: ClientSession, proxy: str = None) -> ConnectionHandler: """ Return connection handler instance for the endpoint @@ -399,31 +426,35 @@ class ESUserEndpoint(Endpoint): """ return ConnectionHandler("https", "wss", self.server, self.port, "", session, proxy) - def __str__(self): + def __str__(self) -> str: return self.inline() - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: if isinstance(other, ESUserEndpoint): return self.server == other.server and self.port == other.port else: return False - def __hash__(self): + def __hash__(self) -> int: return hash((self.server, self.port)) +# required to type hint cls in classmethod +ESSubscribtionEndpointType = TypeVar('ESSubscribtionEndpointType', bound='ESSubscribtionEndpoint') + + class ESSubscribtionEndpoint(Endpoint): API = "ES_SUBSCRIPTION_API" re_inline = re.compile( '^ES_SUBSCRIPTION_API ((?:{host_regex})|(?:{ipv4_regex})) ([0-9]+)$'.format(host_regex=HOST_REGEX, ipv4_regex=IPV4_REGEX)) - def __init__(self, server, port): + def __init__(self, server: str, port: int) -> None: self.server = server self.port = port @classmethod - def from_inline(cls, inline): + def from_inline(cls: Type[ESSubscribtionEndpointType], inline: str) -> ESSubscribtionEndpointType: m = ESSubscribtionEndpoint.re_inline.match(inline) if m is None: raise MalformedDocumentError(ESSubscribtionEndpoint.API) @@ -431,11 +462,11 @@ class ESSubscribtionEndpoint(Endpoint): port = int(m.group(2)) return cls(server, port) - def inline(self): + def inline(self) -> str: inlined = [str(info) for info in (self.server, self.port) if info] return ESSubscribtionEndpoint.API + " " + " ".join(inlined) - def conn_handler(self, session: aiohttp.ClientSession, proxy: str = None) -> ConnectionHandler: + def conn_handler(self, session: ClientSession, proxy: str = None) -> ConnectionHandler: """ Return connection handler instance for the endpoint @@ -445,16 +476,16 @@ class ESSubscribtionEndpoint(Endpoint): """ return ConnectionHandler("https", "wss", self.server, self.port, "", session, proxy) - def __str__(self): + def __str__(self) -> str: return self.inline() - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: if isinstance(other, ESSubscribtionEndpoint): return self.server == other.server and self.port == other.port else: return False - def __hash__(self): + def __hash__(self) -> int: return hash((ESSubscribtionEndpoint.API, self.server, self.port)) @@ -465,10 +496,10 @@ MANAGED_API = { ESCoreEndpoint.API: ESCoreEndpoint, ESUserEndpoint.API: ESUserEndpoint, ESSubscribtionEndpoint.API: ESSubscribtionEndpoint -} +} # type: Dict[str, Any] -def endpoint(value): +def endpoint(value: Any) -> Any: if issubclass(type(value), Endpoint): return value elif isinstance(value, str): diff --git a/duniterpy/api/ws2p/network.py b/duniterpy/api/ws2p/network.py index ec9b3a14..e8b8b1ff 100644 --- a/duniterpy/api/ws2p/network.py +++ b/duniterpy/api/ws2p/network.py @@ -55,6 +55,7 @@ WS2P_HEADS_SCHEMA = { } +# fixme: ws2p heads support must be handled by websocket def heads(client: Client): """ GET Certification data over a member -- GitLab