libbeachcomber 0.9.0__tar.gz → 0.10.0__tar.gz
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.
- libbeachcomber-0.10.0/PKG-INFO +233 -0
- libbeachcomber-0.10.0/README.md +209 -0
- {libbeachcomber-0.9.0 → libbeachcomber-0.10.0}/conformance_runner.py +76 -113
- {libbeachcomber-0.9.0 → libbeachcomber-0.10.0}/libbeachcomber/__init__.py +20 -21
- {libbeachcomber-0.9.0 → libbeachcomber-0.10.0}/libbeachcomber/_native.py +19 -37
- libbeachcomber-0.10.0/libbeachcomber/client.py +605 -0
- libbeachcomber-0.10.0/libbeachcomber/discovery.py +73 -0
- libbeachcomber-0.10.0/libbeachcomber/exceptions.py +143 -0
- libbeachcomber-0.10.0/libbeachcomber/types.py +102 -0
- {libbeachcomber-0.9.0 → libbeachcomber-0.10.0}/pyproject.toml +1 -1
- {libbeachcomber-0.9.0 → libbeachcomber-0.10.0}/tests/conftest.py +6 -65
- libbeachcomber-0.10.0/tests/test_api_surface.py +89 -0
- libbeachcomber-0.10.0/tests/test_client_options.py +57 -0
- libbeachcomber-0.10.0/tests/test_closed.py +28 -0
- libbeachcomber-0.10.0/tests/test_discovery.py +91 -0
- libbeachcomber-0.10.0/tests/test_exceptions.py +60 -0
- libbeachcomber-0.10.0/tests/test_integration.py +289 -0
- libbeachcomber-0.9.0/PKG-INFO +0 -145
- libbeachcomber-0.9.0/README.md +0 -121
- libbeachcomber-0.9.0/libbeachcomber/client.py +0 -583
- libbeachcomber-0.9.0/libbeachcomber/discovery.py +0 -82
- libbeachcomber-0.9.0/libbeachcomber/exceptions.py +0 -149
- libbeachcomber-0.9.0/libbeachcomber/result.py +0 -59
- libbeachcomber-0.9.0/libbeachcomber/types.py +0 -75
- libbeachcomber-0.9.0/tests/test_discovery.py +0 -77
- libbeachcomber-0.9.0/tests/test_exceptions.py +0 -76
- libbeachcomber-0.9.0/tests/test_integration.py +0 -157
- libbeachcomber-0.9.0/tests/test_result.py +0 -87
- {libbeachcomber-0.9.0 → libbeachcomber-0.10.0}/.gitignore +0 -0
- {libbeachcomber-0.9.0 → libbeachcomber-0.10.0}/tests/__init__.py +0 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: libbeachcomber
|
|
3
|
+
Version: 0.10.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/NavistAu/beachcomber) (`comb`) shell-state daemon.
|
|
28
|
+
It follows the SDK API table ([`docs/sdk-api.md`](../../docs/sdk-api.md)): the
|
|
29
|
+
same methods, options, value shapes and error kinds as every other SDK, which
|
|
30
|
+
`tests/test_api_surface.py` checks against [`sdks/api.json`](../api.json).
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- Python 3.9+
|
|
35
|
+
- No external dependencies (stdlib `ctypes` binds directly to the native
|
|
36
|
+
`libbeachcomber.{so,dylib}` — no subprocess, no socket code in this SDK)
|
|
37
|
+
- A running `comb` daemon (or `autostart` left enabled, the default)
|
|
38
|
+
- `libbeachcomber.{so,dylib}` discoverable — see "Library discovery" below
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
pip install libbeachcomber
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or with `uv`:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
uv add libbeachcomber
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Quick start
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import os
|
|
56
|
+
|
|
57
|
+
from libbeachcomber import Client
|
|
58
|
+
|
|
59
|
+
client = Client()
|
|
60
|
+
|
|
61
|
+
# What `comb get git.branch` prints in that directory; None on a miss
|
|
62
|
+
branch = client.get("git.branch", cwd="/path/to/repo") # "main"
|
|
63
|
+
|
|
64
|
+
# A full provider (a dict)
|
|
65
|
+
git = client.get("git", cwd="/path/to/repo")
|
|
66
|
+
print(git["branch"], git["dirty"])
|
|
67
|
+
|
|
68
|
+
# The caller's environment for env.* refs; force a run, and wait for it
|
|
69
|
+
client.get("aws.profile", cwd="/path/to/repo", env=dict(os.environ))
|
|
70
|
+
client.get("git.branch", cwd="/path/to/repo", force=True, wait="forever")
|
|
71
|
+
# Answer at once with what is stored, however stale
|
|
72
|
+
client.get("git.branch", cwd="/path/to/repo", wait="0")
|
|
73
|
+
|
|
74
|
+
# The template itself, unrendered: a facet, read like any key
|
|
75
|
+
client.get("aws:template.profile")
|
|
76
|
+
|
|
77
|
+
# Force recomputation
|
|
78
|
+
client.refresh("git", path="/path/to/repo")
|
|
79
|
+
|
|
80
|
+
# Daemon status
|
|
81
|
+
status = client.status()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## `get`
|
|
85
|
+
|
|
86
|
+
`client.get(key, cwd=None, env=None, force=False, wait=None)` returns the
|
|
87
|
+
value `comb get` prints for this key, cwd and env — the bare value, `None`
|
|
88
|
+
on a miss. The daemon holds the templates (`<provider>:template`, seeded
|
|
89
|
+
from configuration or `put`) and the client renders them: a plain field is
|
|
90
|
+
its cached value, a templated one is rendered, and a bare provider name is
|
|
91
|
+
the whole merged namespace. `cwd=None` is the global entry. The unrendered
|
|
92
|
+
halves are facets, read like any key: `p:template.x` is the template as
|
|
93
|
+
stored, `p:put.x` and `p:source.x` those layers' values.
|
|
94
|
+
|
|
95
|
+
`force=True` re-executes every Source the read demands, as `comb get
|
|
96
|
+
--force`. `wait` is how long the read waits for a run it needs before it
|
|
97
|
+
answers what is stored, as `comb get --wait`: a duration (`"0"`, `"50ms"`,
|
|
98
|
+
`"2s"`) or `"forever"`; anything else is a `ParseError`. Left out, a stored
|
|
99
|
+
record waits its Source's budget and a cold read waits for its run. The read
|
|
100
|
+
may take `timeout_ms` plus the wait (`"forever"` has no deadline), so a cold
|
|
101
|
+
read with no `wait` can outlast the default 250 ms and raise `TimeoutError`;
|
|
102
|
+
its run goes on, and the next read has the value.
|
|
103
|
+
|
|
104
|
+
## Sessions
|
|
105
|
+
|
|
106
|
+
For multiple queries use a session to reuse a single connection:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
with client.session() as session:
|
|
110
|
+
session.set_context("/path/to/repo")
|
|
111
|
+
branch = session.get("git.branch") # reads at the session's directory
|
|
112
|
+
dirty = session.get("git.dirty")
|
|
113
|
+
hostname = session.get("hostname")
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Eval
|
|
117
|
+
|
|
118
|
+
`client.eval(template_str, cwd=None, env=None)` renders a template that is
|
|
119
|
+
stored nowhere — the same value `comb eval` prints; `eval("{{ p.f }}")` and
|
|
120
|
+
`get("p.f")` agree. The template accepts a single `{{ expr }}` tag, or
|
|
121
|
+
literal text/several tags — the former keeps the expression's natural type,
|
|
122
|
+
the latter is always a string (a source with no tags at all is its literal
|
|
123
|
+
text).
|
|
124
|
+
|
|
125
|
+
`exec()` inside a template runs only on a client created with
|
|
126
|
+
`Client(exec=True)`; it is off by default, and a template that calls it
|
|
127
|
+
then fails with `exec() is disabled in this context`.
|
|
128
|
+
|
|
129
|
+
## Watch
|
|
130
|
+
|
|
131
|
+
`client.watch(key, cwd=None, env=None)` streams `get`'s value for that key,
|
|
132
|
+
cwd and env, again each time it changes:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
with client.watch("git.branch", cwd="/path/to/repo") as stream:
|
|
136
|
+
for event in stream: # ends when the stream does
|
|
137
|
+
print(event.data) # None is a miss, not the end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`stream.next_event(timeout_ms=None)` answers the next `WatchEvent`, or
|
|
141
|
+
`None` once the stream has ended; a `timeout_ms` that runs out with no event
|
|
142
|
+
raises `TimeoutError`, and the stream stays open. `stream.cancel()` ends it
|
|
143
|
+
from any thread, and `stream.close()` ends it and frees it.
|
|
144
|
+
|
|
145
|
+
`status()`, `hello()` and `introspect(subject, duration_secs=None)` are the
|
|
146
|
+
client's; a session has none of them. `status()` answers typed `CacheRow`s,
|
|
147
|
+
`hello()` a `HelloInfo`, and `introspect()` an `IntrospectResponse` whose
|
|
148
|
+
`daemon` is a typed `DaemonHealth` (with its `ReaperStatus` and `Verdict`s)
|
|
149
|
+
for the `daemon` subject and whose `other` is the JSON for the rest.
|
|
150
|
+
|
|
151
|
+
## Client options
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
client = Client(socket_path="/tmp/beachcomber-1000/sock", timeout_ms=500, connect_timeout_ms=0)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
| Option | Default | Meaning |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `socket_path` | `$BEACHCOMBER_SOCKET`, else `/tmp/beachcomber-<uid>/sock` | The daemon's socket. |
|
|
160
|
+
| `timeout_ms` | 250 | How long one read or write on the socket may take; at least 1. |
|
|
161
|
+
| `connect_timeout_ms` | 1500 | How long connecting keeps retrying before it gives up; 0 is one attempt. |
|
|
162
|
+
| `autostart` | on | Start a daemon when none answers an auto-discovered socket. |
|
|
163
|
+
| `exec` | off | Whether a template this client renders may call `exec()`. |
|
|
164
|
+
|
|
165
|
+
The defaults are the library's (`bc_client_new`): this SDK passes on the
|
|
166
|
+
options you give and adds none of its own.
|
|
167
|
+
|
|
168
|
+
## Library discovery
|
|
169
|
+
|
|
170
|
+
This SDK is a `ctypes` binding over `libbeachcomber`'s C ABI — no socket
|
|
171
|
+
code or wire-protocol framing lives in Python; the native library owns the
|
|
172
|
+
connection (including the daemon socket's own auto-discovery and
|
|
173
|
+
autostart). It is loaded on first use.
|
|
174
|
+
|
|
175
|
+
The library is found by the one rule every SDK follows
|
|
176
|
+
([`docs/sdk-api.md`, "Library discovery"](../../docs/sdk-api.md#library-discovery)). The file is
|
|
177
|
+
`libbeachcomber.dylib` on macOS and `libbeachcomber.so` on every other
|
|
178
|
+
platform, and the SDK tries, in order:
|
|
179
|
+
|
|
180
|
+
1. `$BEACHCOMBER_LIB`, when it is set and not empty. It is then the only
|
|
181
|
+
candidate: a path set on purpose that does not load is an error, not a
|
|
182
|
+
reason to load some other library.
|
|
183
|
+
2. Beside `comb`: `<dir>/../lib/<file>`, where `<dir>` is the directory of
|
|
184
|
+
the first executable `comb` on `$PATH`, its symlinks resolved. A Homebrew
|
|
185
|
+
`comb` is a symlink into the Cellar, and the library sits in the Cellar
|
|
186
|
+
beside it.
|
|
187
|
+
3. The system loader, given the bare file name.
|
|
188
|
+
|
|
189
|
+
When none loads, `LibraryNotFoundError` names every candidate and why each
|
|
190
|
+
failed; there is no fallback to spawning `comb` as a subprocess. The tests
|
|
191
|
+
check this against the shared fixture, `sdks/discovery.json`.
|
|
192
|
+
|
|
193
|
+
## Socket discovery
|
|
194
|
+
|
|
195
|
+
Once the library is loaded, the daemon socket path itself is resolved by
|
|
196
|
+
`libbeachcomber` (not this SDK): `$BEACHCOMBER_SOCKET` if set, else a
|
|
197
|
+
stable per-user default. Pass `Client(socket_path=...)` to override it
|
|
198
|
+
directly.
|
|
199
|
+
|
|
200
|
+
## Exceptions
|
|
201
|
+
|
|
202
|
+
Every exception is one `CombError` subclass per error kind, carrying
|
|
203
|
+
`.kind` (the slug, matching the C ABI envelope's `error.kind`), `.message`
|
|
204
|
+
(the ABI's message, unchanged — `str(exc)` is the same) and
|
|
205
|
+
`.library_version` (the loaded library's `bc_version()`), so callers should
|
|
206
|
+
not need to string-match `str(exc)`.
|
|
207
|
+
|
|
208
|
+
| Exception | `.kind` | When raised |
|
|
209
|
+
|---|---|---|
|
|
210
|
+
| `DaemonNotRunningError` | `daemon_not_running` | No daemon answers, and none was started |
|
|
211
|
+
| `ConnectionFailedError` | `connection_failed` | An explicit `socket_path` could not be connected to |
|
|
212
|
+
| `IoError` | `io_error` | A read or write on the socket failed |
|
|
213
|
+
| `ParseError` | `parse_error` | A request, a response or a template could not be read |
|
|
214
|
+
| `ServerError` | `server_error` | The daemon refused the request, or a template failed to render |
|
|
215
|
+
| `TimeoutError` | `timeout` | A read or write ran out `timeout_ms`, or `next_event` ran out its `timeout_ms` |
|
|
216
|
+
| `BusyError` | `busy` | A session or watch is in use by another caller |
|
|
217
|
+
| `PanicError` | `panic` | The library panicked; the panic was caught at the boundary |
|
|
218
|
+
| `LibraryNotFoundError` | `library_not_found` | No `libbeachcomber` was found where discovery looks |
|
|
219
|
+
| `LibrarySymbolError` | `library_symbol` | The library found lacks a function this SDK calls |
|
|
220
|
+
| `UnsupportedError` | `unsupported` | A transport cannot do this operation (never raised here: this SDK has one transport) |
|
|
221
|
+
| `CombError` | varies | Base class for all of them |
|
|
222
|
+
|
|
223
|
+
## Running tests
|
|
224
|
+
|
|
225
|
+
```sh
|
|
226
|
+
cd sdks/python
|
|
227
|
+
uv run --with pytest python -m pytest # unit and integration tests
|
|
228
|
+
COMB_BIN=/path/to/comb python3 conformance_runner.py # the shared fixtures under tests/conformance/
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The integration tests spawn a `comb daemon` and load `libbeachcomber` from
|
|
232
|
+
the workspace's debug build (`../../target/debug/`, made by `cargo build`)
|
|
233
|
+
unless `BEACHCOMBER_LIB` / `COMB_BIN` are set.
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# beachcomber Python SDK
|
|
2
|
+
|
|
3
|
+
Python client for the [beachcomber](https://github.com/NavistAu/beachcomber) (`comb`) shell-state daemon.
|
|
4
|
+
It follows the SDK API table ([`docs/sdk-api.md`](../../docs/sdk-api.md)): the
|
|
5
|
+
same methods, options, value shapes and error kinds as every other SDK, which
|
|
6
|
+
`tests/test_api_surface.py` checks against [`sdks/api.json`](../api.json).
|
|
7
|
+
|
|
8
|
+
## Requirements
|
|
9
|
+
|
|
10
|
+
- Python 3.9+
|
|
11
|
+
- No external dependencies (stdlib `ctypes` binds directly to the native
|
|
12
|
+
`libbeachcomber.{so,dylib}` — no subprocess, no socket code in this SDK)
|
|
13
|
+
- A running `comb` daemon (or `autostart` left enabled, the default)
|
|
14
|
+
- `libbeachcomber.{so,dylib}` discoverable — see "Library discovery" below
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
pip install libbeachcomber
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or with `uv`:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
uv add libbeachcomber
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
import os
|
|
32
|
+
|
|
33
|
+
from libbeachcomber import Client
|
|
34
|
+
|
|
35
|
+
client = Client()
|
|
36
|
+
|
|
37
|
+
# What `comb get git.branch` prints in that directory; None on a miss
|
|
38
|
+
branch = client.get("git.branch", cwd="/path/to/repo") # "main"
|
|
39
|
+
|
|
40
|
+
# A full provider (a dict)
|
|
41
|
+
git = client.get("git", cwd="/path/to/repo")
|
|
42
|
+
print(git["branch"], git["dirty"])
|
|
43
|
+
|
|
44
|
+
# The caller's environment for env.* refs; force a run, and wait for it
|
|
45
|
+
client.get("aws.profile", cwd="/path/to/repo", env=dict(os.environ))
|
|
46
|
+
client.get("git.branch", cwd="/path/to/repo", force=True, wait="forever")
|
|
47
|
+
# Answer at once with what is stored, however stale
|
|
48
|
+
client.get("git.branch", cwd="/path/to/repo", wait="0")
|
|
49
|
+
|
|
50
|
+
# The template itself, unrendered: a facet, read like any key
|
|
51
|
+
client.get("aws:template.profile")
|
|
52
|
+
|
|
53
|
+
# Force recomputation
|
|
54
|
+
client.refresh("git", path="/path/to/repo")
|
|
55
|
+
|
|
56
|
+
# Daemon status
|
|
57
|
+
status = client.status()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## `get`
|
|
61
|
+
|
|
62
|
+
`client.get(key, cwd=None, env=None, force=False, wait=None)` returns the
|
|
63
|
+
value `comb get` prints for this key, cwd and env — the bare value, `None`
|
|
64
|
+
on a miss. The daemon holds the templates (`<provider>:template`, seeded
|
|
65
|
+
from configuration or `put`) and the client renders them: a plain field is
|
|
66
|
+
its cached value, a templated one is rendered, and a bare provider name is
|
|
67
|
+
the whole merged namespace. `cwd=None` is the global entry. The unrendered
|
|
68
|
+
halves are facets, read like any key: `p:template.x` is the template as
|
|
69
|
+
stored, `p:put.x` and `p:source.x` those layers' values.
|
|
70
|
+
|
|
71
|
+
`force=True` re-executes every Source the read demands, as `comb get
|
|
72
|
+
--force`. `wait` is how long the read waits for a run it needs before it
|
|
73
|
+
answers what is stored, as `comb get --wait`: a duration (`"0"`, `"50ms"`,
|
|
74
|
+
`"2s"`) or `"forever"`; anything else is a `ParseError`. Left out, a stored
|
|
75
|
+
record waits its Source's budget and a cold read waits for its run. The read
|
|
76
|
+
may take `timeout_ms` plus the wait (`"forever"` has no deadline), so a cold
|
|
77
|
+
read with no `wait` can outlast the default 250 ms and raise `TimeoutError`;
|
|
78
|
+
its run goes on, and the next read has the value.
|
|
79
|
+
|
|
80
|
+
## Sessions
|
|
81
|
+
|
|
82
|
+
For multiple queries use a session to reuse a single connection:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
with client.session() as session:
|
|
86
|
+
session.set_context("/path/to/repo")
|
|
87
|
+
branch = session.get("git.branch") # reads at the session's directory
|
|
88
|
+
dirty = session.get("git.dirty")
|
|
89
|
+
hostname = session.get("hostname")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Eval
|
|
93
|
+
|
|
94
|
+
`client.eval(template_str, cwd=None, env=None)` renders a template that is
|
|
95
|
+
stored nowhere — the same value `comb eval` prints; `eval("{{ p.f }}")` and
|
|
96
|
+
`get("p.f")` agree. The template accepts a single `{{ expr }}` tag, or
|
|
97
|
+
literal text/several tags — the former keeps the expression's natural type,
|
|
98
|
+
the latter is always a string (a source with no tags at all is its literal
|
|
99
|
+
text).
|
|
100
|
+
|
|
101
|
+
`exec()` inside a template runs only on a client created with
|
|
102
|
+
`Client(exec=True)`; it is off by default, and a template that calls it
|
|
103
|
+
then fails with `exec() is disabled in this context`.
|
|
104
|
+
|
|
105
|
+
## Watch
|
|
106
|
+
|
|
107
|
+
`client.watch(key, cwd=None, env=None)` streams `get`'s value for that key,
|
|
108
|
+
cwd and env, again each time it changes:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
with client.watch("git.branch", cwd="/path/to/repo") as stream:
|
|
112
|
+
for event in stream: # ends when the stream does
|
|
113
|
+
print(event.data) # None is a miss, not the end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`stream.next_event(timeout_ms=None)` answers the next `WatchEvent`, or
|
|
117
|
+
`None` once the stream has ended; a `timeout_ms` that runs out with no event
|
|
118
|
+
raises `TimeoutError`, and the stream stays open. `stream.cancel()` ends it
|
|
119
|
+
from any thread, and `stream.close()` ends it and frees it.
|
|
120
|
+
|
|
121
|
+
`status()`, `hello()` and `introspect(subject, duration_secs=None)` are the
|
|
122
|
+
client's; a session has none of them. `status()` answers typed `CacheRow`s,
|
|
123
|
+
`hello()` a `HelloInfo`, and `introspect()` an `IntrospectResponse` whose
|
|
124
|
+
`daemon` is a typed `DaemonHealth` (with its `ReaperStatus` and `Verdict`s)
|
|
125
|
+
for the `daemon` subject and whose `other` is the JSON for the rest.
|
|
126
|
+
|
|
127
|
+
## Client options
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
client = Client(socket_path="/tmp/beachcomber-1000/sock", timeout_ms=500, connect_timeout_ms=0)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
| Option | Default | Meaning |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| `socket_path` | `$BEACHCOMBER_SOCKET`, else `/tmp/beachcomber-<uid>/sock` | The daemon's socket. |
|
|
136
|
+
| `timeout_ms` | 250 | How long one read or write on the socket may take; at least 1. |
|
|
137
|
+
| `connect_timeout_ms` | 1500 | How long connecting keeps retrying before it gives up; 0 is one attempt. |
|
|
138
|
+
| `autostart` | on | Start a daemon when none answers an auto-discovered socket. |
|
|
139
|
+
| `exec` | off | Whether a template this client renders may call `exec()`. |
|
|
140
|
+
|
|
141
|
+
The defaults are the library's (`bc_client_new`): this SDK passes on the
|
|
142
|
+
options you give and adds none of its own.
|
|
143
|
+
|
|
144
|
+
## Library discovery
|
|
145
|
+
|
|
146
|
+
This SDK is a `ctypes` binding over `libbeachcomber`'s C ABI — no socket
|
|
147
|
+
code or wire-protocol framing lives in Python; the native library owns the
|
|
148
|
+
connection (including the daemon socket's own auto-discovery and
|
|
149
|
+
autostart). It is loaded on first use.
|
|
150
|
+
|
|
151
|
+
The library is found by the one rule every SDK follows
|
|
152
|
+
([`docs/sdk-api.md`, "Library discovery"](../../docs/sdk-api.md#library-discovery)). The file is
|
|
153
|
+
`libbeachcomber.dylib` on macOS and `libbeachcomber.so` on every other
|
|
154
|
+
platform, and the SDK tries, in order:
|
|
155
|
+
|
|
156
|
+
1. `$BEACHCOMBER_LIB`, when it is set and not empty. It is then the only
|
|
157
|
+
candidate: a path set on purpose that does not load is an error, not a
|
|
158
|
+
reason to load some other library.
|
|
159
|
+
2. Beside `comb`: `<dir>/../lib/<file>`, where `<dir>` is the directory of
|
|
160
|
+
the first executable `comb` on `$PATH`, its symlinks resolved. A Homebrew
|
|
161
|
+
`comb` is a symlink into the Cellar, and the library sits in the Cellar
|
|
162
|
+
beside it.
|
|
163
|
+
3. The system loader, given the bare file name.
|
|
164
|
+
|
|
165
|
+
When none loads, `LibraryNotFoundError` names every candidate and why each
|
|
166
|
+
failed; there is no fallback to spawning `comb` as a subprocess. The tests
|
|
167
|
+
check this against the shared fixture, `sdks/discovery.json`.
|
|
168
|
+
|
|
169
|
+
## Socket discovery
|
|
170
|
+
|
|
171
|
+
Once the library is loaded, the daemon socket path itself is resolved by
|
|
172
|
+
`libbeachcomber` (not this SDK): `$BEACHCOMBER_SOCKET` if set, else a
|
|
173
|
+
stable per-user default. Pass `Client(socket_path=...)` to override it
|
|
174
|
+
directly.
|
|
175
|
+
|
|
176
|
+
## Exceptions
|
|
177
|
+
|
|
178
|
+
Every exception is one `CombError` subclass per error kind, carrying
|
|
179
|
+
`.kind` (the slug, matching the C ABI envelope's `error.kind`), `.message`
|
|
180
|
+
(the ABI's message, unchanged — `str(exc)` is the same) and
|
|
181
|
+
`.library_version` (the loaded library's `bc_version()`), so callers should
|
|
182
|
+
not need to string-match `str(exc)`.
|
|
183
|
+
|
|
184
|
+
| Exception | `.kind` | When raised |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| `DaemonNotRunningError` | `daemon_not_running` | No daemon answers, and none was started |
|
|
187
|
+
| `ConnectionFailedError` | `connection_failed` | An explicit `socket_path` could not be connected to |
|
|
188
|
+
| `IoError` | `io_error` | A read or write on the socket failed |
|
|
189
|
+
| `ParseError` | `parse_error` | A request, a response or a template could not be read |
|
|
190
|
+
| `ServerError` | `server_error` | The daemon refused the request, or a template failed to render |
|
|
191
|
+
| `TimeoutError` | `timeout` | A read or write ran out `timeout_ms`, or `next_event` ran out its `timeout_ms` |
|
|
192
|
+
| `BusyError` | `busy` | A session or watch is in use by another caller |
|
|
193
|
+
| `PanicError` | `panic` | The library panicked; the panic was caught at the boundary |
|
|
194
|
+
| `LibraryNotFoundError` | `library_not_found` | No `libbeachcomber` was found where discovery looks |
|
|
195
|
+
| `LibrarySymbolError` | `library_symbol` | The library found lacks a function this SDK calls |
|
|
196
|
+
| `UnsupportedError` | `unsupported` | A transport cannot do this operation (never raised here: this SDK has one transport) |
|
|
197
|
+
| `CombError` | varies | Base class for all of them |
|
|
198
|
+
|
|
199
|
+
## Running tests
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
cd sdks/python
|
|
203
|
+
uv run --with pytest python -m pytest # unit and integration tests
|
|
204
|
+
COMB_BIN=/path/to/comb python3 conformance_runner.py # the shared fixtures under tests/conformance/
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The integration tests spawn a `comb daemon` and load `libbeachcomber` from
|
|
208
|
+
the workspace's debug build (`../../target/debug/`, made by `cargo build`)
|
|
209
|
+
unless `BEACHCOMBER_LIB` / `COMB_BIN` are set.
|