From 04e52048190bd8ab94e6ac8d21610b2cbf57febe Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Tue, 15 Oct 2019 11:29:16 +0200 Subject: [PATCH] Start work on the YOSHI REFACTOR --- requirements.txt | 2 +- royalnet/__init__.py | 4 +- royalnet/bots/generic.py | 48 ++++---- royalnet/network/__init__.py | 20 ---- royalnet/network/networkconfig.py | 9 -- royalnet/network/networklink.py | 184 ------------------------------ royalnet/network/networkserver.py | 146 ------------------------ royalnet/network/package.py | 111 ------------------ royalnet/network/request.py | 24 ---- royalnet/network/response.py | 61 ---------- setup.py | 2 +- tests/test_network.py | 57 --------- 12 files changed, 28 insertions(+), 640 deletions(-) delete mode 100644 royalnet/network/__init__.py delete mode 100644 royalnet/network/networkconfig.py delete mode 100644 royalnet/network/networklink.py delete mode 100644 royalnet/network/networkserver.py delete mode 100644 royalnet/network/package.py delete mode 100644 royalnet/network/request.py delete mode 100644 royalnet/network/response.py delete mode 100644 tests/test_network.py diff --git a/requirements.txt b/requirements.txt index 79a5f07f..15bbe760 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ bcrypt>=3.1.7 discord.py>=1.2.2 python-telegram-bot>=11.1.0 -websockets>=6.0 +royalherald>=5.0b4 pytest>=4.3.1 psycopg2-binary>=2.8 aiohttp>=3.5.4 diff --git a/royalnet/__init__.py b/royalnet/__init__.py index 71fbe7d4..76815422 100644 --- a/royalnet/__init__.py +++ b/royalnet/__init__.py @@ -1,4 +1,4 @@ -from . import audio, bots, database, network, utils, error, web, version +from . import audio, bots, database, utils, error, web, version from royalnet import commands -__all__ = ["audio", "bots", "commands", "database", "network", "utils", "error", "web", "version"] +__all__ = ["audio", "bots", "commands", "database", "utils", "error", "web", "version"] diff --git a/royalnet/bots/generic.py b/royalnet/bots/generic.py index 2e3acb89..f977254d 100644 --- a/royalnet/bots/generic.py +++ b/royalnet/bots/generic.py @@ -3,11 +3,11 @@ import asyncio import logging import sentry_sdk import keyring +import royalherald as rh from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration from sentry_sdk.integrations.aiohttp import AioHttpIntegration from sentry_sdk.integrations.logging import LoggingIntegration from ..utils import * -from ..network import * from ..database import * from ..commands import * from ..error import * @@ -62,16 +62,16 @@ class GenericBot: def unregister_net_handler(ci, message_type: str): del self.network_handlers[message_type] - async def net_request(ci, request: Request, destination: str) -> dict: + async def net_request(ci, request: rh.Request, destination: str) -> dict: if self.network is None: raise InvalidConfigError("Royalnet is not enabled on this bot") response_dict: dict = await self.network.request(request.to_dict(), destination) if "type" not in response_dict: raise RoyalnetResponseError("Response is missing a type") elif response_dict["type"] == "ResponseSuccess": - response: typing.Union[ResponseSuccess, ResponseError] = ResponseSuccess.from_dict(response_dict) + response: typing.Union[rh.ResponseSuccess, rh.ResponseFailure] = rh.ResponseSuccess.from_dict(response_dict) elif response_dict["type"] == "ResponseError": - response = ResponseError.from_dict(response_dict) + response = rh.ResponseFailure.from_dict(response_dict) else: raise RoyalnetResponseError("Response type is unknown") response.raise_on_error() @@ -83,47 +83,47 @@ class GenericBot: raise NotImplementedError() def _init_network(self): - """Create a :py:class:`royalnet.network.NetworkLink`, and run it as a :py:class:`asyncio.Task`.""" + """Create a :py:class:`royalherald.Link`, and run it as a :py:class:`asyncio.Task`.""" if self.uninitialized_network_config is not None: - self.network: NetworkLink = NetworkLink(self.uninitialized_network_config.master_uri, - self.uninitialized_network_config.master_secret, - self.interface_name, self._network_handler) + self.network: rh.Link = rh.Link(self.uninitialized_network_config.master_uri, + self.uninitialized_network_config.master_secret, + self.interface_name, self._network_handler) log.debug(f"Running NetworkLink {self.network}") self.loop.create_task(self.network.run()) async def _network_handler(self, request_dict: dict) -> dict: - """Handle a single :py:class:`dict` received from the :py:class:`royalnet.network.NetworkLink`. + """Handle a single :py:class:`dict` received from the :py:class:`royalherald.Link`. Returns: - Another :py:class:`dict`, formatted as a :py:class:`royalnet.network.Response`.""" + Another :py:class:`dict`, formatted as a :py:class:`royalherald.Response`.""" # Convert the dict to a Request try: - request: Request = Request.from_dict(request_dict) + request: rh.Request = rh.Request.from_dict(request_dict) except TypeError: log.warning(f"Invalid request received: {request_dict}") - return ResponseError("invalid_request", - f"The Request that you sent was invalid. Check extra_info to see what you sent.", - extra_info={"you_sent": request_dict}).to_dict() + return rh.ResponseFailure("invalid_request", + f"The Request that you sent was invalid. Check extra_info to see what you sent.", + extra_info={"you_sent": request_dict}).to_dict() log.debug(f"Received {request} from the NetworkLink") try: network_handler = self.network_handlers[request.handler] except KeyError: log.warning(f"Missing network_handler for {request.handler}") - return ResponseError("no_handler", f"This Link is missing a network handler for {request.handler}.").to_dict() + return rh.ResponseFailure("no_handler", f"This Link is missing a network handler for {request.handler}.").to_dict() try: log.debug(f"Using {network_handler} as handler for {request.handler}") - response: Response = await getattr(network_handler, self.interface_name)(self, request.data) + response: typing.Union[rh.ResponseSuccess, rh.ResponseFailure] = await getattr(network_handler, self.interface_name)(self, request.data) return response.to_dict() except Exception as e: sentry_sdk.capture_exception(e) log.debug(f"Exception {e} in {network_handler}") - return ResponseError("exception_in_handler", - f"An exception was raised in {network_handler} for {request.handler}. Check " - f"extra_info for details.", - extra_info={ - "type": e.__class__.__name__, - "str": str(e) - }).to_dict() + return rh.ResponseError("exception_in_handler", + f"An exception was raised in {network_handler} for {request.handler}. Check " + f"extra_info for details.", + extra_info={ + "type": e.__class__.__name__, + "str": str(e) + }).to_dict() def _init_database(self): """Create an :py:class:`royalnet.database.Alchemy` with the tables required by the commands. Then, @@ -168,7 +168,7 @@ class GenericBot: self.loop = self.uninitialized_loop def __init__(self, *, - network_config: typing.Optional[NetworkConfig] = None, + network_config: typing.Optional[rh.Config] = None, database_config: typing.Optional[DatabaseConfig] = None, commands: typing.List[typing.Type[Command]] = None, sentry_dsn: typing.Optional[str] = None, diff --git a/royalnet/network/__init__.py b/royalnet/network/__init__.py deleted file mode 100644 index ea229aaf..00000000 --- a/royalnet/network/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Royalnet (websocket) related classes.""" -from .request import Request -from .response import Response, ResponseSuccess, ResponseError -from .package import Package -from .networklink import NetworkLink, NetworkError, NotConnectedError, NotIdentifiedError, ConnectionClosedError -from .networkserver import NetworkServer -from .networkconfig import NetworkConfig - -__all__ = ["NetworkLink", - "NetworkError", - "NotConnectedError", - "NotIdentifiedError", - "Package", - "NetworkServer", - "NetworkConfig", - "ConnectionClosedError", - "Request", - "Response", - "ResponseSuccess", - "ResponseError"] diff --git a/royalnet/network/networkconfig.py b/royalnet/network/networkconfig.py deleted file mode 100644 index 704962b7..00000000 --- a/royalnet/network/networkconfig.py +++ /dev/null @@ -1,9 +0,0 @@ -class NetworkConfig: - def __init__(self, - master_uri: str, - master_secret: str): - if not (master_uri.startswith("ws://") - or master_uri.startswith("wss://")): - raise ValueError("Invalid protocol (must be ws:// or wss://)") - self.master_uri = master_uri - self.master_secret = master_secret diff --git a/royalnet/network/networklink.py b/royalnet/network/networklink.py deleted file mode 100644 index d65f44e8..00000000 --- a/royalnet/network/networklink.py +++ /dev/null @@ -1,184 +0,0 @@ -import asyncio -import websockets -import uuid -import functools -import math -import numbers -import logging as _logging -import typing -from .package import Package - - -log = _logging.getLogger(__name__) - - -class NotConnectedError(Exception): - """The :py:class:`royalnet.network.NetworkLink` is not connected to a :py:class:`royalnet.network.NetworkServer`.""" - - -class NotIdentifiedError(Exception): - """The :py:class:`royalnet.network.NetworkLink` has not identified yet to a :py:class:`royalnet.network.NetworkServer`.""" - - -class ConnectionClosedError(Exception): - """The :py:class:`royalnet.network.NetworkLink`'s connection was closed unexpectedly. The link can't be used anymore.""" - - -class InvalidServerResponseError(Exception): - """The :py:class:`royalnet.network.NetworkServer` sent invalid data to the :py:class:`royalnet.network.NetworkLink`.""" - - -class NetworkError(Exception): - def __init__(self, error_data: dict, *args): - super().__init__(*args) - self.error_data: dict = error_data - - -class PendingRequest: - def __init__(self, *, loop: asyncio.AbstractEventLoop = None): - if loop is None: - self.loop = asyncio.get_event_loop() - else: - self.loop = loop - self.event: asyncio.Event = asyncio.Event(loop=loop) - self.data: typing.Optional[dict] = None - - def __repr__(self): - if self.event.is_set(): - return f"" - return f"" - - def set(self, data): - self.data = data - self.event.set() - - -def requires_connection(func): - @functools.wraps(func) - async def new_func(self, *args, **kwargs): - await self.connect_event.wait() - return await func(self, *args, **kwargs) - return new_func - - -def requires_identification(func): - @functools.wraps(func) - async def new_func(self, *args, **kwargs): - await self.identify_event.wait() - return await func(self, *args, **kwargs) - return new_func - - -class NetworkLink: - def __init__(self, master_uri: str, secret: str, link_type: str, request_handler, *, - loop: asyncio.AbstractEventLoop = None): - if ":" in link_type: - raise ValueError("Link types cannot contain colons.") - self.master_uri: str = master_uri - self.link_type: str = link_type - self.nid: str = str(uuid.uuid4()) - self.secret: str = secret - self.websocket: typing.Optional[websockets.WebSocketClientProtocol] = None - self.request_handler = request_handler - self._pending_requests: typing.Dict[str, PendingRequest] = {} - if loop is None: - self._loop = asyncio.get_event_loop() - else: - self._loop = loop - self.error_event: asyncio.Event = asyncio.Event(loop=self._loop) - self.connect_event: asyncio.Event = asyncio.Event(loop=self._loop) - self.identify_event: asyncio.Event = asyncio.Event(loop=self._loop) - - def __repr__(self): - if self.identify_event.is_set(): - return f"<{self.__class__.__name__} (identified)>" - elif self.connect_event.is_set(): - return f"<{self.__class__.__name__} (connected)>" - elif self.error_event.is_set(): - return f"<{self.__class__.__name__} (error)>" - else: - return f"<{self.__class__.__name__} (disconnected)>" - - async def connect(self): - """Connect to the :py:class:`royalnet.network.NetworkServer` at ``self.master_uri``.""" - log.info(f"Connecting to {self.master_uri}...") - self.websocket = await websockets.connect(self.master_uri, loop=self._loop) - self.connect_event.set() - log.info(f"Connected!") - - @requires_connection - async def receive(self) -> Package: - """Recieve a :py:class:`Package` from the :py:class:`royalnet.network.NetworkServer`. - - Raises: - :py:exc:`royalnet.network.royalnetlink.ConnectionClosedError` if the connection closes.""" - try: - jbytes: bytes = await self.websocket.recv() - package: Package = Package.from_json_bytes(jbytes) - except websockets.ConnectionClosed: - self.error_event.set() - self.connect_event.clear() - self.identify_event.clear() - log.info(f"Connection to {self.master_uri} was closed.") - # What to do now? Let's just reraise. - raise ConnectionClosedError() - if self.identify_event.is_set() and package.destination != self.nid: - raise InvalidServerResponseError("Package is not addressed to this NetworkLink.") - log.debug(f"Received package: {package}") - return package - - @requires_connection - async def identify(self) -> None: - log.info(f"Identifying to {self.master_uri}...") - await self.websocket.send(f"Identify {self.nid}:{self.link_type}:{self.secret}") - response: Package = await self.receive() - if not response.source == "": - raise InvalidServerResponseError("Received a non-service package before identification.") - if "type" not in response.data: - raise InvalidServerResponseError("Missing 'type' in response data") - if response.data["type"] == "error": - raise ConnectionClosedError(f"Identification error: {response.data['type']}") - assert response.data["type"] == "success" - self.identify_event.set() - log.info(f"Identified successfully!") - - @requires_identification - async def send(self, package: Package): - await self.websocket.send(package.to_json_bytes()) - log.debug(f"Sent package: {package}") - - @requires_identification - async def request(self, message, destination): - package = Package(message, source=self.nid, destination=destination) - request = PendingRequest(loop=self._loop) - self._pending_requests[package.source_conv_id] = request - await self.send(package) - log.debug(f"Sent request: {message} -> {destination}") - await request.event.wait() - response: dict = request.data - log.debug(f"Received response: {request} -> {response}") - return response - - async def run(self): - """Blockingly run the Link.""" - log.debug(f"Running main client loop for {self.nid}.") - if self.error_event.is_set(): - raise ConnectionClosedError("RoyalnetLinks can't be rerun after an error.") - while True: - if not self.connect_event.is_set(): - await self.connect() - if not self.identify_event.is_set(): - await self.identify() - package: Package = await self.receive() - # Package is a response - if package.destination_conv_id in self._pending_requests: - request = self._pending_requests[package.destination_conv_id] - request.set(package.data) - continue - # Package is a request - assert isinstance(package, Package) - log.debug(f"Received request {package.source_conv_id}: {package}") - response = await self.request_handler(package.data) - response_package: Package = package.reply(response) - await self.send(response_package) - log.debug(f"Replied to request {response_package.source_conv_id}: {response_package}") diff --git a/royalnet/network/networkserver.py b/royalnet/network/networkserver.py deleted file mode 100644 index 863bc32d..00000000 --- a/royalnet/network/networkserver.py +++ /dev/null @@ -1,146 +0,0 @@ -import typing -import websockets -import re -import datetime -import uuid -import asyncio -import logging as _logging -from .package import Package - - -log = _logging.getLogger(__name__) - - -class ConnectedClient: - """The :py:class:`royalnet.network.NetworkServer`-side representation of a connected :py:class:`royalnet.network.NetworkLink`.""" - def __init__(self, socket: websockets.WebSocketServerProtocol): - self.socket: websockets.WebSocketServerProtocol = socket - self.nid: typing.Optional[str] = None - self.link_type: typing.Optional[str] = None - self.connection_datetime: datetime.datetime = datetime.datetime.now() - - @property - def is_identified(self) -> bool: - """Has the client sent a valid identification package?""" - return bool(self.nid) - - async def send_service(self, msg_type: str, message: str): - await self.send(Package({"type": msg_type, "service": message}, - source="", - destination=self.nid)) - - async def send(self, package: Package): - """Send a :py:class:`royalnet.network.Package` to the :py:class:`royalnet.network.NetworkLink`.""" - await self.socket.send(package.to_json_bytes()) - - -class NetworkServer: - def __init__(self, address: str, port: int, required_secret: str, *, loop: asyncio.AbstractEventLoop = None): - self.address: str = address - self.port: int = port - self.required_secret: str = required_secret - self.identified_clients: typing.List[ConnectedClient] = [] - self.loop = loop - - def find_client(self, *, nid: str = None, link_type: str = None) -> typing.List[ConnectedClient]: - assert not (nid and link_type) - if nid: - matching = [client for client in self.identified_clients if client.nid == nid] - assert len(matching) <= 1 - return matching - if link_type: - matching = [client for client in self.identified_clients if client.link_type == link_type] - return matching or [] - - async def listener(self, websocket: websockets.server.WebSocketServerProtocol, path): - log.info(f"{websocket.remote_address} connected to the server.") - connected_client = ConnectedClient(websocket) - # Wait for identification - identify_msg = await websocket.recv() - log.debug(f"{websocket.remote_address} identified itself with: {identify_msg}.") - if not isinstance(identify_msg, str): - await connected_client.send_service("error", "Invalid identification message (not a str)") - return - identification = re.match(r"Identify ([^:\s]+):([^:\s]+):([^:\s]+)", identify_msg) - if identification is None: - await connected_client.send_service("error", "Invalid identification message (regex failed)") - return - secret = identification.group(3) - if secret != self.required_secret: - await connected_client.send_service("error", "Invalid secret") - return - # Identification successful - connected_client.nid = identification.group(1) - connected_client.link_type = identification.group(2) - self.identified_clients.append(connected_client) - log.debug(f"{websocket.remote_address} identified successfully as {connected_client.nid} ({connected_client.link_type}).") - await connected_client.send_service("success", "Identification successful!") - log.debug(f"{connected_client.nid}'s identification confirmed.") - # Main loop - while True: - # Receive packages - raw_bytes = await websocket.recv() - package: Package = Package.from_json_bytes(raw_bytes) - log.debug(f"Received package: {package}") - # Check if the package destination is the server itself. - if package.destination == "": - # TODO: do stuff - pass - # Otherwise, route the package to its destination - # noinspection PyAsyncCall - self.loop.create_task(self.route_package(package)) - - def find_destination(self, package: Package) -> typing.List[ConnectedClient]: - """Find a list of destinations for the package. - - Parameters: - package: The package to find the destination of. - - Returns: - A :py:class:`list` of :py:class:`ConnectedClients` to send the package to.""" - # Parse destination - # Is it nothing? - if package.destination == "": - return [] - # Is it a valid nid? - try: - destination = str(uuid.UUID(package.destination)) - except ValueError: - pass - else: - return self.find_client(nid=destination) - # Is it a link_type? - return self.find_client(link_type=package.destination) - - async def route_package(self, package: Package) -> None: - """Executed every time a package is received and must be routed somewhere.""" - destinations = self.find_destination(package) - log.debug(f"Routing package: {package} -> {destinations}") - for destination in destinations: - # This may have some consequences - specific_package = Package(package.data, - source=package.source, - destination=destination.nid, - source_conv_id=package.source_conv_id, - destination_conv_id=package.destination_conv_id) - await destination.send(specific_package) - - def serve(self): - log.debug(f"Serving on ws://{self.address}:{self.port}") - server = self.loop.run_until_complete(websockets.serve(self.listener, - host=self.address, - port=self.port, - loop=self.loop)) - self.loop.run_forever() - - def run_blocking(self, verbose=False): - if verbose: - core_logger = _logging.getLogger("royalnet") - core_logger.setLevel(_logging.DEBUG) - stream_handler = _logging.StreamHandler() - stream_handler.formatter = _logging.Formatter("{asctime}\t{name}\t{levelname}\t{message}", style="{") - core_logger.addHandler(stream_handler) - core_logger.debug("Logging setup complete.") - if self.loop is None: - self.loop = asyncio.get_event_loop() - self.serve() diff --git a/royalnet/network/package.py b/royalnet/network/package.py deleted file mode 100644 index cdb0ddd6..00000000 --- a/royalnet/network/package.py +++ /dev/null @@ -1,111 +0,0 @@ -import json -import uuid -import typing - - -class Package: - """A Royalnet package, the data type with which a :py:class:`royalnet.network.NetworkLink` communicates with a :py:class:`royalnet.network.NetworkServer` or another link. - Contains info about the source and the destination.""" - - def __init__(self, - data: dict, - *, - source: str, - destination: str, - source_conv_id: typing.Optional[str] = None, - destination_conv_id: typing.Optional[str] = None): - """Create a Package. - - Parameters: - data: The data that should be sent. Usually a :py:class:`royalnet.network.Message`. - source: The ``nid`` of the node that created this Package. - destination: The ``link_type`` of the destination node, or alternatively, the ``nid`` of the node. Can also be the ``NULL`` value to send the message to nobody. - source_conv_id: The conversation id of the node that created this package. Akin to the sequence number on IP packets. - destination_conv_id: The conversation id of the node that this Package is a reply to.""" - # TODO: something is not right in these type hints. Check them. - self.data: dict = data - self.source: str = source - self.source_conv_id: str = source_conv_id or str(uuid.uuid4()) - self.destination: str = destination - self.destination_conv_id: typing.Optional[str] = destination_conv_id - - def __repr__(self): - return f"" - - def __eq__(self, other): - if isinstance(other, Package): - return (self.data == other.data) and \ - (self.source == other.source) and \ - (self.destination == other.destination) and \ - (self.source_conv_id == other.source_conv_id) and \ - (self.destination_conv_id == other.destination_conv_id) - return False - - def reply(self, data) -> "Package": - """Reply to this Package with another Package. - - Parameters: - data: The data that should be sent. Usually a :py:class:`royalnet.network.Message`. - - Returns: - The reply Package.""" - return Package(data, - source=self.destination, - destination=self.source, - source_conv_id=self.destination_conv_id or str(uuid.uuid4()), - destination_conv_id=self.source_conv_id) - - @staticmethod - def from_dict(d) -> "Package": - """Create a Package from a dictionary.""" - if "source" not in d: - raise ValueError("Missing source field") - if "nid" not in d["source"]: - raise ValueError("Missing source.nid field") - if "conv_id" not in d["source"]: - raise ValueError("Missing source.conv_id field") - if "destination" not in d: - raise ValueError("Missing destination field") - if "nid" not in d["destination"]: - raise ValueError("Missing destination.nid field") - if "conv_id" not in d["destination"]: - raise ValueError("Missing destination.conv_id field") - if "data" not in d: - raise ValueError("Missing data field") - return Package(d["data"], - source=d["source"]["nid"], - destination=d["destination"]["nid"], - source_conv_id=d["source"]["conv_id"], - destination_conv_id=d["destination"]["conv_id"]) - - def to_dict(self) -> dict: - """Convert the Package into a dictionary.""" - return { - "source": { - "nid": self.source, - "conv_id": self.source_conv_id - }, - "destination": { - "nid": self.destination, - "conv_id": self.destination_conv_id - }, - "data": self.data - } - - @staticmethod - def from_json_string(string: str) -> "Package": - """Create a Package from a JSON string.""" - return Package.from_dict(json.loads(string)) - - def to_json_string(self) -> str: - """Convert the Package into a JSON string.""" - return json.dumps(self.to_dict()) - - @staticmethod - def from_json_bytes(b: bytes) -> "Package": - """Create a Package from UTF8-encoded JSON bytes.""" - return Package.from_json_string(str(b, encoding="utf8")) - - def to_json_bytes(self) -> bytes: - """Convert the Package into UTF8-encoded JSON bytes.""" - return bytes(self.to_json_string(), encoding="utf8") diff --git a/royalnet/network/request.py b/royalnet/network/request.py deleted file mode 100644 index 657d97ac..00000000 --- a/royalnet/network/request.py +++ /dev/null @@ -1,24 +0,0 @@ -class Request: - """A request sent from a :py:class:`royalnet.network.NetworkLink` to another. - - It contains the name of the requested handler, in addition to the data.""" - - def __init__(self, handler: str, data: dict): - super().__init__() - self.handler: str = handler - self.data: dict = data - - def to_dict(self): - return self.__dict__ - - @staticmethod - def from_dict(d: dict): - return Request(**d) - - def __eq__(self, other): - if isinstance(other, Request): - return self.handler == other.handler and self.data == other.data - return False - - def __repr__(self): - return f"royalnet.network.Request(handler={self.handler}, data={self.data})" diff --git a/royalnet/network/response.py b/royalnet/network/response.py deleted file mode 100644 index 7a519334..00000000 --- a/royalnet/network/response.py +++ /dev/null @@ -1,61 +0,0 @@ -import typing -from ..error import RoyalnetRequestError - - -class Response: - """A base class to be inherited by all other response types.""" - - def to_dict(self) -> dict: - """Prepare the Response to be sent by converting it to a JSONable :py:class:`dict`.""" - return { - "type": self.__class__.__name__, - **self.__dict__ - } - - def __eq__(self, other): - if isinstance(other, Response): - return self.to_dict() == other.to_dict() - return False - - @classmethod - def from_dict(cls, d: dict) -> "Response": - """Recreate the response from a received :py:class:`dict`.""" - # Ignore type in dict - del d["type"] - # noinspection PyArgumentList - return cls(**d) - - def raise_on_error(self): - """Raise an :py:class:`Exception` if the Response is an error, do nothing otherwise.""" - raise NotImplementedError("Please override Response.raise_on_error()") - - -class ResponseSuccess(Response): - """A response to a successful :py:class:`royalnet.network.Request`.""" - - def __init__(self, data: typing.Optional[dict] = None): - if data is None: - self.data = {} - else: - self.data = data - - def __repr__(self): - return f"royalnet.network.ResponseSuccess(data={self.data})" - - def raise_on_error(self): - pass - - -class ResponseError(Response): - """A response to a invalid :py:class:`royalnet.network.Request`.""" - - def __init__(self, name: str, description: str, extra_info: typing.Optional[dict] = None): - self.name: str = name - self.description: str = description - self.extra_info: typing.Optional[dict] = extra_info - - def __repr__(self): - return f"royalnet.network.ResponseError(name={self.name}, description={self.description}, extra_info={self.extra_info})" - - def raise_on_error(self): - raise RoyalnetRequestError(self) diff --git a/setup.py b/setup.py index 057c07e4..a81166ec 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ setuptools.setup( packages=setuptools.find_packages(), install_requires=["python-telegram-bot>=11.1.0", "discord.py>=1.0.1", - "websockets>=6.0", + "royalherald>=5.0b4", "psycopg2-binary>=2.8", "aiohttp>=3.5.4", "sqlalchemy>=1.3.2", diff --git a/tests/test_network.py b/tests/test_network.py deleted file mode 100644 index 9a83a339..00000000 --- a/tests/test_network.py +++ /dev/null @@ -1,57 +0,0 @@ -import pytest -import uuid -import asyncio -import logging -from royalnet.network import Package, NetworkLink, NetworkServer, ConnectionClosedError, Request - - -log = logging.root -stream_handler = logging.StreamHandler() -stream_handler.formatter = logging.Formatter("{asctime}\t{name}\t{levelname}\t{message}", style="{") -log.addHandler(stream_handler) -log.setLevel(logging.WARNING) - - -@pytest.fixture -def async_loop(): - loop = asyncio.get_event_loop() - yield loop - loop.close() - - -async def echo_request_handler(message): - return message - - -def test_package_serialization(): - pkg = Package({"ciao": "ciao"}, - source=str(uuid.uuid4()), - destination=str(uuid.uuid4()), - source_conv_id=str(uuid.uuid4()), - destination_conv_id=str(uuid.uuid4())) - assert pkg == Package.from_dict(pkg.to_dict()) - assert pkg == Package.from_json_string(pkg.to_json_string()) - assert pkg == Package.from_json_bytes(pkg.to_json_bytes()) - - -def test_request_creation(): - request = Request("pytest", {"testing": "is fun", "bugs": "are less fun"}) - assert request == Request.from_dict(request.to_dict()) - - -def test_links(async_loop: asyncio.AbstractEventLoop): - address, port = "127.0.0.1", 1235 - master = NetworkServer(address, port, "test", loop=async_loop) - run_server_task = async_loop.create_task(master.run()) - # Test invalid secret - wrong_secret_link = NetworkLink("ws://127.0.0.1:1235", "invalid", "test", echo_request_handler, loop=async_loop) - with pytest.raises(ConnectionClosedError): - async_loop.run_until_complete(wrong_secret_link.run()) - # Test regular connection - link1 = NetworkLink("ws://127.0.0.1:1235", "test", "one", echo_request_handler, loop=async_loop) - async_loop.create_task(link1.run()) - link2 = NetworkLink("ws://127.0.0.1:1235", "test", "two", echo_request_handler, loop=async_loop) - async_loop.create_task(link2.run()) - message = {"ciao": "ciao"} - response = async_loop.run_until_complete(link1.request(message, "two")) - assert message == response