omlish 0.0.0.dev4__py3-none-any.whl → 0.0.0.dev5__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 (86) hide show
  1. omlish/__about__.py +1 -1
  2. omlish/__init__.py +1 -1
  3. omlish/asyncs/__init__.py +1 -4
  4. omlish/asyncs/anyio.py +66 -0
  5. omlish/asyncs/flavors.py +27 -1
  6. omlish/asyncs/trio_asyncio.py +24 -18
  7. omlish/c3.py +1 -1
  8. omlish/cached.py +1 -2
  9. omlish/collections/__init__.py +4 -1
  10. omlish/collections/cache/impl.py +1 -1
  11. omlish/collections/indexed.py +1 -1
  12. omlish/collections/utils.py +38 -6
  13. omlish/configs/__init__.py +5 -0
  14. omlish/configs/classes.py +53 -0
  15. omlish/configs/dotenv.py +586 -0
  16. omlish/configs/props.py +589 -49
  17. omlish/dataclasses/impl/api.py +1 -1
  18. omlish/dataclasses/impl/as_.py +1 -1
  19. omlish/dataclasses/impl/fields.py +1 -0
  20. omlish/dataclasses/impl/init.py +1 -1
  21. omlish/dataclasses/impl/main.py +1 -0
  22. omlish/dataclasses/impl/metaclass.py +6 -1
  23. omlish/dataclasses/impl/order.py +1 -1
  24. omlish/dataclasses/impl/reflect.py +15 -2
  25. omlish/defs.py +1 -1
  26. omlish/diag/procfs.py +29 -1
  27. omlish/diag/procstats.py +32 -0
  28. omlish/diag/replserver/console.py +3 -3
  29. omlish/diag/replserver/server.py +6 -5
  30. omlish/diag/threads.py +86 -0
  31. omlish/docker.py +19 -0
  32. omlish/fnpairs.py +26 -18
  33. omlish/graphs/dags.py +113 -0
  34. omlish/graphs/domination.py +268 -0
  35. omlish/graphs/trees.py +2 -2
  36. omlish/http/__init__.py +25 -0
  37. omlish/http/asgi.py +131 -0
  38. omlish/http/consts.py +31 -4
  39. omlish/http/cookies.py +194 -0
  40. omlish/http/dates.py +70 -0
  41. omlish/http/encodings.py +6 -0
  42. omlish/http/json.py +273 -0
  43. omlish/http/sessions.py +197 -0
  44. omlish/inject/__init__.py +8 -2
  45. omlish/inject/bindings.py +3 -3
  46. omlish/inject/exceptions.py +3 -3
  47. omlish/inject/impl/elements.py +33 -24
  48. omlish/inject/impl/injector.py +1 -0
  49. omlish/inject/impl/multis.py +74 -0
  50. omlish/inject/impl/providers.py +19 -39
  51. omlish/inject/{proxy.py → impl/proxy.py} +2 -2
  52. omlish/inject/impl/scopes.py +1 -0
  53. omlish/inject/injector.py +1 -0
  54. omlish/inject/keys.py +3 -9
  55. omlish/inject/multis.py +70 -0
  56. omlish/inject/providers.py +23 -23
  57. omlish/inject/scopes.py +7 -3
  58. omlish/inject/types.py +0 -8
  59. omlish/iterators.py +13 -0
  60. omlish/json.py +2 -1
  61. omlish/lang/__init__.py +4 -0
  62. omlish/lang/classes/restrict.py +1 -1
  63. omlish/lang/classes/virtual.py +2 -2
  64. omlish/lang/contextmanagers.py +64 -0
  65. omlish/lang/datetimes.py +6 -5
  66. omlish/lang/functions.py +10 -0
  67. omlish/lang/imports.py +11 -2
  68. omlish/lang/typing.py +1 -0
  69. omlish/logs/utils.py +1 -1
  70. omlish/marshal/datetimes.py +1 -1
  71. omlish/reflect.py +8 -2
  72. omlish/sync.py +70 -0
  73. omlish/term.py +6 -1
  74. omlish/testing/pytest/__init__.py +5 -0
  75. omlish/testing/pytest/helpers.py +0 -24
  76. omlish/testing/pytest/inject/harness.py +1 -1
  77. omlish/testing/pytest/marks.py +48 -0
  78. omlish/testing/pytest/plugins/__init__.py +2 -0
  79. omlish/testing/pytest/plugins/managermarks.py +60 -0
  80. omlish/testing/testing.py +10 -0
  81. omlish/text/delimit.py +4 -0
  82. {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/METADATA +1 -1
  83. {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/RECORD +86 -69
  84. {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/WHEEL +1 -1
  85. {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/LICENSE +0 -0
  86. {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/top_level.txt +0 -0
@@ -24,6 +24,7 @@ from .simple import MatchArgsProcessor
24
24
  from .simple import OverridesProcessor
25
25
  from .slots import add_slots
26
26
 
27
+
27
28
  if ta.TYPE_CHECKING:
28
29
  from . import metaclass
29
30
  else:
@@ -39,7 +39,12 @@ def confer_kwargs(
39
39
  for ck in bmp.confer:
40
40
  if ck in kwargs:
41
41
  continue
42
- if ck in ('frozen', 'generic_init', 'kw_only'):
42
+ if ck in (
43
+ 'frozen',
44
+ 'generic_init',
45
+ 'kw_only',
46
+ 'reorder',
47
+ ):
43
48
  confer_kwarg(out, ck, get_params(base).frozen)
44
49
  elif ck == 'confer':
45
50
  confer_kwarg(out, 'confer', bmp.confer)
@@ -1,7 +1,7 @@
1
1
  import typing as ta
2
2
 
3
- from .utils import Namespace
4
3
  from .processing import Processor
4
+ from .utils import Namespace
5
5
  from .utils import create_fn
6
6
  from .utils import set_new_attribute
7
7
  from .utils import tuple_str
@@ -28,9 +28,22 @@ from .params import get_params_extras
28
28
  from .utils import Namespace
29
29
 
30
30
 
31
+ try:
32
+ import annotationlib # noqa
33
+ except ImportError:
34
+ annotationlib = None
35
+
36
+
31
37
  MISSING = dc.MISSING
32
38
 
33
39
 
40
+ def _get_annotations(obj):
41
+ if annotationlib is not None:
42
+ return annotationlib.get_annotations(obj, format=annotationlib.Format.FORWARDREF) # noqa
43
+ else:
44
+ return inspect.get_annotations(obj)
45
+
46
+
34
47
  class ClassInfo:
35
48
 
36
49
  def __init__(self, cls: type, *, _constructing: bool = False) -> None:
@@ -54,7 +67,7 @@ class ClassInfo:
54
67
 
55
68
  @cached.property
56
69
  def cls_annotations(self) -> ta.Mapping[str, ta.Any]:
57
- return inspect.get_annotations(self._cls)
70
+ return _get_annotations(self._cls)
58
71
 
59
72
  ##
60
73
 
@@ -136,7 +149,7 @@ class ClassInfo:
136
149
 
137
150
  @cached.property
138
151
  def generic_mro_lookup(self) -> ta.Mapping[type, rfl.Type]:
139
- return col.unique_dict((check.not_none(rfl.get_concrete_type(g)), g) for g in self.generic_mro)
152
+ return col.unique_map((check.not_none(rfl.get_concrete_type(g)), g) for g in self.generic_mro)
140
153
 
141
154
  @cached.property
142
155
  def generic_replaced_field_types(self) -> ta.Mapping[str, rfl.Type]:
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/procfs.py CHANGED
@@ -15,6 +15,7 @@ from .. import iterators as it
15
15
  from .. import json
16
16
  from .. import lang
17
17
  from .. import os as oos
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)
@@ -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
+ )
@@ -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
omlish/docker.py CHANGED
@@ -26,12 +26,16 @@ from . import json
26
26
  from . import lang
27
27
  from . import marshal as msh
28
28
 
29
+
29
30
  if ta.TYPE_CHECKING:
30
31
  import yaml
31
32
  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
  '('
omlish/fnpairs.py CHANGED
@@ -18,31 +18,34 @@ import typing as ta
18
18
 
19
19
  from . import lang
20
20
 
21
+
21
22
  if ta.TYPE_CHECKING:
22
23
  import bz2 as _bz2
23
- import cloudpickle as _cloudpickle
24
24
  import gzip as _gzip
25
25
  import json as _json
26
- import lz4.frame as _lz4_frame
27
26
  import lzma as _lzma
28
27
  import pickle as _pickle
29
- import snappy as _snappy
30
28
  import struct as _struct
31
29
  import tomllib as _tomllib
30
+
31
+ import cloudpickle as _cloudpickle
32
+ import lz4.frame as _lz4_frame
33
+ import snappy as _snappy
32
34
  import yaml as _yaml
33
35
  import zstd as _zstd
34
36
 
35
37
  else:
36
38
  _bz2 = lang.proxy_import('bz2')
37
- _cloudpickle = lang.proxy_import('cloudpickle')
38
39
  _gzip = lang.proxy_import('gzip')
39
40
  _json = lang.proxy_import('json')
40
- _lz4_frame = lang.proxy_import('lz4.frame')
41
41
  _lzma = lang.proxy_import('lzma')
42
42
  _pickle = lang.proxy_import('pickle')
43
- _snappy = lang.proxy_import('snappy')
44
43
  _struct = lang.proxy_import('struct')
45
44
  _tomllib = lang.proxy_import('tomllib')
45
+
46
+ _cloudpickle = lang.proxy_import('cloudpickle')
47
+ _lz4_frame = lang.proxy_import('lz4.frame')
48
+ _snappy = lang.proxy_import('snappy')
46
49
  _yaml = lang.proxy_import('yaml')
47
50
  _zstd = lang.proxy_import('zstd')
48
51
 
@@ -177,13 +180,13 @@ UTF8 = text('utf-8')
177
180
 
178
181
 
179
182
  @dc.dataclass(frozen=True)
180
- class Optional(FnPair[ta.Optional[F], ta.Optional[T]]):
183
+ class Optional(FnPair[F | None, T | None]):
181
184
  fp: FnPair[F, T]
182
185
 
183
- def forward(self, f: ta.Optional[F]) -> ta.Optional[T]:
186
+ def forward(self, f: F | None) -> T | None:
184
187
  return None if f is None else self.fp.forward(f)
185
188
 
186
- def backward(self, t: ta.Optional[T]) -> ta.Optional[F]:
189
+ def backward(self, t: T | None) -> F | None:
187
190
  return None if t is None else self.fp.backward(t)
188
191
 
189
192
 
@@ -305,15 +308,20 @@ class Struct(FnPair[tuple, bytes]):
305
308
  ##
306
309
 
307
310
 
308
- class Object(FnPair[ta.Any, T], lang.Abstract): # noqa
311
+ Object: ta.TypeAlias = FnPair[ta.Any, T]
312
+ ObjectStr: ta.TypeAlias = Object[str]
313
+ ObjectBytes: ta.TypeAlias = Object[bytes]
314
+
315
+
316
+ class Object_(FnPair[ta.Any, T], lang.Abstract): # noqa
309
317
  pass
310
318
 
311
319
 
312
- class ObjectStr(Object[str], lang.Abstract): # noqa
320
+ class ObjectStr_(Object_[str], lang.Abstract): # noqa
313
321
  pass
314
322
 
315
323
 
316
- class ObjectBytes(Object[bytes], lang.Abstract): # noqa
324
+ class ObjectBytes_(Object_[bytes], lang.Abstract): # noqa
317
325
  pass
318
326
 
319
327
 
@@ -322,7 +330,7 @@ class ObjectBytes(Object[bytes], lang.Abstract): # noqa
322
330
 
323
331
  @_register_extension('pkl')
324
332
  @dc.dataclass(frozen=True)
325
- class Pickle(ObjectBytes):
333
+ class Pickle(ObjectBytes_):
326
334
  protocol: int | None = None
327
335
 
328
336
  def forward(self, f: ta.Any) -> bytes:
@@ -334,7 +342,7 @@ class Pickle(ObjectBytes):
334
342
 
335
343
  @_register_extension('json')
336
344
  @dc.dataclass(frozen=True)
337
- class Json(ObjectStr):
345
+ class Json(ObjectStr_):
338
346
  indent: int | str | None = dc.field(default=None, kw_only=True)
339
347
  separators: tuple[str, str] | None = dc.field(default=None, kw_only=True)
340
348
 
@@ -360,7 +368,7 @@ class JsonLines(FnPair[ta.Sequence[ta.Any], str]):
360
368
 
361
369
 
362
370
  @_register_extension('toml')
363
- class Toml(ObjectStr):
371
+ class Toml(ObjectStr_):
364
372
  def forward(self, f: ta.Any) -> str:
365
373
  raise NotImplementedError
366
374
 
@@ -373,7 +381,7 @@ class Toml(ObjectStr):
373
381
 
374
382
  @_register_extension('cpkl')
375
383
  @dc.dataclass(frozen=True)
376
- class Cloudpickle(ObjectBytes):
384
+ class Cloudpickle(ObjectBytes_):
377
385
  protocol: int | None = None
378
386
 
379
387
  def forward(self, f: ta.Any) -> bytes:
@@ -384,7 +392,7 @@ class Cloudpickle(ObjectBytes):
384
392
 
385
393
 
386
394
  @_register_extension('yml', 'yaml')
387
- class Yaml(ObjectStr):
395
+ class Yaml(ObjectStr_):
388
396
  def forward(self, f: ta.Any) -> str:
389
397
  return _yaml.dump(f)
390
398
 
@@ -392,7 +400,7 @@ class Yaml(ObjectStr):
392
400
  return _yaml.safe_load(t)
393
401
 
394
402
 
395
- class YamlUnsafe(ObjectStr):
403
+ class YamlUnsafe(ObjectStr_):
396
404
  def forward(self, f: ta.Any) -> str:
397
405
  return _yaml.dump(f)
398
406
 
omlish/graphs/dags.py ADDED
@@ -0,0 +1,113 @@
1
+ """
2
+ TODO:
3
+ - parser?
4
+ - js? viz.js, d3, visjs
5
+ - cycle detection
6
+ - networkx adapter
7
+ - https://docs.python.org/3.9/library/graphlib.html#module-graphlib
8
+ """
9
+ import typing as ta
10
+
11
+ from .. import check
12
+ from .. import lang
13
+
14
+
15
+ K = ta.TypeVar('K')
16
+ V = ta.TypeVar('V')
17
+ T = ta.TypeVar('T')
18
+ U = ta.TypeVar('U')
19
+
20
+
21
+ def traverse_links(data: ta.Mapping[T, ta.Iterable[T]], keys: ta.Iterable[T]) -> set[T]:
22
+ keys = set(keys)
23
+ todo = set(keys)
24
+ seen: set[T] = set()
25
+ while todo:
26
+ key = todo.pop()
27
+ seen.add(key)
28
+ cur = data.get(key, [])
29
+ todo.update(set(cur) - seen)
30
+ return seen - keys
31
+
32
+
33
+ def invert_set_map(src: ta.Mapping[K, ta.Iterable[V]]) -> dict[V, set[K]]:
34
+ dst: dict[V, set[K]] = {}
35
+ for l, rs in src.items():
36
+ for r in rs:
37
+ try:
38
+ s = dst[r]
39
+ except KeyError:
40
+ s = dst[r] = set()
41
+ s.add(l)
42
+ return dst
43
+
44
+
45
+ def invert_symmetric_set_map(src: ta.Mapping[T, ta.Iterable[T]]) -> dict[T, set[T]]:
46
+ dst: dict[T, set[T]] = {l: set() for l in src}
47
+ for l, rs in src.items():
48
+ for r in rs:
49
+ dst[r].add(l)
50
+ return dst
51
+
52
+
53
+ class Dag(ta.Generic[T]):
54
+
55
+ def __init__(self, input_its_by_outputs: ta.Mapping[T, ta.Iterable[T]]) -> None:
56
+ super().__init__()
57
+
58
+ self._input_sets_by_output = {u: set(d) for u, d in input_its_by_outputs.items()}
59
+
60
+ @property
61
+ def input_sets_by_output(self) -> ta.Mapping[T, ta.AbstractSet[T]]:
62
+ return self._input_sets_by_output
63
+
64
+ @lang.cached_property
65
+ def output_sets_by_input(self) -> ta.Mapping[T, ta.AbstractSet[T]]:
66
+ return invert_symmetric_set_map(self._input_sets_by_output)
67
+
68
+ def subdag(self, *args, **kwargs) -> 'Subdag[T]':
69
+ return Subdag(self, *args, **kwargs)
70
+
71
+
72
+ class Subdag(ta.Generic[U]):
73
+
74
+ def __init__(
75
+ self,
76
+ dag: 'Dag[U]',
77
+ targets: ta.Iterable[U],
78
+ *,
79
+ ignored: ta.Iterable[U] | None = None,
80
+ ) -> None:
81
+ super().__init__()
82
+
83
+ self._dag: Dag[U] = check.isinstance(dag, Dag) # type: ignore
84
+ self._targets = set(targets)
85
+ self._ignored = set(ignored or []) - self._targets
86
+
87
+ @property
88
+ def dag(self) -> 'Dag[U]':
89
+ return self._dag
90
+
91
+ @property
92
+ def targets(self) -> ta.AbstractSet[U]:
93
+ return self._targets
94
+
95
+ @property
96
+ def ignored(self) -> ta.AbstractSet[U]:
97
+ return self._ignored
98
+
99
+ @lang.cached_property
100
+ def inputs(self) -> ta.AbstractSet[U]:
101
+ return traverse_links(self.dag.input_sets_by_output, self.targets) - self.ignored
102
+
103
+ @lang.cached_property
104
+ def outputs(self) -> ta.AbstractSet[U]:
105
+ return traverse_links(self.dag.output_sets_by_input, self.targets) - self.ignored
106
+
107
+ @lang.cached_property
108
+ def output_inputs(self) -> ta.AbstractSet[U]:
109
+ return traverse_links(self.dag.input_sets_by_output, self.outputs) - self.ignored
110
+
111
+ @lang.cached_property
112
+ def all(self) -> ta.AbstractSet[U]:
113
+ return self.targets | self.inputs | self.outputs | self.output_inputs