From 550d50f5654ea403adaa536981338011b34d090b Mon Sep 17 00:00:00 2001
From: percyjw-2 <joris.wachsmuth@gmx.de>
Date: Mon, 25 Jul 2022 21:11:46 +0200
Subject: [PATCH] Added Documentation

---
 doc/conf.py                          |  2 +-
 swarm/ConnectionManager.py           | 87 ++++++++++++++++++++++++----
 swarm/ConnectionManagerTCPHandler.py |  6 +-
 swarm/__init__.py                    |  2 +-
 swarm/statics.py                     | 18 +++++-
 tests/test_connection_manager.py     |  8 +--
 6 files changed, 101 insertions(+), 22 deletions(-)

diff --git a/doc/conf.py b/doc/conf.py
index 9cd10a9..405b29b 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -56,4 +56,4 @@ html_theme = 'sphinx_rtd_theme'
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
\ No newline at end of file
+html_static_path = ['_static']
diff --git a/swarm/ConnectionManager.py b/swarm/ConnectionManager.py
index 3d6753c..02e61db 100644
--- a/swarm/ConnectionManager.py
+++ b/swarm/ConnectionManager.py
@@ -5,7 +5,7 @@ import threading
 from functools import partial
 from typing import List, Callable, Optional, Tuple
 
-from .statics import StandardMessages, MessageToBig, _string_to_ip_and_port
+from .statics import StandardMessages, MessageToBig, string_to_ip_and_port
 from .ConnectionManagerTCPHandler import ConnectionManagerTCPHandler
 
 try:
@@ -19,6 +19,9 @@ except ImportError:
 
 
 class ConnectionManager:
+    """
+    Handles Connections and determines which Client is the Master in the Network
+    """
     def __init__(self,
                  addr="localhost",
                  port=6969,
@@ -56,6 +59,12 @@ class ConnectionManager:
         self.disconnect()
 
     def _launch_socket_server(self, address: Tuple[str, int], request_handler=socketserver.BaseRequestHandler):
+        """
+        Launches the socketserver that will handle all incoming connections and messages
+
+        :param address: Contains the Tuple with the address to which the ConnectionManager is started
+        :param request_handler: Contains the Object that will handle all incoming messages
+        """
         socketserver.ThreadingTCPServer.allow_reuse_port = True
         socketserver.ThreadingTCPServer.allow_reuse_address = True
         with socketserver.ThreadingTCPServer(address, request_handler) as server:
@@ -63,6 +72,11 @@ class ConnectionManager:
             server.serve_forever()
 
     def _launch_heartbeat(self):
+        """
+        Launches the Heartbeat that manages all Connections to other Clients.
+        If a client disconnects it will be handled here. Also, the decision who the next master is, is made here if the
+        current one disconnects.
+        """
         while not self.stop_heartbeat:
             to_be_deleted = []
             for address in self.connectedIPs.keys():
@@ -89,6 +103,12 @@ class ConnectionManager:
             time.sleep(self._heartbeat)
 
     def _connect_to_clients(self, ip_list: List[Tuple[str, int]]):
+        """
+        At Launch of the ConnectionManager this Function is called to connect to all Clients that are inside ip_list.
+        Also, it is checked if the Client has a fitting launch time. (If not it is updated throughout the Network)
+
+        :param ip_list: Tuple containing all addresses to connect to
+        """
         changed_start_time = False
         for (ip, port) in ip_list:
             sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@@ -117,6 +137,12 @@ class ConnectionManager:
                 ))
 
     def connect(self, ip_list: List[str]):
+        """
+        Connects Client to the Network. If the Network already is started up at least one Address needs to be provided
+        to connect to the Network.
+
+        :param ip_list: contains all initial addresses to connect to as a list of strings
+        """
         if len(self.sockets) > 0:
             return
         request_handler = partial(ConnectionManagerTCPHandler, self)
@@ -128,21 +154,23 @@ class ConnectionManager:
         self.heartbeatThread = threading.Thread(target=self._launch_heartbeat)
         self.heartbeatThread.daemon = True
         self.heartbeatThread.start()
-        ip_list = [_string_to_ip_and_port(addr) for addr in ip_list]
+        ip_list = [string_to_ip_and_port(addr) for addr in ip_list]
         ip_list = [addr for addr in ip_list if addr != (self._addr, self._port)]
         self._connect_to_clients(ip_list)
         if len(self.connectedIPs) > 0:
             received_msg = self.send_message(StandardMessages.GET_MASTER.value, next(iter(self.connectedIPs.keys())))
