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 ADDED
@@ -0,0 +1,3 @@
1
+ from . import protocol
2
+ from . import server
3
+ from . import client
@@ -0,0 +1,3 @@
1
+ from .relst_client import RelstClient
2
+ from .relst_flask_client import RelstFlaskClient
3
+ from .relst_socket_client import RelstSocketClient
@@ -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)
@@ -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}')