omlish 0.0.0.dev161__py3-none-any.whl → 0.0.0.dev162__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 +2 -2
- omlish/asyncs/bluelet/LICENSE +6 -0
- omlish/asyncs/bluelet/__init__.py +0 -0
- omlish/asyncs/bluelet/all.py +67 -0
- omlish/asyncs/bluelet/api.py +23 -0
- omlish/asyncs/bluelet/core.py +178 -0
- omlish/asyncs/bluelet/events.py +78 -0
- omlish/asyncs/bluelet/files.py +80 -0
- omlish/asyncs/bluelet/runner.py +416 -0
- omlish/asyncs/bluelet/sockets.py +214 -0
- omlish/lite/inject.py +16 -29
- {omlish-0.0.0.dev161.dist-info → omlish-0.0.0.dev162.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev161.dist-info → omlish-0.0.0.dev162.dist-info}/RECORD +17 -8
- {omlish-0.0.0.dev161.dist-info → omlish-0.0.0.dev162.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev161.dist-info → omlish-0.0.0.dev162.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev161.dist-info → omlish-0.0.0.dev162.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev161.dist-info → omlish-0.0.0.dev162.dist-info}/top_level.txt +0 -0
omlish/__about__.py
CHANGED
@@ -0,0 +1,6 @@
|
|
1
|
+
Based on bluelet ( https://github.com/sampsyo/bluelet ) by Adrian Sampson, original license:
|
2
|
+
|
3
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
4
|
+
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
5
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
6
|
+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
File without changes
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# ruff: noqa: I001
|
2
|
+
from .api import ( # noqa
|
3
|
+
bluelet,
|
4
|
+
)
|
5
|
+
|
6
|
+
from .core import ( # noqa
|
7
|
+
BlueletCoro as Coro,
|
8
|
+
BlueletExcInfo as ExcInfo,
|
9
|
+
CoreBlueletEvent as CoreEvent,
|
10
|
+
DelegationBlueletEvent as DelegationEvent,
|
11
|
+
ExceptionBlueletEvent as ExceptionEvent,
|
12
|
+
JoinBlueletEvent as JoinEvent,
|
13
|
+
KillBlueletEvent as KillEvent,
|
14
|
+
ReturnBlueletEvent as ReturnEvent,
|
15
|
+
SleepBlueletEvent as SleepEvent,
|
16
|
+
SpawnBlueletEvent as SpawnEvent,
|
17
|
+
ValueBlueletEvent as ValueEvent,
|
18
|
+
)
|
19
|
+
|
20
|
+
from .events import ( # noqa
|
21
|
+
BlueletEvent as Event,
|
22
|
+
BlueletFuture as Future,
|
23
|
+
BlueletHasFileno as HasFileno,
|
24
|
+
BlueletWaitable as Waitable,
|
25
|
+
BlueletWaitables as Waitables,
|
26
|
+
WaitableBlueletEvent as WaitableEvent,
|
27
|
+
)
|
28
|
+
|
29
|
+
from .files import ( # noqa
|
30
|
+
FileBlueletEvent as FileEvent,
|
31
|
+
ReadBlueletEvent as ReadEvent,
|
32
|
+
WriteBlueletEvent as WriteEvent,
|
33
|
+
)
|
34
|
+
|
35
|
+
from .runner import ( # noqa
|
36
|
+
BlueletCoroException as CoroException,
|
37
|
+
)
|
38
|
+
|
39
|
+
from .sockets import ( # noqa
|
40
|
+
AcceptBlueletEvent as AcceptEvent,
|
41
|
+
BlueletConnection as Connection,
|
42
|
+
BlueletListener as Listener,
|
43
|
+
ReceiveBlueletEvent as ReceiveEvent,
|
44
|
+
SendBlueletEvent as SendEvent,
|
45
|
+
SocketBlueletEvent as SocketEvent,
|
46
|
+
SocketClosedBlueletError as SocketClosedError,
|
47
|
+
)
|
48
|
+
|
49
|
+
|
50
|
+
##
|
51
|
+
|
52
|
+
|
53
|
+
call = bluelet.call
|
54
|
+
end = bluelet.end
|
55
|
+
join = bluelet.join
|
56
|
+
kill = bluelet.kill
|
57
|
+
null = bluelet.null
|
58
|
+
sleep = bluelet.sleep
|
59
|
+
spawn = bluelet.spawn
|
60
|
+
|
61
|
+
read = bluelet.read
|
62
|
+
write = bluelet.write
|
63
|
+
|
64
|
+
run = bluelet.run
|
65
|
+
|
66
|
+
connect = bluelet.connect
|
67
|
+
server = bluelet.server
|
@@ -0,0 +1,23 @@
|
|
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
|
+
from .core import _CoreBlueletApi
|
9
|
+
from .files import _FilesBlueletApi
|
10
|
+
from .runner import _RunnerBlueletApi
|
11
|
+
from .sockets import _SocketsBlueletApi
|
12
|
+
|
13
|
+
|
14
|
+
class BlueletApi(
|
15
|
+
_RunnerBlueletApi,
|
16
|
+
_SocketsBlueletApi,
|
17
|
+
_FilesBlueletApi,
|
18
|
+
_CoreBlueletApi,
|
19
|
+
):
|
20
|
+
pass
|
21
|
+
|
22
|
+
|
23
|
+
bluelet = BlueletApi()
|
@@ -0,0 +1,178 @@
|
|
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 time
|
11
|
+
import types
|
12
|
+
import typing as ta
|
13
|
+
|
14
|
+
from .events import BlueletEvent
|
15
|
+
from .events import BlueletFuture
|
16
|
+
from .events import WaitableBlueletEvent
|
17
|
+
|
18
|
+
|
19
|
+
T = ta.TypeVar('T')
|
20
|
+
|
21
|
+
BlueletExcInfo = ta.Tuple[ta.Type[BaseException], BaseException, types.TracebackType] # ta.TypeAlias
|
22
|
+
|
23
|
+
BlueletCoro = ta.Generator[ta.Union['BlueletEvent', 'BlueletCoro'], ta.Any, None] # ta.TypeAlias
|
24
|
+
|
25
|
+
BlueletSpawnable = ta.Union[BlueletCoro, ta.Awaitable] # ta.TypeAlias
|
26
|
+
|
27
|
+
|
28
|
+
##
|
29
|
+
|
30
|
+
|
31
|
+
@dc.dataclass(frozen=True, eq=False)
|
32
|
+
class _BlueletAwaitableDriver:
|
33
|
+
a: ta.Awaitable
|
34
|
+
|
35
|
+
def __call__(self) -> BlueletCoro:
|
36
|
+
g = self.a.__await__()
|
37
|
+
gi = iter(g)
|
38
|
+
while True:
|
39
|
+
try:
|
40
|
+
f = gi.send(None)
|
41
|
+
except StopIteration as e:
|
42
|
+
yield ReturnBlueletEvent(e.value)
|
43
|
+
break
|
44
|
+
else:
|
45
|
+
if not isinstance(f, BlueletFuture):
|
46
|
+
raise TypeError(f)
|
47
|
+
res = yield f.event
|
48
|
+
f.done = True
|
49
|
+
f.result = res
|
50
|
+
|
51
|
+
|
52
|
+
##
|
53
|
+
|
54
|
+
|
55
|
+
class CoreBlueletEvent(BlueletEvent, abc.ABC): # noqa
|
56
|
+
pass
|
57
|
+
|
58
|
+
|
59
|
+
@dc.dataclass(frozen=True, eq=False)
|
60
|
+
class ValueBlueletEvent(CoreBlueletEvent, ta.Generic[T]):
|
61
|
+
"""An event that does nothing but return a fixed value."""
|
62
|
+
|
63
|
+
value: T
|
64
|
+
|
65
|
+
|
66
|
+
@dc.dataclass(frozen=True, eq=False)
|
67
|
+
class ExceptionBlueletEvent(CoreBlueletEvent):
|
68
|
+
"""Raise an exception at the yield point. Used internally."""
|
69
|
+
|
70
|
+
exc_info: BlueletExcInfo
|
71
|
+
|
72
|
+
|
73
|
+
@dc.dataclass(frozen=True, eq=False)
|
74
|
+
class SpawnBlueletEvent(CoreBlueletEvent):
|
75
|
+
"""Add a new coroutine coro to the scheduler."""
|
76
|
+
|
77
|
+
spawned: BlueletSpawnable
|
78
|
+
|
79
|
+
|
80
|
+
@dc.dataclass(frozen=True, eq=False)
|
81
|
+
class JoinBlueletEvent(CoreBlueletEvent):
|
82
|
+
"""Suspend the coro until the specified child coro has completed."""
|
83
|
+
|
84
|
+
child: BlueletCoro
|
85
|
+
|
86
|
+
|
87
|
+
@dc.dataclass(frozen=True, eq=False)
|
88
|
+
class KillBlueletEvent(CoreBlueletEvent):
|
89
|
+
"""Unschedule a child coro."""
|
90
|
+
|
91
|
+
child: BlueletCoro
|
92
|
+
|
93
|
+
|
94
|
+
@dc.dataclass(frozen=True, eq=False)
|
95
|
+
class DelegationBlueletEvent(CoreBlueletEvent):
|
96
|
+
"""
|
97
|
+
Suspend execution of the current coro, start a new coro and, once the child coro finished, return control to
|
98
|
+
the parent coro.
|
99
|
+
"""
|
100
|
+
|
101
|
+
spawned: BlueletCoro
|
102
|
+
|
103
|
+
|
104
|
+
@dc.dataclass(frozen=True, eq=False)
|
105
|
+
class ReturnBlueletEvent(CoreBlueletEvent, ta.Generic[T]):
|
106
|
+
"""Return a value the current coro's delegator at the point of delegation. Ends the current (delegate) coro."""
|
107
|
+
|
108
|
+
value: ta.Optional[T]
|
109
|
+
|
110
|
+
|
111
|
+
@dc.dataclass(frozen=True, eq=False)
|
112
|
+
class SleepBlueletEvent(WaitableBlueletEvent, CoreBlueletEvent):
|
113
|
+
"""Suspend the coro for a given duration."""
|
114
|
+
|
115
|
+
wakeup_time: float
|
116
|
+
|
117
|
+
def time_left(self) -> float:
|
118
|
+
return max(self.wakeup_time - time.time(), 0.)
|
119
|
+
|
120
|
+
|
121
|
+
##
|
122
|
+
|
123
|
+
|
124
|
+
class _CoreBlueletApi:
|
125
|
+
def value(self, v: T) -> ValueBlueletEvent[T]:
|
126
|
+
"""Event: yield a value."""
|
127
|
+
|
128
|
+
return ValueBlueletEvent(v)
|
129
|
+
|
130
|
+
def null(self) -> ValueBlueletEvent[None]:
|
131
|
+
"""Event: yield to the scheduler without doing anything special."""
|
132
|
+
|
133
|
+
return ValueBlueletEvent(None)
|
134
|
+
|
135
|
+
def spawn(self, spawned: BlueletSpawnable) -> SpawnBlueletEvent:
|
136
|
+
"""Event: add another coroutine to the scheduler. Both the parent and child coroutines run concurrently."""
|
137
|
+
|
138
|
+
if isinstance(spawned, types.CoroutineType):
|
139
|
+
spawned = _BlueletAwaitableDriver(spawned)()
|
140
|
+
|
141
|
+
if not isinstance(spawned, types.GeneratorType):
|
142
|
+
raise TypeError(f'{spawned} is not spawnable')
|
143
|
+
|
144
|
+
return SpawnBlueletEvent(spawned)
|
145
|
+
|
146
|
+
def join(self, coro: BlueletCoro) -> JoinBlueletEvent:
|
147
|
+
"""Suspend the coro until another, previously `spawn`ed coro completes."""
|
148
|
+
|
149
|
+
return JoinBlueletEvent(coro)
|
150
|
+
|
151
|
+
def kill(self, coro: BlueletCoro) -> KillBlueletEvent:
|
152
|
+
"""Halt the execution of a different `spawn`ed coro."""
|
153
|
+
|
154
|
+
return KillBlueletEvent(coro)
|
155
|
+
|
156
|
+
def call(self, spawned: BlueletSpawnable) -> DelegationBlueletEvent:
|
157
|
+
"""
|
158
|
+
Event: delegate to another coroutine. The current coroutine is resumed once the sub-coroutine finishes. If the
|
159
|
+
sub-coroutine returns a value using end(), then this event returns that value.
|
160
|
+
"""
|
161
|
+
|
162
|
+
if isinstance(spawned, types.CoroutineType):
|
163
|
+
spawned = _BlueletAwaitableDriver(spawned)()
|
164
|
+
|
165
|
+
if not isinstance(spawned, types.GeneratorType):
|
166
|
+
raise TypeError(f'{spawned} is not spawnable')
|
167
|
+
|
168
|
+
return DelegationBlueletEvent(spawned)
|
169
|
+
|
170
|
+
def end(self, value: ta.Optional[T] = None) -> ReturnBlueletEvent[T]:
|
171
|
+
"""Event: ends the coroutine and returns a value to its delegator."""
|
172
|
+
|
173
|
+
return ReturnBlueletEvent(value)
|
174
|
+
|
175
|
+
def sleep(self, duration: float) -> SleepBlueletEvent:
|
176
|
+
"""Event: suspend the coro for ``duration`` seconds."""
|
177
|
+
|
178
|
+
return SleepBlueletEvent(time.time() + duration)
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# ruff: noqa: 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
|
+
|
13
|
+
R = ta.TypeVar('R')
|
14
|
+
|
15
|
+
BlueletEventT = ta.TypeVar('BlueletEventT', bound='BlueletEvent') # ta.TypeAlias
|
16
|
+
|
17
|
+
BlueletWaitable = ta.Union[int, 'BlueletHasFileno'] # ta.TypeAlias
|
18
|
+
|
19
|
+
|
20
|
+
##
|
21
|
+
|
22
|
+
|
23
|
+
class BlueletEvent(abc.ABC): # noqa
|
24
|
+
"""
|
25
|
+
Just a base class identifying Bluelet events. An event is an object yielded from a Bluelet coro coroutine to
|
26
|
+
suspend operation and communicate with the scheduler.
|
27
|
+
"""
|
28
|
+
|
29
|
+
def __await__(self):
|
30
|
+
return BlueletFuture(self).__await__()
|
31
|
+
|
32
|
+
|
33
|
+
##
|
34
|
+
|
35
|
+
|
36
|
+
class BlueletHasFileno(ta.Protocol):
|
37
|
+
def fileno(self) -> int: ...
|
38
|
+
|
39
|
+
|
40
|
+
@dc.dataclass(frozen=True)
|
41
|
+
class BlueletWaitables:
|
42
|
+
r: ta.Sequence[BlueletWaitable] = ()
|
43
|
+
w: ta.Sequence[BlueletWaitable] = ()
|
44
|
+
x: ta.Sequence[BlueletWaitable] = ()
|
45
|
+
|
46
|
+
|
47
|
+
class WaitableBlueletEvent(BlueletEvent, abc.ABC): # noqa
|
48
|
+
"""
|
49
|
+
A waitable event is one encapsulating an action that can be waited for using a select() call. That is, it's an event
|
50
|
+
with an associated file descriptor.
|
51
|
+
"""
|
52
|
+
|
53
|
+
def waitables(self) -> BlueletWaitables:
|
54
|
+
"""
|
55
|
+
Return "waitable" objects to pass to select(). Should return three iterables for input readiness, output
|
56
|
+
readiness, and exceptional conditions (i.e., the three lists passed to select()).
|
57
|
+
"""
|
58
|
+
return BlueletWaitables()
|
59
|
+
|
60
|
+
def fire(self) -> ta.Any:
|
61
|
+
"""Called when an associated file descriptor becomes ready (i.e., is returned from a select() call)."""
|
62
|
+
|
63
|
+
|
64
|
+
##
|
65
|
+
|
66
|
+
|
67
|
+
@dc.dataclass(eq=False)
|
68
|
+
class BlueletFuture(ta.Generic[BlueletEventT, R]):
|
69
|
+
event: BlueletEventT
|
70
|
+
done: bool = False
|
71
|
+
result: ta.Optional[R] = None
|
72
|
+
|
73
|
+
def __await__(self):
|
74
|
+
if not self.done:
|
75
|
+
yield self
|
76
|
+
if not self.done:
|
77
|
+
raise RuntimeError("await wasn't used with event future")
|
78
|
+
return self.result
|
@@ -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()
|
omlish/lite/inject.py
CHANGED
@@ -610,7 +610,7 @@ class InjectorBinder:
|
|
610
610
|
def __new__(cls, *args, **kwargs): # noqa
|
611
611
|
raise TypeError
|
612
612
|
|
613
|
-
_FN_TYPES: ta.Tuple[type, ...] = (
|
613
|
+
_FN_TYPES: ta.ClassVar[ta.Tuple[type, ...]] = (
|
614
614
|
types.FunctionType,
|
615
615
|
types.MethodType,
|
616
616
|
|
@@ -632,7 +632,7 @@ class InjectorBinder:
|
|
632
632
|
cls._FN_TYPES = (*cls._FN_TYPES, icls)
|
633
633
|
return icls
|
634
634
|
|
635
|
-
_BANNED_BIND_TYPES: ta.Tuple[type, ...] = (
|
635
|
+
_BANNED_BIND_TYPES: ta.ClassVar[ta.Tuple[type, ...]] = (
|
636
636
|
InjectorProvider,
|
637
637
|
)
|
638
638
|
|
@@ -811,45 +811,35 @@ def bind_injector_eager_key(key: ta.Any) -> InjectorBinding:
|
|
811
811
|
##
|
812
812
|
|
813
813
|
|
814
|
-
class
|
815
|
-
def __new__(cls, *args, **kwargs): # noqa
|
816
|
-
raise TypeError
|
817
|
-
|
814
|
+
class InjectionApi:
|
818
815
|
# keys
|
819
816
|
|
820
|
-
|
821
|
-
def as_key(cls, o: ta.Any) -> InjectorKey:
|
817
|
+
def as_key(self, o: ta.Any) -> InjectorKey:
|
822
818
|
return as_injector_key(o)
|
823
819
|
|
824
|
-
|
825
|
-
def array(cls, o: ta.Any) -> InjectorKey:
|
820
|
+
def array(self, o: ta.Any) -> InjectorKey:
|
826
821
|
return dc.replace(as_injector_key(o), array=True)
|
827
822
|
|
828
|
-
|
829
|
-
def tag(cls, o: ta.Any, t: ta.Any) -> InjectorKey:
|
823
|
+
def tag(self, o: ta.Any, t: ta.Any) -> InjectorKey:
|
830
824
|
return dc.replace(as_injector_key(o), tag=t)
|
831
825
|
|
832
826
|
# bindings
|
833
827
|
|
834
|
-
|
835
|
-
def as_bindings(cls, *args: InjectorBindingOrBindings) -> InjectorBindings:
|
828
|
+
def as_bindings(self, *args: InjectorBindingOrBindings) -> InjectorBindings:
|
836
829
|
return as_injector_bindings(*args)
|
837
830
|
|
838
|
-
|
839
|
-
def override(cls, p: InjectorBindings, *args: InjectorBindingOrBindings) -> InjectorBindings:
|
831
|
+
def override(self, p: InjectorBindings, *args: InjectorBindingOrBindings) -> InjectorBindings:
|
840
832
|
return injector_override(p, *args)
|
841
833
|
|
842
834
|
# injector
|
843
835
|
|
844
|
-
|
845
|
-
def create_injector(cls, *args: InjectorBindingOrBindings, parent: ta.Optional[Injector] = None) -> Injector:
|
836
|
+
def create_injector(self, *args: InjectorBindingOrBindings, parent: ta.Optional[Injector] = None) -> Injector:
|
846
837
|
return _Injector(as_injector_bindings(*args), parent)
|
847
838
|
|
848
839
|
# binder
|
849
840
|
|
850
|
-
@classmethod
|
851
841
|
def bind(
|
852
|
-
|
842
|
+
self,
|
853
843
|
obj: ta.Any,
|
854
844
|
*,
|
855
845
|
key: ta.Any = None,
|
@@ -884,32 +874,29 @@ class Injection:
|
|
884
874
|
|
885
875
|
# helpers
|
886
876
|
|
887
|
-
@classmethod
|
888
877
|
def bind_factory(
|
889
|
-
|
878
|
+
self,
|
890
879
|
fn: ta.Callable[..., T],
|
891
880
|
cls_: U,
|
892
881
|
ann: ta.Any = None,
|
893
882
|
) -> InjectorBindingOrBindings:
|
894
|
-
return
|
883
|
+
return self.bind(make_injector_factory(fn, cls_, ann))
|
895
884
|
|
896
|
-
@classmethod
|
897
885
|
def bind_array(
|
898
|
-
|
886
|
+
self,
|
899
887
|
obj: ta.Any = None,
|
900
888
|
*,
|
901
889
|
tag: ta.Any = None,
|
902
890
|
) -> InjectorBindingOrBindings:
|
903
891
|
return bind_injector_array(obj, tag=tag)
|
904
892
|
|
905
|
-
@classmethod
|
906
893
|
def bind_array_type(
|
907
|
-
|
894
|
+
self,
|
908
895
|
ele: ta.Union[InjectorKey, InjectorKeyCls],
|
909
896
|
cls_: U,
|
910
897
|
ann: ta.Any = None,
|
911
898
|
) -> InjectorBindingOrBindings:
|
912
|
-
return
|
899
|
+
return self.bind(make_injector_array_type(ele, cls_, ann))
|
913
900
|
|
914
901
|
|
915
|
-
inj =
|
902
|
+
inj = InjectionApi()
|
@@ -1,5 +1,5 @@
|
|
1
1
|
omlish/.manifests.json,sha256=RX24SRc6DCEg77PUVnaXOKCWa5TF_c9RQJdGIf7gl9c,1135
|
2
|
-
omlish/__about__.py,sha256=
|
2
|
+
omlish/__about__.py,sha256=RGH11TXe-0tI-c-75Cmj5Em5DfZ8d1t-Xincc1SM3bE,3409
|
3
3
|
omlish/__init__.py,sha256=SsyiITTuK0v74XpKV8dqNaCmjOlan1JZKrHQv5rWKPA,253
|
4
4
|
omlish/c3.py,sha256=ubu7lHwss5V4UznbejAI0qXhXahrU01MysuHOZI9C4U,8116
|
5
5
|
omlish/cached.py,sha256=UI-XTFBwA6YXWJJJeBn-WkwBkfzDjLBBaZf4nIJA9y0,510
|
@@ -92,6 +92,15 @@ omlish/asyncs/asyncio/channels.py,sha256=ZbmsEmdK1fV96liHdcVpRqA2dAMkXJt4Q3rFAg3
|
|
92
92
|
omlish/asyncs/asyncio/streams.py,sha256=Uc9PCWSfBqrK2kdVtfjjQU1eaYTWYmZm8QISDj2xiuw,1004
|
93
93
|
omlish/asyncs/asyncio/subprocesses.py,sha256=XlIWwSxpVB7sMVc75-f7dI6r08JkdipNFRWXUKS8zAw,6960
|
94
94
|
omlish/asyncs/asyncio/timeouts.py,sha256=Rj5OU9BIAPcVZZKp74z7SzUXF5xokh4dgsWkUqOy1aE,355
|
95
|
+
omlish/asyncs/bluelet/LICENSE,sha256=VHf3oPQihOHnWyIR8LcXX0dpONa1lgyJnjWC2qVuRR0,559
|
96
|
+
omlish/asyncs/bluelet/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
97
|
+
omlish/asyncs/bluelet/all.py,sha256=aUV6PwnR8DqnEBS9wsZuPW_UtP6G9M8_KY-mmxZeVG0,1516
|
98
|
+
omlish/asyncs/bluelet/api.py,sha256=yeM5SauiIbvKj26mDdxn5mOj8s5fFIw2xBSe-P6HVC0,904
|
99
|
+
omlish/asyncs/bluelet/core.py,sha256=NpGQhb865aeXPKV_67l-6FiuQzdDJJ85zjfvCR5R8Pc,5430
|
100
|
+
omlish/asyncs/bluelet/events.py,sha256=iXpRWmy64YcshT_nuyiJ39jbketZdtj8LrdlX3JmpoY,2440
|
101
|
+
omlish/asyncs/bluelet/files.py,sha256=pgcLV_3oGbpqQmOrii8SeizyYLp8XKofQJhqM82RlKw,2389
|
102
|
+
omlish/asyncs/bluelet/runner.py,sha256=cwgVjwRRXzqfOqRD0rkPHjt_Udyt9ZKpVs36uAJ_Q7c,15473
|
103
|
+
omlish/asyncs/bluelet/sockets.py,sha256=RrC2vU52dOEBYKzvoh1qA39uUE8p3uCB_oxnhaP1AeA,6752
|
95
104
|
omlish/bootstrap/__init__.py,sha256=-Rtsg7uPQNhh1dIT9nqrz96XlqizwoLnWf-FwOEstJI,730
|
96
105
|
omlish/bootstrap/__main__.py,sha256=4jCwsaogp0FrJjJZ85hzF4-WqluPeheHbfeoKynKvNs,194
|
97
106
|
omlish/bootstrap/base.py,sha256=d8hqn4hp1XMMi5PgcJBQXPKmW47epu8CxBlqDZiRZb4,1073
|
@@ -339,7 +348,7 @@ omlish/lite/__init__.py,sha256=Y3l4WY4JRi2uLG6kgbGp93fuGfkxkKwZDvhsa0Rwgtk,15
|
|
339
348
|
omlish/lite/cached.py,sha256=O7ozcoDNFm1Hg2wtpHEqYSp_i_nCLNOP6Ueq_Uk-7mU,1300
|
340
349
|
omlish/lite/check.py,sha256=KvcO86LqWlh2j4ORaZXRR4FM8fFb7kUkNqq3lTs0Ta0,12821
|
341
350
|
omlish/lite/contextmanagers.py,sha256=m9JO--p7L7mSl4cycXysH-1AO27weDKjP3DZG61cwwM,1683
|
342
|
-
omlish/lite/inject.py,sha256=
|
351
|
+
omlish/lite/inject.py,sha256=EEaioN9ESAveVCMe2s5osjwI97FPRUVoU8P95vGUiYo,23376
|
343
352
|
omlish/lite/json.py,sha256=7-02Ny4fq-6YAu5ynvqoijhuYXWpLmfCI19GUeZnb1c,740
|
344
353
|
omlish/lite/logs.py,sha256=CWFG0NKGhqNeEgryF5atN2gkPYbUdTINEw_s1phbINM,51
|
345
354
|
omlish/lite/marshal.py,sha256=ldoZs_yiQIUpOjBviV9f4mwm7hSZy0hRLXrvQA-6POU,14257
|
@@ -526,9 +535,9 @@ omlish/text/glyphsplit.py,sha256=Ug-dPRO7x-OrNNr8g1y6DotSZ2KH0S-VcOmUobwa4B0,329
|
|
526
535
|
omlish/text/indent.py,sha256=6Jj6TFY9unaPa4xPzrnZemJ-fHsV53IamP93XGjSUHs,1274
|
527
536
|
omlish/text/parts.py,sha256=7vPF1aTZdvLVYJ4EwBZVzRSy8XB3YqPd7JwEnNGGAOo,6495
|
528
537
|
omlish/text/random.py,sha256=jNWpqiaKjKyTdMXC-pWAsSC10AAP-cmRRPVhm59ZWLk,194
|
529
|
-
omlish-0.0.0.
|
530
|
-
omlish-0.0.0.
|
531
|
-
omlish-0.0.0.
|
532
|
-
omlish-0.0.0.
|
533
|
-
omlish-0.0.0.
|
534
|
-
omlish-0.0.0.
|
538
|
+
omlish-0.0.0.dev162.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
|
539
|
+
omlish-0.0.0.dev162.dist-info/METADATA,sha256=thW6t3ofpA5bodN1Uc7hDhNjXOwZd68S1SqJZuv4hIw,4264
|
540
|
+
omlish-0.0.0.dev162.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
541
|
+
omlish-0.0.0.dev162.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
|
542
|
+
omlish-0.0.0.dev162.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
|
543
|
+
omlish-0.0.0.dev162.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|