morebuiltins 0.0.1__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.
- morebuiltins/__init__.py +7 -0
- morebuiltins/functools.py +169 -0
- morebuiltins/ipc.py +376 -0
- morebuiltins/request.py +343 -0
- morebuiltins/utils.py +810 -0
- morebuiltins-0.0.1.dist-info/LICENSE +21 -0
- morebuiltins-0.0.1.dist-info/METADATA +108 -0
- morebuiltins-0.0.1.dist-info/RECORD +9 -0
- morebuiltins-0.0.1.dist-info/WHEEL +4 -0
morebuiltins/__init__.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from itertools import chain
|
|
5
|
+
from typing import Optional, OrderedDict, Union
|
|
6
|
+
from weakref import WeakSet
|
|
7
|
+
|
|
8
|
+
__all__ = ["lru_cache_ttl", "threads"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def lru_cache_ttl(
|
|
12
|
+
maxsize: int,
|
|
13
|
+
ttl: Optional[Union[int, float]] = None,
|
|
14
|
+
controls=False,
|
|
15
|
+
auto_clear=True,
|
|
16
|
+
timer=time.time,
|
|
17
|
+
):
|
|
18
|
+
"""A Least Recently Used (LRU) cache with a Time To Live (TTL) feature.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
maxsize (int): maxsize of cache
|
|
22
|
+
ttl (Optional[Union[int, float]], optional): time to live. Defaults to None.
|
|
23
|
+
controls (bool, optional): set cache/ttl_clean attributes. Defaults to False.
|
|
24
|
+
auto_clear (bool, optional): clear dead cache automatically. Defaults to True.
|
|
25
|
+
timer (callable, optional): Defaults to time.time.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
callable: decorator function
|
|
29
|
+
|
|
30
|
+
>>> import time
|
|
31
|
+
>>> # test ttl
|
|
32
|
+
>>> values = [1, 2]
|
|
33
|
+
>>> @lru_cache_ttl(1, 0.1)
|
|
34
|
+
... def func1(i):
|
|
35
|
+
... return values.pop(0)
|
|
36
|
+
>>> [func1(1), func1(1), time.sleep(0.11), func1(1)]
|
|
37
|
+
[1, 1, None, 2]
|
|
38
|
+
>>> # test maxsize
|
|
39
|
+
>>> values = [1, 2, 3]
|
|
40
|
+
>>> func = lambda i: values.pop(0)
|
|
41
|
+
>>> func1 = lru_cache_ttl(2)(func)
|
|
42
|
+
>>> [func1(i) for i in [1, 1, 1, 2, 2, 2, 3, 3, 3]]
|
|
43
|
+
[1, 1, 1, 2, 2, 2, 3, 3, 3]
|
|
44
|
+
>>> # test auto_clear=True, with controls
|
|
45
|
+
>>> values = [1, 2, 3, 4]
|
|
46
|
+
>>> func = lambda i: values.pop(0)
|
|
47
|
+
>>> func1 = lru_cache_ttl(5, 0.1, controls=True, auto_clear=True)(func)
|
|
48
|
+
>>> [func1(1), func1(2), func1(3)]
|
|
49
|
+
[1, 2, 3]
|
|
50
|
+
>>> time.sleep(0.11)
|
|
51
|
+
>>> func1(3)
|
|
52
|
+
4
|
|
53
|
+
>>> len(func1.cache)
|
|
54
|
+
1
|
|
55
|
+
>>> # test auto_clear=False
|
|
56
|
+
>>> values = [1, 2, 3, 4]
|
|
57
|
+
>>> @lru_cache_ttl(5, 0.1, controls=True, auto_clear=False)
|
|
58
|
+
... def func1(i):
|
|
59
|
+
... return values.pop(0)
|
|
60
|
+
>>> [func1(1), func1(2), func1(3)]
|
|
61
|
+
[1, 2, 3]
|
|
62
|
+
>>> time.sleep(0.11)
|
|
63
|
+
>>> func1(3)
|
|
64
|
+
4
|
|
65
|
+
>>> len(func1.cache)
|
|
66
|
+
3
|
|
67
|
+
"""
|
|
68
|
+
cache: OrderedDict = OrderedDict()
|
|
69
|
+
move_to_end = cache.move_to_end
|
|
70
|
+
popitem = cache.popitem
|
|
71
|
+
next_clear_ts = timer()
|
|
72
|
+
|
|
73
|
+
def ttl_clean(expire):
|
|
74
|
+
for k, v in tuple(cache.items()):
|
|
75
|
+
if v[1] <= expire:
|
|
76
|
+
cache.pop(k, None)
|
|
77
|
+
|
|
78
|
+
def decorator(func):
|
|
79
|
+
@wraps(func)
|
|
80
|
+
def wrapped(*args, **kwargs):
|
|
81
|
+
nonlocal next_clear_ts
|
|
82
|
+
now = timer()
|
|
83
|
+
if ttl is None:
|
|
84
|
+
expire_cleared = True
|
|
85
|
+
else:
|
|
86
|
+
expire_cleared = auto_clear and now > next_clear_ts
|
|
87
|
+
if expire_cleared:
|
|
88
|
+
ttl_clean(now)
|
|
89
|
+
next_clear_ts = now + ttl
|
|
90
|
+
# key = _make_key(args, kwargs, False)
|
|
91
|
+
key = hash(tuple(chain(args, kwargs.items())))
|
|
92
|
+
if key in cache:
|
|
93
|
+
if expire_cleared is True or now < cache[key][1]:
|
|
94
|
+
# 1. key in cache; 2. cache is valid
|
|
95
|
+
# set newest, return result
|
|
96
|
+
move_to_end(key)
|
|
97
|
+
return cache[key][0]
|
|
98
|
+
# call function
|
|
99
|
+
result = func(*args, **kwargs)
|
|
100
|
+
# ensure size
|
|
101
|
+
while len(cache) >= maxsize:
|
|
102
|
+
popitem(last=False)
|
|
103
|
+
if ttl is None:
|
|
104
|
+
# no need save expired ts
|
|
105
|
+
cache[key] = (result,)
|
|
106
|
+
else:
|
|
107
|
+
# setitem(key, (result, now + ttl))
|
|
108
|
+
cache[key] = (result, timer() + ttl)
|
|
109
|
+
# {key: (result, expired_ts)}
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
if controls:
|
|
113
|
+
setattr(wrapped, "cache", cache)
|
|
114
|
+
setattr(wrapped, "ttl_clean", ttl_clean)
|
|
115
|
+
return wrapped
|
|
116
|
+
|
|
117
|
+
return decorator
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def threads(n: Optional[int] = None, executor_class=None, **kws):
|
|
121
|
+
"""Quickly convert synchronous functions to be concurrency-able. (similar to madisonmay/Tomorrow)
|
|
122
|
+
|
|
123
|
+
>>> @threads(10)
|
|
124
|
+
... def test(i):
|
|
125
|
+
... time.sleep(i)
|
|
126
|
+
... return i
|
|
127
|
+
>>> start = time.time()
|
|
128
|
+
>>> tasks = [test(i) for i in [0.1] * 5]
|
|
129
|
+
>>> len(test.pool._threads)
|
|
130
|
+
5
|
|
131
|
+
>>> len(test.tasks)
|
|
132
|
+
5
|
|
133
|
+
>>> for i in tasks:
|
|
134
|
+
... i.result() if hasattr(i, 'result') else i
|
|
135
|
+
0.1
|
|
136
|
+
0.1
|
|
137
|
+
0.1
|
|
138
|
+
0.1
|
|
139
|
+
0.1
|
|
140
|
+
>>> time.time() - start < 0.2
|
|
141
|
+
True
|
|
142
|
+
>>> len(test.pool._threads)
|
|
143
|
+
5
|
|
144
|
+
>>> len(test.tasks)
|
|
145
|
+
0
|
|
146
|
+
"""
|
|
147
|
+
pool = (executor_class or ThreadPoolExecutor)(max_workers=n, **kws)
|
|
148
|
+
tasks: WeakSet = WeakSet()
|
|
149
|
+
|
|
150
|
+
def decorator(func):
|
|
151
|
+
@wraps(func)
|
|
152
|
+
def wrapped(*args, **kwargs):
|
|
153
|
+
future = pool.submit(func, *args, **kwargs)
|
|
154
|
+
future.add_done_callback(lambda task: tasks.discard(task))
|
|
155
|
+
tasks.add(future)
|
|
156
|
+
return future
|
|
157
|
+
|
|
158
|
+
setattr(wrapped, "pool", pool)
|
|
159
|
+
setattr(wrapped, "tasks", tasks)
|
|
160
|
+
return wrapped
|
|
161
|
+
|
|
162
|
+
return decorator
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
if __name__ == "__main__":
|
|
166
|
+
__name__ = "morebuiltins.functools"
|
|
167
|
+
import doctest
|
|
168
|
+
|
|
169
|
+
doctest.testmod()
|
morebuiltins/ipc.py
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import pickle
|
|
3
|
+
import sys
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from logging import LogRecord
|
|
6
|
+
from logging.handlers import SocketHandler
|
|
7
|
+
from typing import Any, Callable, Dict, Literal, Optional, Union
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"IPCEncoder",
|
|
11
|
+
"JSONEncoder",
|
|
12
|
+
"PickleEncoder",
|
|
13
|
+
"SocketLogHandlerEncoder",
|
|
14
|
+
"SocketServer",
|
|
15
|
+
"SocketClient",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class IPCEncoder(ABC):
|
|
20
|
+
"""An abstract base class for all encoders; implementing the necessary communication protocol requires only the definition of two abstract methods. Be mindful that varying header lengths will impact the maximum packaging size."""
|
|
21
|
+
|
|
22
|
+
# HEAD_LENGTH: Package length
|
|
23
|
+
# 1: 256 B, 2: 64 KB, 3: 16 MB, 4: 4 GB, 5: 1 TB, 6: 256 TB
|
|
24
|
+
HEAD_SIZE = 4
|
|
25
|
+
BYTEORDER: Literal["little", "big"] = "big"
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def _dumps(self, raw: Any) -> bytes:
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def _loads(self, raw: bytes) -> Any:
|
|
33
|
+
raise NotImplementedError
|
|
34
|
+
|
|
35
|
+
def dumps(self, raw: Any) -> bytes:
|
|
36
|
+
result = self._dumps(raw)
|
|
37
|
+
head = len(result).to_bytes(self.HEAD_SIZE, self.BYTEORDER)
|
|
38
|
+
return head + result
|
|
39
|
+
|
|
40
|
+
def loads(self, raw: Any) -> bytes:
|
|
41
|
+
return self._loads(raw)
|
|
42
|
+
|
|
43
|
+
def get_size(self, head: bytes):
|
|
44
|
+
return int.from_bytes(head, self.BYTEORDER)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class JSONEncoder(IPCEncoder):
|
|
48
|
+
_DUMP_KWARGS: Dict = {}
|
|
49
|
+
_LOAD_KWARGS: Dict = {}
|
|
50
|
+
_ENCODING = "utf-8"
|
|
51
|
+
|
|
52
|
+
def __init__(self):
|
|
53
|
+
import json
|
|
54
|
+
|
|
55
|
+
self._encoder = json.dumps
|
|
56
|
+
self._decoder = json.loads
|
|
57
|
+
super().__init__()
|
|
58
|
+
|
|
59
|
+
def _dumps(self, raw: Any) -> bytes:
|
|
60
|
+
return self._encoder(raw, **self._DUMP_KWARGS).encode(self._ENCODING)
|
|
61
|
+
|
|
62
|
+
def _loads(self, raw: bytes) -> Any:
|
|
63
|
+
return self._decoder(raw.decode(self._ENCODING), **self._LOAD_KWARGS)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class PickleEncoder(IPCEncoder):
|
|
67
|
+
_DUMP_KWARGS: Dict = {}
|
|
68
|
+
_LOAD_KWARGS: Dict = {}
|
|
69
|
+
|
|
70
|
+
def __init__(self):
|
|
71
|
+
super().__init__()
|
|
72
|
+
|
|
73
|
+
def _dumps(self, raw: Any) -> bytes:
|
|
74
|
+
return pickle.dumps(raw, **self._DUMP_KWARGS)
|
|
75
|
+
|
|
76
|
+
def _loads(self, raw: bytes) -> Any:
|
|
77
|
+
return pickle.loads(raw, **self._LOAD_KWARGS)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class SocketLogHandlerEncoder(IPCEncoder):
|
|
81
|
+
"""For a practical demonstration, refer to the test code: morebuiltins\ipc.py:_test_ipc_logging.
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
async def _test_ipc_logging():
|
|
85
|
+
import logging
|
|
86
|
+
|
|
87
|
+
host = "127.0.0.1"
|
|
88
|
+
port = 8090
|
|
89
|
+
async with SocketServer(host=host, port=port, encoder=SocketLogHandlerEncoder()):
|
|
90
|
+
# socket logger demo
|
|
91
|
+
# ==================
|
|
92
|
+
logger = logging.getLogger("test_logger")
|
|
93
|
+
logger.setLevel(logging.DEBUG)
|
|
94
|
+
h = SocketHandler(host, port)
|
|
95
|
+
h.setLevel(logging.DEBUG)
|
|
96
|
+
logger.addHandler(h)
|
|
97
|
+
logger.info("test socket")
|
|
98
|
+
# ==================
|
|
99
|
+
|
|
100
|
+
# ensure test case
|
|
101
|
+
await asyncio.sleep(0.1)
|
|
102
|
+
assert pickle.loads(h.sock.recv(100000)[4:])["name"] == logger.name
|
|
103
|
+
```
|
|
104
|
+
And provide a simple implementation for generating logs for coroutine code with Client usage.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
_DUMP_KWARGS: Dict = {"protocol": 1}
|
|
108
|
+
_LOAD_KWARGS: Dict = {}
|
|
109
|
+
|
|
110
|
+
def __init__(self):
|
|
111
|
+
super().__init__()
|
|
112
|
+
|
|
113
|
+
def makePickle(self, d: Union[dict, LogRecord]):
|
|
114
|
+
if isinstance(d, LogRecord):
|
|
115
|
+
msg = d.getMessage()
|
|
116
|
+
d = dict(d.__dict__)
|
|
117
|
+
d["msg"] = msg
|
|
118
|
+
d["args"] = None
|
|
119
|
+
d["exc_info"] = None
|
|
120
|
+
d.pop("message", None)
|
|
121
|
+
return pickle.dumps(d, **self._DUMP_KWARGS)
|
|
122
|
+
|
|
123
|
+
def _dumps(self, raw: Union[LogRecord, dict]) -> bytes:
|
|
124
|
+
return self.makePickle(raw)
|
|
125
|
+
|
|
126
|
+
def _loads(self, raw: bytes) -> Any:
|
|
127
|
+
return pickle.loads(raw, **self._LOAD_KWARGS)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class SocketServer:
|
|
131
|
+
"""To see an example in action, view the test code: morebuiltins\ipc.py:_test_ipc.
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
async def test_client(host="127.0.0.1", port=8090, encoder=None, cases=None):
|
|
135
|
+
async with SocketClient(host=host, port=port, encoder=encoder) as c:
|
|
136
|
+
for case in cases:
|
|
137
|
+
await c.send(case)
|
|
138
|
+
response = await c.recv()
|
|
139
|
+
if globals().get("print_log"):
|
|
140
|
+
print("[Client]", "send:", repr(case), "=>", "recv:", repr(response))
|
|
141
|
+
assert case == response or str(case) == response, [case, response]
|
|
142
|
+
await c.send("[shutdown server]")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
async def _test_ipc():
|
|
146
|
+
import platform
|
|
147
|
+
|
|
148
|
+
JSONEncoder._DUMP_KWARGS["default"] = str
|
|
149
|
+
for enc, cases in [
|
|
150
|
+
[PickleEncoder, [123, "123", None, {"a"}, ["a"], ("a",), {"a": 1}]],
|
|
151
|
+
[JSONEncoder, [123, "123", None, {"a"}, ["a"], {"a": 1}]],
|
|
152
|
+
]:
|
|
153
|
+
encoder = enc()
|
|
154
|
+
if platform.system() == "Linux":
|
|
155
|
+
# test unix domain socket
|
|
156
|
+
print("Test Linux Unix Domain Socket")
|
|
157
|
+
host = "/tmp/uds.sock"
|
|
158
|
+
port = None
|
|
159
|
+
async with SocketServer(host=host, port=port, encoder=encoder):
|
|
160
|
+
await test_client(host, port=None, encoder=encoder, cases=cases)
|
|
161
|
+
|
|
162
|
+
# test socket
|
|
163
|
+
host = "127.0.0.1"
|
|
164
|
+
port = 8090
|
|
165
|
+
async with SocketServer(host=host, port=port, encoder=encoder):
|
|
166
|
+
await test_client(host="127.0.0.1", port=8090, encoder=encoder, cases=cases)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
def __init__(
|
|
173
|
+
self,
|
|
174
|
+
host: str = "127.0.0.1",
|
|
175
|
+
port: int = 8090,
|
|
176
|
+
handler: Optional[Callable] = None,
|
|
177
|
+
encoder: Optional[IPCEncoder] = None,
|
|
178
|
+
connect_kwargs: Optional[dict] = None,
|
|
179
|
+
):
|
|
180
|
+
if port is None and sys.platform == "win32":
|
|
181
|
+
raise SystemError("not support UDS(unix domain socket) on win32 platform")
|
|
182
|
+
self.host = host
|
|
183
|
+
self.port = port
|
|
184
|
+
self.handler = handler or self.default_handler
|
|
185
|
+
self.encoder: IPCEncoder = encoder or PickleEncoder()
|
|
186
|
+
self.connect_kwargs = connect_kwargs or {}
|
|
187
|
+
|
|
188
|
+
self.server: Optional[asyncio.base_events.Server] = None
|
|
189
|
+
self._shutdown_ev: Optional[asyncio.Event] = None
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
async def default_handler(self: "SocketServer", item: Any):
|
|
193
|
+
if globals().get("print_log"):
|
|
194
|
+
print("[Server] recv:", repr(item), "=>", "send:", repr(item), flush=True)
|
|
195
|
+
if item == "[shutdown server]":
|
|
196
|
+
await self.close()
|
|
197
|
+
else:
|
|
198
|
+
return self.encoder.dumps(item)
|
|
199
|
+
|
|
200
|
+
async def connect_callback(
|
|
201
|
+
self,
|
|
202
|
+
reader: asyncio.StreamReader,
|
|
203
|
+
writer: asyncio.StreamWriter,
|
|
204
|
+
) -> None:
|
|
205
|
+
head_size = self.encoder.HEAD_SIZE
|
|
206
|
+
need_await = asyncio.iscoroutinefunction(self.handler)
|
|
207
|
+
while self.is_serving() and not reader.at_eof():
|
|
208
|
+
head = await reader.read(head_size)
|
|
209
|
+
if len(head) < head_size:
|
|
210
|
+
raise RuntimeError()
|
|
211
|
+
content_length = self.encoder.get_size(head)
|
|
212
|
+
# read the whole package
|
|
213
|
+
head = await reader.read(content_length)
|
|
214
|
+
while len(head) < content_length:
|
|
215
|
+
head = head + await reader.read(content_length - len(head))
|
|
216
|
+
item = self.encoder._loads(head)
|
|
217
|
+
result = self.handler(self, item)
|
|
218
|
+
if need_await:
|
|
219
|
+
result = await result
|
|
220
|
+
if isinstance(result, bytes):
|
|
221
|
+
writer.write(result)
|
|
222
|
+
await writer.drain()
|
|
223
|
+
writer.close()
|
|
224
|
+
await writer.wait_closed()
|
|
225
|
+
|
|
226
|
+
async def close(self):
|
|
227
|
+
if self.is_serving():
|
|
228
|
+
self.server.close()
|
|
229
|
+
await self.server.wait_closed()
|
|
230
|
+
self._shutdown_ev.set()
|
|
231
|
+
|
|
232
|
+
async def start(self):
|
|
233
|
+
self._shutdown_ev = asyncio.Event()
|
|
234
|
+
if self.port:
|
|
235
|
+
self.server = await asyncio.start_server(
|
|
236
|
+
self.connect_callback,
|
|
237
|
+
host=self.host,
|
|
238
|
+
port=self.port,
|
|
239
|
+
**self.connect_kwargs,
|
|
240
|
+
)
|
|
241
|
+
await self.server.start_serving()
|
|
242
|
+
else:
|
|
243
|
+
self.server = await asyncio.start_unix_server(
|
|
244
|
+
self.connect_callback, path=self.host, **self.connect_kwargs
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def is_serving(self):
|
|
248
|
+
return self.server and self.server.is_serving()
|
|
249
|
+
|
|
250
|
+
async def wait_closed(self):
|
|
251
|
+
if self._shutdown_ev:
|
|
252
|
+
await self._shutdown_ev.wait()
|
|
253
|
+
|
|
254
|
+
async def __aenter__(self):
|
|
255
|
+
await self.start()
|
|
256
|
+
return self
|
|
257
|
+
|
|
258
|
+
async def __aexit__(self, *_errors):
|
|
259
|
+
await self.close()
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class SocketClient:
|
|
263
|
+
def __init__(
|
|
264
|
+
self,
|
|
265
|
+
host: str = "127.0.0.1",
|
|
266
|
+
port: int = 8090,
|
|
267
|
+
encoder: Optional[IPCEncoder] = None,
|
|
268
|
+
connect_kwargs: Optional[dict] = None,
|
|
269
|
+
):
|
|
270
|
+
self.host = host
|
|
271
|
+
self.port = port
|
|
272
|
+
self.encoder: IPCEncoder = encoder or PickleEncoder()
|
|
273
|
+
self.connect_kwargs = connect_kwargs or {}
|
|
274
|
+
|
|
275
|
+
async def __aenter__(self):
|
|
276
|
+
if self.port:
|
|
277
|
+
self.reader, self.writer = await asyncio.open_connection(
|
|
278
|
+
self.host, self.port, **self.connect_kwargs
|
|
279
|
+
)
|
|
280
|
+
else:
|
|
281
|
+
self.reader, self.writer = await asyncio.open_unix_connection(
|
|
282
|
+
path=self.host, **self.connect_kwargs
|
|
283
|
+
)
|
|
284
|
+
self.lock = asyncio.Lock()
|
|
285
|
+
return self
|
|
286
|
+
|
|
287
|
+
async def __aexit__(self, *_):
|
|
288
|
+
if self.writer:
|
|
289
|
+
self.writer.close()
|
|
290
|
+
await self.writer.wait_closed()
|
|
291
|
+
|
|
292
|
+
async def send(self, item: Any):
|
|
293
|
+
assert self.writer, "use `async with`"
|
|
294
|
+
async with self.lock:
|
|
295
|
+
self.writer.write(self.encoder.dumps(item))
|
|
296
|
+
await self.writer.drain()
|
|
297
|
+
|
|
298
|
+
async def recv(self) -> Any:
|
|
299
|
+
reader = self.reader
|
|
300
|
+
assert reader, "use `async with`"
|
|
301
|
+
async with self.lock:
|
|
302
|
+
assert not reader.at_eof()
|
|
303
|
+
head = await reader.read(self.encoder.HEAD_SIZE)
|
|
304
|
+
if len(head) < self.encoder.HEAD_SIZE:
|
|
305
|
+
raise RuntimeError()
|
|
306
|
+
content_length = self.encoder.get_size(head)
|
|
307
|
+
head = await reader.read(content_length)
|
|
308
|
+
while len(head) < content_length:
|
|
309
|
+
head = head + await reader.read(content_length - len(head))
|
|
310
|
+
return self.encoder.loads(head)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
async def test_client(host="127.0.0.1", port=8090, encoder=None, cases=None):
|
|
314
|
+
async with SocketClient(host=host, port=port, encoder=encoder) as c:
|
|
315
|
+
for case in cases:
|
|
316
|
+
await c.send(case)
|
|
317
|
+
response = await c.recv()
|
|
318
|
+
if globals().get("print_log"):
|
|
319
|
+
print("[Client]", "send:", repr(case), "=>", "recv:", repr(response))
|
|
320
|
+
assert case == response or str(case) == response, [case, response]
|
|
321
|
+
await c.send("[shutdown server]")
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
async def _test_ipc():
|
|
325
|
+
import platform
|
|
326
|
+
|
|
327
|
+
JSONEncoder._DUMP_KWARGS["default"] = str
|
|
328
|
+
for enc, cases in [
|
|
329
|
+
[PickleEncoder, [123, "123", None, {"a"}, ["a"], ("a",), {"a": 1}]],
|
|
330
|
+
[JSONEncoder, [123, "123", None, {"a"}, ["a"], {"a": 1}]],
|
|
331
|
+
]:
|
|
332
|
+
encoder = enc()
|
|
333
|
+
if platform.system() == "Linux":
|
|
334
|
+
# test unix domain socket
|
|
335
|
+
print("Test Linux Unix Domain Socket")
|
|
336
|
+
host = "/tmp/uds.sock"
|
|
337
|
+
port = None
|
|
338
|
+
async with SocketServer(host=host, port=port, encoder=encoder):
|
|
339
|
+
await test_client(host, port=None, encoder=encoder, cases=cases)
|
|
340
|
+
|
|
341
|
+
# test socket
|
|
342
|
+
host = "127.0.0.1"
|
|
343
|
+
port = 8090
|
|
344
|
+
async with SocketServer(host=host, port=port, encoder=encoder):
|
|
345
|
+
await test_client(host="127.0.0.1", port=8090, encoder=encoder, cases=cases)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
async def _test_ipc_logging():
|
|
349
|
+
import logging
|
|
350
|
+
|
|
351
|
+
host = "127.0.0.1"
|
|
352
|
+
port = 8090
|
|
353
|
+
async with SocketServer(host=host, port=port, encoder=SocketLogHandlerEncoder()):
|
|
354
|
+
# socket logger demo
|
|
355
|
+
# ==================
|
|
356
|
+
logger = logging.getLogger("test_logger")
|
|
357
|
+
logger.setLevel(logging.DEBUG)
|
|
358
|
+
h = SocketHandler(host, port)
|
|
359
|
+
h.setLevel(logging.DEBUG)
|
|
360
|
+
logger.addHandler(h)
|
|
361
|
+
logger.info("test socket")
|
|
362
|
+
# ==================
|
|
363
|
+
|
|
364
|
+
# ensure test case
|
|
365
|
+
await asyncio.sleep(0.1)
|
|
366
|
+
assert pickle.loads(h.sock.recv(100000)[4:])["name"] == logger.name
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def test():
|
|
370
|
+
globals().setdefault("print_log", True) # local test show logs
|
|
371
|
+
for function in [_test_ipc, _test_ipc_logging]:
|
|
372
|
+
asyncio.get_event_loop().run_until_complete(function())
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
if __name__ == "__main__":
|
|
376
|
+
test()
|