Updated Readme some. Added a family of messages that can pack/unpack/compress themselves to be analogous to HTTP Req/Resp.
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
from pe.connect import Connection
|
||||
from threading import Lock
|
||||
from msgpack import Unpacker
|
||||
from msgpack import packb, unpackb
|
||||
from enum import Enum
|
||||
import bz2
|
||||
from typing import Union, Self
|
||||
|
||||
|
||||
class PacketServerConnection(Connection):
|
||||
@@ -32,3 +36,207 @@ class PacketServerConnection(Connection):
|
||||
def query_accept(cls, port, call_from, call_to):
|
||||
return True
|
||||
|
||||
|
||||
class Message:
|
||||
"""Base class for communication encapsulated in msgpack objects."""
|
||||
|
||||
class CompressionType(Enum):
|
||||
NONE = 0
|
||||
BZIP2 = 1
|
||||
GZIP = 2
|
||||
DEFLATE = 3
|
||||
|
||||
class MessageType(Enum):
|
||||
REQUEST = 0
|
||||
RESPONSE = 1
|
||||
|
||||
def __init__(self, msg_type: MessageType, compression: CompressionType, payload: dict):
|
||||
self.type = Message.MessageType(msg_type)
|
||||
self.compression = Message.CompressionType(compression)
|
||||
self.data = payload
|
||||
|
||||
@property
|
||||
def vars(self) -> dict:
|
||||
if 'v' in self.data:
|
||||
if type(self.data['v']) is dict:
|
||||
return self.data['v']
|
||||
return {}
|
||||
|
||||
def get_var(self, key: str):
|
||||
if 'v' not in self.data:
|
||||
raise KeyError(f"Variable '{key}' not found.")
|
||||
if str(key) not in self.data['v']:
|
||||
raise KeyError(f"Variable '{key}' not found.")
|
||||
return self.data['v'][str(key)]
|
||||
|
||||
def set_var(self, key: str, value):
|
||||
if 'v' not in self.data:
|
||||
self.data['v'] = {}
|
||||
self.data['v'][str(key)] = value
|
||||
|
||||
@property
|
||||
def data_bytes(self):
|
||||
return packb(self.data)
|
||||
|
||||
def pack(self) -> bytes:
|
||||
output = {'t': self.type.value, 'c': self.compression.value}
|
||||
data_bytes = self.data_bytes
|
||||
|
||||
if (self.compression is self.CompressionType.NONE) or (len(data_bytes) < 30):
|
||||
output['d'] = data_bytes
|
||||
return packb(output)
|
||||
|
||||
if self.compression is self.CompressionType.BZIP2:
|
||||
compressed = bz2.compress(packb(self.data))
|
||||
else:
|
||||
raise NotImplementedError(f"Compression type {self.compression.name} is not implemented yet.")
|
||||
|
||||
if len(compressed) < len(data_bytes):
|
||||
output['d'] = compressed
|
||||
else:
|
||||
output['d'] = data_bytes
|
||||
output['c'] = self.CompressionType.NONE.value
|
||||
return packb(output)
|
||||
|
||||
@property
|
||||
def payload(self):
|
||||
if 'd' in self.data:
|
||||
pl = self.data['d']
|
||||
if type(pl) in (dict, str, bytes):
|
||||
return pl
|
||||
else:
|
||||
return str(pl)
|
||||
else:
|
||||
return ""
|
||||
|
||||
@payload.setter
|
||||
def payload(self, payload: Union[str, bytes, dict]):
|
||||
if type(payload) in (str, bytes, dict):
|
||||
self.data['d'] = payload
|
||||
else:
|
||||
self.data['d'] = str(payload)
|
||||
|
||||
@classmethod
|
||||
def unpack(cls, msg_bytes: bytes) -> Self:
|
||||
try:
|
||||
unpacked = unpackb(msg_bytes)
|
||||
except Exception as e:
|
||||
raise ValueError("ERROR: msg_bytes didn't contain a valid msgpack object.\n" + str(e))
|
||||
for i in ('t', 'c', 'd'):
|
||||
if i not in unpacked:
|
||||
raise ValueError("ERROR: unpacked bytes do not contain a valid Message object.")
|
||||
|
||||
comp = Message.CompressionType(unpacked['c'])
|
||||
msg_type = Message.MessageType(unpacked['t'])
|
||||
raw_data = unpacked['d']
|
||||
|
||||
if comp is Message.CompressionType.NONE:
|
||||
data = unpackb(raw_data)
|
||||
elif comp is Message.CompressionType.BZIP2:
|
||||
data = unpackb(bz2.decompress(raw_data))
|
||||
else:
|
||||
raise NotImplementedError(f"Compression type {comp.name} is not implemented yet.")
|
||||
return Message(msg_type, comp, data)
|
||||
|
||||
class Request(Message):
|
||||
class Method(Enum):
|
||||
GET = 0
|
||||
POST = 1
|
||||
UPDATE = 2
|
||||
DELETE = 3
|
||||
|
||||
def __init__(self, msg: Message):
|
||||
if msg.type is not Message.MessageType.REQUEST:
|
||||
raise ValueError(f"Can't create a Request Object from a {msg.type} Message object.")
|
||||
|
||||
super().__init__(msg.type, msg.compression, msg.data)
|
||||
|
||||
if ('p' in msg.data) and (type(msg.data['p']) is not str):
|
||||
raise ValueError("Path of Request must be a string.")
|
||||
|
||||
if 'm' in msg.data:
|
||||
if type(msg.data['m']) is not bytes:
|
||||
raise ValueError("Method of Request must be bytes.")
|
||||
self.Method(int(self.data['m'][0]))
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
if 'p' in self.data:
|
||||
return str(self.data['p'])
|
||||
else:
|
||||
return ""
|
||||
|
||||
@path.setter
|
||||
def path(self, path: str):
|
||||
self.data['p'] = str(path.strip())
|
||||
|
||||
@property
|
||||
def method(self) -> Method:
|
||||
if 'm' in self.data:
|
||||
return self.Method(int(self.data['m'][0]))
|
||||
else:
|
||||
return self.Method.GET
|
||||
|
||||
@method.setter
|
||||
def method(self, meth: Method):
|
||||
meth = self.Method(meth)
|
||||
self.data['m'] = meth.value.to_bytes(1)
|
||||
|
||||
@classmethod
|
||||
def unpack(cls, msg_bytes: bytes) -> Self:
|
||||
msg = super().unpack(msg_bytes)
|
||||
return Request(msg)
|
||||
|
||||
@classmethod
|
||||
def blank(cls) -> Self:
|
||||
msg = Message(Message.MessageType.REQUEST, Message.CompressionType.NONE, {})
|
||||
return Request(msg)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Request: {self.method.name} '{self.path}'>"
|
||||
|
||||
class Response(Message):
|
||||
def __init__(self, msg: Message):
|
||||
if msg.type is not Message.MessageType.RESPONSE:
|
||||
raise ValueError(f"Can't create a Response Object from a {msg.type} Message object.")
|
||||
|
||||
super().__init__(msg.type, msg.compression, msg.data)
|
||||
if 'c' in msg.data:
|
||||
status_bytes = self.data['c']
|
||||
if type(status_bytes) is not bytes:
|
||||
raise ValueError("Invalid Response data")
|
||||
status_code = int.from_bytes(status_bytes)
|
||||
if status_code >= 600:
|
||||
raise ValueError("Invalid status code.")
|
||||
|
||||
@classmethod
|
||||
def unpack(cls, msg_bytes: bytes) -> Self:
|
||||
msg = super().unpack(msg_bytes)
|
||||
return Response(msg)
|
||||
|
||||
@classmethod
|
||||
def blank(cls) -> Self:
|
||||
msg = Message(Message.MessageType.RESPONSE, Message.CompressionType.NONE, {})
|
||||
return Response(msg)
|
||||
|
||||
@property
|
||||
def status_code(self) -> int:
|
||||
if 'c' in self.data:
|
||||
status_bytes = self.data['c']
|
||||
if type(status_bytes) is not bytes:
|
||||
raise ValueError("Invalid Response data")
|
||||
status_code = int.from_bytes(status_bytes)
|
||||
if status_code >= 600:
|
||||
raise ValueError("Invalid status code.")
|
||||
return status_code
|
||||
else:
|
||||
return 200
|
||||
|
||||
@status_code.setter
|
||||
def status_code(self, code: int):
|
||||
if (code <= 0) or (code >= 600):
|
||||
raise ValueError("Status must be a positive integer <= 600")
|
||||
self.data['c'] = code.to_bytes(2)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Response: {self.status_code}>"
|
||||
@@ -1,9 +1,8 @@
|
||||
import pe
|
||||
from ..common import PacketServerConnection
|
||||
import ax25
|
||||
from configparser import ConfigParser
|
||||
from pathlib import Path
|
||||
import ZODB, transaction
|
||||
import ZODB, transaction, ZODB.FileStorage
|
||||
|
||||
class Server:
|
||||
def __init__(self, pe_server: str, port: int, server_callsign: str, data_dir: str = None):
|
||||
@@ -13,8 +12,27 @@ class Server:
|
||||
self.pe_server = pe_server
|
||||
self.pe_port = port
|
||||
if data_dir:
|
||||
if Path.is_dir(data_dir):
|
||||
data_path = Path(data_dir)
|
||||
else:
|
||||
data_path = Path.home().joinpath(".packetserver")
|
||||
if data_path.is_dir():
|
||||
if data_path.joinpath("data.zopedb").exists():
|
||||
if not data_path.joinpath("data.zopedb").is_file():
|
||||
raise FileExistsError("data.zopedb exists as non-file in specified path")
|
||||
self.home_dir = data_dir
|
||||
else:
|
||||
if data_path.exists():
|
||||
raise FileExistsError(f"Non-Directory path '{data_dir}' already exists.")
|
||||
else:
|
||||
data_path.mkdir()
|
||||
self.home_dir = data_dir
|
||||
self.storage = ZODB.FileStorage.FileStorage(self.data_file)
|
||||
self.db = ZODB.DB(self.storage)
|
||||
|
||||
|
||||
@property
|
||||
def data_file(self) -> str:
|
||||
return str(Path(self.home_dir).joinpath('data.zopedb'))
|
||||
|
||||
|
||||
def server_receiver(self, conn: PacketServerConnection):
|
||||
|
||||
Reference in New Issue
Block a user