-            self.master_addr = _string_to_ip_and_port(received_msg)
+            self.master_addr = string_to_ip_and_port(received_msg)
             received_msg = self.send_message(StandardMessages.GET_ADDRESSES.value, self.master_addr)
-            network_ips = [_string_to_ip_and_port(addr) for addr in received_msg.split(",") if addr != ""]
+            network_ips = [string_to_ip_and_port(addr) for addr in received_msg.split(",") if addr != ""]
             network_ips = [addr for addr in network_ips if addr not in self.connectedIPs]
             network_ips = [addr for addr in network_ips if addr != (self._addr, self._port)]
             self._connect_to_clients(network_ips)
 
     def disconnect(self):
-        # if len(self.sockets) == 0:
-        #     return
+        """
+        Disconnects all Sockets, stops Heartbeats and stops the Socketserver.
+        Needs to be called if not using context manager
+        """
         if self.heartbeatThread is not None:
             self.stop_heartbeat = True
         for sock in list(self.sockets.values()):
@@ -154,7 +182,15 @@ class ConnectionManager:
             if self.socketServer is not None:
                 self.socketServer.shutdown()
 
-    def send_message(self, message: str, address: Tuple[str, int]):
+    def send_message(self, message: str, address: Tuple[str, int]) -> Optional[str]:
+        """
+        Sends a message to a specified address and returns the received message.
+        The specified address needs to be pointing to a client connected to the Network
+
+        :param message: string that is sent to the specified address
+        :param address: address that points to the client that should receive the message
+        :return: the received string from the connected server
+        """
         if self.sockets.get(address) is None:
             return
         data = bytes(message, "utf-8")
@@ -163,17 +199,44 @@ class ConnectionManager:
         self.sockets[address].sendall(data)
         return str(self.sockets[address].recv(self._buffer_size), "utf-8")
 
-    def get_current_addresses(self):
+    def get_current_addresses(self) -> List[Tuple[str, int]]:
+        """
+        Fetches all addresses that are connected to the client
+
+        :return: List of Tuples containing address and port of each client
+        """
         return list(self.connectedIPs.keys())
 
-    def get_current_master(self):
+    def get_current_master(self) -> Tuple[str, int]:
+        """
+        Fetches current master of the network
+
+        :return: Tuple containing address and port of the current master
+        """
         return self.master_addr
 
-    def get_ip(self):
+    def get_ip(self) -> Tuple[str, int]:
+        """
+        Fetches current ip of the client
+
+        :return: Tuple containing address and port of the current master
+        """
         return self._addr, self._port
 
-    def add_listener(self, function: Callable[[str], Optional[str]]):
+    def add_listener(self, function: Callable[[str], str]):
+        """
+        Adds a function that should handle custom messages that are not part of StandardMessages.
+        This Function needs to have a parameter for the message and should always return a string that is sent back to
+        the client that sent the message.
+
+        :param function: Function that should have the format: [def custom_handler(message: str) -> str:]
+        """
         self.listeners.append(function)
 
-    def remove_listener(self, function: Callable[[str], Optional[str]]):
+    def remove_listener(self, function: Callable[[str], str]):
+        """
+        Removes a function that was added with add_listener.
+
+        :param function: Function that should have the format: [def custom_handler(message: str) -> str:]
+        """
         self.listeners.remove(function)
diff --git a/swarm/ConnectionManagerTCPHandler.py b/swarm/ConnectionManagerTCPHandler.py
index bf6ca1d..12b3590 100644
--- a/swarm/ConnectionManagerTCPHandler.py
+++ b/swarm/ConnectionManagerTCPHandler.py
@@ -2,7 +2,7 @@ import socket
 import socketserver
 from typing import Union
 
-from .statics import _string_to_ip_and_port, StandardMessages
+from .statics import string_to_ip_and_port, StandardMessages
 
 
 class ConnectionManagerTCPHandler(socketserver.BaseRequestHandler):
@@ -26,7 +26,7 @@ class ConnectionManagerTCPHandler(socketserver.BaseRequestHandler):
         :param launch_time: time when the connecting client launched
         :param addr: address of the socketserver of the connecting client
         """
-        address_parsed = _string_to_ip_and_port(addr)
+        address_parsed = string_to_ip_and_port(addr)
         self.connection_manager.sockets[address_parsed] = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
         self.connection_manager.sockets[address_parsed].connect(address_parsed)
         announced_launch_time = int(launch_time)
@@ -48,7 +48,7 @@ class ConnectionManagerTCPHandler(socketserver.BaseRequestHandler):
         :param launch_time: updated time when the connecting client started
         :param addr: address of the connecting client
         """
