omlish 0.0.0.dev25__py3-none-any.whl → 0.0.0.dev27__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.
- omlish/__about__.py +8 -5
- omlish/bootstrap/diag.py +25 -0
- omlish/bootstrap/harness.py +3 -2
- omlish/check.py +1 -1
- omlish/collections/__init__.py +1 -0
- omlish/collections/utils.py +7 -2
- omlish/dataclasses/__init__.py +4 -4
- omlish/dataclasses/impl/api.py +5 -3
- omlish/dataclasses/impl/exceptions.py +2 -2
- omlish/dataclasses/impl/fields.py +7 -4
- omlish/dataclasses/impl/init.py +8 -8
- omlish/dataclasses/impl/metadata.py +3 -3
- omlish/dataclasses/impl/params.py +4 -3
- omlish/diag/replserver/server.py +17 -2
- omlish/docker.py +74 -6
- omlish/formats/yaml.py +10 -0
- omlish/graphs/dot/__init__.py +31 -19
- omlish/graphs/dot/make.py +16 -0
- omlish/graphs/dot/rendering.py +9 -8
- omlish/http/cookies.py +2 -1
- omlish/inject/impl/scopes.py +2 -0
- omlish/inject/keys.py +1 -1
- omlish/inject/multis.py +4 -4
- omlish/inject/providers.py +1 -1
- omlish/lang/__init__.py +8 -0
- omlish/lang/classes/__init__.py +3 -0
- omlish/lang/classes/restrict.py +25 -4
- omlish/lang/classes/simple.py +0 -4
- omlish/lang/classes/virtual.py +6 -4
- omlish/lang/datetimes.py +9 -0
- omlish/lang/resources.py +29 -10
- omlish/lite/check.py +6 -0
- omlish/lite/logs.py +30 -25
- omlish/lite/secrets.py +3 -1
- omlish/logs/configs.py +2 -2
- omlish/marshal/dataclasses.py +1 -1
- omlish/secrets/secrets.py +1 -1
- omlish/stats.py +1 -1
- {omlish-0.0.0.dev25.dist-info → omlish-0.0.0.dev27.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev25.dist-info → omlish-0.0.0.dev27.dist-info}/RECORD +44 -42
- {omlish-0.0.0.dev25.dist-info → omlish-0.0.0.dev27.dist-info}/WHEEL +1 -1
- /omlish/{_manifests.json → .manifests.json} +0 -0
- {omlish-0.0.0.dev25.dist-info → omlish-0.0.0.dev27.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev25.dist-info → omlish-0.0.0.dev27.dist-info}/top_level.txt +0 -0
omlish/__about__.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
__version__ = '0.0.0.
|
2
|
-
__revision__ = '
|
1
|
+
__version__ = '0.0.0.dev27'
|
2
|
+
__revision__ = 'c62e03dc2f032aa532d2c2b9f0d71ff96158cdec'
|
3
3
|
|
4
4
|
|
5
5
|
#
|
@@ -41,7 +41,9 @@ class Project(ProjectBase):
|
|
41
41
|
|
42
42
|
'compression': [
|
43
43
|
'lz4 ~= 4.0',
|
44
|
+
|
44
45
|
'python-snappy ~= 0.7; python_version < "3.13"',
|
46
|
+
|
45
47
|
'zstd ~= 1.5',
|
46
48
|
],
|
47
49
|
|
@@ -69,6 +71,7 @@ class Project(ProjectBase):
|
|
69
71
|
|
70
72
|
'misc': [
|
71
73
|
'jinja2 ~= 3.1',
|
74
|
+
|
72
75
|
'wrapt ~= 1.14',
|
73
76
|
],
|
74
77
|
|
@@ -81,6 +84,7 @@ class Project(ProjectBase):
|
|
81
84
|
|
82
85
|
'pg8000 ~= 1.31',
|
83
86
|
# 'psycopg2 ~= 2.9',
|
87
|
+
# 'psycopg ~= 3.2',
|
84
88
|
|
85
89
|
'pymysql ~= 1.1',
|
86
90
|
# 'mysql-connector-python ~= 9.0',
|
@@ -122,11 +126,10 @@ class SetuptoolsBase:
|
|
122
126
|
'*': [
|
123
127
|
'*.c',
|
124
128
|
'*.cc',
|
129
|
+
'*.cu',
|
125
130
|
'*.h',
|
126
131
|
|
127
|
-
'
|
128
|
-
|
129
|
-
'*.sql',
|
132
|
+
'.manifests.json',
|
130
133
|
|
131
134
|
'LICENSE',
|
132
135
|
],
|
omlish/bootstrap/diag.py
CHANGED
@@ -3,6 +3,7 @@ import contextlib
|
|
3
3
|
import dataclasses as dc
|
4
4
|
import signal
|
5
5
|
import sys
|
6
|
+
import threading
|
6
7
|
import typing as ta
|
7
8
|
|
8
9
|
from .. import check
|
@@ -17,6 +18,7 @@ if ta.TYPE_CHECKING:
|
|
17
18
|
import pstats
|
18
19
|
|
19
20
|
from ..diag import pycharm as diagpc
|
21
|
+
from ..diag import replserver as diagrs
|
20
22
|
from ..diag import threads as diagt
|
21
23
|
|
22
24
|
else:
|
@@ -24,6 +26,7 @@ else:
|
|
24
26
|
pstats = lang.proxy_import('pstats')
|
25
27
|
|
26
28
|
diagpc = lang.proxy_import('..diag.pycharm', __package__)
|
29
|
+
diagrs = lang.proxy_import('..diag.replserver', __package__)
|
27
30
|
diagt = lang.proxy_import('..diag.threads', __package__)
|
28
31
|
|
29
32
|
|
@@ -175,3 +178,25 @@ class PycharmBootstrap(SimpleBootstrap['PycharmBootstrap.Config']):
|
|
175
178
|
self._config.debug_port,
|
176
179
|
version=self._config.version,
|
177
180
|
)
|
181
|
+
|
182
|
+
|
183
|
+
##
|
184
|
+
|
185
|
+
|
186
|
+
class ReplServerBootstrap(ContextBootstrap['ReplServerBootstrap.Config']):
|
187
|
+
@dc.dataclass(frozen=True)
|
188
|
+
class Config(Bootstrap.Config):
|
189
|
+
path: str | None = None
|
190
|
+
|
191
|
+
@contextlib.contextmanager
|
192
|
+
def enter(self) -> ta.Iterator[None]:
|
193
|
+
if self._config.path is None:
|
194
|
+
return
|
195
|
+
|
196
|
+
with diagrs.ReplServer(diagrs.ReplServer.Config(
|
197
|
+
path=self._config.path,
|
198
|
+
)) as rs:
|
199
|
+
thread = threading.Thread(target=rs.run, name='replserver')
|
200
|
+
thread.start()
|
201
|
+
|
202
|
+
yield
|
omlish/bootstrap/harness.py
CHANGED
@@ -6,6 +6,7 @@ TODO:
|
|
6
6
|
- multiprocess profiling - afterfork, suffix with pid
|
7
7
|
|
8
8
|
TODO diag:
|
9
|
+
- tracemalloc
|
9
10
|
- yappi
|
10
11
|
- stackscope
|
11
12
|
- https://github.com/pythonspeed/filprofiler
|
@@ -13,10 +14,10 @@ TODO diag:
|
|
13
14
|
- https://pypi.org/project/memory-profiler/
|
14
15
|
- https://pypi.org/project/Pympler/
|
15
16
|
|
17
|
+
|
16
18
|
TODO new items:
|
17
|
-
- pydevd connect-back
|
18
19
|
- debugging / pdb
|
19
|
-
|
20
|
+
- https://github.com/inducer/pudb
|
20
21
|
- packaging fixups
|
21
22
|
- daemonize ( https://github.com/thesharp/daemonize/blob/master/daemonize.py )
|
22
23
|
"""
|
omlish/check.py
CHANGED
@@ -332,7 +332,7 @@ def single(obj: ta.Iterable[T], message: Message = None) -> T:
|
|
332
332
|
return value
|
333
333
|
|
334
334
|
|
335
|
-
def
|
335
|
+
def opt_single(obj: ta.Iterable[T], message: Message = None) -> T | None:
|
336
336
|
it = iter(obj)
|
337
337
|
try:
|
338
338
|
value = next(it)
|
omlish/collections/__init__.py
CHANGED
omlish/collections/utils.py
CHANGED
@@ -39,7 +39,12 @@ def toposort(data: ta.Mapping[T, ta.AbstractSet[T]]) -> ta.Iterator[set[T]]:
|
|
39
39
|
##
|
40
40
|
|
41
41
|
|
42
|
-
|
42
|
+
class PartitionResult(ta.NamedTuple, ta.Generic[T]):
|
43
|
+
t: list[T]
|
44
|
+
f: list[T]
|
45
|
+
|
46
|
+
|
47
|
+
def partition(items: ta.Iterable[T], pred: ta.Callable[[T], bool]) -> PartitionResult[T]:
|
43
48
|
t: list[T] = []
|
44
49
|
f: list[T] = []
|
45
50
|
for e in items:
|
@@ -47,7 +52,7 @@ def partition(items: ta.Iterable[T], pred: ta.Callable[[T], bool]) -> tuple[list
|
|
47
52
|
t.append(e)
|
48
53
|
else:
|
49
54
|
f.append(e)
|
50
|
-
return t, f
|
55
|
+
return PartitionResult(t, f)
|
51
56
|
|
52
57
|
|
53
58
|
def unique(
|
omlish/dataclasses/__init__.py
CHANGED
@@ -59,8 +59,8 @@ globals()['make_dataclass'] = xmake_dataclass
|
|
59
59
|
|
60
60
|
|
61
61
|
from .impl.exceptions import ( # noqa
|
62
|
-
|
63
|
-
|
62
|
+
FieldValidationError,
|
63
|
+
ValidationError,
|
64
64
|
)
|
65
65
|
|
66
66
|
from .impl.metaclass import ( # noqa
|
@@ -76,8 +76,8 @@ from .impl.metadata import ( # noqa
|
|
76
76
|
UserMetadata,
|
77
77
|
metadata,
|
78
78
|
|
79
|
-
|
80
|
-
|
79
|
+
Validate,
|
80
|
+
validate,
|
81
81
|
|
82
82
|
Init,
|
83
83
|
init,
|
omlish/dataclasses/impl/api.py
CHANGED
@@ -30,8 +30,9 @@ def field( # noqa
|
|
30
30
|
metadata=None,
|
31
31
|
kw_only=MISSING,
|
32
32
|
|
33
|
-
|
34
|
-
|
33
|
+
derive: ta.Callable[..., ta.Any] | None = None,
|
34
|
+
coerce: bool | ta.Callable[[ta.Any], ta.Any] | None = None,
|
35
|
+
validate: ta.Callable[[ta.Any], bool] | None = None,
|
35
36
|
check_type: bool | None = None,
|
36
37
|
override: bool = False,
|
37
38
|
repr_fn: ta.Callable[[ta.Any], str | None] | None = None,
|
@@ -40,8 +41,9 @@ def field( # noqa
|
|
40
41
|
raise ValueError('cannot specify both default and default_factory')
|
41
42
|
|
42
43
|
fx = FieldExtras(
|
44
|
+
derive=derive,
|
43
45
|
coerce=coerce,
|
44
|
-
|
46
|
+
validate=validate,
|
45
47
|
check_type=check_type,
|
46
48
|
override=override,
|
47
49
|
repr_fn=repr_fn,
|
@@ -175,15 +175,18 @@ def field_init(
|
|
175
175
|
else:
|
176
176
|
pass
|
177
177
|
|
178
|
+
if fx.derive is not None:
|
179
|
+
raise NotImplementedError
|
180
|
+
|
178
181
|
if fx.coerce is not None:
|
179
182
|
cn = f'__dataclass_coerce__{f.name}__'
|
180
183
|
locals[cn] = fx.coerce
|
181
184
|
lines.append(f'{value} = {cn}({value})')
|
182
185
|
|
183
|
-
if fx.
|
184
|
-
cn = f'
|
185
|
-
locals[cn] = fx.
|
186
|
-
lines.append(f'if not {cn}({value}): raise
|
186
|
+
if fx.validate is not None:
|
187
|
+
cn = f'__dataclass_validate__{f.name}__'
|
188
|
+
locals[cn] = fx.validate
|
189
|
+
lines.append(f'if not {cn}({value}): raise __dataclass_FieldValidationError__({f.name})')
|
187
190
|
|
188
191
|
if fx.check_type:
|
189
192
|
cn = f'__dataclass_check_type__{f.name}__'
|
omlish/dataclasses/impl/init.py
CHANGED
@@ -3,16 +3,16 @@ import inspect
|
|
3
3
|
import typing as ta
|
4
4
|
|
5
5
|
from ... import lang
|
6
|
-
from .exceptions import
|
7
|
-
from .exceptions import
|
6
|
+
from .exceptions import FieldValidationError
|
7
|
+
from .exceptions import ValidationError
|
8
8
|
from .fields import field_init
|
9
9
|
from .fields import field_type
|
10
10
|
from .fields import has_default
|
11
11
|
from .internals import HAS_DEFAULT_FACTORY
|
12
12
|
from .internals import POST_INIT_NAME
|
13
13
|
from .internals import FieldType
|
14
|
-
from .metadata import Check
|
15
14
|
from .metadata import Init
|
15
|
+
from .metadata import Validate
|
16
16
|
from .processing import Processor
|
17
17
|
from .reflect import ClassInfo
|
18
18
|
from .utils import Namespace
|
@@ -100,8 +100,8 @@ class InitBuilder:
|
|
100
100
|
'__dataclass_builtins_object__': object,
|
101
101
|
'__dataclass_builtins_isinstance__': isinstance,
|
102
102
|
'__dataclass_builtins_TypeError__': TypeError,
|
103
|
-
'
|
104
|
-
'
|
103
|
+
'__dataclass_ValidationError__': ValidationError,
|
104
|
+
'__dataclass_FieldValidationError__': FieldValidationError,
|
105
105
|
})
|
106
106
|
|
107
107
|
body_lines: list[str] = []
|
@@ -121,14 +121,14 @@ class InitBuilder:
|
|
121
121
|
params_str = ','.join(f.name for f in ifs.all if field_type(f) is FieldType.INIT)
|
122
122
|
body_lines.append(f'{self._self_name}.{POST_INIT_NAME}({params_str})')
|
123
123
|
|
124
|
-
for i, fn in enumerate(self._info.merged_metadata.get(
|
124
|
+
for i, fn in enumerate(self._info.merged_metadata.get(Validate, [])):
|
125
125
|
if isinstance(fn, staticmethod):
|
126
126
|
fn = fn.__func__
|
127
|
-
cn = f'
|
127
|
+
cn = f'__dataclass_validate_{i}__'
|
128
128
|
locals[cn] = fn
|
129
129
|
csig = inspect.signature(fn)
|
130
130
|
cas = ', '.join(p.name for p in csig.parameters.values())
|
131
|
-
body_lines.append(f'if not {cn}({cas}): raise
|
131
|
+
body_lines.append(f'if not {cn}({cas}): raise __dataclass_ValidationError__')
|
132
132
|
|
133
133
|
inits = self._info.merged_metadata.get(Init, [])
|
134
134
|
mro_dct = lang.build_mro_dict(self._info.cls)
|
@@ -54,12 +54,12 @@ def metadata(cls_dct, *args) -> None:
|
|
54
54
|
|
55
55
|
|
56
56
|
@_class_merged
|
57
|
-
class
|
57
|
+
class Validate(lang.Marker):
|
58
58
|
pass
|
59
59
|
|
60
60
|
|
61
|
-
def
|
62
|
-
_append_cls_md(
|
61
|
+
def validate(fn: ta.Callable[..., bool] | staticmethod) -> None:
|
62
|
+
_append_cls_md(Validate, fn)
|
63
63
|
|
64
64
|
|
65
65
|
##
|
@@ -40,10 +40,11 @@ from .metadata import METADATA_ATTR
|
|
40
40
|
##
|
41
41
|
|
42
42
|
|
43
|
-
@dc.dataclass(frozen=True)
|
43
|
+
@dc.dataclass(frozen=True, kw_only=True)
|
44
44
|
class FieldExtras(lang.Final):
|
45
|
+
derive: ta.Callable[..., ta.Any] | None = None
|
45
46
|
coerce: bool | ta.Callable[[ta.Any], ta.Any] | None = None
|
46
|
-
|
47
|
+
validate: ta.Callable[[ta.Any], bool] | None = None
|
47
48
|
check_type: bool | None = None
|
48
49
|
override: bool = False
|
49
50
|
repr_fn: ta.Callable[[ta.Any], str | None] | None = None
|
@@ -79,7 +80,7 @@ def get_params(obj: ta.Any) -> Params:
|
|
79
80
|
##
|
80
81
|
|
81
82
|
|
82
|
-
@dc.dataclass(frozen=True)
|
83
|
+
@dc.dataclass(frozen=True, kw_only=True)
|
83
84
|
class ParamsExtras(lang.Final):
|
84
85
|
reorder: bool = False
|
85
86
|
cache_hash: bool = False
|
omlish/diag/replserver/server.py
CHANGED
@@ -1,8 +1,13 @@
|
|
1
1
|
"""
|
2
|
+
FIXME:
|
3
|
+
- lol shutdown deadlocks
|
4
|
+
- whole thing is just gross is this its own thread or what
|
5
|
+
|
2
6
|
TODO:
|
3
7
|
- !!! ANYIO !!!
|
4
8
|
- optional paramiko ssh-server
|
5
9
|
- optional ipython embed
|
10
|
+
- https://github.com/python/cpython/tree/56470004e58911b146c016fc9fec4461b8f69454/Lib/_pyrepl
|
6
11
|
|
7
12
|
lookit:
|
8
13
|
- https://github.com/vxgmichel/aioconsole/blob/e55f4b0601da3b3a40a88c965526d35ab38b5841/aioconsole/server.py
|
@@ -38,7 +43,7 @@ class ReplServer:
|
|
38
43
|
@dc.dataclass(frozen=True)
|
39
44
|
class Config:
|
40
45
|
path: str
|
41
|
-
file_mode: int | None =
|
46
|
+
file_mode: int | None = 0o660
|
42
47
|
poll_interval: float = 0.5
|
43
48
|
exit_timeout: float = 10.0
|
44
49
|
|
@@ -80,7 +85,17 @@ class ReplServer:
|
|
80
85
|
|
81
86
|
self._socket = sock.socket(sock.AF_UNIX, sock.SOCK_STREAM)
|
82
87
|
self._socket.settimeout(self._config.poll_interval)
|
83
|
-
|
88
|
+
|
89
|
+
if self._config.file_mode is not None:
|
90
|
+
prev_umask = os.umask(~self._config.file_mode)
|
91
|
+
else:
|
92
|
+
prev_umask = None
|
93
|
+
try:
|
94
|
+
self._socket.bind(self._config.path)
|
95
|
+
finally:
|
96
|
+
if prev_umask is not None:
|
97
|
+
os.umask(prev_umask)
|
98
|
+
|
84
99
|
with contextlib.closing(self._socket):
|
85
100
|
self._socket.listen(1)
|
86
101
|
|
omlish/docker.py
CHANGED
@@ -7,12 +7,6 @@ TODO:
|
|
7
7
|
- https://stackoverflow.com/questions/55386202/how-can-i-use-the-docker-registry-api-to-pull-information-about-a-container-get
|
8
8
|
- https://ops.tips/blog/inspecting-docker-image-without-pull/
|
9
9
|
|
10
|
-
repo=library/nginx
|
11
|
-
token=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repo}:pull" | jq -r '.token')
|
12
|
-
curl -H "Authorization: Bearer $token" -s "https://registry-1.docker.io/v2/${repo}/tags/list" | jq
|
13
|
-
api="application/vnd.docker.distribution.manifest.v2+json"
|
14
|
-
apil="application/vnd.docker.distribution.manifest.list.v2+json"
|
15
|
-
curl -H "Accept: ${api}" -H "Accept: ${apil}" -H "Authorization: Bearer $token" -s "https://registry-1.docker.io/v2/${repo}/manifests/latest" | jq .
|
16
10
|
""" # noqa
|
17
11
|
import datetime
|
18
12
|
import os
|
@@ -21,6 +15,7 @@ import shlex
|
|
21
15
|
import subprocess
|
22
16
|
import sys
|
23
17
|
import typing as ta
|
18
|
+
import urllib.request
|
24
19
|
|
25
20
|
from . import check
|
26
21
|
from . import dataclasses as dc
|
@@ -191,3 +186,76 @@ DOCKER_HOST_PLATFORM_KEY = 'DOCKER_HOST_PLATFORM'
|
|
191
186
|
|
192
187
|
def get_docker_host_platform() -> str | None:
|
193
188
|
return os.environ.get(DOCKER_HOST_PLATFORM_KEY)
|
189
|
+
|
190
|
+
|
191
|
+
##
|
192
|
+
|
193
|
+
|
194
|
+
@dc.dataclass(frozen=True)
|
195
|
+
class HubRepoInfo:
|
196
|
+
repo: str
|
197
|
+
tags: ta.Mapping[str, ta.Any]
|
198
|
+
latest_manifests: ta.Mapping[str, ta.Any]
|
199
|
+
|
200
|
+
|
201
|
+
def get_hub_repo_info(
|
202
|
+
repo: str,
|
203
|
+
*,
|
204
|
+
auth_url: str = 'https://auth.docker.io/',
|
205
|
+
api_url: str = 'https://registry-1.docker.io/v2/',
|
206
|
+
) -> HubRepoInfo:
|
207
|
+
"""
|
208
|
+
https://stackoverflow.com/a/39376254
|
209
|
+
|
210
|
+
==
|
211
|
+
|
212
|
+
repo=library/nginx
|
213
|
+
token=$(
|
214
|
+
curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repo}:pull" \
|
215
|
+
| jq -r '.token' \
|
216
|
+
)
|
217
|
+
curl -H "Authorization: Bearer $token" -s "https://registry-1.docker.io/v2/${repo}/tags/list" | jq
|
218
|
+
curl \
|
219
|
+
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
|
220
|
+
-H "Accept: application/vnd.docker.distribution.manifest.list.v2+json" \
|
221
|
+
-H "Authorization: Bearer $token" \
|
222
|
+
-s "https://registry-1.docker.io/v2/${repo}/manifests/latest" \
|
223
|
+
| jq .
|
224
|
+
"""
|
225
|
+
|
226
|
+
auth_url = auth_url.rstrip('/')
|
227
|
+
api_url = api_url.rstrip('/')
|
228
|
+
|
229
|
+
#
|
230
|
+
|
231
|
+
def req_json(url: str, **kwargs: ta.Any) -> ta.Any:
|
232
|
+
with urllib.request.urlopen(urllib.request.Request(url, **kwargs)) as resp: # noqa
|
233
|
+
return json.loads(resp.read().decode('utf-8'))
|
234
|
+
|
235
|
+
#
|
236
|
+
|
237
|
+
token_dct = req_json(f'{auth_url}/token?service=registry.docker.io&scope=repository:{repo}:pull')
|
238
|
+
token = token_dct['token']
|
239
|
+
|
240
|
+
req_hdrs = {'Authorization': f'Bearer {token}'}
|
241
|
+
|
242
|
+
#
|
243
|
+
|
244
|
+
tags_dct = req_json(
|
245
|
+
f'{api_url}/{repo}/tags/list',
|
246
|
+
headers=req_hdrs,
|
247
|
+
)
|
248
|
+
|
249
|
+
latest_mani_dct = req_json(
|
250
|
+
f'{api_url}/{repo}/manifests/latest',
|
251
|
+
headers={
|
252
|
+
**req_hdrs,
|
253
|
+
'Accept': 'application/vnd.docker.distribution.manifest.v2+json',
|
254
|
+
},
|
255
|
+
)
|
256
|
+
|
257
|
+
return HubRepoInfo(
|
258
|
+
repo,
|
259
|
+
tags_dct,
|
260
|
+
latest_mani_dct,
|
261
|
+
)
|
omlish/formats/yaml.py
CHANGED
@@ -5,6 +5,7 @@ TODO:
|
|
5
5
|
- goal: perfect rewrites (comments, whitespace)
|
6
6
|
- or at least comments
|
7
7
|
- rename 'objects'? codecs/serde interplay still unresolved
|
8
|
+
- look ma, a monad
|
8
9
|
"""
|
9
10
|
import datetime
|
10
11
|
import types
|
@@ -26,6 +27,9 @@ else:
|
|
26
27
|
T = ta.TypeVar('T')
|
27
28
|
|
28
29
|
|
30
|
+
##
|
31
|
+
|
32
|
+
|
29
33
|
@dc.dataclass(frozen=True)
|
30
34
|
class NodeWrapped(lang.Final, ta.Generic[T]):
|
31
35
|
value: T
|
@@ -124,6 +128,9 @@ class NodeWrappingConstructorMixin:
|
|
124
128
|
return self.__construct_yaml_pairs(node, super().construct_yaml_pairs) # type: ignore # noqa
|
125
129
|
|
126
130
|
|
131
|
+
##
|
132
|
+
|
133
|
+
|
127
134
|
class _cached_class_property: # noqa
|
128
135
|
def __init__(self, fn):
|
129
136
|
super().__init__()
|
@@ -204,6 +211,9 @@ class WrappedLoaders(lang.Namespace):
|
|
204
211
|
return cls.CUnsafe(*args, **kwargs)
|
205
212
|
|
206
213
|
|
214
|
+
##
|
215
|
+
|
216
|
+
|
207
217
|
def load(stream, Loader): # noqa
|
208
218
|
with lang.disposing(Loader(stream)) as loader:
|
209
219
|
return loader.get_single_data()
|
omlish/graphs/dot/__init__.py
CHANGED
@@ -1,19 +1,31 @@
|
|
1
|
-
from .items import
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
from .
|
18
|
-
|
19
|
-
|
1
|
+
from .items import ( # noqa
|
2
|
+
Attrs,
|
3
|
+
Cell,
|
4
|
+
Edge,
|
5
|
+
Graph,
|
6
|
+
Id,
|
7
|
+
Item,
|
8
|
+
Node,
|
9
|
+
Raw,
|
10
|
+
RawStmt,
|
11
|
+
Row,
|
12
|
+
Stmt,
|
13
|
+
Table,
|
14
|
+
Text,
|
15
|
+
)
|
16
|
+
|
17
|
+
from .make import ( # noqa
|
18
|
+
make_simple,
|
19
|
+
)
|
20
|
+
|
21
|
+
from .rendering import ( # noqa
|
22
|
+
Renderer,
|
23
|
+
open_dot,
|
24
|
+
render,
|
25
|
+
)
|
26
|
+
|
27
|
+
from .utils import ( # noqa
|
28
|
+
Color,
|
29
|
+
escape,
|
30
|
+
gen_rainbow,
|
31
|
+
)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import typing as ta
|
2
|
+
|
3
|
+
from ... import lang
|
4
|
+
from .items import Edge
|
5
|
+
from .items import Graph
|
6
|
+
from .items import Node
|
7
|
+
|
8
|
+
|
9
|
+
T = ta.TypeVar('T')
|
10
|
+
|
11
|
+
|
12
|
+
def make_simple(graph: ta.Mapping[T, ta.Iterable[T]]) -> Graph:
|
13
|
+
return Graph([
|
14
|
+
*[Node(n) for n in {*graph, *lang.flatten(graph.values())}],
|
15
|
+
*[Edge(k, v) for k, vs in graph.items() for v in vs],
|
16
|
+
])
|
omlish/graphs/dot/rendering.py
CHANGED
@@ -120,6 +120,7 @@ def open_dot(
|
|
120
120
|
*,
|
121
121
|
timeout_s: float = 1.,
|
122
122
|
sleep_s: float = 0.,
|
123
|
+
delete: bool = False,
|
123
124
|
) -> None:
|
124
125
|
stdout, _ = subprocess.Popen(
|
125
126
|
['dot', '-Tpdf'],
|
@@ -132,16 +133,16 @@ def open_dot(
|
|
132
133
|
|
133
134
|
with tempfile.NamedTemporaryFile(
|
134
135
|
suffix='.pdf',
|
135
|
-
delete=
|
136
|
+
delete=delete,
|
136
137
|
) as pdf:
|
137
138
|
pdf.file.write(stdout)
|
138
139
|
pdf.file.flush()
|
139
140
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
141
|
+
_, _ = subprocess.Popen(
|
142
|
+
['open', pdf.name],
|
143
|
+
).communicate(
|
144
|
+
timeout=timeout_s,
|
145
|
+
)
|
145
146
|
|
146
|
-
|
147
|
-
|
147
|
+
if sleep_s > 0.:
|
148
|
+
time.sleep(sleep_s)
|
omlish/http/cookies.py
CHANGED
@@ -29,6 +29,7 @@ import typing as ta
|
|
29
29
|
import urllib.parse
|
30
30
|
|
31
31
|
from .. import collections as col
|
32
|
+
from .. import lang
|
32
33
|
from .dates import http_date
|
33
34
|
|
34
35
|
|
@@ -140,7 +141,7 @@ def dump_cookie(
|
|
140
141
|
if not isinstance(expires, str):
|
141
142
|
expires = http_date(expires)
|
142
143
|
elif max_age is not None and sync_expires:
|
143
|
-
expires = http_date(
|
144
|
+
expires = http_date(lang.utcnow().timestamp() + max_age)
|
144
145
|
|
145
146
|
if samesite is not None:
|
146
147
|
samesite = samesite.title()
|
omlish/inject/impl/scopes.py
CHANGED