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.
Files changed (44) hide show
  1. omlish/__about__.py +8 -5
  2. omlish/bootstrap/diag.py +25 -0
  3. omlish/bootstrap/harness.py +3 -2
  4. omlish/check.py +1 -1
  5. omlish/collections/__init__.py +1 -0
  6. omlish/collections/utils.py +7 -2
  7. omlish/dataclasses/__init__.py +4 -4
  8. omlish/dataclasses/impl/api.py +5 -3
  9. omlish/dataclasses/impl/exceptions.py +2 -2
  10. omlish/dataclasses/impl/fields.py +7 -4
  11. omlish/dataclasses/impl/init.py +8 -8
  12. omlish/dataclasses/impl/metadata.py +3 -3
  13. omlish/dataclasses/impl/params.py +4 -3
  14. omlish/diag/replserver/server.py +17 -2
  15. omlish/docker.py +74 -6
  16. omlish/formats/yaml.py +10 -0
  17. omlish/graphs/dot/__init__.py +31 -19
  18. omlish/graphs/dot/make.py +16 -0
  19. omlish/graphs/dot/rendering.py +9 -8
  20. omlish/http/cookies.py +2 -1
  21. omlish/inject/impl/scopes.py +2 -0
  22. omlish/inject/keys.py +1 -1
  23. omlish/inject/multis.py +4 -4
  24. omlish/inject/providers.py +1 -1
  25. omlish/lang/__init__.py +8 -0
  26. omlish/lang/classes/__init__.py +3 -0
  27. omlish/lang/classes/restrict.py +25 -4
  28. omlish/lang/classes/simple.py +0 -4
  29. omlish/lang/classes/virtual.py +6 -4
  30. omlish/lang/datetimes.py +9 -0
  31. omlish/lang/resources.py +29 -10
  32. omlish/lite/check.py +6 -0
  33. omlish/lite/logs.py +30 -25
  34. omlish/lite/secrets.py +3 -1
  35. omlish/logs/configs.py +2 -2
  36. omlish/marshal/dataclasses.py +1 -1
  37. omlish/secrets/secrets.py +1 -1
  38. omlish/stats.py +1 -1
  39. {omlish-0.0.0.dev25.dist-info → omlish-0.0.0.dev27.dist-info}/METADATA +1 -1
  40. {omlish-0.0.0.dev25.dist-info → omlish-0.0.0.dev27.dist-info}/RECORD +44 -42
  41. {omlish-0.0.0.dev25.dist-info → omlish-0.0.0.dev27.dist-info}/WHEEL +1 -1
  42. /omlish/{_manifests.json → .manifests.json} +0 -0
  43. {omlish-0.0.0.dev25.dist-info → omlish-0.0.0.dev27.dist-info}/LICENSE +0 -0
  44. {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.dev25'
2
- __revision__ = '123812c0156cbb06af42b6e2ee3b7912d5bf5fc8'
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
- '*.json',
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
@@ -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
- - repl server
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 optional_single(obj: ta.Iterable[T], message: Message = None) -> T | None:
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)
@@ -91,6 +91,7 @@ from .unmodifiable import ( # noqa
91
91
  )
92
92
 