-        address_parsed = _string_to_ip_and_port(addr)
+        address_parsed = string_to_ip_and_port(addr)
         self.connection_manager.connectedIPs[address_parsed] = int(launch_time)
 
     def heartbeat(self):
diff --git a/swarm/__init__.py b/swarm/__init__.py
index 04a135a..12f535e 100644
--- a/swarm/__init__.py
+++ b/swarm/__init__.py
@@ -9,5 +9,5 @@ from .ConnectionManager import\
     ConnectionManager,\
     StandardMessages,\
     MessageToBig,\
-    _string_to_ip_and_port
+    string_to_ip_and_port
 from .statics import InvalidIPString
diff --git a/swarm/statics.py b/swarm/statics.py
index e5bb625..e0c4e18 100644
--- a/swarm/statics.py
+++ b/swarm/statics.py
@@ -4,6 +4,9 @@ from typing import Tuple
 
 
 class StandardMessages(Enum):
+    """
+    Messages that are used for core functionalities
+    """
     ANNOUNCE = "announce"
     UPDATE_LAUNCH = "update"
     ACKNOWLEDGED = "acknowledged"
@@ -13,14 +16,27 @@ class StandardMessages(Enum):
 
 
 class MessageToBig(Exception):
+    """
+    If a Message is too big for the specified buffer of the ConnectionManager this Exception is thrown
+    """
     pass
 
 
 class InvalidIPString(Exception):
+    """
+    If a String passed to string_to_ip_and_port that could not be parsed to an ip and a port this Exception is thrown
+    """
     pass
 
 
-def _string_to_ip_and_port(message: str) -> Tuple[str, int]:
+def string_to_ip_and_port(message: str) -> Tuple[str, int]:
+    """
+    Parses Strings to a Tuple of a String and Integer that contain a Network Address and the Port. If the string is not
+    valid a InvalidIPString Exception is thrown
+
+    :param message: String that is parsed
+    :return: parsed Tuple that contains the Address and Port
+    """
     valid_ipv4 = re.compile(r"^(\d?\d?\d.){3}\d?\d?\d:(\d?){4}\d$")
     valid_ipv6 = re.compile(r"^([a-f\d:]+:+)+[a-f\d]+:(\d?){4}\d$")
     valid_address = re.compile(r"^(localhost)|(\*+.\*):(\d?){4}\d$")
diff --git a/tests/test_connection_manager.py b/tests/test_connection_manager.py
index c465f33..522cb9d 100644
--- a/tests/test_connection_manager.py
+++ b/tests/test_connection_manager.py
@@ -14,20 +14,20 @@ class TestStatics(TestCase):
         for i in range(1000):
             addr_str = str(IPv4Address(random.getrandbits(32)))
             port = random.randint(1, 65535)
-            (ip, port_e) = swarm.statics._string_to_ip_and_port(addr_str + ":" + str(port))
+            (ip, port_e) = swarm.statics.string_to_ip_and_port(addr_str + ":" + str(port))
             self.assertEqual((ip, port_e), (addr_str, port))
 
     def test_ipv6_str_parsing(self):
         for i in range(1000):
             addr_str = str(IPv6Address(random.getrandbits(128)))
             port = random.randint(1, 65535)
-            (ip, port_e) = swarm.statics._string_to_ip_and_port(addr_str + ":" + str(port))
+            (ip, port_e) = swarm.statics.string_to_ip_and_port(addr_str + ":" + str(port))
             self.assertEqual((ip, port_e), (addr_str, port))
 
     def test_invalid_str_parsing(self):
         invalid_sting = "invalid"
         try:
-            swarm.statics._string_to_ip_and_port(invalid_sting)
+            swarm.statics.string_to_ip_and_port(invalid_sting)
         except swarm.statics.InvalidIPString:
             return
         self.assertTrue(False)
@@ -53,7 +53,7 @@ class TestConnections(TestCase):
             conn_mans[ip_str].connect(to_connect)
             to_connect.append(ip_str)
 
-        master = swarm.statics._string_to_ip_and_port(to_connect[0])
+        master = swarm.statics.string_to_ip_and_port(to_connect[0])
         for manager in conn_mans.values():
             self.assertEqual(master, manager.get_current_master())
         for manager in conn_mans.values():
-- 
GitLab