omlish 0.0.0.dev1__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 (187) hide show
  1. omlish/__about__.py +7 -0
  2. omlish/__init__.py +0 -0
  3. omlish/argparse.py +223 -0
  4. omlish/asyncs/__init__.py +17 -0
  5. omlish/asyncs/anyio.py +23 -0
  6. omlish/asyncs/asyncio.py +19 -0
  7. omlish/asyncs/asyncs.py +76 -0
  8. omlish/asyncs/futures.py +179 -0
  9. omlish/asyncs/trio.py +11 -0
  10. omlish/c3.py +173 -0
  11. omlish/cached.py +9 -0
  12. omlish/check.py +231 -0
  13. omlish/collections/__init__.py +63 -0
  14. omlish/collections/_abc.py +156 -0
  15. omlish/collections/_io_abc.py +78 -0
  16. omlish/collections/cache/__init__.py +11 -0
  17. omlish/collections/cache/descriptor.py +188 -0
  18. omlish/collections/cache/impl.py +485 -0
  19. omlish/collections/cache/types.py +37 -0
  20. omlish/collections/coerce.py +337 -0
  21. omlish/collections/frozen.py +148 -0
  22. omlish/collections/identity.py +106 -0
  23. omlish/collections/indexed.py +75 -0
  24. omlish/collections/mappings.py +127 -0
  25. omlish/collections/ordered.py +81 -0
  26. omlish/collections/persistent.py +36 -0
  27. omlish/collections/skiplist.py +193 -0
  28. omlish/collections/sorted.py +126 -0
  29. omlish/collections/treap.py +228 -0
  30. omlish/collections/treapmap.py +144 -0
  31. omlish/collections/unmodifiable.py +174 -0
  32. omlish/collections/utils.py +110 -0
  33. omlish/configs/__init__.py +0 -0
  34. omlish/configs/flattening.py +147 -0
  35. omlish/configs/props.py +64 -0
  36. omlish/dataclasses/__init__.py +83 -0
  37. omlish/dataclasses/impl/__init__.py +6 -0
  38. omlish/dataclasses/impl/api.py +260 -0
  39. omlish/dataclasses/impl/as_.py +76 -0
  40. omlish/dataclasses/impl/exceptions.py +2 -0
  41. omlish/dataclasses/impl/fields.py +148 -0
  42. omlish/dataclasses/impl/frozen.py +55 -0
  43. omlish/dataclasses/impl/hashing.py +85 -0
  44. omlish/dataclasses/impl/init.py +173 -0
  45. omlish/dataclasses/impl/internals.py +118 -0
  46. omlish/dataclasses/impl/main.py +150 -0
  47. omlish/dataclasses/impl/metaclass.py +126 -0
  48. omlish/dataclasses/impl/metadata.py +74 -0
  49. omlish/dataclasses/impl/order.py +47 -0
  50. omlish/dataclasses/impl/params.py +150 -0
  51. omlish/dataclasses/impl/processing.py +16 -0
  52. omlish/dataclasses/impl/reflect.py +173 -0
  53. omlish/dataclasses/impl/replace.py +40 -0
  54. omlish/dataclasses/impl/repr.py +34 -0
  55. omlish/dataclasses/impl/simple.py +92 -0
  56. omlish/dataclasses/impl/slots.py +80 -0
  57. omlish/dataclasses/impl/utils.py +167 -0
  58. omlish/defs.py +193 -0
  59. omlish/dispatch/__init__.py +3 -0
  60. omlish/dispatch/dispatch.py +137 -0
  61. omlish/dispatch/functions.py +52 -0
  62. omlish/dispatch/methods.py +162 -0
  63. omlish/docker.py +149 -0
  64. omlish/dynamic.py +220 -0
  65. omlish/graphs/__init__.py +0 -0
  66. omlish/graphs/dot/__init__.py +19 -0
  67. omlish/graphs/dot/items.py +162 -0
  68. omlish/graphs/dot/rendering.py +147 -0
  69. omlish/graphs/dot/utils.py +30 -0
  70. omlish/graphs/trees.py +249 -0
  71. omlish/http/__init__.py +0 -0
  72. omlish/http/consts.py +20 -0
  73. omlish/http/wsgi.py +34 -0
  74. omlish/inject/__init__.py +85 -0
  75. omlish/inject/binder.py +12 -0
  76. omlish/inject/bindings.py +49 -0
  77. omlish/inject/eagers.py +21 -0
  78. omlish/inject/elements.py +43 -0
  79. omlish/inject/exceptions.py +49 -0
  80. omlish/inject/impl/__init__.py +0 -0
  81. omlish/inject/impl/bindings.py +19 -0
  82. omlish/inject/impl/elements.py +154 -0
  83. omlish/inject/impl/injector.py +182 -0
  84. omlish/inject/impl/inspect.py +98 -0
  85. omlish/inject/impl/private.py +109 -0
  86. omlish/inject/impl/providers.py +132 -0
  87. omlish/inject/impl/scopes.py +198 -0
  88. omlish/inject/injector.py +40 -0
  89. omlish/inject/inspect.py +14 -0
  90. omlish/inject/keys.py +43 -0
  91. omlish/inject/managed.py +24 -0
  92. omlish/inject/overrides.py +18 -0
  93. omlish/inject/private.py +29 -0
  94. omlish/inject/providers.py +111 -0
  95. omlish/inject/proxy.py +48 -0
  96. omlish/inject/scopes.py +84 -0
  97. omlish/inject/types.py +21 -0
  98. omlish/iterators.py +184 -0
  99. omlish/json.py +194 -0
  100. omlish/lang/__init__.py +112 -0
  101. omlish/lang/cached.py +267 -0
  102. omlish/lang/classes/__init__.py +24 -0
  103. omlish/lang/classes/abstract.py +74 -0
  104. omlish/lang/classes/restrict.py +137 -0
  105. omlish/lang/classes/simple.py +120 -0
  106. omlish/lang/classes/test/__init__.py +0 -0
  107. omlish/lang/classes/test/test_abstract.py +89 -0
  108. omlish/lang/classes/test/test_restrict.py +71 -0
  109. omlish/lang/classes/test/test_simple.py +58 -0
  110. omlish/lang/classes/test/test_virtual.py +72 -0
  111. omlish/lang/classes/virtual.py +130 -0
  112. omlish/lang/clsdct.py +67 -0
  113. omlish/lang/cmp.py +63 -0
  114. omlish/lang/contextmanagers.py +249 -0
  115. omlish/lang/datetimes.py +67 -0
  116. omlish/lang/descriptors.py +52 -0
  117. omlish/lang/functions.py +126 -0
  118. omlish/lang/imports.py +153 -0
  119. omlish/lang/iterables.py +54 -0
  120. omlish/lang/maybes.py +136 -0
  121. omlish/lang/objects.py +103 -0
  122. omlish/lang/resolving.py +50 -0
  123. omlish/lang/strings.py +128 -0
  124. omlish/lang/typing.py +92 -0
  125. omlish/libc.py +532 -0
  126. omlish/logs/__init__.py +9 -0
  127. omlish/logs/_abc.py +247 -0
  128. omlish/logs/configs.py +62 -0
  129. omlish/logs/filters.py +9 -0
  130. omlish/logs/formatters.py +67 -0
  131. omlish/logs/utils.py +20 -0
  132. omlish/marshal/__init__.py +52 -0
  133. omlish/marshal/any.py +25 -0
  134. omlish/marshal/base.py +201 -0
  135. omlish/marshal/base64.py +25 -0
  136. omlish/marshal/dataclasses.py +115 -0
  137. omlish/marshal/datetimes.py +90 -0
  138. omlish/marshal/enums.py +43 -0
  139. omlish/marshal/exceptions.py +7 -0
  140. omlish/marshal/factories.py +129 -0
  141. omlish/marshal/global_.py +33 -0
  142. omlish/marshal/iterables.py +57 -0
  143. omlish/marshal/mappings.py +66 -0
  144. omlish/marshal/naming.py +17 -0
  145. omlish/marshal/objects.py +106 -0
  146. omlish/marshal/optionals.py +49 -0
  147. omlish/marshal/polymorphism.py +147 -0
  148. omlish/marshal/primitives.py +43 -0
  149. omlish/marshal/registries.py +57 -0
  150. omlish/marshal/standard.py +80 -0
  151. omlish/marshal/utils.py +23 -0
  152. omlish/marshal/uuids.py +29 -0
  153. omlish/marshal/values.py +30 -0
  154. omlish/math.py +184 -0
  155. omlish/os.py +32 -0
  156. omlish/reflect.py +359 -0
  157. omlish/replserver/__init__.py +5 -0
  158. omlish/replserver/__main__.py +4 -0
  159. omlish/replserver/console.py +247 -0
  160. omlish/replserver/server.py +146 -0
  161. omlish/runmodule.py +28 -0
  162. omlish/stats.py +342 -0
  163. omlish/term.py +222 -0
  164. omlish/testing/__init__.py +7 -0
  165. omlish/testing/pydevd.py +225 -0
  166. omlish/testing/pytest/__init__.py +8 -0
  167. omlish/testing/pytest/helpers.py +35 -0
  168. omlish/testing/pytest/inject/__init__.py +1 -0
  169. omlish/testing/pytest/inject/harness.py +159 -0
  170. omlish/testing/pytest/plugins/__init__.py +20 -0
  171. omlish/testing/pytest/plugins/_registry.py +6 -0
  172. omlish/testing/pytest/plugins/logging.py +13 -0
  173. omlish/testing/pytest/plugins/pycharm.py +54 -0
  174. omlish/testing/pytest/plugins/repeat.py +19 -0
  175. omlish/testing/pytest/plugins/skips.py +32 -0
  176. omlish/testing/pytest/plugins/spacing.py +19 -0
  177. omlish/testing/pytest/plugins/switches.py +70 -0
  178. omlish/testing/testing.py +102 -0
  179. omlish/text/__init__.py +0 -0
  180. omlish/text/delimit.py +171 -0
  181. omlish/text/indent.py +50 -0
  182. omlish/text/parts.py +265 -0
  183. omlish-0.0.0.dev1.dist-info/LICENSE +21 -0
  184. omlish-0.0.0.dev1.dist-info/METADATA +17 -0
  185. omlish-0.0.0.dev1.dist-info/RECORD +187 -0
  186. omlish-0.0.0.dev1.dist-info/WHEEL +5 -0
  187. omlish-0.0.0.dev1.dist-info/top_level.txt +1 -0
