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