libbeachcomber 0.1.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.
@@ -0,0 +1,47 @@
1
+ """beachcomber — Python client SDK for the comb shell-state daemon.
2
+
3
+ Quick start::
4
+
5
+ from beachcomber import Client
6
+
7
+ client = Client()
8
+ result = client.get("git.branch", path="/path/to/repo")
9
+ if result.is_hit:
10
+ print(result.data) # "main"
11
+ print(result.age_ms) # 234
12
+ print(result.stale) # False
13
+
14
+ # Full provider object
15
+ result = client.get("git", path="/path/to/repo")
16
+ if result.is_hit:
17
+ print(result["branch"]) # subscript into dict data
18
+
19
+ # Force recomputation
20
+ client.poke("git", path="/path/to/repo")
21
+
22
+ # Persistent connection for multiple queries
23
+ with client.session() as session:
24
+ session.set_context("/path/to/repo")
25
+ branch = session.get("git.branch")
26
+ host = session.get("hostname")
27
+
28
+ # List providers and daemon status
29
+ providers = client.list()
30
+ status = client.status()
31
+ """
32
+
33
+ from .client import Client, Session
34
+ from .exceptions import CombError, DaemonNotRunning, ProtocolError, ServerError
35
+ from .result import CombResult
36
+
37
+ __all__ = [
38
+ "Client",
39
+ "Session",
40
+ "CombResult",
41
+ "CombError",
42
+ "DaemonNotRunning",
43
+ "ProtocolError",
44
+ "ServerError",
45
+ ]
46
+
47
+ __version__ = "0.1.0"
@@ -0,0 +1,319 @@
1
+ """Client and Session classes for the beachcomber daemon.
2
+
3
+ Typical usage::
4
+
5
+ from beachcomber import Client
6
+
7
+ client = Client()
8
+ result = client.get("git.branch", path="/path/to/repo")
9
+ if result.is_hit:
10
+ print(result.data)
11
+
12
+ For multiple queries on one connection use a session::
13
+
14
+ with client.session() as session:
15
+ session.set_context("/path/to/repo")
16
+ branch = session.get("git.branch")
17
+ status = session.get("git")
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import io
23
+ import socket
24
+ from contextlib import contextmanager
25
+ from typing import Any, Generator, List, Optional
26
+
27
+ from .discovery import discover_socket_path
28
+ from .exceptions import DaemonNotRunning, ProtocolError
29
+ from .protocol import (
30
+ build_context_request,
31
+ build_get_request,
32
+ build_list_request,
33
+ build_poke_request,
34
+ build_status_request,
35
+ decode_response,
36
+ )
37
+ from .result import CombResult
38
+
39
+ # Default socket timeout in seconds (matches Rust client: 100 ms).
40
+ _DEFAULT_TIMEOUT: float = 0.1
41
+
42
+
43
+ def _connect(socket_path: str, timeout: float) -> socket.socket:
44
+ """Open a Unix domain socket connection to the daemon.
45
+
46
+ Args:
47
+ socket_path: Absolute path to the Unix domain socket.
48
+ timeout: Read/write timeout in seconds.
49
+
50
+ Returns:
51
+ Connected :class:`socket.socket`.
52
+
53
+ Raises:
54
+ DaemonNotRunning: If the connection is refused or the socket does
55
+ not exist.
56
+ """
57
+ try:
58
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
59
+ sock.settimeout(timeout)
60
+ sock.connect(socket_path)
61
+ return sock
62
+ except (ConnectionRefusedError, FileNotFoundError, OSError) as exc:
63
+ raise DaemonNotRunning(socket_path) from exc
64
+
65
+
66
+ def _send_recv(sock: socket.socket, request: bytes) -> dict[str, Any]:
67
+ """Send a request and read one response line.
68
+
69
+ Args:
70
+ sock: Connected socket.
71
+ request: Encoded request bytes (must include trailing newline).
72
+
73
+ Returns:
74
+ Parsed response dict (``ok`` has already been verified to be
75
+ ``True``).
76
+
77
+ Raises:
78
+ ProtocolError: On I/O or parse failure.
79
+ ServerError: If the daemon returns ``ok: false``.
80
+ """
81
+ try:
82
+ sock.sendall(request)
83
+ except OSError as exc:
84
+ raise ProtocolError(f"failed to send request: {exc}") from exc
85
+
86
+ # Read until newline using a file-like wrapper.
87
+ reader = sock.makefile("r", encoding="utf-8")
88
+ try:
89
+ line = reader.readline()
90
+ except OSError as exc:
91
+ raise ProtocolError(f"failed to read response: {exc}") from exc
92
+ finally:
93
+ reader.detach() # Do not close the underlying socket.
94
+
95
+ return decode_response(line)
96
+
97
+
98
+ def _result_from_response(resp: dict[str, Any]) -> CombResult:
99
+ """Build a :class:`CombResult` from a parsed ``get`` response dict."""
100
+ data = resp.get("data")
101
+ age_ms = int(resp.get("age_ms", 0) or 0)
102
+ stale = bool(resp.get("stale", False))
103
+ return CombResult(ok=True, data=data, age_ms=age_ms, stale=stale)
104
+
105
+
106
+ class Client:
107
+ """One-shot client for the beachcomber daemon.
108
+
109
+ Each method opens a new socket connection, sends one request, reads
110
+ the response, then closes the connection. This is simple and safe
111
+ for occasional queries.
112
+
113
+ For repeated queries (e.g. populating a shell prompt) prefer
114
+ :meth:`session` which reuses the connection.
115
+
116
+ Args:
117
+ socket_path: Explicit path to the daemon socket. If ``None`` the
118
+ path is auto-discovered via
119
+ :func:`~beachcomber.discovery.discover_socket_path`.
120
+ timeout: Socket read/write timeout in seconds. Default ``0.1``.
121
+ """
122
+
123
+ def __init__(
124
+ self,
125
+ socket_path: Optional[str] = None,
126
+ timeout: float = _DEFAULT_TIMEOUT,
127
+ ) -> None:
128
+ self._socket_path = socket_path
129
+ self._timeout = timeout
130
+
131
+ def _resolve_path(self) -> str:
132
+ return self._socket_path or discover_socket_path()
133
+
134
+ def get(self, key: str, path: Optional[str] = None) -> CombResult:
135
+ """Read a cached value from the daemon.
136
+
137
+ Args:
138
+ key: Provider key. Use ``"provider.field"`` for a single
139
+ scalar (e.g. ``"git.branch"``) or ``"provider"`` for the
140
+ full provider object (e.g. ``"git"``).
141
+ path: Working-directory path for per-directory providers.
142
+ Global providers (e.g. ``"hostname"``) ignore this.
143
+
144
+ Returns:
145
+ :class:`~beachcomber.result.CombResult` with ``is_hit``
146
+ ``True`` when a cached value exists.
147
+
148
+ Raises:
149
+ DaemonNotRunning: If the socket cannot be reached.
150
+ ServerError: If the daemon returns an error response.
151
+ ProtocolError: On I/O or JSON parse failure.
152
+ """
153
+ sock = _connect(self._resolve_path(), self._timeout)
154
+ try:
155
+ resp = _send_recv(sock, build_get_request(key, path))
156
+ finally:
157
+ sock.close()
158
+ return _result_from_response(resp)
159
+
160
+ def poke(self, key: str, path: Optional[str] = None) -> None:
161
+ """Trigger recomputation of a provider.
162
+
163
+ The daemon will recompute the value in the background. This is
164
+ fire-and-forget — the method returns once the daemon acknowledges
165
+ the poke.
166
+
167
+ Args:
168
+ key: Provider key to recompute.
169
+ path: Working-directory path for per-directory providers.
170
+
171
+ Raises:
172
+ DaemonNotRunning: If the socket cannot be reached.
173
+ ServerError: If the daemon returns an error response.
174
+ ProtocolError: On I/O or JSON parse failure.
175
+ """
176
+ sock = _connect(self._resolve_path(), self._timeout)
177
+ try:
178
+ _send_recv(sock, build_poke_request(key, path))
179
+ finally:
180
+ sock.close()
181
+
182
+ def list(self) -> List[dict[str, Any]]:
183
+ """Return available providers from the daemon.
184
+
185
+ Returns:
186
+ List of provider dicts. Each dict has at least ``"name"``,
187
+ ``"global"``, and ``"fields"`` keys.
188
+
189
+ Raises:
190
+ DaemonNotRunning: If the socket cannot be reached.
191
+ ServerError: If the daemon returns an error response.
192
+ ProtocolError: On I/O or JSON parse failure.
193
+ """
194
+ sock = _connect(self._resolve_path(), self._timeout)
195
+ try:
196
+ resp = _send_recv(sock, build_list_request())
197
+ finally:
198
+ sock.close()
199
+ return resp.get("data", [])
200
+
201
+ def status(self) -> dict[str, Any]:
202
+ """Return daemon scheduler and cache status.
203
+
204
+ Returns:
205
+ Status dict as returned by the daemon.
206
+
207
+ Raises:
208
+ DaemonNotRunning: If the socket cannot be reached.
209
+ ServerError: If the daemon returns an error response.
210
+ ProtocolError: On I/O or JSON parse failure.
211
+ """
212
+ sock = _connect(self._resolve_path(), self._timeout)
213
+ try:
214
+ resp = _send_recv(sock, build_status_request())
215
+ finally:
216
+ sock.close()
217
+ return resp.get("data", {})
218
+
219
+ @contextmanager
220
+ def session(self) -> Generator[Session, None, None]:
221
+ """Open a persistent connection as a context manager.
222
+
223
+ Yields a :class:`Session` that reuses a single socket for all
224
+ operations within the ``with`` block.
225
+
226
+ Example::
227
+
228
+ with client.session() as session:
229
+ session.set_context("/my/repo")
230
+ result = session.get("git.branch")
231
+
232
+ Raises:
233
+ DaemonNotRunning: If the socket cannot be reached.
234
+ """
235
+ sock = _connect(self._resolve_path(), self._timeout)
236
+ session = Session(sock)
237
+ try:
238
+ yield session
239
+ finally:
240
+ sock.close()
241
+
242
+
243
+ class Session:
244
+ """Persistent connection to the beachcomber daemon.
245
+
246
+ Reuses a single Unix domain socket across multiple operations.
247
+ Create via :meth:`Client.session` rather than directly.
248
+
249
+ Args:
250
+ sock: Already-connected :class:`socket.socket`.
251
+ """
252
+
253
+ def __init__(self, sock: socket.socket) -> None:
254
+ self._sock = sock
255
+
256
+ def set_context(self, path: str) -> None:
257
+ """Set the default working-directory path for this connection.
258
+
259
+ After calling this, :meth:`get` and :meth:`poke` calls do not
260
+ need an explicit ``path`` argument.
261
+
262
+ Args:
263
+ path: Absolute path to set as the session context.
264
+
265
+ Raises:
266
+ ServerError: If the daemon returns an error response.
267
+ ProtocolError: On I/O or JSON parse failure.
268
+ """
269
+ _send_recv(self._sock, build_context_request(path))
270
+
271
+ def get(self, key: str, path: Optional[str] = None) -> CombResult:
272
+ """Read a cached value using the persistent connection.
273
+
274
+ Args:
275
+ key: Provider key (``"git.branch"``, ``"git"``, etc.).
276
+ path: Optional path override. If omitted and
277
+ :meth:`set_context` has been called, the session context
278
+ is used by the daemon.
279
+
280
+ Returns:
281
+ :class:`~beachcomber.result.CombResult`.
282
+
283
+ Raises:
284
+ ServerError: If the daemon returns an error response.
285
+ ProtocolError: On I/O or JSON parse failure.
286
+ """
287
+ resp = _send_recv(self._sock, build_get_request(key, path))
288
+ return _result_from_response(resp)
289
+
290
+ def poke(self, key: str, path: Optional[str] = None) -> None:
291
+ """Trigger recomputation via the persistent connection.
292
+
293
+ Args:
294
+ key: Provider key to recompute.
295
+ path: Optional path override.
296
+
297
+ Raises:
298
+ ServerError: If the daemon returns an error response.
299
+ ProtocolError: On I/O or JSON parse failure.
300
+ """
301
+ _send_recv(self._sock, build_poke_request(key, path))
302
+
303
+ def list(self) -> List[dict[str, Any]]:
304
+ """Return available providers via the persistent connection.
305
+
306
+ Returns:
307
+ List of provider dicts.
308
+ """
309
+ resp = _send_recv(self._sock, build_list_request())
310
+ return resp.get("data", [])
311
+
312
+ def status(self) -> dict[str, Any]:
313
+ """Return daemon status via the persistent connection.
314
+
315
+ Returns:
316
+ Status dict as returned by the daemon.
317
+ """
318
+ resp = _send_recv(self._sock, build_status_request())
319
+ return resp.get("data", {})
@@ -0,0 +1,35 @@
1
+ """Socket path discovery for the beachcomber daemon.
2
+
3
+ Discovery order:
4
+ 1. ``$XDG_RUNTIME_DIR/beachcomber/sock``
5
+ 2. ``$TMPDIR/beachcomber-<uid>/sock``
6
+ 3. ``/tmp/beachcomber-<uid>/sock``
7
+ """
8
+
9
+ import os
10
+
11
+
12
+ def get_uid() -> int:
13
+ """Return the effective user ID of the current process."""
14
+ return os.geteuid()
15
+
16
+
17
+ def discover_socket_path() -> str:
18
+ """Return the expected socket path for the running daemon.
19
+
20
+ Checks the standard locations in order. Returns the first path that
21
+ *should* exist according to the discovery rules — callers are
22
+ responsible for verifying the socket is reachable.
23
+
24
+ Returns:
25
+ Absolute path string for the Unix domain socket.
26
+ """
27
+ xdg_runtime = os.environ.get("XDG_RUNTIME_DIR", "")
28
+ if xdg_runtime:
29
+ candidate = os.path.join(xdg_runtime, "beachcomber", "sock")
30
+ if os.path.exists(candidate):
31
+ return candidate
32
+
33
+ uid = get_uid()
34
+ tmpdir = os.environ.get("TMPDIR", "/tmp").rstrip("/")
35
+ return os.path.join(tmpdir, f"beachcomber-{uid}", "sock")
@@ -0,0 +1,35 @@
1
+ """Exceptions raised by the beachcomber client."""
2
+
3
+
4
+ class CombError(Exception):
5
+ """Base exception for all beachcomber client errors."""
6
+
7
+
8
+ class DaemonNotRunning(CombError):
9
+ """Raised when the beachcomber daemon socket cannot be connected to.
10
+
11
+ This usually means ``comb daemon`` is not running. Start it before
12
+ using the client.
13
+ """
14
+
15
+ def __init__(self, socket_path: str) -> None:
16
+ self.socket_path = socket_path
17
+ super().__init__(
18
+ f"beachcomber daemon is not running (tried {socket_path})"
19
+ )
20
+
21
+
22
+ class ServerError(CombError):
23
+ """Raised when the daemon returns ``ok: false`` in its response.
24
+
25
+ Attributes:
26
+ message: The error string from the daemon.
27
+ """
28
+
29
+ def __init__(self, message: str) -> None:
30
+ self.message = message
31
+ super().__init__(f"daemon error: {message}")
32
+
33
+
34
+ class ProtocolError(CombError):
35
+ """Raised when a response cannot be parsed as valid JSON or is malformed."""
@@ -0,0 +1,119 @@
1
+ """Newline-delimited JSON protocol helpers for the beachcomber daemon.
2
+
3
+ All requests are JSON objects with an ``"op"`` field. All responses are
4
+ JSON objects with an ``"ok"`` field that is ``true`` or ``false``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from typing import Any, Optional
11
+
12
+ from .exceptions import ProtocolError, ServerError
13
+
14
+
15
+ def encode_request(op: str, **fields: Any) -> bytes:
16
+ """Serialise a request object to a newline-terminated JSON bytes value.
17
+
18
+ Args:
19
+ op: The operation name (``"get"``, ``"poke"``, ``"context"``,
20
+ ``"list"``, or ``"status"``).
21
+ **fields: Additional key/value pairs to include in the request.
22
+
23
+ Returns:
24
+ UTF-8 encoded JSON followed by a newline byte.
25
+ """
26
+ payload: dict[str, Any] = {"op": op}
27
+ payload.update(fields)
28
+ return (json.dumps(payload, separators=(",", ":")) + "\n").encode()
29
+
30
+
31
+ def decode_response(line: str) -> dict[str, Any]:
32
+ """Parse a single newline-delimited JSON response line.
33
+
34
+ Args:
35
+ line: A single text line received from the daemon.
36
+
37
+ Returns:
38
+ Parsed response dict.
39
+
40
+ Raises:
41
+ ProtocolError: If the line is not valid JSON or is missing the
42
+ ``"ok"`` field.
43
+ ServerError: If the response contains ``"ok": false``.
44
+ """
45
+ line = line.strip()
46
+ if not line:
47
+ raise ProtocolError("received empty response from daemon")
48
+
49
+ try:
50
+ data: dict[str, Any] = json.loads(line)
51
+ except json.JSONDecodeError as exc:
52
+ raise ProtocolError(f"invalid JSON from daemon: {exc}") from exc
53
+
54
+ if not isinstance(data, dict):
55
+ raise ProtocolError(f"expected JSON object, got {type(data).__name__}")
56
+
57
+ ok = data.get("ok")
58
+ if ok is None:
59
+ raise ProtocolError("response missing 'ok' field")
60
+
61
+ if ok is False:
62
+ error_msg = data.get("error", "unknown error")
63
+ raise ServerError(str(error_msg))
64
+
65
+ return data
66
+
67
+
68
+ def build_get_request(key: str, path: Optional[str] = None) -> bytes:
69
+ """Build a ``get`` request.
70
+
71
+ Args:
72
+ key: Provider key, e.g. ``"git.branch"`` or ``"git"``.
73
+ path: Optional working-directory path for per-directory providers.
74
+
75
+ Returns:
76
+ Encoded request bytes.
77
+ """
78
+ kwargs: dict[str, Any] = {"key": key}
79
+ if path is not None:
80
+ kwargs["path"] = path
81
+ return encode_request("get", **kwargs)
82
+
83
+
84
+ def build_poke_request(key: str, path: Optional[str] = None) -> bytes:
85
+ """Build a ``poke`` request.
86
+
87
+ Args:
88
+ key: Provider key to recompute.
89
+ path: Optional working-directory path.
90
+
91
+ Returns:
92
+ Encoded request bytes.
93
+ """
94
+ kwargs: dict[str, Any] = {"key": key}
95
+ if path is not None:
96
+ kwargs["path"] = path
97
+ return encode_request("poke", **kwargs)
98
+
99
+
100
+ def build_context_request(path: str) -> bytes:
101
+ """Build a ``context`` request to set the default path for a session.
102
+
103
+ Args:
104
+ path: Directory path to set as the connection context.
105
+
106
+ Returns:
107
+ Encoded request bytes.
108
+ """
109
+ return encode_request("context", path=path)
110
+
111
+
112
+ def build_list_request() -> bytes:
113
+ """Build a ``list`` request."""
114
+ return encode_request("list")
115
+
116
+
117
+ def build_status_request() -> bytes:
118
+ """Build a ``status`` request."""
119
+ return encode_request("status")
@@ -0,0 +1,59 @@
1
+ """CombResult dataclass returned by client get() calls."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Optional
7
+
8
+
9
+ @dataclass
10
+ class CombResult:
11
+ """Result of a ``get`` query to the beachcomber daemon.
12
+
13
+ Attributes:
14
+ ok: Whether the daemon responded without error (always ``True``
15
+ when returned from the client — errors raise exceptions).
16
+ data: The cached value. ``None`` on a cache miss. May be a string,
17
+ number, bool, or dict depending on the provider and key.
18
+ age_ms: How many milliseconds old the cached value is. ``0`` on a
19
+ miss.
20
+ stale: ``True`` when the value is past its TTL but no fresh value
21
+ is available yet.
22
+ error: Error message string when ``ok`` is ``False``. The client
23
+ raises :class:`~beachcomber.exceptions.ServerError` for these,
24
+ so callers typically do not see this field set.
25
+ """
26
+
27
+ ok: bool = True
28
+ data: Any = None
29
+ age_ms: int = 0
30
+ stale: bool = False
31
+ error: Optional[str] = None
32
+
33
+ @property
34
+ def is_hit(self) -> bool:
35
+ """``True`` when the cache contains a value for this key."""
36
+ return self.data is not None
37
+
38
+ def __getitem__(self, key: str) -> Any:
39
+ """Access a field from dict data by subscript.
40
+
41
+ Useful when querying a full provider that returns an object,
42
+ e.g. ``result["branch"]`` after ``client.get("git")``.
43
+
44
+ Args:
45
+ key: Field name to look up in the data dict.
46
+
47
+ Returns:
48
+ The field value.
49
+
50
+ Raises:
51
+ TypeError: If ``data`` is not a dict.
52
+ KeyError: If ``key`` is not present in the dict.
53
+ """
54
+ if not isinstance(self.data, dict):
55
+ raise TypeError(
56
+ f"CombResult.data is {type(self.data).__name__}, not dict; "
57
+ "subscript access is only valid for object responses"
58
+ )
59
+ return self.data[key]
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: libbeachcomber
3
+ Version: 0.1.0
4
+ Summary: Python client SDK for the beachcomber (comb) shell-state daemon
5
+ Project-URL: Homepage, https://github.com/NavistAu/beachcomber
6
+ Project-URL: Repository, https://github.com/NavistAu/beachcomber
7
+ Project-URL: Issues, https://github.com/NavistAu/beachcomber/issues
8
+ Author: NavistAu
9
+ License: MIT
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: POSIX
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: System :: Shells
20
+ Requires-Python: >=3.9
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=7; extra == 'dev'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # beachcomber Python SDK
26
+
27
+ Python client for the [beachcomber](https://github.com/jhogendorn/beachcomber) (`comb`) shell-state daemon.
28
+
29
+ ## Requirements
30
+
31
+ - Python 3.9+
32
+ - No external dependencies (stdlib only)
33
+ - A running `comb` daemon
34
+
35
+ ## Installation
36
+
37
+ ```sh
38
+ pip install beachcomber
39
+ ```
40
+
41
+ Or with `uv`:
42
+
43
+ ```sh
44
+ uv add beachcomber
45
+ ```
46
+
47
+ ## Quick start
48
+
49
+ ```python
50
+ from beachcomber import Client
51
+
52
+ client = Client()
53
+
54
+ # Read a single field
55
+ result = client.get("git.branch", path="/path/to/repo")
56
+ if result.is_hit:
57
+ print(result.data) # "main"
58
+ print(result.age_ms) # 234
59
+ print(result.stale) # False
60
+
61
+ # Read a full provider (returns dict)
62
+ result = client.get("git", path="/path/to/repo")
63
+ if result.is_hit:
64
+ print(result["branch"]) # "main"
65
+ print(result["dirty"]) # False
66
+
67
+ # Force recomputation
68
+ client.poke("git", path="/path/to/repo")
69
+
70
+ # List available providers
71
+ providers = client.list()
72
+
73
+ # Daemon status
74
+ status = client.status()
75
+ ```
76
+
77
+ ## Sessions
78
+
79
+ For multiple queries use a session to reuse a single connection:
80
+
81
+ ```python
82
+ with client.session() as session:
83
+ session.set_context("/path/to/repo")
84
+ branch = session.get("git.branch")
85
+ dirty = session.get("git.dirty")
86
+ hostname = session.get("hostname")
87
+ ```
88
+
89
+ ## Custom socket path
90
+
91
+ ```python
92
+ client = Client(socket_path="/run/user/1000/beachcomber/sock")
93
+ ```
94
+
95
+ ## Socket discovery
96
+
97
+ The SDK discovers the daemon socket at:
98
+
99
+ 1. `$XDG_RUNTIME_DIR/beachcomber/sock`
100
+ 2. `$TMPDIR/beachcomber-<uid>/sock`
101
+ 3. `/tmp/beachcomber-<uid>/sock`
102
+
103
+ ## Exceptions
104
+
105
+ | Exception | When raised |
106
+ |---|---|
107
+ | `DaemonNotRunning` | Cannot connect to the socket |
108
+ | `ServerError` | Daemon returns `ok: false` |
109
+ | `ProtocolError` | Malformed JSON or I/O failure |
110
+ | `CombError` | Base class for all SDK errors |
@@ -0,0 +1,9 @@
1
+ libbeachcomber/__init__.py,sha256=1peyIUg6de5QRlmQpSNzMlyZJ0Pj3iAavHhFLrArHCg,1219
2
+ libbeachcomber/client.py,sha256=NI7KTwVZ66Klq-bhIpsbgFmKTMrNaOHEhKfVV8ek2co,10323
3
+ libbeachcomber/discovery.py,sha256=4vKonS5cLHo4tPiio6LAWWWMI4CCbs3ENOxf7op1v_U,1039
4
+ libbeachcomber/exceptions.py,sha256=u-4UOMQvXX3NJHVFFC3tjy8HdGTLQGbo9Ou_oFAQ6YM,973
5
+ libbeachcomber/protocol.py,sha256=JDLcdp9uwHC3hEjWM8jqE2En89g6A1zWvZNek2_xDHI,3296
6
+ libbeachcomber/result.py,sha256=kQiCdRXfRyG4jODbJ_Z6BCEUt8b-40Th61zFbAIMncE,2003
7
+ libbeachcomber-0.1.0.dist-info/METADATA,sha256=VYtD3PqBeXaXSQ5TNMxtO-potxCm6wk8XUNvQ7Xa858,2715
8
+ libbeachcomber-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
9
+ libbeachcomber-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any