relst 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- relst/__init__.py +3 -0
- relst/client/__init__.py +3 -0
- relst/client/relst_client.py +100 -0
- relst/client/relst_flask_client.py +32 -0
- relst/client/relst_socket_client.py +61 -0
- relst/client/relst_ui.py +22 -0
- relst/protocol/__init__.py +38 -0
- relst/protocol/binary_codec.py +486 -0
- relst/protocol/codec.py +45 -0
- relst/protocol/errors.py +11 -0
- relst/protocol/json_codec.py +119 -0
- relst/protocol/values.py +125 -0
- relst/protocol/xml_codec.py +243 -0
- relst/server/__init__.py +4 -0
- relst/server/binary_server.py +106 -0
- relst/server/name_generator.py +24 -0
- relst/server/relst_server.py +245 -0
- relst/server/textual_server.py +185 -0
- relst/server/tracing.py +44 -0
- relst/server/ui/index.html +239 -0
- relst-1.1.0.dist-info/METADATA +228 -0
- relst-1.1.0.dist-info/RECORD +24 -0
- relst-1.1.0.dist-info/WHEEL +5 -0
- relst-1.1.0.dist-info/top_level.txt +1 -0
relst/__init__.py
ADDED
relst/client/__init__.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from typing import Dict, Set, Tuple
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from ..protocol import Codec, get_codec, DEFAULT_PROTOCOL
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
GET_CLASS_NAME_REQUEST = '__get_class_name'
|
|
7
|
+
GET_CLASS_HELP_REQUEST = '__get_class_help'
|
|
8
|
+
GET_METHODS_LIST_REQUEST = '__get_methods_list'
|
|
9
|
+
GET_METHOD_HELP_REQUEST = '__get_method_help'
|
|
10
|
+
UPDATE_SERVER_REQUEST = '__update_server'
|
|
11
|
+
|
|
12
|
+
PRIVATE_OR_PROTECTED_PREFIX = '_'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RelstClient(ABC):
|
|
16
|
+
"""
|
|
17
|
+
Client of a relst server. After UPDATE_METHODS (or entering a `with` block)
|
|
18
|
+
every public server-method is available as a python method of the client.
|
|
19
|
+
NOTE: Uppercase names are reserved for the client, server methods can't override them
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, protocol: str = DEFAULT_PROTOCOL):
|
|
23
|
+
self._codec: Codec = get_codec(protocol)
|
|
24
|
+
self._dynamic_methods: Set[str] = set()
|
|
25
|
+
|
|
26
|
+
# NOTE: transports that carry the method-name themselves (http url) set this to False
|
|
27
|
+
_embed_method = True
|
|
28
|
+
|
|
29
|
+
def UPDATE_METHODS(self):
|
|
30
|
+
self._remove_methods()
|
|
31
|
+
methods_list = self._handle_request(GET_METHODS_LIST_REQUEST)
|
|
32
|
+
|
|
33
|
+
for method_name, method_description in methods_list.items():
|
|
34
|
+
self._add_method(method_name, method_description)
|
|
35
|
+
|
|
36
|
+
def CLASS_NAME(self) -> str:
|
|
37
|
+
return self._handle_request(GET_CLASS_NAME_REQUEST)
|
|
38
|
+
|
|
39
|
+
def CLASS_HELP(self) -> str:
|
|
40
|
+
return self._handle_request(GET_CLASS_HELP_REQUEST)
|
|
41
|
+
|
|
42
|
+
def METHOD_HELP(self, method_name: str) -> str:
|
|
43
|
+
return self._handle_request(GET_METHOD_HELP_REQUEST, method_name=method_name)
|
|
44
|
+
|
|
45
|
+
def UPDATE_SERVER(self):
|
|
46
|
+
self._handle_request(UPDATE_SERVER_REQUEST)
|
|
47
|
+
|
|
48
|
+
def _handle_request(self, method_name: str, /, **method_args):
|
|
49
|
+
"""
|
|
50
|
+
:except: RelstRemoteError when the server reports an error, RelstProtocolError for bad payloads
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
request = self._codec.encode_request(method_name, method_args, embed_method=self._embed_method)
|
|
54
|
+
status, response = self._send(method_name, request)
|
|
55
|
+
return self._codec.decode_response(response, status)
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def _send(self, method_name: str, request: bytes) -> Tuple[int, bytes]:
|
|
59
|
+
"""transport-level exchange, returns (http-like status, response bytes)"""
|
|
60
|
+
|
|
61
|
+
def _remove_methods(self):
|
|
62
|
+
for method_name in self._dynamic_methods:
|
|
63
|
+
self.__dict__.pop(method_name, None)
|
|
64
|
+
|
|
65
|
+
self._dynamic_methods.clear()
|
|
66
|
+
|
|
67
|
+
def _add_method(self, method_name: str, description: Dict):
|
|
68
|
+
# NOTE: server must not shadow client's own attributes
|
|
69
|
+
if method_name.startswith(PRIVATE_OR_PROTECTED_PREFIX) or hasattr(type(self), method_name):
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
parameters = [param['name'] for param in description.get('params', [])]
|
|
73
|
+
|
|
74
|
+
def method_caller(*args, **kwargs):
|
|
75
|
+
if len(args) > len(parameters):
|
|
76
|
+
raise TypeError(f'{method_name}() takes at most {len(parameters)} positional arguments')
|
|
77
|
+
|
|
78
|
+
for parameter_name, argument in zip(parameters, args):
|
|
79
|
+
if parameter_name in kwargs:
|
|
80
|
+
raise TypeError(f'{method_name}() got multiple values for argument {parameter_name!r}')
|
|
81
|
+
|
|
82
|
+
kwargs[parameter_name] = argument
|
|
83
|
+
|
|
84
|
+
return self._handle_request(method_name, **kwargs)
|
|
85
|
+
|
|
86
|
+
method_caller.__name__ = method_name
|
|
87
|
+
method_caller.__doc__ = description.get('doc')
|
|
88
|
+
|
|
89
|
+
self._dynamic_methods.add(method_name)
|
|
90
|
+
setattr(self, method_name, method_caller)
|
|
91
|
+
|
|
92
|
+
def close(self):
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
def __enter__(self):
|
|
96
|
+
self.UPDATE_METHODS()
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
100
|
+
self.close()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from typing import Tuple
|
|
3
|
+
from .relst_client import RelstClient
|
|
4
|
+
from ..protocol import DEFAULT_PROTOCOL
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RelstFlaskClient(RelstClient):
|
|
8
|
+
"""Client of RelstTextualServer, speaks json / xml / binary over http"""
|
|
9
|
+
|
|
10
|
+
_embed_method = False
|
|
11
|
+
|
|
12
|
+
def __init__(self, server_address: str, server_port: int, is_https: bool = False,
|
|
13
|
+
protocol: str = DEFAULT_PROTOCOL, timeout: float = 30):
|
|
14
|
+
super().__init__(protocol)
|
|
15
|
+
|
|
16
|
+
requests_protocol = "https" if is_https else "http"
|
|
17
|
+
self.__request_prefix = f"{requests_protocol}://{server_address}:{server_port}"
|
|
18
|
+
self.__timeout = timeout
|
|
19
|
+
self.__session = requests.Session()
|
|
20
|
+
|
|
21
|
+
def _send(self, method_name: str, request: bytes) -> Tuple[int, bytes]:
|
|
22
|
+
response = self.__session.post(
|
|
23
|
+
f"{self.__request_prefix}/{method_name}",
|
|
24
|
+
data=request,
|
|
25
|
+
headers={'Content-Type': self._codec.content_type, 'Accept': self._codec.content_type},
|
|
26
|
+
timeout=self.__timeout,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
return response.status_code, response.content
|
|
30
|
+
|
|
31
|
+
def close(self):
|
|
32
|
+
self.__session.close()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
import threading
|
|
3
|
+
from typing import Tuple
|
|
4
|
+
from .relst_client import RelstClient
|
|
5
|
+
from ..protocol import Frame, RelstProtocolError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RelstSocketClient(RelstClient):
|
|
9
|
+
"""Client of RelstBinaryServer, keeps one tcp connection (thread-safe, calls are serialized)"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, server_address: str, server_port: int, timeout: float = 30):
|
|
12
|
+
super().__init__('binary')
|
|
13
|
+
|
|
14
|
+
self.__address = (server_address, server_port)
|
|
15
|
+
self.__timeout = timeout
|
|
16
|
+
self.__socket = None
|
|
17
|
+
self.__lock = threading.Lock()
|
|
18
|
+
|
|
19
|
+
def _send(self, method_name: str, request: bytes) -> Tuple[int, bytes]:
|
|
20
|
+
with self.__lock:
|
|
21
|
+
try:
|
|
22
|
+
connection = self.__connection()
|
|
23
|
+
connection.sendall(request)
|
|
24
|
+
|
|
25
|
+
_, raw_response = Frame.read(lambda size: self.__read(connection, size))
|
|
26
|
+
except (OSError, RelstProtocolError):
|
|
27
|
+
self.__disconnect()
|
|
28
|
+
raise
|
|
29
|
+
|
|
30
|
+
# NOTE: binary frames carry their own kind, status is only informative
|
|
31
|
+
return 200, raw_response
|
|
32
|
+
|
|
33
|
+
def close(self):
|
|
34
|
+
with self.__lock:
|
|
35
|
+
self.__disconnect()
|
|
36
|
+
|
|
37
|
+
def __connection(self) -> socket.socket:
|
|
38
|
+
if self.__socket is None:
|
|
39
|
+
self.__socket = socket.create_connection(self.__address, timeout=self.__timeout)
|
|
40
|
+
|
|
41
|
+
return self.__socket
|
|
42
|
+
|
|
43
|
+
def __disconnect(self):
|
|
44
|
+
if self.__socket is not None:
|
|
45
|
+
self.__socket.close()
|
|
46
|
+
self.__socket = None
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def __read(connection: socket.socket, size: int) -> bytes:
|
|
50
|
+
chunks = []
|
|
51
|
+
|
|
52
|
+
while size > 0:
|
|
53
|
+
chunk = connection.recv(size)
|
|
54
|
+
|
|
55
|
+
if not chunk:
|
|
56
|
+
raise ConnectionError('server closed the connection')
|
|
57
|
+
|
|
58
|
+
chunks.append(chunk)
|
|
59
|
+
size -= len(chunk)
|
|
60
|
+
|
|
61
|
+
return b''.join(chunks)
|
relst/client/relst_ui.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import eel
|
|
2
|
+
from argparse import ArgumentParser
|
|
3
|
+
from .relst_flask_client import RelstFlaskClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main(args):
|
|
7
|
+
client = RelstFlaskClient(args.server_address, args.server_port)
|
|
8
|
+
|
|
9
|
+
@eel.expose
|
|
10
|
+
def handle_request(function_name, **function_args):
|
|
11
|
+
return client.handle_request(function_name, **function_args)
|
|
12
|
+
|
|
13
|
+
eel.init("frontend")
|
|
14
|
+
eel.start()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
args_parser = ArgumentParser("GUI-Client for relst-api server")
|
|
19
|
+
args_parser.add_argument("server_address", type=str, help='name or ip of relst-api server')
|
|
20
|
+
args_parser.add_argument("server_port", type=int, help='port of relst-api server')
|
|
21
|
+
|
|
22
|
+
main(args_parser.parse_args())
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from .codec import Codec
|
|
2
|
+
from .json_codec import JsonCodec
|
|
3
|
+
from .xml_codec import XmlCodec
|
|
4
|
+
from .binary_codec import BinaryCodec, Frame, FrameHeader, FrameKind, ValueTag, FRAME_HEADER_SIZE
|
|
5
|
+
from .errors import RelstProtocolError, RelstRemoteError
|
|
6
|
+
from .values import to_plain
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
CODECS = {codec.name: codec for codec in (JsonCodec(), XmlCodec(), BinaryCodec())}
|
|
10
|
+
DEFAULT_PROTOCOL = 'json'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_codec(protocol: str) -> Codec:
|
|
14
|
+
try:
|
|
15
|
+
return CODECS[protocol.lower()]
|
|
16
|
+
except (KeyError, AttributeError):
|
|
17
|
+
raise RelstProtocolError(f'unknown protocol {protocol!r}, expected one of {sorted(CODECS)}') from None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def codec_from_content_type(content_type) -> Codec:
|
|
21
|
+
"""
|
|
22
|
+
:brief: picks codec by http Content-Type (parameters like charset are ignored)
|
|
23
|
+
:except: RelstProtocolError for unsupported types
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
media_type = (content_type or '').split(';')[0].strip().lower()
|
|
27
|
+
|
|
28
|
+
if not media_type:
|
|
29
|
+
return CODECS[DEFAULT_PROTOCOL]
|
|
30
|
+
|
|
31
|
+
for codec in CODECS.values():
|
|
32
|
+
if media_type == codec.content_type:
|
|
33
|
+
return codec
|
|
34
|
+
|
|
35
|
+
if media_type in ('text/xml', 'text/json'):
|
|
36
|
+
return CODECS['xml' if media_type == 'text/xml' else 'json']
|
|
37
|
+
|
|
38
|
+
raise RelstProtocolError(f'unsupported content-type {media_type!r}')
|