moat-lib-stream 0.1.2__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.
@@ -0,0 +1,95 @@
1
+ """
2
+ Stream infrastructure for handling data streams in a structured manner.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ # Import base classes directly (not lazy)
8
+ from .base import Base as Base
9
+ from .base import BaseBlk as BaseBlk
10
+ from .base import BaseBuf as BaseBuf
11
+ from .base import BaseConn as BaseConn
12
+ from .base import BaseMsg as BaseMsg
13
+ from .base import StackedBlk as StackedBlk
14
+ from .base import StackedBuf as StackedBuf
15
+ from .base import StackedConn as StackedConn
16
+ from .base import StackedMsg as StackedMsg
17
+
18
+ from typing import TYPE_CHECKING as _TC
19
+
20
+ if _TC:
21
+ from .anyio import AnyioBuf as AnyioBuf
22
+ from .anyio import BufAnyio as BufAnyio
23
+ from .anyio import FilenoBuf as FilenoBuf
24
+ from .anyio import ProcessBuf as ProcessBuf
25
+ from .anyio import ProcessDeadError as ProcessDeadError
26
+ from .anyio import RemoteBufAnyio as RemoteBufAnyio
27
+ from .anyio import SingleAnyioBuf as SingleAnyioBuf
28
+ from .cbor import CBORMsgBlk as CBORMsgBlk
29
+ from .cbor import CBORMsgBuf as CBORMsgBuf
30
+ from .log import LogBlk as LogBlk
31
+ from .log import LogBuf as LogBuf
32
+ from .log import LogMsg as LogMsg
33
+ from .reliable import EphemeralMsg as EphemeralMsg
34
+ from .reliable import ReliableMsg as ReliableMsg
35
+ from .tcp import TcpLink as TcpLink
36
+ from .terminal import FilenoTerm as FilenoTerm
37
+ from .terminal import TermBuf as TermBuf
38
+ from .unix import UnixLink as UnixLink
39
+
40
+
41
+ # Lazy loading
42
+ _imports = {
43
+ # Logging
44
+ "LogMsg": "log",
45
+ "LogBlk": "log",
46
+ "LogBuf": "log",
47
+ # CBOR
48
+ "CBORMsgBuf": "cbor",
49
+ "CBORMsgBlk": "cbor",
50
+ # AnyIO
51
+ "ProcessDeadError": "anyio",
52
+ "AnyioBuf": "anyio",
53
+ "FilenoBuf": "anyio",
54
+ "RemoteBufAnyio": "anyio",
55
+ "BufAnyio": "anyio",
56
+ "SingleAnyioBuf": "anyio",
57
+ "ProcessBuf": "anyio",
58
+ # Reliable messaging
59
+ "ReliableMsg": "reliable",
60
+ "EphemeralMsg": "reliable",
61
+ # Network connections
62
+ "TcpLink": "tcp",
63
+ "UnixLink": "unix",
64
+ # Terminal
65
+ "FilenoTerm": "terminal",
66
+ "TermBuf": "terminal",
67
+ }
68
+
69
+
70
+ def __getattr__(attr: str):
71
+ try:
72
+ mod = _imports[attr]
73
+ except KeyError:
74
+ raise AttributeError(attr) from None
75
+ value = getattr(__import__(mod, globals(), None, True, 1), attr)
76
+ globals()[attr] = value
77
+ return value
78
+
79
+
80
+ __all__ = [
81
+ # Base classes (not lazy)
82
+ "Base",
83
+ "BaseBlk",
84
+ "BaseBuf",
85
+ "BaseConn",
86
+ "BaseMsg",
87
+ "StackedBlk",
88
+ "StackedBuf",
89
+ "StackedConn",
90
+ "StackedMsg",
91
+ ] + list(_imports.keys())
92
+
93
+
94
+ def __dir__():
95
+ return __all__
@@ -0,0 +1,148 @@
1
+ """
2
+ CBOR message encoding/decoding for stream layers.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from moat.lib.micro import AC_use, Lock, log
8
+ from moat.lib.stream import BaseBuf, StackedMsg
9
+
10
+ from ._console import _CReader
11
+
12
+ from typing import TYPE_CHECKING # isort:skip
13
+
14
+ if TYPE_CHECKING:
15
+ from moat.lib.codec import Codec
16
+ from moat.util.liner import Liner
17
+
18
+ from collections.abc import Awaitable
19
+ from typing import Any
20
+
21
+
22
+ class _CBORMsgBuf(StackedMsg):
23
+ """
24
+ structured messages > CBOR bytestream
25
+
26
+ Use this if your stream is reliable (TCP, USB, …) but doesn't support
27
+ message boundaries.
28
+
29
+ If @console is set and a prefix is used, sends data atomically.
30
+ Otherwise two separate write calls are used to save on message copying.
31
+
32
+ Config:
33
+ console (bool):
34
+ Flag how to handle non-framed data.
35
+ True: collect for crd/cwr, False: print incoming, None: ignore.
36
+ msg_prefix(int):
37
+ bytecode of prefix for messages (as opposed to console data)
38
+ """
39
+
40
+ cons = False
41
+ codec: Codec = None
42
+ liner: Liner = None
43
+
44
+ def __init__(self, stream: BaseBuf, cfg: dict):
45
+ #
46
+ # console: size of console buffer, 128 if True
47
+ # msg_prefix: int: code for start-of-packet
48
+ #
49
+ super().__init__(stream, cfg)
50
+ self.w_lock = Lock()
51
+
52
+ pref = cfg.get("msg_prefix")
53
+ if pref is not None:
54
+ pref = bytes((pref,))
55
+ self.pref = pref
56
+
57
+ cons = cfg.get("console", False)
58
+ if cons:
59
+ _CReader.__init__(self, cons)
60
+
61
+ async def setup(self):
62
+ await super().setup()
63
+ if self.cons is False:
64
+ from moat.util.liner import Liner # noqa:PLC0415
65
+
66
+ self.liner = await AC_use(self, Liner())
67
+
68
+ async def cwr(self, buf):
69
+ if not self.cons:
70
+ return
71
+ return await self.s.wr(buf)
72
+
73
+ def crd(self, buf) -> Awaitable:
74
+ return _CReader.crd(self, buf)
75
+
76
+ async def send(self, msg: Any) -> None:
77
+ try:
78
+ msg = self.codec.encode(msg)
79
+ except Exception:
80
+ log("MSG:\n%r", msg)
81
+ raise
82
+ async with self.w_lock:
83
+ if self.pref is not None:
84
+ if True: # self.cons:
85
+ msg = self.pref + msg # must be atomic
86
+ else:
87
+ await self.s.wr(self.pref)
88
+ await self.s.wr(msg)
89
+
90
+ async def recv(self) -> Any:
91
+ """
92
+ Receive the next object.
93
+ """
94
+ # Pre+postcondition: the codec does not have an object in progress.
95
+
96
+ buf = bytearray(64)
97
+ if self.pref is None:
98
+ # easy case
99
+ while True:
100
+ try:
101
+ r = next(self.codec)
102
+ except StopIteration:
103
+ n = await self.s.rd(buf)
104
+ self.codec.feed(memoryview(buf)[:n])
105
+ else:
106
+ if self.cons and isinstance(r, int) and r >= 0:
107
+ _CReader.cput(self, r)
108
+ else:
109
+ return r
110
+
111
+ while True:
112
+ b = bytearray(1)
113
+ # read until we get a prefix byte
114
+ if self.codec.unfeed(b) == 0:
115
+ n = await self.s.rd(buf)
116
+ self.codec.feed(memoryview(buf)[:n])
117
+ elif b == self.pref:
118
+ break
119
+ elif self.cons:
120
+ _CReader.cput(self, b[0])
121
+ elif self.liner is not None:
122
+ self.liner(b)
123
+
124
+ while True:
125
+ # read until we get an object
126
+ try:
127
+ return next(self.codec)
128
+ except StopIteration:
129
+ pass
130
+
131
+ n = await self.s.rd(buf)
132
+ self.codec.feed(memoryview(buf)[:n])
133
+
134
+
135
+ class _CBORMsgBlk(StackedMsg):
136
+ """
137
+ structured messages > chunked bytestrings
138
+
139
+ Use this if the layer below supports byte boundaries
140
+ (one bytestring-ized message per call).
141
+ """
142
+
143
+ async def send(self, msg):
144
+ await self.s.snd(self.codec.encode(msg))
145
+
146
+ async def recv(self):
147
+ m = await self.s.rcv()
148
+ return self.codec.decode(m)
@@ -0,0 +1,62 @@
1
+ """
2
+ Console data handling for stream layers.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from moat.lib.micro import Event
8
+
9
+
10
+ class _CReader:
11
+ """
12
+ A mix-in that processes incoming console data.
13
+ """
14
+
15
+ def __init__(self, cons: bool | int):
16
+ if cons is True:
17
+ try:
18
+ import machine # noqa: PLC0415,F401
19
+ except ImportError:
20
+ cons = 32768
21
+ else:
22
+ cons = 240
23
+ self.cevt = Event()
24
+ self.cpos = 0
25
+ self.cbuf = bytearray(cons)
26
+ self.cons = cons
27
+ self.intr = 0
28
+
29
+ async def crd(self, buf: bytearray):
30
+ """read waiting console data"""
31
+ if not self.cons:
32
+ raise EOFError
33
+ if not self.cpos:
34
+ await self.cevt.wait()
35
+ self.cevt = Event()
36
+ n = min(len(buf), self.cpos)
37
+ buf[:n] = self.cbuf[:n]
38
+ if n < self.cpos:
39
+ self.cbuf[: self.cpos - n] = self.cbuf[n : self.cpos]
40
+ self.cpos -= n
41
+ else:
42
+ self.cpos = 0
43
+ return n
44
+
45
+ def cput(self, b: int):
46
+ """store a byte in the console buffer"""
47
+ if self.cpos == len(self.cbuf):
48
+ if len(self.cbuf) > 100:
49
+ bfull = b"\n?BUF\n"
50
+ self.cbuf[0 : len(bfull)] = bfull
51
+ self.cpos = len(bfull)
52
+ else:
53
+ self.cpos = 0
54
+ if b != 3:
55
+ self.intr = 0
56
+ elif self.intr > 2:
57
+ raise KeyboardInterrupt
58
+ else:
59
+ self.intr += 1
60
+ self.cbuf[self.cpos] = b
61
+ self.cpos += 1
62
+ self.cevt.set()
@@ -0,0 +1,72 @@
1
+ """
2
+ SerialPacker protocol support for stream layers.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from moat.lib.micro import Lock
8
+ from moat.lib.stream import BaseBuf, StackedBlk
9
+
10
+ from ._console import _CReader
11
+
12
+
13
+ class SerialPackerBlkBuf(StackedBlk):
14
+ """
15
+ chunked bytestrings > SerialPacker-ized stream
16
+
17
+ Use this (and a CBORHandler and a Reliable) if your AIO stream
18
+ is unreliable (TTL serial).
19
+ """
20
+
21
+ cons = False
22
+
23
+ def __init__(self, stream: BaseBuf, frame: dict, console: bool | int = False):
24
+ super().__init__(None)
25
+
26
+ from serialpacker import SerialPacker # noqa: PLC0415
27
+
28
+ self.s = stream
29
+ self.p = SerialPacker(**frame)
30
+ self.buf = bytearray(16)
31
+ self.i = 0
32
+ self.n = 0
33
+ self.w_lock = Lock()
34
+ if console:
35
+ _CReader.__init__(self, console)
36
+
37
+ async def crd(self, buf):
38
+ if not self.cons:
39
+ raise EOFError
40
+ return await _CReader.crd(self, buf)
41
+
42
+ async def cwr(self, buf):
43
+ if not self.cons:
44
+ return
45
+ return await super().wr(buf)
46
+
47
+ async def recv(self):
48
+ while True:
49
+ while self.i < self.n:
50
+ msg = self.p.feed(self.buf[self.i])
51
+ self.i += 1
52
+ if isinstance(msg, int):
53
+ if self.cons:
54
+ _CReader.cput(self, msg)
55
+ elif msg is not None:
56
+ return msg
57
+
58
+ n = await self.s.rd(self.buf)
59
+ if not n:
60
+ raise EOFError
61
+ self.i = 0
62
+ self.n = n
63
+
64
+ async def send(self, msg):
65
+ h, msg, t = self.p.frame(msg)
66
+ async with self.w_lock:
67
+ if not self.cons:
68
+ await self.s.wr(h)
69
+ await self.s.wr(msg)
70
+ await self.s.wr(t)
71
+ else:
72
+ await self.s.wr(h + msg + t)
@@ -0,0 +1,285 @@
1
+ """
2
+ AnyIO stream adaptors for MoaT (CPython-specific).
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import anyio
8
+ import fcntl
9
+ import os
10
+ import sys
11
+ import termios
12
+ from contextlib import asynccontextmanager
13
+
14
+ from moat.util import CtxObj
15
+ from moat.lib.micro import AC_use
16
+ from moat.lib.stream import BaseBuf
17
+
18
+ from typing import TYPE_CHECKING # isort:skip
19
+
20
+ if TYPE_CHECKING:
21
+ from moat.micro.cmd.tree.dir import SubDispatch
22
+
23
+
24
+ class ProcessDeadError(RuntimeError):
25
+ """Process has died"""
26
+
27
+
28
+ class AnyioBuf(BaseBuf):
29
+ """
30
+ Adapts an anyio stream to MoaT.
31
+ """
32
+
33
+ async def stream(self) -> anyio.abc.ByteStream:
34
+ """
35
+ Create the stream to use.
36
+
37
+ Use `AC_use` to arrange for closing it. This class will not do it
38
+ for you.
39
+ """
40
+ raise NotImplementedError(f"Override {self.__class__.__name__}.stream")
41
+
42
+ async def wr(self, buf) -> int:
43
+ "basic send"
44
+ try:
45
+ await self.s.send(buf)
46
+ except (anyio.EndOfStream, anyio.ClosedResourceError):
47
+ raise EOFError from None
48
+ else:
49
+ return len(buf)
50
+
51
+ async def rd(self, buf) -> int:
52
+ "basic receive-into"
53
+ try:
54
+ res = await self.s.receive(len(buf))
55
+ except (anyio.EndOfStream, anyio.ClosedResourceError):
56
+ raise EOFError from None
57
+ else:
58
+ buf[: len(res)] = res
59
+ return len(res)
60
+
61
+
62
+ class FilenoBuf(BaseBuf):
63
+ """
64
+ Adapts a Unix file descriptor to MoaT.
65
+
66
+ """
67
+
68
+ rfd: int
69
+ wfd: int
70
+ rfl: int
71
+ wfl: int
72
+ term: bytes
73
+
74
+ def __init__(self, cfg, fd: int, wfd: int | None = None):
75
+ super().__init__(cfg)
76
+ self.rfd = fd
77
+ self.wfd = fd if wfd is None else wfd
78
+ self.term = None
79
+
80
+ async def stream(self) -> anyio.abc.ByteStream:
81
+ "Dummy here"
82
+ pass
83
+
84
+ async def setup(self):
85
+ "Change to nonblocking and raw"
86
+ self.rfl = fcntl.fcntl(self.rfd, fcntl.F_SETFL, 0)
87
+ if self.rfd != self.wfd:
88
+ self.wfl = fcntl.fcntl(self.wfd, fcntl.F_SETFL, 0)
89
+
90
+ fcntl.fcntl(self.rfd, fcntl.F_SETFL, self.rfl | os.O_NDELAY)
91
+ if self.rfd != self.wfd:
92
+ fcntl.fcntl(self.wfd, fcntl.F_SETFL, self.wfl | os.O_NDELAY)
93
+
94
+ try:
95
+ self.term = termios.tcgetattr(self.rfd)
96
+ except OSError:
97
+ pass
98
+ else:
99
+ new = self.term[:]
100
+ new[3] = new[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG
101
+ new[6][termios.VMIN] = 1
102
+ new[6][termios.VTIME] = 0
103
+ termios.tcsetattr(self.rfd, termios.TCSANOW, new)
104
+
105
+ async def teardown(self):
106
+ "Return to blocking and cooked"
107
+ fcntl.fcntl(self.rfd, fcntl.F_SETFL, self.rfl)
108
+ if self.rfd != self.wfd:
109
+ fcntl.fcntl(self.wfd, fcntl.F_SETFL, self.wfl)
110
+ if self.term is not None:
111
+ termios.tcsetattr(self.rfd, termios.TCSANOW, self.term)
112
+
113
+ async def wr(self, buf) -> int:
114
+ "basic send"
115
+ on = len(buf)
116
+ while n := len(buf):
117
+ await anyio.wait_writable(self.wfd)
118
+ nn = os.write(self.wfd, buf)
119
+ if nn <= 0:
120
+ raise OSError
121
+ if nn == n:
122
+ return on
123
+ buf = memoryview(buf)[nn:]
124
+
125
+ async def rd(self, buf) -> int:
126
+ "basic receive-into"
127
+ await anyio.wait_readable(self.rfd)
128
+ bf = os.read(self.rfd, len(buf))
129
+ buf[: len(bf)] = bf
130
+ return len(bf)
131
+
132
+
133
+ class RemoteBufAnyio(anyio.abc.ByteStream):
134
+ """
135
+ Adapts a MoaT buf stream to a remote buffer read/write
136
+
137
+ TODO: use remote iteration for receiving
138
+ """
139
+
140
+ def __init__(self, disp: SubDispatch):
141
+ self.disp = disp
142
+
143
+ async def receive(self, max_bytes=256):
144
+ "forward to ``.rd``"
145
+ return await self.disp.rd(n=max_bytes)
146
+
147
+ async def send(self, buf):
148
+ "forward to ``.wr``"
149
+ await self.disp.wr(b=buf)
150
+
151
+ async def aclose(self):
152
+ "no-op"
153
+
154
+ async def send_eof(self):
155
+ "not implemented"
156
+ raise NotImplementedError("EOF")
157
+
158
+
159
+ class BufAnyio(anyio.abc.ByteStream):
160
+ """
161
+ Adapts a MoaT Buf stream to an anyio bytestream.
162
+ """
163
+
164
+ par = None
165
+
166
+ def __init__(self, stream: BaseBuf):
167
+ self.stream = stream
168
+
169
+ async def __aenter__(self):
170
+ self.s = await self.stream.__aenter__()
171
+
172
+ async def __aexit__(self, *tb):
173
+ return await self.stream.__aexit__(*tb)
174
+
175
+ async def receive(self, max_bytes=256):
176
+ "forward to ``.rd``"
177
+ b = bytearray(max_bytes)
178
+ r = await self.s.rd(b)
179
+ if r == max_bytes:
180
+ return b
181
+ elif r <= max_bytes >> 2:
182
+ return bytes(b[:r])
183
+ else:
184
+ b = memoryview(b)
185
+ return b[:r]
186
+
187
+ async def send(self, buf):
188
+ "forward to ``.wr``"
189
+ await self.s.wr(buf)
190
+
191
+
192
+ class SingleAnyioBuf(AnyioBuf):
193
+ """
194
+ Adapts an AnyIO stream to MoaT.
195
+
196
+ The stream is passed to the class constructor and can only be used
197
+ once.
198
+ """
199
+
200
+ def __init__(self, stream):
201
+ self._s = stream
202
+
203
+ async def stream(self): # noqa:D102
204
+ return await AC_use(self, self._s)
205
+
206
+
207
+ class ProcessBuf(CtxObj, AnyioBuf):
208
+ """
209
+ A stream that connects to an external process.
210
+
211
+ Config:
212
+ - exec: path to the executable
213
+ - argv: Arguments
214
+ - env: environment vars
215
+
216
+ You can set these as attributes on the object, statically or in your
217
+ subclass's `setup` method. Configuration can then override them.
218
+ """
219
+
220
+ proc: anyio.Process = None
221
+ exec: str | None = None
222
+ cwd: str | None = None
223
+ argv: list[str] | None = None
224
+ env: dict[str, str] | None = None
225
+
226
+ def __init__(self, cfg, executable: str | None = None, **kw):
227
+ super().__init__(cfg)
228
+ kw.setdefault("stderr", sys.stderr)
229
+ self.kw = kw
230
+ self.exec = executable
231
+
232
+ def open_args(self):
233
+ """Return keyword arguments for `anyio.open_process`.
234
+
235
+ Default is whatever has been passed to the ProcessBuf constructor.
236
+ """
237
+ # Ugh, anyio doesn't accept 'executable'
238
+ if self.exec is not None:
239
+ # self.kw["executable"] = self.exec
240
+ self.argv[0] = self.exec
241
+ elif "/" in (a0 := str(self.argv[0])): # noqa:F841 # a0 unused
242
+ # self.kw["executable"] = a0
243
+ # self.argv[0] = a0.rsplit("/",1)[1]
244
+ pass
245
+ for k in ("cwd", "env"):
246
+ if (v := getattr(self, k)) is not None:
247
+ self.kw[k] = v
248
+
249
+ return self.kw
250
+
251
+ @asynccontextmanager
252
+ async def _ctx(self):
253
+ await self.setup()
254
+ proc = None
255
+ for k in ("exec", "argv", "env", "cwd"):
256
+ if (v := self.cfg.get(k, None)) is not None:
257
+ setattr(self, k, v)
258
+ if self.argv is None:
259
+ raise ValueError("Don't know what")
260
+
261
+ try:
262
+ async with await anyio.open_process(self.argv, **self.open_args()) as proc:
263
+ try:
264
+ async with SingleAnyioBuf(
265
+ anyio.streams.stapled.StapledByteStream(proc.stdin, proc.stdout),
266
+ ) as s:
267
+ yield s
268
+ await proc.wait()
269
+ except BaseException:
270
+ try:
271
+ proc.kill()
272
+ with anyio.CancelScope(shield=True):
273
+ await proc.wait()
274
+ raise
275
+ finally:
276
+ proc = None # noqa:PLW2901
277
+ finally:
278
+ if proc is not None and proc.returncode != 0 and proc.returncode != -9:
279
+ raise ProcessDeadError(f"{self} died with {proc.returncode}")
280
+
281
+ async def setup(self): # noqa:D102
282
+ pass
283
+
284
+ async def stream(self): # noqa:D102
285
+ raise RuntimeError("should not be called")