omlish 0.0.0.dev4__py3-none-any.whl → 0.0.0.dev6__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.

Potentially problematic release.


This version of omlish might be problematic. Click here for more details.

Files changed (143) hide show
  1. omlish/__about__.py +1 -1
  2. omlish/__init__.py +1 -1
  3. omlish/asyncs/__init__.py +10 -4
  4. omlish/asyncs/anyio.py +142 -12
  5. omlish/asyncs/asyncio.py +23 -0
  6. omlish/asyncs/asyncs.py +9 -6
  7. omlish/asyncs/bridge.py +316 -0
  8. omlish/asyncs/flavors.py +27 -1
  9. omlish/asyncs/trio_asyncio.py +28 -18
  10. omlish/c3.py +1 -1
  11. omlish/cached.py +1 -2
  12. omlish/collections/__init__.py +5 -1
  13. omlish/collections/cache/impl.py +1 -1
  14. omlish/collections/identity.py +7 -0
  15. omlish/collections/indexed.py +1 -1
  16. omlish/collections/utils.py +38 -6
  17. omlish/configs/__init__.py +5 -0
  18. omlish/configs/classes.py +53 -0
  19. omlish/configs/strings.py +94 -0
  20. omlish/dataclasses/__init__.py +9 -0
  21. omlish/dataclasses/impl/api.py +1 -1
  22. omlish/dataclasses/impl/as_.py +1 -1
  23. omlish/dataclasses/impl/copy.py +30 -0
  24. omlish/dataclasses/impl/exceptions.py +6 -0
  25. omlish/dataclasses/impl/fields.py +25 -25
  26. omlish/dataclasses/impl/init.py +5 -3
  27. omlish/dataclasses/impl/main.py +3 -0
  28. omlish/dataclasses/impl/metaclass.py +6 -1
  29. omlish/dataclasses/impl/order.py +1 -1
  30. omlish/dataclasses/impl/reflect.py +15 -2
  31. omlish/dataclasses/utils.py +44 -0
  32. omlish/defs.py +1 -1
  33. omlish/diag/__init__.py +4 -0
  34. omlish/diag/procfs.py +31 -3
  35. omlish/diag/procstats.py +32 -0
  36. omlish/{testing → diag}/pydevd.py +35 -0
  37. omlish/diag/replserver/console.py +3 -3
  38. omlish/diag/replserver/server.py +6 -5
  39. omlish/diag/threads.py +86 -0
  40. omlish/dispatch/_dispatch2.py +65 -0
  41. omlish/dispatch/_dispatch3.py +104 -0
  42. omlish/docker.py +20 -1
  43. omlish/fnpairs.py +37 -18
  44. omlish/graphs/dags.py +113 -0
  45. omlish/graphs/domination.py +268 -0
  46. omlish/graphs/trees.py +2 -2
  47. omlish/http/__init__.py +25 -0
  48. omlish/http/asgi.py +132 -0
  49. omlish/http/collections.py +15 -0
  50. omlish/http/consts.py +47 -5
  51. omlish/http/cookies.py +194 -0
  52. omlish/http/dates.py +70 -0
  53. omlish/http/encodings.py +6 -0
  54. omlish/http/json.py +273 -0
  55. omlish/http/sessions.py +204 -0
  56. omlish/inject/__init__.py +51 -17
  57. omlish/inject/binder.py +185 -5
  58. omlish/inject/bindings.py +3 -36
  59. omlish/inject/eagers.py +2 -8
  60. omlish/inject/elements.py +30 -9
  61. omlish/inject/exceptions.py +3 -3
  62. omlish/inject/impl/elements.py +65 -31
  63. omlish/inject/impl/injector.py +20 -2
  64. omlish/inject/impl/inspect.py +33 -5
  65. omlish/inject/impl/multis.py +74 -0
  66. omlish/inject/impl/origins.py +75 -0
  67. omlish/inject/impl/{private.py → privates.py} +2 -2
  68. omlish/inject/impl/providers.py +19 -39
  69. omlish/inject/{proxy.py → impl/proxy.py} +2 -2
  70. omlish/inject/impl/scopes.py +7 -2
  71. omlish/inject/injector.py +9 -4
  72. omlish/inject/inspect.py +18 -0
  73. omlish/inject/keys.py +11 -23
  74. omlish/inject/listeners.py +26 -0
  75. omlish/inject/managed.py +76 -10
  76. omlish/inject/multis.py +120 -0
  77. omlish/inject/origins.py +27 -0
  78. omlish/inject/overrides.py +5 -4
  79. omlish/inject/{private.py → privates.py} +6 -10
  80. omlish/inject/providers.py +12 -85
  81. omlish/inject/scopes.py +20 -9
  82. omlish/inject/types.py +2 -8
  83. omlish/iterators.py +13 -0
  84. omlish/lang/__init__.py +12 -2
  85. omlish/lang/cached.py +2 -2
  86. omlish/lang/classes/restrict.py +3 -2
  87. omlish/lang/classes/simple.py +18 -8
  88. omlish/lang/classes/virtual.py +2 -2
  89. omlish/lang/contextmanagers.py +75 -2
  90. omlish/lang/datetimes.py +6 -5
  91. omlish/lang/descriptors.py +131 -0
  92. omlish/lang/functions.py +18 -28
  93. omlish/lang/imports.py +11 -2
  94. omlish/lang/iterables.py +20 -1
  95. omlish/lang/typing.py +6 -0
  96. omlish/lifecycles/__init__.py +34 -0
  97. omlish/lifecycles/abstract.py +43 -0
  98. omlish/lifecycles/base.py +51 -0
  99. omlish/lifecycles/contextmanagers.py +74 -0
  100. omlish/lifecycles/controller.py +116 -0
  101. omlish/lifecycles/manager.py +161 -0
  102. omlish/lifecycles/states.py +43 -0
  103. omlish/lifecycles/transitions.py +64 -0
  104. omlish/logs/formatters.py +1 -1
  105. omlish/logs/utils.py +1 -1
  106. omlish/marshal/__init__.py +4 -0
  107. omlish/marshal/datetimes.py +1 -1
  108. omlish/marshal/naming.py +4 -0
  109. omlish/marshal/objects.py +1 -0
  110. omlish/marshal/polymorphism.py +4 -4
  111. omlish/reflect.py +139 -18
  112. omlish/secrets/__init__.py +7 -0
  113. omlish/secrets/marshal.py +41 -0
  114. omlish/secrets/passwords.py +120 -0
  115. omlish/secrets/secrets.py +47 -0
  116. omlish/serde/__init__.py +0 -0
  117. omlish/serde/dotenv.py +574 -0
  118. omlish/{json.py → serde/json.py} +4 -2
  119. omlish/serde/props.py +604 -0
  120. omlish/serde/yaml.py +223 -0
  121. omlish/sql/dbs.py +1 -1
  122. omlish/sql/duckdb.py +136 -0
  123. omlish/sql/sqlean.py +17 -0
  124. omlish/sync.py +70 -0
  125. omlish/term.py +7 -2
  126. omlish/testing/pytest/__init__.py +8 -2
  127. omlish/testing/pytest/helpers.py +0 -24
  128. omlish/testing/pytest/inject/harness.py +4 -4
  129. omlish/testing/pytest/marks.py +45 -0
  130. omlish/testing/pytest/plugins/__init__.py +3 -0
  131. omlish/testing/pytest/plugins/asyncs.py +136 -0
  132. omlish/testing/pytest/plugins/managermarks.py +60 -0
  133. omlish/testing/pytest/plugins/pydevd.py +1 -1
  134. omlish/testing/testing.py +10 -0
  135. omlish/text/delimit.py +4 -0
  136. omlish/text/glyphsplit.py +92 -0
  137. {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev6.dist-info}/METADATA +1 -1
  138. omlish-0.0.0.dev6.dist-info/RECORD +240 -0
  139. {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev6.dist-info}/WHEEL +1 -1
  140. omlish/configs/props.py +0 -64
  141. omlish-0.0.0.dev4.dist-info/RECORD +0 -195
  142. {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev6.dist-info}/LICENSE +0 -0
  143. {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,44 @@
1
+ import collections
2
+ import dataclasses as dc
3
+ import types
4
+ import typing as ta
5
+
6
+ from .. import check
7
+
8
+
9
+ T = ta.TypeVar('T')
10
+
11
+
12
+ def maybe_post_init(sup: ta.Any) -> bool:
13
+ try:
14
+ fn = sup.__post_init__
15
+ except AttributeError:
16
+ return False
17
+ fn()
18
+ return True
19
+
20
+
21
+ def opt_repr(o: ta.Any) -> str | None:
22
+ return repr(o) if o is not None else None
23
+
24
+
25
+ class field_modifier: # noqa
26
+ def __init__(self, fn: ta.Callable[[dc.Field], dc.Field]) -> None:
27
+ super().__init__()
28
+ self.fn = fn
29
+
30
+ def __ror__(self, other: T) -> T:
31
+ return self(other)
32
+
33
+ def __call__(self, f: T) -> T:
34
+ return check.isinstance(self.fn(check.isinstance(f, dc.Field)), dc.Field) # type: ignore
35
+
36
+
37
+ def chain_metadata(*mds: ta.Mapping) -> types.MappingProxyType:
38
+ return types.MappingProxyType(collections.ChainMap(*mds)) # type: ignore # noqa
39
+
40
+
41
+ def update_field_metadata(f: dc.Field, nmd: ta.Mapping) -> dc.Field:
42
+ check.isinstance(f, dc.Field)
43
+ f.metadata = chain_metadata(nmd, f.metadata)
44
+ return f
omlish/defs.py CHANGED
@@ -141,7 +141,7 @@ def hash_eq(cls_dct, *attrs):
141
141
  def __eq__(self, other): # noqa
142
142
  if type(other) is not type(self):
143
143
  return False
144
- for attr in attrs:
144
+ for attr in attrs: # noqa
145
145
  if getattr(self, attr) != getattr(other, attr):
146
146
  return False
147
147
  return True
omlish/diag/__init__.py CHANGED
@@ -0,0 +1,4 @@
1
+ """
2
+ TODO:
3
+ - https://github.com/aristocratos/btop/blob/00c90884c328eb3e44a0ab094e833161092aae9c/src/linux/btop_collect.cpp#L443
4
+ """
omlish/diag/procfs.py CHANGED
@@ -12,9 +12,10 @@ import sys
12
12
  import typing as ta
13
13
 
14
14
  from .. import iterators as it
15
- from .. import json
16
15
  from .. import lang
17
16
  from .. import os as oos
17
+ from ..serde import json
18
+ from .procstats import ProcStats
18
19
 
19
20
 
20
21
  log = logging.getLogger(__name__)
@@ -23,6 +24,9 @@ log = logging.getLogger(__name__)
23
24
  PidLike = int | str
24
25
 
25
26
 
27
+ ##
28
+
29
+
26
30
  RLIMIT_RESOURCES = {
27
31
  getattr(resource, k): k
28
32
  for k in dir(resource)
@@ -38,7 +42,7 @@ def parse_size(s: str) -> int:
38
42
  return int(v) * us[u]
39
43
 
40
44
 
41
- class ProcStat(lang.Namespace):
45
+ class ProcStat(lang.Namespace, lang.Final):
42
46
  PID = 0
43
47
  COMM = 1
44
48
  STATE = 2
@@ -98,6 +102,9 @@ def _check_linux() -> None:
98
102
  raise OSError
99
103
 
100
104
 
105
+ ##
106
+
107
+
101
108
  def get_process_stats(pid: PidLike = 'self') -> list[str]:
102
109
  """http://man7.org/linux/man-pages/man5/proc.5.html -> /proc/[pid]/stat"""
103
110
 
@@ -109,6 +116,18 @@ def get_process_stats(pid: PidLike = 'self') -> list[str]:
109
116
  return [pid.strip(), comm, *r.strip().split(' ')]
110
117
 
111
118
 
119
+ def get_process_procstats(pid: int | None = None) -> ProcStats:
120
+ st = get_process_stats('self' if pid is None else pid)
121
+ return ProcStats(
122
+ pid=int(st[ProcStat.PID]),
123
+
124
+ rss=int(st[ProcStat.RSS]),
125
+ )
126
+
127
+
128
+ ##
129
+
130
+
112
131
  def get_process_chain(pid: PidLike = 'self') -> list[tuple[int, str]]:
113
132
  _check_linux()
114
133
  lst = []
@@ -148,6 +167,9 @@ def set_process_oom_score_adj(score: str, pid: PidLike = 'self') -> None:
148
167
  f.write(str(score))
149
168
 
150
169
 
170
+ ##
171
+
172
+
151
173
  MAP_LINE_RX = re.compile(
152
174
  r'^'
153
175
  r'(?P<address>[A-Fa-f0-9]+)-(?P<end_address>[A-Fa-f0-9]+)\s+'
@@ -156,7 +178,7 @@ MAP_LINE_RX = re.compile(
156
178
  r'(?P<device>\S+)\s+'
157
179
  r'(?P<inode>\d+)\s+'
158
180
  r'(?P<path>.*)'
159
- r'$'
181
+ r'$',
160
182
  )
161
183
 
162
184
 
@@ -198,6 +220,9 @@ def get_process_maps(pid: PidLike = 'self', sharing: bool = False) -> ta.Iterato
198
220
  yield d
199
221
 
200
222
 
223
+ ##
224
+
225
+
201
226
  PAGEMAP_KEYS = (
202
227
  'address',
203
228
  'pfn',
@@ -243,6 +268,9 @@ def get_process_pagemaps(pid: PidLike = 'self') -> ta.Iterable[dict[str, int]]:
243
268
  yield from get_process_range_pagemaps(m['address'], m['end_address'], pid)
244
269
 
245
270
 
271
+ ##
272
+
273
+
246
274
  def _dump_cmd(args: ta.Any) -> None:
247
275
  total = 0
248
276
  dirty_total = 0
@@ -0,0 +1,32 @@
1
+ import dataclasses as dc
2
+ import os
3
+ import typing as ta
4
+
5
+ from .. import lang
6
+
7
+
8
+ if ta.TYPE_CHECKING:
9
+ import psutil as _psutil
10
+ else:
11
+ _psutil = lang.proxy_import('psutil')
12
+
13
+
14
+ @dc.dataclass(frozen=True, kw_only=True)
15
+ class ProcStats:
16
+ pid: int
17
+
18
+ rss: int
19
+
20
+
21
+ def get_psutil_procstats(pid: int | None = None) -> ProcStats:
22
+ if pid is None:
23
+ pid = os.getpid()
24
+
25
+ proc = _psutil.Process(pid)
26
+ mi = proc.memory_info()
27
+
28
+ return ProcStats(
29
+ pid=pid,
30
+
31
+ rss=mi.rss,
32
+ )
@@ -32,6 +32,7 @@ import os
32
32
  import sys
33
33
  import tempfile
34
34
  import textwrap
35
+ import threading
35
36
  import types
36
37
  import typing as ta
37
38
 
@@ -243,3 +244,37 @@ def maybe_reexec(
243
244
  args = [args[0], bootstrap_path]
244
245
 
245
246
  os.execvp(sys.executable, args)
247
+
248
+
249
+ def debug_unhandled_exception(exc_info: ta.Any = None) -> None:
250
+ if exc_info is None:
251
+ exc_info = sys.exc_info()
252
+
253
+ try:
254
+ import pydevd
255
+ from pydevd import pydevd_tracing
256
+
257
+ except ImportError:
258
+ return
259
+
260
+ exctype, value, traceback = exc_info
261
+ frames = []
262
+ while traceback:
263
+ frames.append(traceback.tb_frame)
264
+ traceback = traceback.tb_next
265
+
266
+ thread = threading.current_thread()
267
+ frames_by_id = {id(frame): frame for frame in frames}
268
+ frame = frames[-1]
269
+ exception = (exctype, value, traceback)
270
+
271
+ if hasattr(thread, 'additional_info'):
272
+ thread.additional_info.pydev_message = 'server exception'
273
+ try:
274
+ debugger = pydevd.debugger # noqa
275
+ except AttributeError:
276
+ debugger = pydevd.get_global_debugger() # noqa
277
+
278
+ pydevd_tracing.SetTrace(None)
279
+
280
+ debugger.stop_on_unhandled_exception(thread, frame, frames_by_id, exception)
@@ -74,7 +74,7 @@ class InteractiveSocketConsole:
74
74
  CPRT = 'Type "help", "copyright", "credits" or "license" for more information.'
75
75
 
76
76
  def interact(self, banner: str | None = None, exitmsg: str | None = None) -> None:
77
- log.info(f'Console {id(self)} on thread {threading.current_thread().ident} interacting')
77
+ log.info('Console %x on thread %r interacting', id(self), threading.current_thread().ident)
78
78
 
79
79
  try:
80
80
  ps1 = getattr(sys, 'ps1', '>>> ')
@@ -115,7 +115,7 @@ class InteractiveSocketConsole:
115
115
  pass
116
116
 
117
117
  finally:
118
- log.info(f'Console {id(self)} on thread {threading.current_thread().ident} finished')
118
+ log.info('Console %x on thread %r finished', id(self), threading.current_thread().ident)
119
119
 
120
120
  def push_line(self, line: str) -> bool:
121
121
  self._buffer.append(line)
@@ -208,7 +208,7 @@ class InteractiveSocketConsole:
208
208
  exec(code, self._locals)
209
209
  except SystemExit:
210
210
  raise
211
- except Exception:
211
+ except Exception: # noqa
212
212
  self.show_traceback()
213
213
  else:
214
214
  if self._count == self._write_count:
@@ -84,7 +84,7 @@ class ReplServer:
84
84
  with contextlib.closing(self._socket):
85
85
  self._socket.listen(1)
86
86
 
87
- log.info(f'Repl server listening on file {self._config.path}')
87
+ log.info('Repl server listening on file %s', self._config.path)
88
88
 
89
89
  self._is_running = True
90
90
  try:
@@ -94,7 +94,7 @@ class ReplServer:
94
94
  except TimeoutError:
95
95
  continue
96
96
 
97
- log.info(f'Got repl server connection on file {self._config.path}')
97
+ log.info('Got repl server connection on file %s', self._config.path)
98
98
 
99
99
  def run(conn):
100
100
  with contextlib.closing(conn):
@@ -104,9 +104,10 @@ class ReplServer:
104
104
  variables['__console__'] = console
105
105
 
106
106
  log.info(
107
- f'Starting console {id(console)} repl server connection '
108
- f'on file {self._config.path} '
109
- f'on thread {threading.current_thread().ident}',
107
+ 'Starting console %x repl server connection on file %s on thread %r',
108
+ id(console),
109
+ self._config.path,
110
+ threading.current_thread().ident,
110
111
  )
111
112
  self._consoles_by_threads[threading.current_thread()] = console
112
113
  console.interact()
omlish/diag/threads.py ADDED
@@ -0,0 +1,86 @@
1
+ import io
2
+ import itertools
3
+ import os
4
+ import signal
5
+ import sys
6
+ import threading
7
+ import time
8
+ import traceback
9
+ import typing as ta
10
+
11
+
12
+ _DEBUG_THREAD_COUNTER = itertools.count()
13
+
14
+
15
+ def create_thread_dump_thread(
16
+ *,
17
+ interval_s: float = 5.,
18
+ out: ta.TextIO = sys.stderr,
19
+ start: bool = False,
20
+ nodaemon: bool = False,
21
+ ) -> threading.Thread:
22
+ def dump():
23
+ cthr = threading.current_thread()
24
+ thrs_by_tid = {t.ident: t for t in threading.enumerate()}
25
+
26
+ buf = io.StringIO()
27
+ for tid, fr in sys._current_frames().items(): # noqa
28
+ if tid == cthr.ident:
29
+ continue
30
+
31
+ try:
32
+ thr = thrs_by_tid[tid]
33
+ except KeyError:
34
+ thr_rpr = repr(tid)
35
+ else:
36
+ thr_rpr = repr(thr)
37
+
38
+ tb = traceback.format_stack(fr)
39
+
40
+ buf.write(f'{thr_rpr}\n')
41
+ buf.write('\n'.join(l.strip() for l in tb))
42
+ buf.write('\n\n')
43
+
44
+ out.write(buf.getvalue())
45
+
46
+ def proc():
47
+ while True:
48
+ time.sleep(interval_s)
49
+ try:
50
+ dump()
51
+ except Exception as e: # noqa
52
+ out.write(repr(e) + '\n\n')
53
+
54
+ dthr = threading.Thread(
55
+ target=proc,
56
+ daemon=not nodaemon,
57
+ name=f'thread-dump-thread-{next(_DEBUG_THREAD_COUNTER)}',
58
+ )
59
+ if start:
60
+ dthr.start()
61
+ return dthr
62
+
63
+
64
+ def create_suicide_thread(
65
+ *,
66
+ sig: int = signal.SIGKILL,
67
+ interval_s: float = 1.,
68
+ parent_thread: threading.Thread | None = None,
69
+ start: bool = False,
70
+ ) -> threading.Thread:
71
+ if parent_thread is None:
72
+ parent_thread = threading.current_thread()
73
+
74
+ def proc():
75
+ while True:
76
+ parent_thread.join(interval_s)
77
+ if not parent_thread.is_alive():
78
+ os.kill(os.getpid(), sig)
79
+
80
+ dthr = threading.Thread(
81
+ target=proc,
82
+ name=f'suicide-thread-{next(_DEBUG_THREAD_COUNTER)}',
83
+ )
84
+ if start:
85
+ dthr.start()
86
+ return dthr
@@ -0,0 +1,65 @@
1
+ import abc
2
+ import typing as ta
3
+ import weakref
4
+
5
+ from .dispatch import find_impl
6
+
7
+
8
+ T = ta.TypeVar('T')
9
+
10
+
11
+ ##
12
+
13
+
14
+ class Dispatcher(ta.Generic[T]):
15
+ def __init__(self) -> None:
16
+ super().__init__()
17
+
18
+ self._impls_by_arg_cls: dict[type, T] = {}
19
+ self._dispatch_cache: dict[ta.Any, T | None] = {}
20
+
21
+ def cache_remove(k, self_ref=weakref.ref(self)):
22
+ if (ref_self := self_ref()) is not None:
23
+ cache = ref_self._dispatch_cache # noqa
24
+ try:
25
+ del cache[k]
26
+ except KeyError:
27
+ pass
28
+
29
+ self._cache_remove = cache_remove
30
+
31
+ self._cache_token: ta.Any = None
32
+
33
+ def cache_size(self) -> int:
34
+ return len(self._dispatch_cache)
35
+
36
+ def register(self, impl: T, cls_col: ta.Iterable[type]) -> T:
37
+ for cls in cls_col:
38
+ self._impls_by_arg_cls[cls] = impl
39
+
40
+ if self._cache_token is None and hasattr(cls, '__abstractmethods__'):
41
+ self._cache_token = abc.get_cache_token()
42
+
43
+ self._dispatch_cache.clear()
44
+ return impl
45
+
46
+ def dispatch(self, cls: type) -> T | None:
47
+ if self._cache_token is not None and (current_token := abc.get_cache_token()) != self._cache_token:
48
+ self._dispatch_cache.clear()
49
+ self._cache_token = current_token
50
+
51
+ cls_ref = weakref.ref(cls)
52
+ try:
53
+ return self._dispatch_cache[cls_ref]
54
+ except KeyError:
55
+ pass
56
+ del cls_ref
57
+
58
+ impl: T | None
59
+ try:
60
+ impl = self._impls_by_arg_cls[cls]
61
+ except KeyError:
62
+ impl = find_impl(cls, self._impls_by_arg_cls)
63
+
64
+ self._dispatch_cache[weakref.ref(cls, self._cache_remove)] = impl
65
+ return impl
@@ -0,0 +1,104 @@
1
+ import abc
2
+ import typing as ta
3
+ import weakref
4
+
5
+ from .dispatch import find_impl
6
+
7
+
8
+ T = ta.TypeVar('T')
9
+
10
+
11
+ ##
12
+
13
+
14
+ class DispatchCacheProtocol(ta.Protocol[T]):
15
+ def size(self) -> int: ...
16
+ def prepare(self, cls: type) -> None: ...
17
+ def clear(self) -> None: ...
18
+ def put(self, cls: type, impl: T) -> None: ...
19
+ def get(self, cls: type) -> T: ... # Raises[KeyError]
20
+
21
+
22
+ class DispatcherProtocol(ta.Protocol[T]):
23
+ def cache_size(self) -> int: ...
24
+ def register(self, impl: T, cls_col: ta.Iterable[type]) -> T: ...
25
+ def dispatch(self, cls: type) -> T | None: ...
26
+
27
+
28
+ ##
29
+
30
+
31
+ class DispatchCache(DispatchCacheProtocol[T]):
32
+ def __init__(self) -> None:
33
+ super().__init__()
34
+
35
+ self._dct: dict[ta.Any, T] = {}
36
+
37
+ def remove(k, self_ref=weakref.ref(self)):
38
+ if (ref_self := self_ref()) is not None:
39
+ dct = ref_self._dct # noqa
40
+ try:
41
+ del dct[k]
42
+ except KeyError:
43
+ pass
44
+
45
+ self._remove = remove
46
+
47
+ self._token: ta.Any = None
48
+
49
+ def size(self) -> int:
50
+ return len(self._dct)
51
+
52
+ def prepare(self, cls: type) -> None:
53
+ if self._token is None and hasattr(cls, '__abstractmethods__'):
54
+ self._token = abc.get_cache_token()
55
+
56
+ self.clear()
57
+
58
+ def clear(self) -> None:
59
+ if self._dct:
60
+ self._dct.clear()
61
+
62
+ def put(self, cls: type, impl: T) -> None:
63
+ self._dct[weakref.ref(cls, self._remove)] = impl
64
+
65
+ def get(self, cls: type) -> T:
66
+ if self._token is not None and (current_token := abc.get_cache_token()) != self._token:
67
+ self._dct.clear()
68
+ self._token = current_token
69
+
70
+ cls_ref = weakref.ref(cls)
71
+ return self._dct[cls_ref]
72
+
73
+
74
+ class Dispatcher(DispatcherProtocol[T]):
75
+ def __init__(self) -> None:
76
+ super().__init__()
77
+
78
+ self._impls_by_arg_cls: dict[type, T] = {}
79
+ self._cache: DispatchCache[T | None] = DispatchCache()
80
+
81
+ def cache_size(self) -> int:
82
+ return self._cache.size()
83
+
84
+ def register(self, impl: T, cls_col: ta.Iterable[type]) -> T:
85
+ for cls in cls_col:
86
+ self._impls_by_arg_cls[cls] = impl
87
+ self._cache.prepare(cls)
88
+
89
+ return impl
90
+
91
+ def dispatch(self, cls: type) -> T | None:
92
+ try:
93
+ return self._cache.get(cls)
94
+ except KeyError:
95
+ pass
96
+
97
+ impl: T | None
98
+ try:
99
+ impl = self._impls_by_arg_cls[cls]
100
+ except KeyError:
101
+ impl = find_impl(cls, self._impls_by_arg_cls)
102
+
103
+ self._cache.put(cls, impl)
104
+ return impl
omlish/docker.py CHANGED
@@ -22,9 +22,10 @@ import typing as ta
22
22
 
23
23
  from . import check
24
24
  from . import dataclasses as dc
25
- from . import json
26
25
  from . import lang
27
26
  from . import marshal as msh
27
+ from .serde import json
28
+
28
29
 
29
30
  if ta.TYPE_CHECKING:
30
31
  import yaml
@@ -32,6 +33,9 @@ else:
32
33
  yaml = lang.proxy_import('yaml')
33
34
 
34
35
 
36
+ ##
37
+
38
+
35
39
  @dc.dataclass(frozen=True)
36
40
  class PsItem(lang.Final):
37
41
  dc.metadata(msh.ObjectMetadata(
@@ -113,6 +117,9 @@ def cli_inspect(ids: list[str]) -> list[Inspect]:
113
117
  return msh.unmarshal(json.loads(o.decode()), list[Inspect])
114
118
 
115
119
 
120
+ ##
121
+
122
+
116
123
  class ComposeConfig:
117
124
  def __init__(
118
125
  self,
@@ -141,6 +148,18 @@ class ComposeConfig:
141
148
  return ret
142
149
 
143
150
 
151
+ def get_compose_port(cfg: ta.Mapping[str, ta.Any], default: int) -> int:
152
+ return check.single(
153
+ int(l)
154
+ for p in cfg['ports']
155
+ for l, r in [p.split(':')]
156
+ if int(r) == default
157
+ )
158
+
159
+
160
+ ##
161
+
162
+
144
163
  def timebomb_payload(delay_s: float, name: str = 'omlish-docker-timebomb') -> str:
145
164
  return (
146
165
  '('