93
93
  from .utils import ( # noqa
94
+ PartitionResult,
94
95
  all_equal,
95
96
  all_not_equal,
96
97
  indexes,
@@ -39,7 +39,12 @@ def toposort(data: ta.Mapping[T, ta.AbstractSet[T]]) -> ta.Iterator[set[T]]:
39
39
  ##
40
40
 
41
41
 
42
- def partition(items: ta.Iterable[T], pred: ta.Callable[[T], bool]) -> tuple[list[T], list[T]]:
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(
@@ -59,8 +59,8 @@ globals()['make_dataclass'] = xmake_dataclass
59
59
 
60
60
 
61
61
  from .impl.exceptions import ( # noqa
62
- CheckError,
63
- FieldCheckError,
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
- Check,
80
- check,
79
+ Validate,
80
+ validate,
81
81
 
82
82
  Init,
83
83
  init,
@@ -30,8 +30,9 @@ def field( # noqa
30
30
  metadata=None,
31
31
  kw_only=MISSING,
32
32
 
33
- coerce: ta.Callable[[ta.Any], ta.Any] | None = None,
34
- check: ta.Callable[[ta.Any], bool] | None = None,
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
- check=check,
46
+ validate=validate,
45
47
  check_type=check_type,
46
48
  override=override,
47
49
  repr_fn=repr_fn,
@@ -1,8 +1,8 @@
1
- class CheckError(Exception):
1
+ class ValidationError(Exception):
2
2
  pass
3
3
 
4
4
 
5
- class FieldCheckError(CheckError):
5
+ class FieldValidationError(ValidationError):
6
6
  def __init__(self, field: str) -> None:
7
7
  super().__init__(field)
8
8
  self.field = field
@@ -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.check is not None:
184
- cn = f'__dataclass_check__{f.name}__'
185
- locals[cn] = fx.check
186
- lines.append(f'if not {cn}({value}): raise __dataclass_FieldCheckError__({f.name})')
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}__'
@@ -3,16 +3,16 @@ import inspect
3
3
  import typing as ta
4
4
 
5
5
  from ... import lang
6
- from .exceptions import CheckError
7
- from .exceptions import FieldCheckError
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
- '__dataclass_CheckError__': CheckError,
104
- '__dataclass_FieldCheckError__': FieldCheckError,
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(Check, [])):
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'__dataclass_check_{i}__'
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 __dataclass_CheckError__')
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 Check(lang.Marker):
57
+ class Validate(lang.Marker):
58
58
  pass
59
59
 
60
60
 
61
- def check(fn: ta.Callable[..., bool] | staticmethod) -> None:
62
- _append_cls_md(Check, fn)
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
- check: ta.Callable[[ta.Any], bool] | None = None
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
@@ -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 = 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
- self._socket.bind(self._config.path)
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()
@@ -1,19 +1,31 @@
1
- from .items import Attrs # noqa
2
- from .items import Cell # noqa
3
- from .items import Edge # noqa
4
- from .items import Graph # noqa
5
- from .items import Id # noqa
6
- from .items import Item # noqa
7
- from .items import Node # noqa
8
- from .items import Raw # noqa
9
- from .items import RawStmt # noqa
10
- from .items import Row # noqa
11
- from .items import Stmt # noqa
12
- from .items import Table # noqa
13
- from .items import Text # noqa
14
- from .rendering import Renderer # noqa
15
- from .rendering import open_dot # noqa
16
- from .rendering import render # noqa
17
- from .utils import Color # noqa
18
- from .utils import escape # noqa
19
- from .utils import gen_rainbow # noqa
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
+ ])
@@ -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=True,
136
+ delete=delete,
136
137
  ) as pdf:
137
138
  pdf.file.write(stdout)
138
139
  pdf.file.flush()
139
140
 
140
- _, _ = subprocess.Popen(
141
- ['open', pdf.name],
142
- ).communicate(
143
- timeout=timeout_s,
144
- )
141
+ _, _ = subprocess.Popen(
142
+ ['open', pdf.name],
143
+ ).communicate(
144
+ timeout=timeout_s,
145
+ )
145
146
 
146
- if sleep_s > 0.:
147
- time.sleep(sleep_s)
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(datetime.datetime.now(tz=datetime.UTC).timestamp() + max_age)
144
+ expires = http_date(lang.utcnow().timestamp() + max_age)
144
145
 
145
146
  if samesite is not None:
146
147
  samesite = samesite.title()
@@ -1,6 +1,8 @@
1
1
  """
2
2
  TODO:
3
3
  - ContextVar ('context')
4
+ - greenlet?
5
+ - dynamic? https://github.com/wrmsr/iceworm/blob/2f6b4d5e9d237ef9665f7d57cfa6ce328efa0757/iceworm/utils/inject.py#L44
4
6
  """
5
7
  import abc
6
8
  import contextlib
omlish/inject/keys.py CHANGED
@@ -18,7 +18,7 @@ class Key(lang.Final, ta.Generic[T]):
18
18
  tag: ta.Any = dc.xfield(
19
19
  default=None,
20
20
  kw_only=True,
21
- check=lambda o: not isinstance(o, Tag),
21
+ validate=lambda o: not isinstance(o, Tag),
22
22
  repr_fn=dc.opt_repr,
23
23
  )
24
24