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.
- omlish/__about__.py +1 -1
- omlish/__init__.py +1 -1
- omlish/asyncs/__init__.py +1 -4
- omlish/asyncs/anyio.py +66 -0
- omlish/asyncs/flavors.py +27 -1
- omlish/asyncs/trio_asyncio.py +24 -18
- omlish/c3.py +1 -1
- omlish/cached.py +1 -2
- omlish/collections/__init__.py +4 -1
- omlish/collections/cache/impl.py +1 -1
- omlish/collections/indexed.py +1 -1
- omlish/collections/utils.py +38 -6
- omlish/configs/__init__.py +5 -0
- omlish/configs/classes.py +53 -0
- omlish/configs/dotenv.py +586 -0
- omlish/configs/props.py +589 -49
- omlish/dataclasses/impl/api.py +1 -1
- omlish/dataclasses/impl/as_.py +1 -1
- omlish/dataclasses/impl/fields.py +1 -0
- omlish/dataclasses/impl/init.py +1 -1
- omlish/dataclasses/impl/main.py +1 -0
- omlish/dataclasses/impl/metaclass.py +6 -1
- omlish/dataclasses/impl/order.py +1 -1
- omlish/dataclasses/impl/reflect.py +15 -2
- omlish/defs.py +1 -1
- omlish/diag/procfs.py +29 -1
- omlish/diag/procstats.py +32 -0
- omlish/diag/replserver/console.py +3 -3
- omlish/diag/replserver/server.py +6 -5
- omlish/diag/threads.py +86 -0
- omlish/docker.py +19 -0
- omlish/fnpairs.py +26 -18
- omlish/graphs/dags.py +113 -0
- omlish/graphs/domination.py +268 -0
- omlish/graphs/trees.py +2 -2
- omlish/http/__init__.py +25 -0
- omlish/http/asgi.py +131 -0
- omlish/http/consts.py +31 -4
- omlish/http/cookies.py +194 -0
- omlish/http/dates.py +70 -0
- omlish/http/encodings.py +6 -0
- omlish/http/json.py +273 -0
- omlish/http/sessions.py +197 -0
- omlish/inject/__init__.py +8 -2
- omlish/inject/bindings.py +3 -3
- omlish/inject/exceptions.py +3 -3
- omlish/inject/impl/elements.py +33 -24
- omlish/inject/impl/injector.py +1 -0
- omlish/inject/impl/multis.py +74 -0
- omlish/inject/impl/providers.py +19 -39
- omlish/inject/{proxy.py → impl/proxy.py} +2 -2
- omlish/inject/impl/scopes.py +1 -0
- omlish/inject/injector.py +1 -0
- omlish/inject/keys.py +3 -9
- omlish/inject/multis.py +70 -0
- omlish/inject/providers.py +23 -23
- omlish/inject/scopes.py +7 -3
- omlish/inject/types.py +0 -8
- omlish/iterators.py +13 -0
- omlish/json.py +2 -1
- omlish/lang/__init__.py +4 -0
- omlish/lang/classes/restrict.py +1 -1
- omlish/lang/classes/virtual.py +2 -2
- omlish/lang/contextmanagers.py +64 -0
- omlish/lang/datetimes.py +6 -5
- omlish/lang/functions.py +10 -0
- omlish/lang/imports.py +11 -2
- omlish/lang/typing.py +1 -0
- omlish/logs/utils.py +1 -1
- omlish/marshal/datetimes.py +1 -1
- omlish/reflect.py +8 -2
- omlish/sync.py +70 -0
- omlish/term.py +6 -1
- omlish/testing/pytest/__init__.py +5 -0
- omlish/testing/pytest/helpers.py +0 -24
- omlish/testing/pytest/inject/harness.py +1 -1
- omlish/testing/pytest/marks.py +48 -0
- omlish/testing/pytest/plugins/__init__.py +2 -0
- omlish/testing/pytest/plugins/managermarks.py +60 -0
- omlish/testing/testing.py +10 -0
- omlish/text/delimit.py +4 -0
- {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/RECORD +86 -69
- {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/WHEEL +1 -1
- {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev4.dist-info → omlish-0.0.0.dev5.dist-info}/top_level.txt +0 -0
omlish/dataclasses/impl/main.py
CHANGED
|
@@ -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 (
|
|
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)
|
omlish/dataclasses/impl/order.py
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
omlish/diag/procstats.py
ADDED
|
@@ -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(
|
|
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(
|
|
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:
|
omlish/diag/replserver/server.py
CHANGED
|
@@ -84,7 +84,7 @@ class ReplServer:
|
|
|
84
84
|
with contextlib.closing(self._socket):
|
|
85
85
|
self._socket.listen(1)
|
|
86
86
|
|
|
87
|
-
log.info(
|
|
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(
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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[
|
|
183
|
+
class Optional(FnPair[F | None, T | None]):
|
|
181
184
|
fp: FnPair[F, T]
|
|
182
185
|
|
|
183
|
-
def forward(self, f:
|
|
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:
|
|
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
|
-
|
|
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
|
|
320
|
+
class ObjectStr_(Object_[str], lang.Abstract): # noqa
|
|
313
321
|
pass
|
|
314
322
|
|
|
315
323
|
|
|
316
|
-
class
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|