omlish/__about__.py ADDED
@@ -0,0 +1,7 @@
1
+ __name__ = 'omlish'
2
+ __author__ = 'wrmsr'
3
+ __url__ = 'https://github.com/wrmsr/omlish'
4
+ __license__ = 'BSD-3-Clause'
5
+ __requires_python__ = '>=3.11'
6
+
7
+ __version__ = '0.0.0.dev1'
omlish/__init__.py ADDED
File without changes
omlish/argparse.py ADDED
@@ -0,0 +1,223 @@
1
+ """
2
+ TODO:
3
+ - default command
4
+ """
5
+ import argparse
6
+ import dataclasses as dc
7
+ import functools
8
+ import sys
9
+ import typing as ta
10
+
11
+ from . import c3
12
+ from . import check
13
+
14
+
15
+ T = ta.TypeVar('T')
16
+
17
+
18
+ SUPPRESS = argparse.SUPPRESS
19
+
20
+ OPTIONAL = argparse.OPTIONAL
21
+ ZERO_OR_MORE = argparse.ZERO_OR_MORE
22
+ ONE_OR_MORE = argparse.ONE_OR_MORE
23
+ PARSER = argparse.PARSER
24
+ REMAINDER = argparse.REMAINDER
25
+
26
+ HelpFormatter = argparse.HelpFormatter
27
+ RawDescriptionHelpFormatter = argparse.RawDescriptionHelpFormatter
28
+ RawTextHelpFormatter = argparse.RawTextHelpFormatter
29
+ ArgumentDefaultsHelpFormatter = argparse.ArgumentDefaultsHelpFormatter
30
+
31
+ MetavarTypeHelpFormatter = argparse.MetavarTypeHelpFormatter
32
+
33
+ ArgumentError = argparse.ArgumentError
34
+ ArgumentTypeError = argparse.ArgumentTypeError
35
+
36
+ Action = argparse.Action
37
+ BooleanOptionalAction = argparse.BooleanOptionalAction
38
+ SubParsersAction = argparse._SubParsersAction # noqa
39
+
40
+ FileType = argparse.FileType
41
+
42
+ Namespace = argparse.Namespace
43
+
44
+ ArgumentParser = argparse.ArgumentParser
45
+
46
+
47
+ @dc.dataclass(eq=False)
48
+ class Arg:
49
+ args: ta.Sequence[ta.Any]
50
+ kwargs: ta.Mapping[str, ta.Any]
51
+ dest: ta.Optional[str] = None
52
+
53
+ def __get__(self, instance, owner=None):
54
+ if instance is None:
55
+ return self
56
+ return getattr(instance.args, self.dest) # type: ignore
57
+
58
+
59
+ def arg(*args, **kwargs) -> Arg:
60
+ return Arg(args, kwargs)
61
+
62
+
63
+ CommandFn = ta.Callable[[], None]
64
+
65
+
66
+ @dc.dataclass(eq=False)
67
+ class Command:
68
+ name: str
69
+ fn: CommandFn
70
+ args: ta.Sequence[Arg] = ()
71
+ parent: ta.Optional['Command'] = None
72
+
73
+ def __post_init__(self) -> None:
74
+ check.isinstance(self.name, str)
75
+ check.not_in('-', self.name)
76
+ check.not_empty(self.name)
77
+
78
+ check.callable(self.fn)
79
+ check.arg(all(isinstance(a, Arg) for a in self.args))
80
+ check.isinstance(self.parent, (Command, None))
81
+
82
+ functools.update_wrapper(self, self.fn)
83
+
84
+ def __get__(self, instance, owner=None):
85
+ if instance is None:
86
+ return self
87
+ return dc.replace(self, fn=self.fn.__get__(instance, owner))
88
+
89
+ def __call__(self, *args, **kwargs) -> None:
90
+ return self.fn(*args, **kwargs)
91
+
92
+
93
+ def command(
94
+ *args: Arg,
95
+ name: ta.Optional[str] = None,
96
+ parent: ta.Optional[Command] = None,
97
+ ) -> ta.Any: # ta.Callable[[CommandFn], Command]: # FIXME
98
+ for arg in args:
99
+ check.isinstance(arg, Arg)
100
+ check.isinstance(name, (str, None))
101
+ check.isinstance(parent, (Command, None))
102
+
103
+ def inner(fn):
104
+ return Command(
105
+ (name if name is not None else fn.__name__).replace('-', '_'),
106
+ fn,
107
+ args,
108
+ parent=parent,
109
+ )
110
+
111
+ return inner
112
+
113
+
114
+ def get_arg_ann_kwargs(ann: ta.Any) -> ta.Mapping[str, ta.Any]:
115
+ if ann is str:
116
+ return {}
117
+ elif ann is int:
118
+ return {'type': int}
119
+ elif ann is bool:
120
+ return {'action': 'store_true'}
121
+ elif ann is list:
122
+ return {'action': 'append'}
123
+ else:
124
+ raise TypeError(ann)
125
+
126
+
127
+ class _AnnotationBox:
128
+
129
+ def __init__(self, annotations: ta.Mapping[str, ta.Any]) -> None:
130
+ super().__init__()
131
+ self.__annotations__ = annotations # type: ignore
132
+
133
+
134
+ class _CliMeta(type):
135
+
136
+ def __new__(mcls, name: str, bases: ta.Sequence[type], namespace: ta.Mapping[str, ta.Any]) -> type:
137
+ if not bases:
138
+ return super().__new__(mcls, name, tuple(bases), dict(namespace))
139
+
140
+ bases = list(bases)
141
+ namespace = dict(namespace)
142
+
143
+ objs = {}
144
+ mro = c3.merge([list(b.__mro__) for b in bases])
145
+ for bns in [bcls.__dict__ for bcls in reversed(mro)] + [namespace]:
146
+ bseen = set() # type: ignore
147
+ for k, v in bns.items():
148
+ if isinstance(v, (Command, Arg)):
149
+ check.not_in(v, bseen)
150
+ bseen.add(v)
151
+ objs[k] = v
152
+ elif k in objs:
153
+ del [k]
154
+
155
+ anns = ta.get_type_hints(_AnnotationBox({
156
+ **{k: v for bcls in reversed(mro) for k, v in getattr(bcls, '__annotations__', {}).items()},
157
+ **namespace.get('__annotations__', {}),
158
+ }), globalns=namespace.get('__globals__', {}))
159
+
160
+ if 'parser' in namespace:
161
+ parser = check.isinstance(namespace.pop('parser'), ArgumentParser)
162
+ else:
163
+ parser = ArgumentParser()
164
+ namespace['_parser'] = parser
165
+
166
+ subparsers = parser.add_subparsers()
167
+ for name, obj in objs.items():
168
+ if isinstance(obj, Command):
169
+ if obj.parent is not None:
170
+ raise NotImplementedError
171
+ cparser = subparsers.add_parser(obj.name)
172
+ for arg in (obj.args or []):
173
+ cparser.add_argument(*arg.args, **arg.kwargs)
174
+ cparser.set_defaults(_cmd=obj)
175
+
176
+ elif isinstance(obj, Arg):
177
+ if name in anns:
178
+ akwargs = get_arg_ann_kwargs(anns[name])
179
+ obj.kwargs = {**akwargs, **obj.kwargs}
180
+ if not obj.dest:
181
+ if 'dest' in obj.kwargs:
182
+ obj.dest = obj.kwargs['dest']
183
+ else:
184
+ obj.dest = obj.kwargs['dest'] = name # type: ignore
185
+ parser.add_argument(*obj.args, **obj.kwargs)
186
+
187
+ else:
188
+ raise TypeError(obj)
189
+
190
+ return super().__new__(mcls, name, tuple(bases), namespace)
191
+
192
+
193
+ class Cli(metaclass=_CliMeta):
194
+
195
+ def __init__(self, argv: ta.Optional[ta.Sequence[str]] = None) -> None:
196
+ super().__init__()
197
+
198
+ self._argv = argv if argv is not None else sys.argv[1:]
199
+ self._args = self.get_parser().parse_args(self._argv)
200
+
201
+ _parser: ta.ClassVar[ArgumentParser]
202
+
203
+ @classmethod
204
+ def get_parser(cls) -> ArgumentParser:
205
+ return cls._parser
206
+
207
+ @property
208
+ def argv(self) -> ta.Sequence[str]:
209
+ return self._argv
210
+
211
+ @property
212
+ def args(self) -> Namespace:
213
+ return self._args
214
+
215
+ def _run_cmd(self, cmd: Command) -> None:
216
+ cmd.__get__(self, type(self))()
217
+
218
+ def __call__(self) -> None:
219
+ cmd = getattr(self.args, '_cmd', None)
220
+ if cmd is None:
221
+ self.get_parser().print_help()
222
+ return
223
+ self._run_cmd(cmd)
@@ -0,0 +1,17 @@
1
+ from .asyncs import ( # noqa
2
+ SyncableIterable,
3
+ async_list,
4
+ sync_await,
5
+ sync_list,
6
+ syncable_iterable,
7
+ )
8
+
9
+
10
+ from .futures import ( # noqa
11
+ FutureException,
12
+ FutureTimeoutException,
13
+ ImmediateExecutor,
14
+ new_thread_or_immediate_executor,
15
+ wait_dependent_futures,
16
+ wait_futures,
17
+ )
omlish/asyncs/anyio.py ADDED
@@ -0,0 +1,23 @@
1
+ """
2
+ lookit:
3
+ - https://github.com/davidbrochart/sqlite-anyio/blob/a3ba4c6ef0535b14a5a60071fcd6ed565a514963/sqlite_anyio/sqlite.py
4
+ - https://github.com/rafalkrupinski/ratelimit-anyio/blob/2910a8a3d6fa54ed17ee6ba457686c9f7a4c4beb/src/ratelimit_anyio/__init__.py
5
+ - https://github.com/nekitdev/async-extensions/tree/main/async_extensions
6
+ - https://github.com/kinnay/anynet/tree/master/anynet
7
+ - https://github.com/M-o-a-T/asyncscope
8
+ - https://github.com/M-o-a-T/aevent
9
+ - https://github.com/florimondmanca/aiometer
10
+ """ # noqa
11
+ import typing as ta
12
+
13
+ import anyio
14
+
15
+
16
+ T = ta.TypeVar('T')
17
+
18
+
19
+ async def anyio_eof_to_empty(fn: ta.Callable[..., ta.Awaitable[T]], *args: ta.Any, **kwargs: ta.Any) -> T | bytes:
20
+ try:
21
+ return await fn(*args, **kwargs)
22
+ except anyio.EndOfStream:
23
+ return b''
@@ -0,0 +1,19 @@
1
+ import asyncio
2
+ import functools
3
+ import typing as ta
4
+
5
+
6
+ CallableT = ta.TypeVar('CallableT', bound=ta.Callable)
7
+
8
+
9
+ def asyncio_once(fn: CallableT) -> CallableT:
10
+ future = None
11
+
12
+ @functools.wraps(fn)
13
+ async def inner(*args, **kwargs):
14
+ nonlocal future
15
+ if not future:
16
+ future = asyncio.create_task(fn(*args, **kwargs))
17
+ return await future
18
+
19
+ return ta.cast(CallableT, inner)
@@ -0,0 +1,76 @@
1
+ """
2
+ TODO:
3
+ - async<->sync greeenlet bridge
4
+ In [5]: %timeit greenlet.greenlet(f).switch()
5
+ 517 ns ± 13.2 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
6
+ - injected io provider - sync vs greenlet aio trampolined
7
+ - push/pull bridge?
8
+
9
+ https://github.com/sqlalchemy/sqlalchemy/blob/1e75c189da721395bc8c2d899c722a5b9a170404/lib/sqlalchemy/util/_concurrency_py3k.py#L83
10
+ """
11
+ import contextlib
12
+ import functools
13
+ import typing as ta
14
+
15
+
16
+ T = ta.TypeVar('T')
17
+
18
+
19
+ def sync_await(fn: ta.Callable[..., T], *args, **kwargs) -> T:
20
+ ret: ta.Any
21
+ ret = missing = object()
22
+
23
+ async def gate():
24
+ nonlocal ret
25
+ ret = await fn(*args, **kwargs) # type: ignore
26
+
27
+ cr = gate()
28
+ with contextlib.closing(cr):
29
+ try:
30
+ cr.send(None)
31
+ except StopIteration:
32
+ pass
33
+ if ret is missing or cr.cr_await is not None or cr.cr_running:
34
+ raise TypeError('Not terminated')
35
+
36
+ return ta.cast(T, ret)
37
+
38
+
39
+ def sync_list(fn: ta.Callable[..., ta.AsyncIterator[T]], *args, **kwargs) -> list[T]:
40
+ lst = None
41
+
42
+ async def inner():
43
+ nonlocal lst
44
+ lst = [v async for v in fn(*args, **kwargs)]
45
+
46
+ sync_await(inner)
47
+ if not isinstance(lst, list):
48
+ raise TypeError(lst)
49
+ return lst
50
+
51
+
52
+ async def async_list(fn: ta.Callable[..., ta.AsyncIterator[T]], *args, **kwargs) -> list[T]:
53
+ return [v async for v in fn(*args, **kwargs)]
54
+
55
+
56
+ class SyncableIterable(ta.Generic[T]):
57
+
58
+ def __init__(self, obj) -> None:
59
+ super().__init__()
60
+ self._obj = obj
61
+
62
+ def __iter__(self) -> ta.Iterator[T]:
63
+ async def inner():
64
+ async for i in self._obj:
65
+ yield i
66
+ return iter(sync_list(inner))
67
+
68
+ def __aiter__(self) -> ta.AsyncIterator[T]:
69
+ return self._obj.__aiter__()
70
+
71
+
72
+ def syncable_iterable(fn: ta.Callable[..., ta.AsyncIterator[T]]) -> ta.Callable[..., SyncableIterable[T]]:
73
+ @functools.wraps(fn)
74
+ def inner(*args, **kwargs):
75
+ return SyncableIterable(fn(*args, **kwargs))
76
+ return inner
@@ -0,0 +1,179 @@
1
+ import concurrent.futures as cf
2
+ import contextlib
3
+ import time
4
+ import typing as ta
5
+
6
+
7
+ T = ta.TypeVar('T')
8
+
9
+
10
+ ##
11
+
12
+
13
+ class FutureException(Exception, ta.Generic[T]):
14
+
15
+ def __init__(self, future: cf.Future, target: ta.Optional[T] = None) -> None:
16
+ super().__init__()
17
+
18
+ self._future = future
19
+ self._target = target
20
+
21
+ @property
22
+ def future(self) -> cf.Future:
23
+ return self._future
24
+
25
+ @property
26
+ def target(self) -> ta.Optional[T]:
27
+ return self._target
28
+
29
+ def __repr__(self) -> str:
30
+ return (
31
+ f'{self.__class__.__qualname__}('
32
+ f'exception={self._future.exception()!r}, '
33
+ f'future={self._future!r}, '
34
+ f'target={self._target})'
35
+ )
36
+
37
+ __str__ = __repr__
38
+
39
+
40
+ class FutureTimeoutException(Exception):
41
+ pass
42
+
43
+
44
+ def wait_futures(
45
+ futures: ta.Sequence[cf.Future],
46
+ *,
47
+ timeout_s: ta.Union[int, float] = 60,
48
+ tick_interval_s: ta.Union[int, float] = 0.5,
49
+ tick_fn: ta.Callable[..., bool] = lambda: True,
50
+ raise_exceptions: bool = False,
51
+ cancel_on_exception: bool = False,
52
+ ) -> bool:
53
+ start = time.time()
54
+
55
+ not_done = set(futures)
56
+ while tick_fn():
57
+ done = {f for f in not_done if f.done()}
58
+ if raise_exceptions:
59
+ for fut in done:
60
+ if fut.exception():
61
+ if cancel_on_exception:
62
+ for cancel_fut in not_done:
63
+ cancel_fut.cancel()
64
+ raise FutureException(fut) from fut.exception()
65
+
66
+ not_done -= done
67
+ if not not_done:
68
+ return True
69
+
70
+ if time.time() >= (start + timeout_s):
71
+ raise FutureTimeoutException
72
+ time.sleep(tick_interval_s)
73
+
74
+ return False
75
+
76
+
77
+ def wait_dependent_futures(
78
+ executor: cf.Executor,
79
+ dependency_sets_by_fn: ta.Mapping[ta.Callable, ta.AbstractSet[ta.Callable]],
80
+ *,
81
+ timeout_s: ta.Union[int, float] = 60,
82
+ tick_interval_s: ta.Union[int, float] = 0.5,
83
+ tick_fn: ta.Callable[..., bool] = lambda: True,
84
+ ) -> ta.Mapping[ta.Callable, cf.Future]:
85
+ for fn, deps in dependency_sets_by_fn.items():
86
+ for dep in deps:
87
+ if dep == fn:
88
+ raise ValueError(fn)
89
+ if dep not in dependency_sets_by_fn:
90
+ raise KeyError(dep)
91
+ if fn in dependency_sets_by_fn[dep]:
92
+ raise Exception(f'Cyclic dependencies: {fn} <-> {dep}', fn, dep)
93
+
94
+ dependent_sets_by_fn: ta.Dict[ta.Callable, ta.Set[ta.Callable]] = {fn: set() for fn in dependency_sets_by_fn}
95
+ for fn, deps in dependency_sets_by_fn.items():
96
+ for dep in deps:
97
+ dependent_sets_by_fn[dep].add(fn)
98
+ remaining_dep_sets_by_fn = {
99
+ fn: set(dependencies) for fn, dependencies in dependency_sets_by_fn.items()
100
+ }
101
+ root_fns = {fn for fn, deps in remaining_dep_sets_by_fn.items() if not deps}
102
+ fns_by_fut = {fut: fn for fn in root_fns for fut in [executor.submit(fn)] if fut is not None}
103
+
104
+ def cancel():
105
+ for cancel_fut in fns_by_fut:
106
+ cancel_fut.cancel()
107
+
108
+ start = time.time()
109
+ not_done = set(fns_by_fut.keys())
110
+ while not_done and tick_fn():
111
+ done, not_done = cf.wait(not_done, timeout=tick_interval_s, return_when=cf.FIRST_COMPLETED)
112
+ not_done = set(not_done)
113
+
114
+ for fut in done:
115
+ if fut.exception():
116
+ cancel()
117
+ raise FutureException(fut) from fut.exception()
118
+
119
+ fn = fns_by_fut[fut]
120
+ for dependent_fn in dependent_sets_by_fn.get(fn, set()):
121
+ remaining_deps = remaining_dep_sets_by_fn[dependent_fn]
122
+ remaining_deps.remove(fn)
123
+ if not remaining_deps:
124
+ downstream_fut = executor.submit(dependent_fn)
125
+ if downstream_fut is not None:
126
+ fns_by_fut[downstream_fut] = dependent_fn
127
+ not_done.add(downstream_fut)
128
+
129
+ if time.time() >= (start + timeout_s):
130
+ cancel()
131
+ raise FutureTimeoutException
132
+
133
+ remaining_fns = {fn: deps for fn, deps in remaining_dep_sets_by_fn.items() if deps}
134
+ if remaining_fns:
135
+ raise Exception(f"Unfinished fns: {remaining_fns}", remaining_fns)
136
+
137
+ futs_by_fn = {fn: fut for fut, fn in fns_by_fut.items()}
138
+ return futs_by_fn
139
+
140
+
141
+ ##
142
+
143
+
144
+ class ImmediateExecutor(cf.Executor):
145
+
146
+ def __init__(self, *, immediate_exceptions: bool = False) -> None:
147
+ super().__init__()
148
+ self._immediate_exceptions = immediate_exceptions
149
+
150
+ def submit(self, fn, *args, **kwargs):
151
+ future: ta.Any = cf.Future()
152
+ try:
153
+ result = fn(*args, **kwargs)
154
+ future.set_result(result)
155
+ except Exception as e:
156
+ if self._immediate_exceptions:
157
+ raise
158
+ future.set_exception(e)
159
+ return future
160
+
161
+
162
+ @contextlib.contextmanager
163
+ def new_thread_or_immediate_executor(
164
+ max_workers: int | None = None,
165
+ *,
166
+ immediate_exceptions: bool = False,
167
+ thread_name_prefix: str = '',
168
+ **kwargs: ta.Any,
169
+ ) -> ta.Generator[cf.Executor, None, None]:
170
+ if max_workers == 0:
171
+ yield ImmediateExecutor(
172
+ immediate_exceptions=immediate_exceptions,
173
+ )
174
+ else:
175
+ with cf.ThreadPoolExecutor(
176
+ thread_name_prefix=thread_name_prefix,
177
+ **kwargs,
178
+ ) as exe:
179
+ yield exe
omlish/asyncs/trio.py ADDED
@@ -0,0 +1,11 @@
1
+ """
2
+ lookit:
3
+ - https://github.com/oremanj/trio-monitor
4
+ - https://github.com/python-trio/trio-monitor/tree/master
5
+ - https://github.com/python-trio/triopg
6
+ - https://github.com/python-trio/trio-mysql
7
+ - https://github.com/goodboy/tractor
8
+ - https://github.com/groove-x/trio-util
9
+ - https://github.com/oremanj/tricycle/tree/master
10
+ - https://github.com/linkdd/triotp
11
+ """