mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-27 13:34:28 +00:00
Rewrite some more things (#53)
This commit is contained in:
parent
75e8fd774c
commit
daf63b9a61
6 changed files with 82 additions and 120 deletions
|
@ -1,23 +1,15 @@
|
||||||
"""Royalnet realated classes."""
|
"""Royalnet realated classes."""
|
||||||
|
|
||||||
from .messages import Message, ServerErrorMessage, InvalidSecretEM, InvalidDestinationEM, InvalidPackageEM, RequestSuccessful, RequestError, Reply
|
|
||||||
from .packages import Package
|
from .packages import Package
|
||||||
from .royalnetlink import RoyalnetLink, NetworkError, NotConnectedError, NotIdentifiedError
|
from .royalnetlink import RoyalnetLink, NetworkError, NotConnectedError, NotIdentifiedError, ConnectionClosedError
|
||||||
from .royalnetserver import RoyalnetServer
|
from .royalnetserver import RoyalnetServer
|
||||||
from .royalnetconfig import RoyalnetConfig
|
from .royalnetconfig import RoyalnetConfig
|
||||||
|
|
||||||
__all__ = ["Message",
|
__all__ = ["RoyalnetLink",
|
||||||
"ServerErrorMessage",
|
|
||||||
"InvalidSecretEM",
|
|
||||||
"InvalidDestinationEM",
|
|
||||||
"InvalidPackageEM",
|
|
||||||
"RoyalnetLink",
|
|
||||||
"NetworkError",
|
"NetworkError",
|
||||||
"NotConnectedError",
|
"NotConnectedError",
|
||||||
"NotIdentifiedError",
|
"NotIdentifiedError",
|
||||||
"Package",
|
"Package",
|
||||||
"RoyalnetServer",
|
"RoyalnetServer",
|
||||||
"RequestSuccessful",
|
|
||||||
"RequestError",
|
|
||||||
"RoyalnetConfig",
|
"RoyalnetConfig",
|
||||||
"Reply"]
|
"ConnectionClosedError"]
|
||||||
|
|
|
@ -1,78 +0,0 @@
|
||||||
import typing
|
|
||||||
import pickle
|
|
||||||
from ..error import RoyalnetError
|
|
||||||
|
|
||||||
|
|
||||||
class Message:
|
|
||||||
"""A message sent through the Royalnet."""
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<{self.__class__.__name__}>"
|
|
||||||
|
|
||||||
|
|
||||||
class IdentifySuccessfulMessage(Message):
|
|
||||||
"""The Royalnet identification step was successful."""
|
|
||||||
|
|
||||||
|
|
||||||
class ServerErrorMessage(Message):
|
|
||||||
"""Something went wrong in the connection to the :py:class:`royalnet.network.RoyalnetServer`."""
|
|
||||||
def __init__(self, reason):
|
|
||||||
super().__init__()
|
|
||||||
self.reason = reason
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidSecretEM(ServerErrorMessage):
|
|
||||||
"""The sent secret was incorrect.
|
|
||||||
|
|
||||||
This message terminates connection to the :py:class:`royalnet.network.RoyalnetServer`."""
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidPackageEM(ServerErrorMessage):
|
|
||||||
"""The sent :py:class:`royalnet.network.Package` was invalid."""
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidDestinationEM(InvalidPackageEM):
|
|
||||||
"""The :py:class:`royalnet.network.Package` destination was invalid or not found."""
|
|
||||||
|
|
||||||
|
|
||||||
class Reply(Message):
|
|
||||||
"""A reply to a request sent through the Royalnet."""
|
|
||||||
|
|
||||||
def raise_on_error(self) -> None:
|
|
||||||
"""If the reply is an error, raise an error, otherwise, do nothing.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
A :py:exc:`RoyalnetError`, if the Reply is an error, otherwise, nothing."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
|
|
||||||
class RequestSuccessful(Reply):
|
|
||||||
"""The sent request was successful."""
|
|
||||||
|
|
||||||
def raise_on_error(self) -> None:
|
|
||||||
"""If the reply is an error, raise an error, otherwise, do nothing.
|
|
||||||
|
|
||||||
Does nothing."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class RequestError(Reply):
|
|
||||||
"""The sent request wasn't successful."""
|
|
||||||
|
|
||||||
def __init__(self, exc: typing.Optional[Exception] = None):
|
|
||||||
"""Create a RequestError.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
exc: The exception that caused the error in the request."""
|
|
||||||
try:
|
|
||||||
pickle.dumps(exc)
|
|
||||||
except TypeError:
|
|
||||||
self.exc: Exception = Exception(repr(exc))
|
|
||||||
else:
|
|
||||||
self.exc = exc
|
|
||||||
|
|
||||||
def raise_on_error(self) -> None:
|
|
||||||
"""If the reply is an error, raise an error, otherwise, do nothing.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
Always raises a :py:exc:`royalnet.error.RoyalnetError`, containing the exception that caused the error."""
|
|
||||||
raise RoyalnetError(exc=self.exc)
|
|
|
@ -7,7 +7,7 @@ class Package:
|
||||||
"""A Royalnet package, the data type with which a :py:class:`royalnet.network.RoyalnetLink` communicates with a :py:class:`royalnet.network.RoyalnetServer` or another link. """
|
"""A Royalnet package, the data type with which a :py:class:`royalnet.network.RoyalnetLink` communicates with a :py:class:`royalnet.network.RoyalnetServer` or another link. """
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
data: typing.Union[None, int, float, str, list, dict],
|
data: dict,
|
||||||
*,
|
*,
|
||||||
source: str,
|
source: str,
|
||||||
destination: str,
|
destination: str,
|
||||||
|
@ -22,7 +22,7 @@ class Package:
|
||||||
source_conv_id: The conversation id of the node that created this package. Akin to the sequence number on IP packets.
|
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."""
|
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.
|
# TODO: something is not right in these type hints. Check them.
|
||||||
self.data: typing.Union[None, int, float, str, list, dict] = data
|
self.data: dict = data
|
||||||
self.source: str = source
|
self.source: str = source
|
||||||
self.source_conv_id: str = source_conv_id or str(uuid.uuid4())
|
self.source_conv_id: str = source_conv_id or str(uuid.uuid4())
|
||||||
self.destination: str = destination
|
self.destination: str = destination
|
||||||
|
|
|
@ -2,10 +2,10 @@ import asyncio
|
||||||
import websockets
|
import websockets
|
||||||
import uuid
|
import uuid
|
||||||
import functools
|
import functools
|
||||||
import typing
|
import math
|
||||||
import pickle
|
import numbers
|
||||||
import logging as _logging
|
import logging as _logging
|
||||||
from .messages import Message, ServerErrorMessage, RequestError
|
import typing
|
||||||
from .packages import Package
|
from .packages import Package
|
||||||
|
|
||||||
default_loop = asyncio.get_event_loop()
|
default_loop = asyncio.get_event_loop()
|
||||||
|
@ -20,16 +20,20 @@ class NotIdentifiedError(Exception):
|
||||||
"""The :py:class:`royalnet.network.RoyalnetLink` has not identified yet to a :py:class:`royalnet.network.RoyalnetServer`."""
|
"""The :py:class:`royalnet.network.RoyalnetLink` has not identified yet to a :py:class:`royalnet.network.RoyalnetServer`."""
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionClosedError(Exception):
|
||||||
|
"""The :py:class:`royalnet.network.RoyalnetLink`'s connection was closed unexpectedly. The link can't be used anymore."""
|
||||||
|
|
||||||
|
|
||||||
class NetworkError(Exception):
|
class NetworkError(Exception):
|
||||||
def __init__(self, error_msg: ServerErrorMessage, *args):
|
def __init__(self, error_data: dict, *args):
|
||||||
super().__init__(*args)
|
super().__init__(*args)
|
||||||
self.error_msg: ServerErrorMessage = error_msg
|
self.error_data: dict = error_data
|
||||||
|
|
||||||
|
|
||||||
class PendingRequest:
|
class PendingRequest:
|
||||||
def __init__(self, *, loop=default_loop):
|
def __init__(self, *, loop=default_loop):
|
||||||
self.event: asyncio.Event = asyncio.Event(loop=loop)
|
self.event: asyncio.Event = asyncio.Event(loop=loop)
|
||||||
self.data: typing.Optional[Message] = None
|
self.data: typing.Optional[dict] = None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self.event.is_set():
|
if self.event.is_set():
|
||||||
|
@ -44,7 +48,7 @@ class PendingRequest:
|
||||||
def requires_connection(func):
|
def requires_connection(func):
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
async def new_func(self, *args, **kwargs):
|
async def new_func(self, *args, **kwargs):
|
||||||
await self._connect_event.wait()
|
await self.connect_event.wait()
|
||||||
return await func(self, *args, **kwargs)
|
return await func(self, *args, **kwargs)
|
||||||
return new_func
|
return new_func
|
||||||
|
|
||||||
|
@ -67,29 +71,35 @@ class RoyalnetLink:
|
||||||
self.secret: str = secret
|
self.secret: str = secret
|
||||||
self.websocket: typing.Optional[websockets.WebSocketClientProtocol] = None
|
self.websocket: typing.Optional[websockets.WebSocketClientProtocol] = None
|
||||||
self.request_handler = request_handler
|
self.request_handler = request_handler
|
||||||
self._pending_requests: typing.Dict[str, typing.Optional[Message]] = {}
|
self._pending_requests: typing.Dict[str, PendingRequest] = {}
|
||||||
self._loop: asyncio.AbstractEventLoop = loop
|
self._loop: asyncio.AbstractEventLoop = loop
|
||||||
self._connect_event: asyncio.Event = asyncio.Event(loop=self._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)
|
self.identify_event: asyncio.Event = asyncio.Event(loop=self._loop)
|
||||||
|
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
|
"""Connect to the :py:class:`royalnet.network.RoyalnetServer` at ``self.master_uri``."""
|
||||||
log.info(f"Connecting to {self.master_uri}...")
|
log.info(f"Connecting to {self.master_uri}...")
|
||||||
self.websocket = await websockets.connect(self.master_uri, loop=self._loop)
|
self.websocket = await websockets.connect(self.master_uri, loop=self._loop)
|
||||||
self._connect_event.set()
|
self.connect_event.set()
|
||||||
log.info(f"Connected!")
|
log.info(f"Connected!")
|
||||||
|
|
||||||
@requires_connection
|
@requires_connection
|
||||||
async def receive(self) -> Package:
|
async def receive(self) -> Package:
|
||||||
|
"""Recieve a :py:class:`Package` from the :py:class:`royalnet.network.RoyalnetServer`.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
:py:exc:`royalnet.network.royalnetlink.ConnectionClosedError` if the connection closes."""
|
||||||
try:
|
try:
|
||||||
raw_pickle = await self.websocket.recv()
|
jbytes = await self.websocket.recv()
|
||||||
|
package: Package = Package.from_json_bytes(jbytes)
|
||||||
except websockets.ConnectionClosed:
|
except websockets.ConnectionClosed:
|
||||||
self.websocket = None
|
self.error_event.set()
|
||||||
self._connect_event.clear()
|
self.connect_event.clear()
|
||||||
self.identify_event.clear()
|
self.identify_event.clear()
|
||||||
log.info(f"Connection to {self.master_uri} was closed.")
|
log.info(f"Connection to {self.master_uri} was closed.")
|
||||||
# What to do now? Let's just reraise.
|
# What to do now? Let's just reraise.
|
||||||
raise
|
raise ConnectionClosedError("")
|
||||||
package: typing.Union[Package, Package] = pickle.loads(raw_pickle)
|
|
||||||
assert package.destination == self.nid
|
assert package.destination == self.nid
|
||||||
log.debug(f"Received package: {package}")
|
log.debug(f"Received package: {package}")
|
||||||
return package
|
return package
|
||||||
|
@ -107,13 +117,12 @@ class RoyalnetLink:
|
||||||
|
|
||||||
@requires_identification
|
@requires_identification
|
||||||
async def send(self, package: Package):
|
async def send(self, package: Package):
|
||||||
raw_pickle: bytes = pickle.dumps(package)
|
await self.websocket.send(package.to_json_bytes())
|
||||||
await self.websocket.send(raw_pickle)
|
|
||||||
log.debug(f"Sent package: {package}")
|
log.debug(f"Sent package: {package}")
|
||||||
|
|
||||||
@requires_identification
|
@requires_identification
|
||||||
async def request(self, message, destination):
|
async def request(self, message, destination):
|
||||||
package = Package(message, destination, self.nid)
|
package = Package(message, source=self.nid, destination=destination)
|
||||||
request = PendingRequest(loop=self._loop)
|
request = PendingRequest(loop=self._loop)
|
||||||
self._pending_requests[package.source_conv_id] = request
|
self._pending_requests[package.source_conv_id] = request
|
||||||
await self.send(package)
|
await self.send(package)
|
||||||
|
@ -125,10 +134,14 @@ class RoyalnetLink:
|
||||||
raise NetworkError(result, "Server returned error while requesting something")
|
raise NetworkError(result, "Server returned error while requesting something")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def run(self):
|
async def run(self, loops: numbers.Real = math.inf):
|
||||||
|
"""Blockingly run the Link."""
|
||||||
log.debug(f"Running main client loop for {self.nid}.")
|
log.debug(f"Running main client loop for {self.nid}.")
|
||||||
while True:
|
if self.error_event.is_set():
|
||||||
if self.websocket is None:
|
raise ConnectionClosedError("RoyalnetLinks can't be rerun after an error.")
|
||||||
|
while loops:
|
||||||
|
loops -= 1
|
||||||
|
if not self.connect_event.is_set():
|
||||||
await self.connect()
|
await self.connect()
|
||||||
if not self.identify_event.is_set():
|
if not self.identify_event.is_set():
|
||||||
await self.identify()
|
await self.identify()
|
||||||
|
|
|
@ -6,7 +6,6 @@ import pickle
|
||||||
import uuid
|
import uuid
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging as _logging
|
import logging as _logging
|
||||||
from .messages import InvalidPackageEM, InvalidSecretEM, IdentifySuccessfulMessage
|
|
||||||
from .packages import Package
|
from .packages import Package
|
||||||
|
|
||||||
default_loop = asyncio.get_event_loop()
|
default_loop = asyncio.get_event_loop()
|
||||||
|
@ -26,9 +25,14 @@ class ConnectedClient:
|
||||||
"""Has the client sent a valid identification package?"""
|
"""Has the client sent a valid identification package?"""
|
||||||
return bool(self.nid)
|
return bool(self.nid)
|
||||||
|
|
||||||
|
async def send_server_error(self, reason: str):
|
||||||
|
await self.send(Package({"error": reason},
|
||||||
|
source="<server>",
|
||||||
|
destination=self.nid))
|
||||||
|
|
||||||
async def send(self, package: Package):
|
async def send(self, package: Package):
|
||||||
"""Send a :py:class:`royalnet.network.Package` to the :py:class:`royalnet.network.RoyalnetLink`."""
|
"""Send a :py:class:`royalnet.network.Package` to the :py:class:`royalnet.network.RoyalnetLink`."""
|
||||||
await self.socket.send(package.pickle())
|
await self.socket.send(package.to_json_bytes())
|
||||||
|
|
||||||
|
|
||||||
class RoyalnetServer:
|
class RoyalnetServer:
|
||||||
|
@ -49,22 +53,22 @@ class RoyalnetServer:
|
||||||
matching = [client for client in self.identified_clients if client.link_type == link_type]
|
matching = [client for client in self.identified_clients if client.link_type == link_type]
|
||||||
return matching or []
|
return matching or []
|
||||||
|
|
||||||
async def listener(self, websocket: websockets.server.WebSocketServerProtocol, request_uri: str):
|
async def listener(self, websocket: websockets.server.WebSocketServerProtocol):
|
||||||
log.info(f"{websocket.remote_address} connected to the server.")
|
log.info(f"{websocket.remote_address} connected to the server.")
|
||||||
connected_client = ConnectedClient(websocket)
|
connected_client = ConnectedClient(websocket)
|
||||||
# Wait for identification
|
# Wait for identification
|
||||||
identify_msg = await websocket.recv()
|
identify_msg = await websocket.recv()
|
||||||
log.debug(f"{websocket.remote_address} identified itself with: {identify_msg}.")
|
log.debug(f"{websocket.remote_address} identified itself with: {identify_msg}.")
|
||||||
if not isinstance(identify_msg, str):
|
if not isinstance(identify_msg, str):
|
||||||
await websocket.send(InvalidPackageEM("Invalid identification message (not a str)"))
|
await websocket.send(connected_client.send_server_error("Invalid identification message (not a str)"))
|
||||||
return
|
return
|
||||||
identification = re.match(r"Identify ([^:\s]+):([^:\s]+):([^:\s]+)", identify_msg)
|
identification = re.match(r"Identify ([^:\s]+):([^:\s]+):([^:\s]+)", identify_msg)
|
||||||
if identification is None:
|
if identification is None:
|
||||||
await websocket.send(InvalidPackageEM("Invalid identification message (regex failed)"))
|
await websocket.send(connected_client.send_server_error("Invalid identification message (regex failed)"))
|
||||||
return
|
return
|
||||||
secret = identification.group(3)
|
secret = identification.group(3)
|
||||||
if secret != self.required_secret:
|
if secret != self.required_secret:
|
||||||
await websocket.send(InvalidSecretEM("Invalid secret"))
|
await websocket.send(connected_client.send_server_error("Invalid secret"))
|
||||||
return
|
return
|
||||||
# Identification successful
|
# Identification successful
|
||||||
connected_client.nid = identification.group(1)
|
connected_client.nid = identification.group(1)
|
||||||
|
|
|
@ -1,10 +1,23 @@
|
||||||
import pytest
|
import pytest
|
||||||
import uuid
|
import uuid
|
||||||
from royalnet.network.packages import Package
|
import asyncio
|
||||||
|
from royalnet.network import Package, RoyalnetLink, RoyalnetServer, ConnectionClosedError
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def async_loop():
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
yield loop
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip("Not a test")
|
||||||
|
def echo_request_handler(message):
|
||||||
|
return message
|
||||||
|
|
||||||
|
|
||||||
def test_package_serialization():
|
def test_package_serialization():
|
||||||
pkg = Package("ciao",
|
pkg = Package({"ciao": "ciao"},
|
||||||
source=str(uuid.uuid4()),
|
source=str(uuid.uuid4()),
|
||||||
destination=str(uuid.uuid4()),
|
destination=str(uuid.uuid4()),
|
||||||
source_conv_id=str(uuid.uuid4()),
|
source_conv_id=str(uuid.uuid4()),
|
||||||
|
@ -12,3 +25,21 @@ def test_package_serialization():
|
||||||
assert pkg == Package.from_dict(pkg.to_dict())
|
assert pkg == Package.from_dict(pkg.to_dict())
|
||||||
assert pkg == Package.from_json_string(pkg.to_json_string())
|
assert pkg == Package.from_json_string(pkg.to_json_string())
|
||||||
assert pkg == Package.from_json_bytes(pkg.to_json_bytes())
|
assert pkg == Package.from_json_bytes(pkg.to_json_bytes())
|
||||||
|
|
||||||
|
|
||||||
|
def test_links(async_loop: asyncio.AbstractEventLoop):
|
||||||
|
address, port = "127.0.0.1", 1234
|
||||||
|
master = RoyalnetServer(address, port, "test")
|
||||||
|
async_loop.run_until_complete(master.start())
|
||||||
|
# Test invalid secret
|
||||||
|
wrong_secret_link = RoyalnetLink("ws://127.0.0.1:1234", "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 = RoyalnetLink("ws://127.0.0.1:1234", "test", "one", echo_request_handler, loop=async_loop)
|
||||||
|
link1_run_task = async_loop.create_task(link1.run())
|
||||||
|
link2 = RoyalnetLink("ws://127.0.0.1:1234", "test", "two", echo_request_handler, loop=async_loop)
|
||||||
|
link2_run_task = async_loop.create_task(link2.run())
|
||||||
|
message = {"ciao": "ciao"}
|
||||||
|
response = async_loop.run_until_complete(link1.request(message, "two"))
|
||||||
|
assert message == response
|
||||||
|
|
Loading…
Reference in a new issue