omlish 0.0.0.dev132__py3-none-any.whl → 0.0.0.dev177__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (210) hide show
  1. omlish/.manifests.json +265 -7
  2. omlish/__about__.py +7 -5
  3. omlish/antlr/_runtime/__init__.py +0 -22
  4. omlish/antlr/_runtime/_all.py +24 -0
  5. omlish/antlr/_runtime/atn/ParserATNSimulator.py +1 -1
  6. omlish/antlr/_runtime/dfa/DFASerializer.py +1 -1
  7. omlish/antlr/_runtime/error/DiagnosticErrorListener.py +2 -1
  8. omlish/antlr/_runtime/xpath/XPath.py +7 -1
  9. omlish/antlr/_runtime/xpath/XPathLexer.py +1 -1
  10. omlish/antlr/delimit.py +106 -0
  11. omlish/antlr/dot.py +31 -0
  12. omlish/antlr/errors.py +11 -0
  13. omlish/antlr/input.py +96 -0
  14. omlish/antlr/parsing.py +19 -0
  15. omlish/antlr/runtime.py +102 -0
  16. omlish/antlr/utils.py +38 -0
  17. omlish/argparse/all.py +45 -0
  18. omlish/{argparse.py → argparse/cli.py} +112 -107
  19. omlish/asyncs/__init__.py +0 -35
  20. omlish/asyncs/all.py +35 -0
  21. omlish/asyncs/asyncio/all.py +7 -0
  22. omlish/asyncs/asyncio/channels.py +40 -0
  23. omlish/asyncs/asyncio/streams.py +45 -0
  24. omlish/asyncs/asyncio/subprocesses.py +238 -0
  25. omlish/asyncs/asyncio/timeouts.py +16 -0
  26. omlish/asyncs/bluelet/LICENSE +6 -0
  27. omlish/asyncs/bluelet/all.py +67 -0
  28. omlish/asyncs/bluelet/api.py +23 -0
  29. omlish/asyncs/bluelet/core.py +178 -0
  30. omlish/asyncs/bluelet/events.py +78 -0
  31. omlish/asyncs/bluelet/files.py +80 -0
  32. omlish/asyncs/bluelet/runner.py +416 -0
  33. omlish/asyncs/bluelet/sockets.py +214 -0
  34. omlish/bootstrap/sys.py +3 -3
  35. omlish/cached.py +2 -2
  36. omlish/check.py +49 -460
  37. omlish/codecs/__init__.py +72 -0
  38. omlish/codecs/base.py +106 -0
  39. omlish/codecs/bytes.py +119 -0
  40. omlish/codecs/chain.py +23 -0
  41. omlish/codecs/funcs.py +39 -0
  42. omlish/codecs/registry.py +139 -0
  43. omlish/codecs/standard.py +4 -0
  44. omlish/codecs/text.py +217 -0
  45. omlish/collections/cache/impl.py +50 -57
  46. omlish/collections/coerce.py +1 -0
  47. omlish/collections/mappings.py +1 -1
  48. omlish/configs/flattening.py +1 -1
  49. omlish/defs.py +1 -1
  50. omlish/diag/_pycharm/runhack.py +8 -2
  51. omlish/diag/procfs.py +8 -8
  52. omlish/docker/__init__.py +0 -36
  53. omlish/docker/all.py +31 -0
  54. omlish/docker/consts.py +4 -0
  55. omlish/{lite/docker.py → docker/detect.py} +18 -0
  56. omlish/docker/{helpers.py → timebomb.py} +0 -21
  57. omlish/formats/cbor.py +31 -0
  58. omlish/formats/cloudpickle.py +31 -0
  59. omlish/formats/codecs.py +93 -0
  60. omlish/formats/json/codecs.py +29 -0
  61. omlish/formats/json/delimted.py +4 -0
  62. omlish/formats/json/stream/errors.py +2 -0
  63. omlish/formats/json/stream/lex.py +12 -6
  64. omlish/formats/json/stream/parse.py +38 -22
  65. omlish/formats/json5.py +31 -0
  66. omlish/formats/pickle.py +31 -0
  67. omlish/formats/repr.py +25 -0
  68. omlish/formats/toml.py +17 -0
  69. omlish/formats/yaml.py +25 -0
  70. omlish/funcs/__init__.py +0 -0
  71. omlish/{genmachine.py → funcs/genmachine.py} +5 -4
  72. omlish/{matchfns.py → funcs/match.py} +1 -1
  73. omlish/funcs/pairs.py +215 -0
  74. omlish/http/__init__.py +0 -48
  75. omlish/http/all.py +48 -0
  76. omlish/http/coro/__init__.py +0 -0
  77. omlish/{lite/fdio/corohttp.py → http/coro/fdio.py} +21 -19
  78. omlish/{lite/http/coroserver.py → http/coro/server.py} +20 -21
  79. omlish/{lite/http → http}/handlers.py +3 -2
  80. omlish/{lite/http → http}/parsing.py +1 -0
  81. omlish/http/sessions.py +1 -1
  82. omlish/{lite/http → http}/versions.py +1 -0
  83. omlish/inject/managed.py +2 -2
  84. omlish/io/__init__.py +0 -3
  85. omlish/{lite/io.py → io/buffers.py} +8 -9
  86. omlish/io/compress/__init__.py +9 -0
  87. omlish/io/compress/abc.py +104 -0
  88. omlish/io/compress/adapters.py +148 -0
  89. omlish/io/compress/base.py +24 -0
  90. omlish/io/compress/brotli.py +47 -0
  91. omlish/io/compress/bz2.py +61 -0
  92. omlish/io/compress/codecs.py +78 -0
  93. omlish/io/compress/gzip.py +350 -0
  94. omlish/io/compress/lz4.py +91 -0
  95. omlish/io/compress/lzma.py +81 -0
  96. omlish/io/compress/snappy.py +34 -0
  97. omlish/io/compress/zlib.py +74 -0
  98. omlish/io/compress/zstd.py +44 -0
  99. omlish/io/fdio/__init__.py +1 -0
  100. omlish/{lite → io}/fdio/handlers.py +5 -5
  101. omlish/{lite → io}/fdio/kqueue.py +8 -8
  102. omlish/{lite → io}/fdio/manager.py +7 -7
  103. omlish/{lite → io}/fdio/pollers.py +13 -13
  104. omlish/io/generators/__init__.py +56 -0
  105. omlish/io/generators/consts.py +1 -0
  106. omlish/io/generators/direct.py +13 -0
  107. omlish/io/generators/readers.py +189 -0
  108. omlish/io/generators/stepped.py +191 -0
  109. omlish/io/pyio.py +5 -2
  110. omlish/iterators/__init__.py +24 -0
  111. omlish/iterators/iterators.py +132 -0
  112. omlish/iterators/recipes.py +18 -0
  113. omlish/iterators/tools.py +96 -0
  114. omlish/iterators/unique.py +67 -0
  115. omlish/lang/__init__.py +13 -1
  116. omlish/lang/functions.py +11 -2
  117. omlish/lang/generators.py +243 -0
  118. omlish/lang/iterables.py +46 -49
  119. omlish/lang/maybes.py +4 -4
  120. omlish/lite/cached.py +39 -6
  121. omlish/lite/check.py +438 -75
  122. omlish/lite/contextmanagers.py +17 -4
  123. omlish/lite/dataclasses.py +42 -0
  124. omlish/lite/inject.py +28 -45
  125. omlish/lite/logs.py +0 -270
  126. omlish/lite/marshal.py +309 -144
  127. omlish/lite/pycharm.py +47 -0
  128. omlish/lite/reflect.py +33 -0
  129. omlish/lite/resources.py +8 -0
  130. omlish/lite/runtime.py +4 -4
  131. omlish/lite/shlex.py +12 -0
  132. omlish/lite/socketserver.py +2 -2
  133. omlish/lite/strings.py +31 -0
  134. omlish/logs/__init__.py +0 -32
  135. omlish/logs/{_abc.py → abc.py} +0 -1
  136. omlish/logs/all.py +37 -0
  137. omlish/logs/{formatters.py → color.py} +1 -2
  138. omlish/logs/configs.py +7 -38
  139. omlish/logs/filters.py +10 -0
  140. omlish/logs/handlers.py +4 -1
  141. omlish/logs/json.py +56 -0
  142. omlish/logs/proxy.py +99 -0
  143. omlish/logs/standard.py +128 -0
  144. omlish/logs/utils.py +2 -2
  145. omlish/manifests/__init__.py +2 -0
  146. omlish/manifests/load.py +209 -0
  147. omlish/manifests/types.py +17 -0
  148. omlish/marshal/base.py +1 -1
  149. omlish/marshal/factories.py +1 -1
  150. omlish/marshal/forbidden.py +1 -1
  151. omlish/marshal/iterables.py +1 -1
  152. omlish/marshal/literals.py +50 -0
  153. omlish/marshal/mappings.py +1 -1
  154. omlish/marshal/maybes.py +1 -1
  155. omlish/marshal/standard.py +5 -1
  156. omlish/marshal/unions.py +1 -1
  157. omlish/os/__init__.py +0 -0
  158. omlish/os/atomics.py +205 -0
  159. omlish/os/deathsig.py +23 -0
  160. omlish/{os.py → os/files.py} +0 -9
  161. omlish/{lite → os}/journald.py +2 -1
  162. omlish/os/linux.py +484 -0
  163. omlish/os/paths.py +36 -0
  164. omlish/{lite → os}/pidfile.py +1 -0
  165. omlish/os/sizes.py +9 -0
  166. omlish/reflect/__init__.py +3 -0
  167. omlish/reflect/subst.py +2 -1
  168. omlish/reflect/types.py +126 -44
  169. omlish/secrets/pwhash.py +1 -1
  170. omlish/secrets/subprocesses.py +3 -1
  171. omlish/specs/jsonrpc/marshal.py +1 -1
  172. omlish/specs/openapi/marshal.py +1 -1
  173. omlish/sql/alchemy/asyncs.py +1 -1
  174. omlish/sql/queries/__init__.py +9 -1
  175. omlish/sql/queries/building.py +3 -0
  176. omlish/sql/queries/exprs.py +10 -27
  177. omlish/sql/queries/idents.py +48 -10
  178. omlish/sql/queries/names.py +80 -13
  179. omlish/sql/queries/params.py +64 -0
  180. omlish/sql/queries/rendering.py +1 -1
  181. omlish/subprocesses.py +340 -0
  182. omlish/term.py +29 -14
  183. omlish/testing/pytest/marks.py +2 -2
  184. omlish/testing/pytest/plugins/asyncs.py +6 -1
  185. omlish/testing/pytest/plugins/logging.py +1 -1
  186. omlish/testing/pytest/plugins/switches.py +1 -1
  187. {omlish-0.0.0.dev132.dist-info → omlish-0.0.0.dev177.dist-info}/METADATA +13 -11
  188. {omlish-0.0.0.dev132.dist-info → omlish-0.0.0.dev177.dist-info}/RECORD +200 -117
  189. omlish/fnpairs.py +0 -496
  190. omlish/formats/json/cli/__main__.py +0 -11
  191. omlish/formats/json/cli/cli.py +0 -298
  192. omlish/formats/json/cli/formats.py +0 -71
  193. omlish/formats/json/cli/io.py +0 -74
  194. omlish/formats/json/cli/parsing.py +0 -82
  195. omlish/formats/json/cli/processing.py +0 -48
  196. omlish/formats/json/cli/rendering.py +0 -92
  197. omlish/iterators.py +0 -300
  198. omlish/lite/subprocesses.py +0 -130
  199. /omlish/{formats/json/cli → argparse}/__init__.py +0 -0
  200. /omlish/{lite/fdio → asyncs/asyncio}/__init__.py +0 -0
  201. /omlish/asyncs/{asyncio.py → asyncio/asyncio.py} +0 -0
  202. /omlish/{lite/http → asyncs/bluelet}/__init__.py +0 -0
  203. /omlish/collections/{_abc.py → abc.py} +0 -0
  204. /omlish/{fnpipes.py → funcs/pipes.py} +0 -0
  205. /omlish/io/{_abc.py → abc.py} +0 -0
  206. /omlish/sql/{_abc.py → abc.py} +0 -0
  207. {omlish-0.0.0.dev132.dist-info → omlish-0.0.0.dev177.dist-info}/LICENSE +0 -0
  208. {omlish-0.0.0.dev132.dist-info → omlish-0.0.0.dev177.dist-info}/WHEEL +0 -0
  209. {omlish-0.0.0.dev132.dist-info → omlish-0.0.0.dev177.dist-info}/entry_points.txt +0 -0
  210. {omlish-0.0.0.dev132.dist-info → omlish-0.0.0.dev177.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,80 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ # Based on bluelet ( https://github.com/sampsyo/bluelet ) by Adrian Sampson, original license:
4
+ # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
5
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
6
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
7
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+ import abc
9
+ import dataclasses as dc
10
+ import typing as ta
11
+
12
+ from .core import DelegationBlueletEvent
13
+ from .core import ReturnBlueletEvent
14
+ from .events import BlueletEvent
15
+ from .events import BlueletWaitables
16
+ from .events import WaitableBlueletEvent
17
+
18
+
19
+ ##
20
+
21
+
22
+ class FileBlueletEvent(BlueletEvent, abc.ABC):
23
+ pass
24
+
25
+
26
+ @dc.dataclass(frozen=True, eq=False)
27
+ class ReadBlueletEvent(WaitableBlueletEvent, FileBlueletEvent):
28
+ """Reads from a file-like object."""
29
+
30
+ fd: ta.IO
31
+ bufsize: int
32
+
33
+ def waitables(self) -> BlueletWaitables:
34
+ return BlueletWaitables(r=[self.fd])
35
+
36
+ def fire(self) -> bytes:
37
+ return self.fd.read(self.bufsize)
38
+
39
+
40
+ @dc.dataclass(frozen=True, eq=False)
41
+ class WriteBlueletEvent(WaitableBlueletEvent, FileBlueletEvent):
42
+ """Writes to a file-like object."""
43
+
44
+ fd: ta.IO
45
+ data: bytes
46
+
47
+ def waitables(self) -> BlueletWaitables:
48
+ return BlueletWaitables(w=[self.fd])
49
+
50
+ def fire(self) -> None:
51
+ self.fd.write(self.data)
52
+
53
+
54
+ ##
55
+
56
+
57
+ class _FilesBlueletApi:
58
+ def read(self, fd: ta.IO, bufsize: ta.Optional[int] = None) -> BlueletEvent:
59
+ """Event: read from a file descriptor asynchronously."""
60
+
61
+ if bufsize is None:
62
+ # Read all.
63
+ def reader():
64
+ buf = []
65
+ while True:
66
+ data = yield self.read(fd, 1024)
67
+ if not data:
68
+ break
69
+ buf.append(data)
70
+ yield ReturnBlueletEvent(''.join(buf))
71
+
72
+ return DelegationBlueletEvent(reader())
73
+
74
+ else:
75
+ return ReadBlueletEvent(fd, bufsize)
76
+
77
+ def write(self, fd: ta.IO, data: bytes) -> BlueletEvent:
78
+ """Event: write to a file descriptor asynchronously."""
79
+
80
+ return WriteBlueletEvent(fd, data)
@@ -0,0 +1,416 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ # Based on bluelet ( https://github.com/sampsyo/bluelet ) by Adrian Sampson, original license:
4
+ # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
5
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
6
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
7
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+ """
9
+ TODO:
10
+ - use fdio
11
+ - wrap coros in Tasks :|
12
+ - (unit)tests lol
13
+ - * subprocesses
14
+ - canceling
15
+ - timeouts
16
+ - task groups
17
+ - gather
18
+ - locks / semaphores / events / etc
19
+ - rename Coro to Bluelet?
20
+ - shutdown
21
+ - ensure resource cleanup
22
+ - run_thread? whatever?
23
+
24
+ Subprocs:
25
+ - https://github.com/python/cpython/issues/120804 - GH-120804: Remove get_child_watcher and set_child_watcher from
26
+ asyncio
27
+ - https://github.com/python/cpython/pull/17063/files bpo-38692: Add os.pidfd_open
28
+ - clone PidfdChildWatcher + ThreadedChildWatcher
29
+ """
30
+ import collections
31
+ import dataclasses as dc
32
+ import errno
33
+ import logging
34
+ import select
35
+ import sys
36
+ import time
37
+ import traceback
38
+ import types
39
+ import typing as ta
40
+ import weakref
41
+
42
+ from .core import BlueletCoro
43
+ from .core import BlueletExcInfo
44
+ from .core import CoreBlueletEvent
45
+ from .core import DelegationBlueletEvent
46
+ from .core import ExceptionBlueletEvent
47
+ from .core import JoinBlueletEvent
48
+ from .core import KillBlueletEvent
49
+ from .core import ReturnBlueletEvent
50
+ from .core import SleepBlueletEvent
51
+ from .core import SpawnBlueletEvent
52
+ from .core import ValueBlueletEvent
53
+ from .core import _BlueletAwaitableDriver
54
+ from .events import BlueletEvent
55
+ from .events import BlueletWaitable
56
+ from .events import WaitableBlueletEvent
57
+
58
+
59
+ ##
60
+
61
+
62
+ class BlueletCoroException(Exception): # noqa
63
+ def __init__(self, coro: BlueletCoro, exc_info: BlueletExcInfo) -> None:
64
+ super().__init__()
65
+ self.coro = coro
66
+ self.exc_info = exc_info
67
+
68
+ @staticmethod
69
+ def _exc_info() -> BlueletExcInfo:
70
+ return sys.exc_info() # type: ignore
71
+
72
+ @staticmethod
73
+ def _reraise(typ: ta.Type[BaseException], exc: BaseException, tb: types.TracebackType) -> ta.NoReturn: # noqa
74
+ raise exc.with_traceback(tb)
75
+
76
+ def reraise(self) -> ta.NoReturn:
77
+ self._reraise(self.exc_info[0], self.exc_info[1], self.exc_info[2])
78
+
79
+
80
+ ##
81
+
82
+
83
+ def _bluelet_event_select(
84
+ events: ta.Iterable[BlueletEvent],
85
+ *,
86
+ log: ta.Optional[logging.Logger] = None,
87
+ ) -> ta.Set[WaitableBlueletEvent]:
88
+ """
89
+ Perform a select() over all the Events provided, returning the ones ready to be fired. Only WaitableEvents
90
+ (including SleepEvents) matter here; all other events are ignored (and thus postponed).
91
+ """
92
+
93
+ waitable_to_event: ta.Dict[ta.Tuple[str, BlueletWaitable], WaitableBlueletEvent] = {}
94
+ rlist: ta.List[BlueletWaitable] = []
95
+ wlist: ta.List[BlueletWaitable] = []
96
+ xlist: ta.List[BlueletWaitable] = []
97
+ earliest_wakeup: ta.Optional[float] = None
98
+
99
+ # Gather waitables and wakeup times.
100
+ for event in events:
101
+ if isinstance(event, SleepBlueletEvent):
102
+ if not earliest_wakeup:
103
+ earliest_wakeup = event.wakeup_time
104
+ else:
105
+ earliest_wakeup = min(earliest_wakeup, event.wakeup_time)
106
+
107
+ elif isinstance(event, WaitableBlueletEvent):
108
+ ew = event.waitables()
109
+ rlist.extend(ew.r)
110
+ wlist.extend(ew.w)
111
+ xlist.extend(ew.x)
112
+ for waitable in ew.r:
113
+ waitable_to_event[('r', waitable)] = event
114
+ for waitable in ew.w:
115
+ waitable_to_event[('w', waitable)] = event
116
+ for waitable in ew.x:
117
+ waitable_to_event[('x', waitable)] = event
118
+
119
+ # If we have a any sleeping coros, determine how long to sleep.
120
+ if earliest_wakeup:
121
+ timeout = max(earliest_wakeup - time.time(), 0.)
122
+ else:
123
+ timeout = None
124
+
125
+ # Perform select() if we have any waitables.
126
+ if rlist or wlist or xlist:
127
+ if log:
128
+ log.debug('_bluelet_event_select: +select: %r %r %r %r', rlist, wlist, xlist, timeout)
129
+ rready, wready, xready = select.select(rlist, wlist, xlist, timeout)
130
+ if log:
131
+ log.debug('_bluelet_event_select: -select: %r %r %r', rready, wready, xready)
132
+
133
+ else:
134
+ rready, wready, xready = [], [], []
135
+ if timeout:
136
+ if log:
137
+ log.debug('_bluelet_event_select: sleep: %r', timeout)
138
+ time.sleep(timeout)
139
+
140
+ # Gather ready events corresponding to the ready waitables.
141
+ ready_events: ta.Set[WaitableBlueletEvent] = set()
142
+ for ready in rready:
143
+ ready_events.add(waitable_to_event[('r', ready)])
144
+ for ready in wready:
145
+ ready_events.add(waitable_to_event[('w', ready)])
146
+ for ready in xready:
147
+ ready_events.add(waitable_to_event[('x', ready)])
148
+
149
+ # Gather any finished sleeps.
150
+ for event in events:
151
+ if isinstance(event, SleepBlueletEvent) and not event.time_left():
152
+ ready_events.add(event)
153
+
154
+ return ready_events
155
+
156
+
157
+ ##
158
+
159
+
160
+ class _SuspendedBlueletEvent(CoreBlueletEvent):
161
+ pass
162
+
163
+
164
+ _BLUELET_SUSPENDED = _SuspendedBlueletEvent() # Special sentinel placeholder for suspended coros.
165
+
166
+
167
+ @dc.dataclass(frozen=True, eq=False)
168
+ class _DelegatedBlueletEvent(CoreBlueletEvent):
169
+ """Placeholder indicating that a coro has delegated execution to a different coro."""
170
+
171
+ child: BlueletCoro
172
+
173
+
174
+ class _BlueletRunner:
175
+ """
176
+ Schedules a coroutine, running it to completion. This encapsulates the Bluelet scheduler, which the root coroutine
177
+ can add to by spawning new coroutines.
178
+ """
179
+
180
+ def __init__(
181
+ self,
182
+ root_coro: BlueletCoro,
183
+ *,
184
+ log: ta.Optional[logging.Logger] = None,
185
+ ) -> None:
186
+ super().__init__()
187
+
188
+ self._root_coro = root_coro
189
+ self._log = log
190
+
191
+ # The "coros" dictionary keeps track of all the currently-executing and suspended coroutines. It maps
192
+ # coroutines to their currently "blocking" event. The event value may be SUSPENDED if the coroutine is waiting
193
+ # on some other condition: namely, a delegated coroutine or a joined coroutine. In this case, the coroutine
194
+ # should *also* appear as a value in one of the below dictionaries `delegators` or `joiners`.
195
+ self._coros: ta.Dict[BlueletCoro, BlueletEvent] = {self._root_coro: ValueBlueletEvent(None)}
196
+
197
+ # Maps child coroutines to delegating parents.
198
+ self._delegators: ta.Dict[BlueletCoro, BlueletCoro] = {}
199
+
200
+ # Maps child coroutines to joining (exit-waiting) parents.
201
+ self._joiners: ta.MutableMapping[BlueletCoro, ta.List[BlueletCoro]] = collections.defaultdict(list)
202
+
203
+ # History of spawned coroutines for joining of already completed coroutines.
204
+ self._history: ta.MutableMapping[BlueletCoro, ta.Optional[BlueletEvent]] = \
205
+ weakref.WeakKeyDictionary({self._root_coro: None})
206
+
207
+ def _complete_coro(self, coro: BlueletCoro, return_value: ta.Any) -> None:
208
+ """
209
+ Remove a coroutine from the scheduling pool, awaking delegators and joiners as necessary and returning the
210
+ specified value to any delegating parent.
211
+ """
212
+
213
+ del self._coros[coro]
214
+
215
+ # Resume delegator.
216
+ if coro in self._delegators:
217
+ self._coros[self._delegators[coro]] = ValueBlueletEvent(return_value)
218
+ del self._delegators[coro]
219
+
220
+ # Resume joiners.
221
+ if coro in self._joiners:
222
+ for parent in self._joiners[coro]:
223
+ self._coros[parent] = ValueBlueletEvent(None)
224
+ del self._joiners[coro]
225
+
226
+ def _advance_coro(self, coro: BlueletCoro, value: ta.Any, is_exc: bool = False) -> None:
227
+ """
228
+ After an event is fired, run a given coroutine associated with it in the coros dict until it yields again. If
229
+ the coroutine exits, then the coro is removed from the pool. If the coroutine raises an exception, it is
230
+ reraised in a CoroException. If is_exc is True, then the value must be an exc_info tuple and the exception is
231
+ thrown into the coroutine.
232
+ """
233
+
234
+ try:
235
+ if is_exc:
236
+ next_event = coro.throw(*value)
237
+ else:
238
+ next_event = coro.send(value)
239
+
240
+ except StopIteration:
241
+ # Coro is done.
242
+ self._complete_coro(coro, None)
243
+
244
+ except BaseException: # noqa
245
+ # Coro raised some other exception.
246
+ del self._coros[coro]
247
+ # Note: Don't use `raise from` as this should support 3.8.
248
+ raise BlueletCoroException(coro, BlueletCoroException._exc_info()) # noqa
249
+
250
+ else:
251
+ if isinstance(next_event, ta.Generator):
252
+ # Automatically invoke sub-coroutines. (Shorthand for explicit bluelet.call().)
253
+ next_event = DelegationBlueletEvent(next_event)
254
+
255
+ if isinstance(next_event, types.CoroutineType): # type: ignore[unreachable]
256
+ next_event = DelegationBlueletEvent(_BlueletAwaitableDriver(next_event)()) # type: ignore[unreachable]
257
+
258
+ if not isinstance(next_event, BlueletEvent):
259
+ raise TypeError(next_event)
260
+
261
+ self._coros[coro] = next_event
262
+
263
+ def _kill_coro(self, coro: BlueletCoro) -> None:
264
+ """Unschedule this coro and its (recursive) delegates."""
265
+
266
+ # Collect all coroutines in the delegation stack.
267
+ coros = [coro]
268
+ while isinstance((cur := self._coros[coro]), _DelegatedBlueletEvent):
269
+ coro = cur.child # noqa
270
+ coros.append(coro)
271
+
272
+ # Complete each coroutine from the top to the bottom of the stack.
273
+ for coro in reversed(coros):
274
+ self._complete_coro(coro, None)
275
+
276
+ def close(self) -> None:
277
+ # If any coros still remain, kill them.
278
+ for coro in self._coros:
279
+ coro.close()
280
+
281
+ self._coros.clear()
282
+
283
+ def _handle_core_event(self, coro: BlueletCoro, event: CoreBlueletEvent) -> bool:
284
+ if self._log:
285
+ self._log.debug(f'{self.__class__.__name__}._handle_core_event: %r %r', coro, event)
286
+
287
+ if isinstance(event, SpawnBlueletEvent):
288
+ sc = ta.cast(BlueletCoro, event.spawned) # FIXME
289
+ self._coros[sc] = ValueBlueletEvent(None) # Spawn.
290
+ self._history[sc] = None # Record in history.
291
+ self._advance_coro(coro, None)
292
+ return True
293
+
294
+ elif isinstance(event, ValueBlueletEvent):
295
+ self._advance_coro(coro, event.value)
296
+ return True
297
+
298
+ elif isinstance(event, ExceptionBlueletEvent):
299
+ self._advance_coro(coro, event.exc_info, True)
300
+ return True
301
+
302
+ elif isinstance(event, DelegationBlueletEvent):
303
+ self._coros[coro] = _DelegatedBlueletEvent(event.spawned) # Suspend.
304
+ self._coros[event.spawned] = ValueBlueletEvent(None) # Spawn.
305
+ self._history[event.spawned] = None # Record in history.
306
+ self._delegators[event.spawned] = coro
307
+ return True
308
+
309
+ elif isinstance(event, ReturnBlueletEvent):
310
+ # Coro is done.
311
+ self._complete_coro(coro, event.value)
312
+ return True
313
+
314
+ elif isinstance(event, JoinBlueletEvent):
315
+ if event.child not in self._coros and event.child in self._history:
316
+ self._coros[coro] = ValueBlueletEvent(None)
317
+ else:
318
+ self._coros[coro] = _BLUELET_SUSPENDED # Suspend.
319
+ self._joiners[event.child].append(coro)
320
+ return True
321
+
322
+ elif isinstance(event, KillBlueletEvent):
323
+ self._coros[coro] = ValueBlueletEvent(None)
324
+ self._kill_coro(event.child)
325
+ return True
326
+
327
+ elif isinstance(event, (_DelegatedBlueletEvent, _SuspendedBlueletEvent)):
328
+ return False
329
+
330
+ else:
331
+ raise TypeError(event)
332
+
333
+ def _step(self) -> ta.Optional[BlueletCoroException]:
334
+ if self._log:
335
+ self._log.debug(f'{self.__class__.__name__}._step') # Noqa
336
+
337
+ try:
338
+ # Look for events that can be run immediately. Continue running immediate events until nothing is ready.
339
+ while True:
340
+ have_ready = False
341
+ for coro, event in list(self._coros.items()):
342
+ if isinstance(event, CoreBlueletEvent) and not isinstance(event, SleepBlueletEvent):
343
+ have_ready |= self._handle_core_event(coro, event)
344
+ elif isinstance(event, WaitableBlueletEvent):
345
+ pass
346
+ else:
347
+ raise TypeError(f'Unknown event type: {event}') # noqa
348
+
349
+ # Only start the select when nothing else is ready.
350
+ if not have_ready:
351
+ break
352
+
353
+ # Wait and fire.
354
+ event2coro = {v: k for k, v in self._coros.items()}
355
+ for event in _bluelet_event_select(self._coros.values()):
356
+ # Run the IO operation, but catch socket errors.
357
+ try:
358
+ value = event.fire()
359
+ except OSError as exc:
360
+ if isinstance(exc.args, tuple) and exc.args[0] == errno.EPIPE:
361
+ # Broken pipe. Remote host disconnected.
362
+ pass
363
+ elif isinstance(exc.args, tuple) and exc.args[0] == errno.ECONNRESET:
364
+ # Connection was reset by peer.
365
+ pass
366
+ else:
367
+ traceback.print_exc()
368
+ # Abort the coroutine.
369
+ self._coros[event2coro[event]] = ReturnBlueletEvent(None)
370
+ else:
371
+ self._advance_coro(event2coro[event], value)
372
+
373
+ except BlueletCoroException as te:
374
+ if self._log and self._log.isEnabledFor(logging.DEBUG):
375
+ self._log.exception(f'{self.__class__.__name__}._step')
376
+
377
+ # Exception raised from inside a coro.
378
+ event = ExceptionBlueletEvent(te.exc_info)
379
+ if te.coro in self._delegators:
380
+ # The coro is a delegate. Raise exception in its delegator.
381
+ self._coros[self._delegators[te.coro]] = event
382
+ del self._delegators[te.coro]
383
+ else:
384
+ # The coro is root-level. Raise in client code.
385
+ return te
386
+
387
+ except BaseException: # noqa
388
+ ei = BlueletCoroException._exc_info() # noqa
389
+
390
+ if self._log and self._log.isEnabledFor(logging.DEBUG):
391
+ self._log.exception(f'{self.__class__.__name__}._step')
392
+
393
+ # For instance, KeyboardInterrupt during select(). Raise into root coro and terminate others.
394
+ self._coros = {self._root_coro: ExceptionBlueletEvent(ei)} # noqa
395
+
396
+ return None
397
+
398
+ def run(self) -> None:
399
+ # Continue advancing coros until root coro exits.
400
+ exit_ce: BlueletCoroException | None = None
401
+ while self._coros:
402
+ exit_ce = self._step()
403
+
404
+ self.close()
405
+
406
+ # If we're exiting with an exception, raise it in the client.
407
+ if exit_ce:
408
+ exit_ce.reraise()
409
+
410
+
411
+ ##
412
+
413
+
414
+ class _RunnerBlueletApi:
415
+ def run(self, root_coro: BlueletCoro) -> None:
416
+ _BlueletRunner(root_coro).run()
@@ -0,0 +1,214 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ # Based on bluelet ( https://github.com/sampsyo/bluelet ) by Adrian Sampson, original license:
4
+ # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
5
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
6
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
7
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+ import abc
9
+ import dataclasses as dc
10
+ import socket
11
+ import typing as ta
12
+
13
+ from .core import BlueletCoro
14
+ from .core import ReturnBlueletEvent
15
+ from .core import ValueBlueletEvent
16
+ from .core import _CoreBlueletApi
17
+ from .events import BlueletEvent
18
+ from .events import BlueletWaitables
19
+ from .events import WaitableBlueletEvent
20
+
21
+
22
+ ##
23
+
24
+
25
+ class SocketClosedBlueletError(Exception):
26
+ pass
27
+
28
+
29
+ class BlueletListener:
30
+ """A socket wrapper object for listening sockets."""
31
+
32
+ def __init__(self, host: str, port: int) -> None:
33
+ """Create a listening socket on the given hostname and port."""
34
+
35
+ super().__init__()
36
+ self._closed = False
37
+ self.host = host
38
+ self.port = port
39
+
40
+ self.sock = sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
41
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
42
+ sock.bind((host, port))
43
+ sock.listen(5)
44
+
45
+ def accept(self) -> 'AcceptBlueletEvent':
46
+ """
47
+ An event that waits for a connection on the listening socket. When a connection is made, the event returns a
48
+ Connection object.
49
+ """
50
+
51
+ if self._closed:
52
+ raise SocketClosedBlueletError
53
+ return AcceptBlueletEvent(self)
54
+
55
+ def close(self) -> None:
56
+ """Immediately close the listening socket. (Not an event.)"""
57
+
58
+ self._closed = True
59
+ self.sock.close()
60
+
61
+
62
+ class BlueletConnection:
63
+ """A socket wrapper object for connected sockets."""
64
+
65
+ def __init__(self, sock: socket.socket, addr: ta.Tuple[str, int]) -> None:
66
+ super().__init__()
67
+ self.sock = sock
68
+ self.addr = addr
69
+ self._buf = bytearray()
70
+ self._closed: bool = False
71
+
72
+ def close(self) -> None:
73
+ """Close the connection."""
74
+
75
+ self._closed = True
76
+ self.sock.close()
77
+
78
+ def recv(self, size: int) -> BlueletEvent:
79
+ """Read at most size bytes of data from the socket."""
80
+
81
+ if self._closed:
82
+ raise SocketClosedBlueletError
83
+
84
+ if self._buf:
85
+ # We already have data read previously.
86
+ out = self._buf[:size]
87
+ self._buf = self._buf[size:]
88
+ return ValueBlueletEvent(bytes(out))
89
+ else:
90
+ return ReceiveBlueletEvent(self, size)
91
+
92
+ def send(self, data: bytes) -> BlueletEvent:
93
+ """Sends data on the socket, returning the number of bytes successfully sent."""
94
+
95
+ if self._closed:
96
+ raise SocketClosedBlueletError
97
+ return SendBlueletEvent(self, data)
98
+
99
+ def sendall(self, data: bytes) -> BlueletEvent:
100
+ """Send all of data on the socket."""
101
+
102
+ if self._closed:
103
+ raise SocketClosedBlueletError
104
+ return SendBlueletEvent(self, data, True)
105
+
106
+ def readline(self, terminator: bytes = b'\n', bufsize: int = 1024) -> BlueletCoro:
107
+ """Reads a line (delimited by terminator) from the socket."""
108
+
109
+ if self._closed:
110
+ raise SocketClosedBlueletError
111
+
112
+ while True:
113
+ if terminator in self._buf:
114
+ line, self._buf = self._buf.split(terminator, 1)
115
+ line += terminator
116
+ yield ReturnBlueletEvent(bytes(line))
117
+ break
118
+
119
+ if (data := (yield ReceiveBlueletEvent(self, bufsize))):
120
+ self._buf += data
121
+ else:
122
+ line = self._buf
123
+ self._buf = bytearray()
124
+ yield ReturnBlueletEvent(bytes(line))
125
+ break
126
+
127
+
128
+ ##
129
+
130
+
131
+ class SocketBlueletEvent(BlueletEvent, abc.ABC): # noqa
132
+ pass
133
+
134
+
135
+ @dc.dataclass(frozen=True, eq=False)
136
+ class AcceptBlueletEvent(WaitableBlueletEvent, SocketBlueletEvent):
137
+ """An event for Listener objects (listening sockets) that suspends execution until the socket gets a connection."""
138
+
139
+ listener: BlueletListener
140
+
141
+ def waitables(self) -> BlueletWaitables:
142
+ return BlueletWaitables(r=[self.listener.sock])
143
+
144
+ def fire(self) -> BlueletConnection:
145
+ sock, addr = self.listener.sock.accept()
146
+ return BlueletConnection(sock, addr)
147
+
148
+
149
+ @dc.dataclass(frozen=True, eq=False)
150
+ class ReceiveBlueletEvent(WaitableBlueletEvent, SocketBlueletEvent):
151
+ """An event for Connection objects (connected sockets) for asynchronously reading data."""
152
+
153
+ conn: BlueletConnection
154
+ bufsize: int
155
+
156
+ def waitables(self) -> BlueletWaitables:
157
+ return BlueletWaitables(r=[self.conn.sock])
158
+
159
+ def fire(self) -> bytes:
160
+ return self.conn.sock.recv(self.bufsize)
161
+
162
+
163
+ @dc.dataclass(frozen=True, eq=False)
164
+ class SendBlueletEvent(WaitableBlueletEvent, SocketBlueletEvent):
165
+ """An event for Connection objects (connected sockets) for asynchronously writing data."""
166
+
167
+ conn: BlueletConnection
168
+ data: bytes
169
+ sendall: bool = False
170
+
171
+ def waitables(self) -> BlueletWaitables:
172
+ return BlueletWaitables(w=[self.conn.sock])
173
+
174
+ def fire(self) -> ta.Optional[int]:
175
+ if self.sendall:
176
+ self.conn.sock.sendall(self.data)
177
+ return None
178
+ else:
179
+ return self.conn.sock.send(self.data)
180
+
181
+
182
+ ##
183
+
184
+
185
+ class _SocketsBlueletApi(_CoreBlueletApi):
186
+ def connect(self, host: str, port: int) -> BlueletEvent:
187
+ """Event: connect to a network address and return a Connection object for communicating on the socket."""
188
+
189
+ addr = (host, port)
190
+ sock = socket.create_connection(addr)
191
+ return ValueBlueletEvent(BlueletConnection(sock, addr))
192
+
193
+ def server(self, host: str, port: int, func) -> BlueletCoro:
194
+ """
195
+ A coroutine that runs a network server. Host and port specify the listening address. func should be a coroutine
196
+ that takes a single parameter, a Connection object. The coroutine is invoked for every incoming connection on
197
+ the listening socket.
198
+ """
199
+
200
+ def handler(conn):
201
+ try:
202
+ yield func(conn)
203
+ finally:
204
+ conn.close()
205
+
206
+ listener = BlueletListener(host, port)
207
+ try:
208
+ while True:
209
+ conn = yield listener.accept()
210
+ yield self.spawn(handler(conn))
211
+ except KeyboardInterrupt:
212
+ pass
213
+ finally:
214
+ listener.close()