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