plexi-sdk 0.4.0__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.
plexi_sdk/_pipe.py ADDED
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ import socket
4
+ import struct
5
+ import threading
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from ._emitter import _emit
9
+
10
+ if TYPE_CHECKING:
11
+ from ._app import App
12
+
13
+
14
+ # ── Pipe ──────────────────────────────────────────────────────────────────────
15
+
16
+ class Pipe:
17
+ """Handle for a typed pipe. For binary mode, call connect() after PipeOpened."""
18
+
19
+ def __init__(self, pipe_id: str, mode: str, direction: str, app: "App"):
20
+ self.pipe_id = pipe_id
21
+ self.mode = mode
22
+ self.direction = direction
23
+ self._app = app
24
+ self._sock: "socket.socket | None" = None
25
+ self._connected = threading.Event()
26
+
27
+ def _on_opened(self, socket_path: str) -> None:
28
+ """Called by App when PipeOpened arrives for this pipe_id."""
29
+ if self.mode == "binary":
30
+ try:
31
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
32
+ sock.connect(socket_path)
33
+ self._sock = sock
34
+ self._connected.set()
35
+ except OSError as e:
36
+ self._app.emit.error(f"pipe_open failed pipe_id={self.pipe_id}: {e}")
37
+
38
+ def connect(self, timeout: float = 5.0) -> bool:
39
+ """Wait for the pipe to be connected. Returns True on success."""
40
+ return self._connected.wait(timeout=timeout)
41
+
42
+ def read_frame(self) -> "bytes | None":
43
+ """Read one length-prefixed frame. Blocks. Returns None on EOF/error/EOS."""
44
+ if not self._sock:
45
+ return None
46
+ try:
47
+ header = self._recv_exact(4)
48
+ if header is None:
49
+ return None
50
+ length = struct.unpack(">I", header)[0]
51
+ if length == 0:
52
+ return None # EOS sentinel from host
53
+ return self._recv_exact(length)
54
+ except OSError as e:
55
+ self._app.emit.error(f"pipe read_frame error pipe_id={self.pipe_id}: {e}")
56
+ return None
57
+
58
+ def write_frame(self, data: bytes) -> None:
59
+ """Write one length-prefixed frame."""
60
+ if not self._sock:
61
+ return
62
+ try:
63
+ header = struct.pack(">I", len(data))
64
+ self._sock.sendall(header + data)
65
+ except OSError as e:
66
+ self._app.emit.error(f"pipe write_frame error pipe_id={self.pipe_id}: {e}")
67
+
68
+ def send(self, payload: Any) -> None:
69
+ """JSON-mode pipe send."""
70
+ _emit({"type": "pipe_send", "pipe_id": self.pipe_id, "payload": payload})
71
+
72
+ def _recv_exact(self, n: int) -> "bytes | None":
73
+ buf = b""
74
+ while len(buf) < n:
75
+ chunk = self._sock.recv(n - len(buf)) # type: ignore[union-attr]
76
+ if not chunk:
77
+ return None
78
+ buf += chunk
79
+ return buf
80
+
81
+ def close(self) -> None:
82
+ if self._sock:
83
+ try:
84
+ # shutdown unblocks any thread blocked in recv() before close().
85
+ self._sock.shutdown(socket.SHUT_RDWR)
86
+ except OSError:
87
+ pass
88
+ try:
89
+ self._sock.close()
90
+ except OSError:
91
+ pass
92
+ self._sock = None
plexi_sdk/_protocol.py ADDED
@@ -0,0 +1,48 @@
1
+ # AUTO-GENERATED — do not edit by hand.
2
+ # Regenerate with: just gen-schema
3
+ # Protocol: pgap/3
4
+
5
+ from __future__ import annotations
6
+ from dataclasses import dataclass
7
+ from typing import Optional
8
+
9
+ PROTOCOL_VERSION = "pgap/3"
10
+
11
+
12
+ @dataclass
13
+ class AiResponse:
14
+ """Result of Emitter.ai_query. tokens_in/tokens_out are zero on error."""
15
+ tokens_in: int
16
+ tokens_out: int
17
+ content: Optional[str] = None
18
+ error: Optional[str] = None
19
+
20
+
21
+ @dataclass
22
+ class MidiPortInfo:
23
+ """One MIDI port. Mirrors MidiPortWire in the Rust protocol."""
24
+ id: str
25
+ name: str
26
+ default: bool
27
+
28
+
29
+ @dataclass
30
+ class MidiDeviceList:
31
+ """Result of Emitter.list_midi_devices."""
32
+ inputs: list
33
+ outputs: list
34
+
35
+
36
+ @dataclass
37
+ class AudioDeviceInfo:
38
+ """One audio device. Mirrors AudioDeviceWire in the Rust protocol."""
39
+ id: str
40
+ name: str
41
+ default: bool
42
+
43
+
44
+ @dataclass
45
+ class AudioDeviceList:
46
+ """Result of Emitter.list_audio_devices."""
47
+ inputs: list
48
+ outputs: list