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.
@@ -0,0 +1,7 @@
1
+ __version__ = "0.0.1"
2
+ __all__ = [
3
+ "morebuiltins.utils",
4
+ "morebuiltins.functools",
5
+ "morebuiltins.ipc",
6
+ "morebuiltins.request",
7
+ ]
@@ -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()