branchkit 0.3.0__tar.gz → 0.5.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.
Files changed (68) hide show
  1. branchkit-0.5.0/PKG-INFO +242 -0
  2. branchkit-0.5.0/README.md +229 -0
  3. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit/__init__.py +6 -0
  4. branchkit-0.5.0/branchkit/_pipe.py +428 -0
  5. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit/actor.py +1 -1
  6. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit/closed_vocab_gen.py +60 -0
  7. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit/collection.py +16 -14
  8. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit/collection_log.py +7 -6
  9. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit/commands.py +25 -32
  10. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit/contracts_gen.py +44 -12
  11. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit/debug.py +1 -1
  12. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit/effects.py +5 -4
  13. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit/harness.py +22 -2
  14. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit/hud.py +6 -5
  15. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit/listen.py +142 -2
  16. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit/log_events_gen.py +16 -12
  17. branchkit-0.5.0/branchkit/methods_gen.py +6467 -0
  18. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit/mirror.py +22 -6
  19. branchkit-0.5.0/branchkit/origin.py +59 -0
  20. branchkit-0.5.0/branchkit/output.py +34 -0
  21. branchkit-0.5.0/branchkit/pipeline/__init__.py +83 -0
  22. branchkit-0.5.0/branchkit/pipeline/audio/__init__.py +4 -0
  23. branchkit-0.5.0/branchkit/pipeline/audio/events_gen.py +48 -0
  24. branchkit-0.5.0/branchkit/pipeline/credit.py +55 -0
  25. branchkit-0.5.0/branchkit/pipeline/events_gen.py +181 -0
  26. branchkit-0.5.0/branchkit/pipeline/monitors/__init__.py +4 -0
  27. branchkit-0.5.0/branchkit/pipeline/monitors/events_gen.py +123 -0
  28. branchkit-0.5.0/branchkit/pipeline/recognition/__init__.py +4 -0
  29. branchkit-0.5.0/branchkit/pipeline/recognition/events_gen.py +128 -0
  30. branchkit-0.5.0/branchkit/pipeline/stage.py +381 -0
  31. branchkit-0.5.0/branchkit/pipeline/stagelog.py +56 -0
  32. branchkit-0.5.0/branchkit/pipeline/wire.py +281 -0
  33. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit/plugin.py +185 -29
  34. branchkit-0.5.0/branchkit/proxy.py +251 -0
  35. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit/settings.py +3 -4
  36. branchkit-0.5.0/branchkit/topic.py +43 -0
  37. branchkit-0.5.0/branchkit/types_gen.py +7082 -0
  38. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit/ui.py +2 -1
  39. branchkit-0.5.0/branchkit.egg-info/PKG-INFO +242 -0
  40. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit.egg-info/SOURCES.txt +26 -1
  41. {branchkit-0.3.0 → branchkit-0.5.0}/pyproject.toml +11 -2
  42. branchkit-0.5.0/tests/test_call_timeout.py +26 -0
  43. branchkit-0.5.0/tests/test_effects.py +84 -0
  44. branchkit-0.5.0/tests/test_kernel.py +651 -0
  45. branchkit-0.5.0/tests/test_listen_relay.py +108 -0
  46. branchkit-0.5.0/tests/test_mirror.py +64 -0
  47. branchkit-0.5.0/tests/test_output.py +64 -0
  48. branchkit-0.5.0/tests/test_pipe.py +476 -0
  49. branchkit-0.5.0/tests/test_pipeline.py +399 -0
  50. branchkit-0.5.0/tests/test_platform.py +58 -0
  51. branchkit-0.5.0/tests/test_topic.py +38 -0
  52. branchkit-0.3.0/PKG-INFO +0 -84
  53. branchkit-0.3.0/README.md +0 -71
  54. branchkit-0.3.0/branchkit/methods_gen.py +0 -4497
  55. branchkit-0.3.0/branchkit/proxy.py +0 -157
  56. branchkit-0.3.0/branchkit/types_gen.py +0 -4853
  57. branchkit-0.3.0/branchkit.egg-info/PKG-INFO +0 -84
  58. branchkit-0.3.0/tests/test_kernel.py +0 -325
  59. {branchkit-0.3.0 → branchkit-0.5.0}/LICENSE +0 -0
  60. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit/correlation.py +0 -0
  61. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit/log.py +0 -0
  62. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit/settings_route.py +0 -0
  63. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit/upstream.py +0 -0
  64. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit.egg-info/dependency_links.txt +0 -0
  65. {branchkit-0.3.0 → branchkit-0.5.0}/branchkit.egg-info/top_level.txt +0 -0
  66. {branchkit-0.3.0 → branchkit-0.5.0}/setup.cfg +0 -0
  67. {branchkit-0.3.0 → branchkit-0.5.0}/tests/test_collection.py +0 -0
  68. {branchkit-0.3.0 → branchkit-0.5.0}/tests/test_settings_tabs.py +0 -0
@@ -0,0 +1,242 @@
1
+ Metadata-Version: 2.4
2
+ Name: branchkit
3
+ Version: 0.5.0
4
+ Summary: BranchKit plugin SDK for Python
5
+ Author: BranchKit
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://branchkit.dev
8
+ Project-URL: Repository, https://github.com/branchkit/plugin-sdk-py
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Dynamic: license-file
13
+
14
+ # BranchKit Plugin SDK (Python)
15
+
16
+ BranchKit is an accessibility plugin platform for the desktop. The platform
17
+ loads plugins, confines each one to what it declared, tracks what is true right
18
+ now (which app has focus, which mode is active), and routes voice commands,
19
+ hotkeys and other input to whichever plugin claims them. This SDK is how a
20
+ Python program becomes one of those plugins. MIT licensed, standard library
21
+ only.
22
+
23
+ **Status:** BranchKit is pre-launch; the application is in private
24
+ development, and this SDK is published and usable today. Versions are 0.x, so a
25
+ minor release can break callers — [CHANGELOG.md](CHANGELOG.md) says what
26
+ changed and how to migrate.
27
+
28
+ ## Install
29
+
30
+ Python 3.11 or later. A plugin carries its own copy of the SDK — a plugin runs
31
+ sandboxed, with no network and no pip — so install it into the plugin
32
+ directory:
33
+
34
+ ```sh
35
+ python3 -m pip install --target . branchkit # latest on PyPI
36
+ python3 -m pip install --target . git+https://github.com/branchkit/plugin-sdk-py # this repository's main
37
+ branchkit-cli runtime install python # once per machine
38
+ ```
39
+
40
+ PyPI releases can lag this repository; the changelog says what each version
41
+ contains. The last command installs the CPython that BranchKit runs Python
42
+ plugins with, which the manifest asks for with `requires.runtimes`.
43
+
44
+ The fastest start is the scaffold, which writes a working plugin and vendors
45
+ the SDK for you:
46
+
47
+ ```sh
48
+ branchkit-cli dev init --name my-plugin --template py
49
+ ```
50
+
51
+ ## A minimal plugin
52
+
53
+ A plugin is a directory with a manifest, the commands it contributes, and a
54
+ program. This is what `dev init` writes, trimmed.
55
+
56
+ `plugin.json` declares who the plugin is, what it may do, and what it offers:
57
+
58
+ ```json
59
+ {
60
+ "id": "my-plugin",
61
+ "name": "My Plugin",
62
+ "version": "0.1.0",
63
+ "min_api_version": "0.2.0",
64
+ "requires": { "privileges": ["input"], "runtimes": ["python"] },
65
+ "run": "python3 main.py",
66
+ "action_prefix": "myplugin",
67
+ "action_types": {
68
+ "greet": { "label": "Greet", "fields": [{ "key": "name", "label": "Name", "field_type": "string" }] }
69
+ },
70
+ "collection_data": { "voice_commands": "commands.json" },
71
+ "implements": { "on_action": true }
72
+ }
73
+ ```
74
+
75
+ `commands.json` maps a spoken phrase to an action:
76
+
77
+ ```json
78
+ [
79
+ {
80
+ "pattern": ["hello", "branchkit"],
81
+ "action": { "type": "myplugin.greet", "params": { "name": "BranchKit" } },
82
+ "description": "Say Hello BranchKit"
83
+ }
84
+ ]
85
+ ```
86
+
87
+ `main.py` handles the action:
88
+
89
+ ```python
90
+ import asyncio
91
+
92
+ import branchkit
93
+
94
+ plugin = branchkit.Plugin()
95
+
96
+
97
+ @plugin.handle_action("myplugin.greet")
98
+ async def greet(req):
99
+ name = (req["params"] or {}).get("name") or "BranchKit"
100
+ await plugin.input_type_text(text=f"Hello, {name}!")
101
+
102
+
103
+ asyncio.run(plugin.run()) # returns when BranchKit stops the plugin
104
+ ```
105
+
106
+ Say "hello branchkit" and the plugin types `Hello, BranchKit!` at the cursor.
107
+ Typing needs the `input` privilege, which is why the manifest asks for it.
108
+
109
+ `branchkit-gen --plugin .` generates `actions_gen.py` from the manifest: a
110
+ `TypedDict` for each action's params and a registrar, so the action string is
111
+ never spelled by hand.
112
+
113
+ Handlers may be `async def`, run on the event loop, or plain `def`, run on a
114
+ worker thread so a blocking body cannot stall the plugin. In a plain `def`,
115
+ call the platform with `plugin.call_sync(method, params)` instead of `await`.
116
+
117
+ ## Calling the platform
118
+
119
+ Every platform method has a generated method on `Plugin`. Its arguments are
120
+ keyword-only, and it returns the typed result:
121
+
122
+ ```python
123
+ rec = await plugin.collection_fetch(id=record_id, name="notes")
124
+
125
+ await plugin.hud_create_channel(channel="status", accepts_input=True)
126
+ ```
127
+
128
+ Arguments are always named, never positional, so each one says what it is; a
129
+ positional call fails at once with a `TypeError`. Leave an optional argument
130
+ out to mean "absent". The generated methods are in
131
+ [branchkit/methods_gen.py](branchkit/methods_gen.py) and their result types in
132
+ [branchkit/types_gen.py](branchkit/types_gen.py), with the platform's own
133
+ description on every parameter.
134
+
135
+ **The manifest is the permission.** A call that needs a privilege the manifest
136
+ did not declare is refused before it runs, with an error of kind `forbidden`
137
+ naming the operation. Some privileges also ask the user the first time. Network
138
+ access works the same way: a host not listed under `requires.network` is
139
+ refused with a `HostRefusedError`.
140
+
141
+ **Errors** from the platform are `branchkit.RpcCallError` (`code`, `kind`,
142
+ `data`), and `branchkit.error_kind_of(e)` reads the kind from any exception:
143
+
144
+ ```python
145
+ try:
146
+ await plugin.input_type_text(text="hi")
147
+ except branchkit.RpcCallError as e:
148
+ if branchkit.error_kind_of(e) == "forbidden":
149
+ ... # declare the privilege
150
+ ```
151
+
152
+ `UnsupportedError` (a method this OS does not provide),
153
+ `RecordingDisabledError` and `CallTimeoutError` are the specific cases.
154
+
155
+ `await plugin.call(method, params)` is the untyped escape hatch, for a method
156
+ too new to have a generated wrapper. Prefer the wrapper whenever one exists.
157
+
158
+ ## What the SDK covers
159
+
160
+ | Need | API |
161
+ |---|---|
162
+ | Handle an action | `@plugin.handle_action("prefix.name")` (alias `@plugin.action`), registrars from `actions_gen.py` |
163
+ | Serve your own method | `@plugin.handle("method")`, `plugin.handle_command` |
164
+ | React to events | `@plugin.on(event)`, `plugin.on_pattern("ext.acme.**", fn)`, `plugin.current_event_origin()`; emit with `events_emit` |
165
+ | Store state | `get` / `list` / `list_page` / `count` / `put` / `put_many` / `patch` / `delete` / `replace`, `subscribe` |
166
+ | Append-only logs | `append`, `append_keyed`, `list_log`, `get_log_entry`, `delete_log_entry` |
167
+ | Keep a live copy | `plugin.mirror_collection(name)`, `plugin.settings(name)` |
168
+ | Contribute commands | `branchkit.command(word("open"), capture("app", "apps")).action(…).build()`, `push_command_specs`, `push_command_group` |
169
+ | A settings tab | `@plugin.settings_tab(key)` + `implements.settings_tabs` in the manifest; `post_button` / `signal_button` / `confirm_button` |
170
+ | Show something | `output_state(state=…)` with `say_action` / `dispatch_action`, `hud_push` |
171
+ | Hold a system effect | `assert_effect`, `retract_effect`, `is_effect_active`, `on_effect_displaced` |
172
+ | Trace a request | `plugin.current_correlation()`, `with branchkit.acting_for(actor):` |
173
+ | Log | `await plugin.info` / `warn` / `error` / `debug` / `trace(tag, data)` to your plugin's log (debug and trace are off by default) |
174
+ | Outbound HTTP | `urllib.request.urlopen` (routed through the platform's proxy), `branchkit.UpstreamClient` |
175
+ | Raw TCP (MQTT, a local daemon) | `branchkit.dial(host, port)` |
176
+ | Accept local connections | `branchkit.listen_local(plugin)` with `requires.sockets.listen` |
177
+ | Test a plugin | `branchkit.harness.Harness` |
178
+
179
+ Package `branchkit.pipeline` is for pipeline stages (audio and monitor
180
+ processes on a separate wire), not for ordinary plugins.
181
+
182
+ ## Testing a plugin
183
+
184
+ `Harness` loads your plugin against a simulated platform and matches phrases
185
+ the way the real matcher does, without audio:
186
+
187
+ ```python
188
+ import unittest
189
+
190
+ from branchkit.harness import Harness
191
+
192
+
193
+ class GreetTests(unittest.TestCase):
194
+ def test_greet_matches(self):
195
+ with Harness.start(".") as h:
196
+ result = h.must_simulate_command("hello branchkit")
197
+ self.assertEqual(result.action_type(), "myplugin.greet")
198
+ ```
199
+
200
+ It runs the `branchkit-test-harness` binary that ships inside BranchKit.app;
201
+ set `BRANCHKIT_TEST_HARNESS` to its path anywhere else.
202
+ `python3 -m unittest` runs your tests; `branchkit-cli dev test .` checks the
203
+ manifest and runs the platform's own conformance checks against the plugin.
204
+
205
+ Against the running app:
206
+
207
+ ```sh
208
+ branchkit-cli plugin install . --build # install it
209
+ branchkit-cli dev watch . # reload on save
210
+ branchkit-cli dev say "hello branchkit" --simulate # match and report, execute nothing
211
+ branchkit-cli dev plog my-plugin --since 30s # read its log
212
+ ```
213
+
214
+ ## Learn more
215
+
216
+ - **API reference:** the docstrings on every export (`help(branchkit)`), and
217
+ the type hints your editor reads.
218
+ - **Local docs:** `branchkit-cli docs path` prints the documentation bundled
219
+ with your installed BranchKit, for reading or grepping offline.
220
+ - **Worked examples:** [helloworld-py](https://github.com/branchkit/branchkit-plugin-helloworld-py)
221
+ (the scaffold), and real plugins built on the Go SDK with the same surface:
222
+ [keyboard](https://github.com/branchkit/branchkit-plugin-keyboard),
223
+ [system](https://github.com/branchkit/branchkit-plugin-system),
224
+ [windows](https://github.com/branchkit/branchkit-plugin-windows).
225
+
226
+ ## Versioning
227
+
228
+ Tags follow semver, 0.x for now: a minor release may break callers, and
229
+ CHANGELOG.md names every break with its migration. The Go
230
+ ([plugin-sdk-go](https://github.com/branchkit/plugin-sdk-go)), TypeScript
231
+ ([plugin-sdk-ts](https://github.com/branchkit/plugin-sdk-ts)) and Python SDKs
232
+ implement the same surface and are held to it by one cross-language
233
+ conformance suite.
234
+
235
+ ## Developing the SDK
236
+
237
+ ```sh
238
+ python3 -m unittest discover -s tests
239
+ ```
240
+
241
+ Files ending in `_gen.py` are generated from the platform's contracts; change
242
+ the contract, not the file.
@@ -0,0 +1,229 @@
1
+ # BranchKit Plugin SDK (Python)
2
+
3
+ BranchKit is an accessibility plugin platform for the desktop. The platform
4
+ loads plugins, confines each one to what it declared, tracks what is true right
5
+ now (which app has focus, which mode is active), and routes voice commands,
6
+ hotkeys and other input to whichever plugin claims them. This SDK is how a
7
+ Python program becomes one of those plugins. MIT licensed, standard library
8
+ only.
9
+
10
+ **Status:** BranchKit is pre-launch; the application is in private
11
+ development, and this SDK is published and usable today. Versions are 0.x, so a
12
+ minor release can break callers — [CHANGELOG.md](CHANGELOG.md) says what
13
+ changed and how to migrate.
14
+
15
+ ## Install
16
+
17
+ Python 3.11 or later. A plugin carries its own copy of the SDK — a plugin runs
18
+ sandboxed, with no network and no pip — so install it into the plugin
19
+ directory:
20
+
21
+ ```sh
22
+ python3 -m pip install --target . branchkit # latest on PyPI
23
+ python3 -m pip install --target . git+https://github.com/branchkit/plugin-sdk-py # this repository's main
24
+ branchkit-cli runtime install python # once per machine
25
+ ```
26
+
27
+ PyPI releases can lag this repository; the changelog says what each version
28
+ contains. The last command installs the CPython that BranchKit runs Python
29
+ plugins with, which the manifest asks for with `requires.runtimes`.
30
+
31
+ The fastest start is the scaffold, which writes a working plugin and vendors
32
+ the SDK for you:
33
+
34
+ ```sh
35
+ branchkit-cli dev init --name my-plugin --template py
36
+ ```
37
+
38
+ ## A minimal plugin
39
+
40
+ A plugin is a directory with a manifest, the commands it contributes, and a
41
+ program. This is what `dev init` writes, trimmed.
42
+
43
+ `plugin.json` declares who the plugin is, what it may do, and what it offers:
44
+
45
+ ```json
46
+ {
47
+ "id": "my-plugin",
48
+ "name": "My Plugin",
49
+ "version": "0.1.0",
50
+ "min_api_version": "0.2.0",
51
+ "requires": { "privileges": ["input"], "runtimes": ["python"] },
52
+ "run": "python3 main.py",
53
+ "action_prefix": "myplugin",
54
+ "action_types": {
55
+ "greet": { "label": "Greet", "fields": [{ "key": "name", "label": "Name", "field_type": "string" }] }
56
+ },
57
+ "collection_data": { "voice_commands": "commands.json" },
58
+ "implements": { "on_action": true }
59
+ }
60
+ ```
61
+
62
+ `commands.json` maps a spoken phrase to an action:
63
+
64
+ ```json
65
+ [
66
+ {
67
+ "pattern": ["hello", "branchkit"],
68
+ "action": { "type": "myplugin.greet", "params": { "name": "BranchKit" } },
69
+ "description": "Say Hello BranchKit"
70
+ }
71
+ ]
72
+ ```
73
+
74
+ `main.py` handles the action:
75
+
76
+ ```python
77
+ import asyncio
78
+
79
+ import branchkit
80
+
81
+ plugin = branchkit.Plugin()
82
+
83
+
84
+ @plugin.handle_action("myplugin.greet")
85
+ async def greet(req):
86
+ name = (req["params"] or {}).get("name") or "BranchKit"
87
+ await plugin.input_type_text(text=f"Hello, {name}!")
88
+
89
+
90
+ asyncio.run(plugin.run()) # returns when BranchKit stops the plugin
91
+ ```
92
+
93
+ Say "hello branchkit" and the plugin types `Hello, BranchKit!` at the cursor.
94
+ Typing needs the `input` privilege, which is why the manifest asks for it.
95
+
96
+ `branchkit-gen --plugin .` generates `actions_gen.py` from the manifest: a
97
+ `TypedDict` for each action's params and a registrar, so the action string is
98
+ never spelled by hand.
99
+
100
+ Handlers may be `async def`, run on the event loop, or plain `def`, run on a
101
+ worker thread so a blocking body cannot stall the plugin. In a plain `def`,
102
+ call the platform with `plugin.call_sync(method, params)` instead of `await`.
103
+
104
+ ## Calling the platform
105
+
106
+ Every platform method has a generated method on `Plugin`. Its arguments are
107
+ keyword-only, and it returns the typed result:
108
+
109
+ ```python
110
+ rec = await plugin.collection_fetch(id=record_id, name="notes")
111
+
112
+ await plugin.hud_create_channel(channel="status", accepts_input=True)
113
+ ```
114
+
115
+ Arguments are always named, never positional, so each one says what it is; a
116
+ positional call fails at once with a `TypeError`. Leave an optional argument
117
+ out to mean "absent". The generated methods are in
118
+ [branchkit/methods_gen.py](branchkit/methods_gen.py) and their result types in
119
+ [branchkit/types_gen.py](branchkit/types_gen.py), with the platform's own
120
+ description on every parameter.
121
+
122
+ **The manifest is the permission.** A call that needs a privilege the manifest
123
+ did not declare is refused before it runs, with an error of kind `forbidden`
124
+ naming the operation. Some privileges also ask the user the first time. Network
125
+ access works the same way: a host not listed under `requires.network` is
126
+ refused with a `HostRefusedError`.
127
+
128
+ **Errors** from the platform are `branchkit.RpcCallError` (`code`, `kind`,
129
+ `data`), and `branchkit.error_kind_of(e)` reads the kind from any exception:
130
+
131
+ ```python
132
+ try:
133
+ await plugin.input_type_text(text="hi")
134
+ except branchkit.RpcCallError as e:
135
+ if branchkit.error_kind_of(e) == "forbidden":
136
+ ... # declare the privilege
137
+ ```
138
+
139
+ `UnsupportedError` (a method this OS does not provide),
140
+ `RecordingDisabledError` and `CallTimeoutError` are the specific cases.
141
+
142
+ `await plugin.call(method, params)` is the untyped escape hatch, for a method
143
+ too new to have a generated wrapper. Prefer the wrapper whenever one exists.
144
+
145
+ ## What the SDK covers
146
+
147
+ | Need | API |
148
+ |---|---|
149
+ | Handle an action | `@plugin.handle_action("prefix.name")` (alias `@plugin.action`), registrars from `actions_gen.py` |
150
+ | Serve your own method | `@plugin.handle("method")`, `plugin.handle_command` |
151
+ | React to events | `@plugin.on(event)`, `plugin.on_pattern("ext.acme.**", fn)`, `plugin.current_event_origin()`; emit with `events_emit` |
152
+ | Store state | `get` / `list` / `list_page` / `count` / `put` / `put_many` / `patch` / `delete` / `replace`, `subscribe` |
153
+ | Append-only logs | `append`, `append_keyed`, `list_log`, `get_log_entry`, `delete_log_entry` |
154
+ | Keep a live copy | `plugin.mirror_collection(name)`, `plugin.settings(name)` |
155
+ | Contribute commands | `branchkit.command(word("open"), capture("app", "apps")).action(…).build()`, `push_command_specs`, `push_command_group` |
156
+ | A settings tab | `@plugin.settings_tab(key)` + `implements.settings_tabs` in the manifest; `post_button` / `signal_button` / `confirm_button` |
157
+ | Show something | `output_state(state=…)` with `say_action` / `dispatch_action`, `hud_push` |
158
+ | Hold a system effect | `assert_effect`, `retract_effect`, `is_effect_active`, `on_effect_displaced` |
159
+ | Trace a request | `plugin.current_correlation()`, `with branchkit.acting_for(actor):` |
160
+ | Log | `await plugin.info` / `warn` / `error` / `debug` / `trace(tag, data)` to your plugin's log (debug and trace are off by default) |
161
+ | Outbound HTTP | `urllib.request.urlopen` (routed through the platform's proxy), `branchkit.UpstreamClient` |
162
+ | Raw TCP (MQTT, a local daemon) | `branchkit.dial(host, port)` |
163
+ | Accept local connections | `branchkit.listen_local(plugin)` with `requires.sockets.listen` |
164
+ | Test a plugin | `branchkit.harness.Harness` |
165
+
166
+ Package `branchkit.pipeline` is for pipeline stages (audio and monitor
167
+ processes on a separate wire), not for ordinary plugins.
168
+
169
+ ## Testing a plugin
170
+
171
+ `Harness` loads your plugin against a simulated platform and matches phrases
172
+ the way the real matcher does, without audio:
173
+
174
+ ```python
175
+ import unittest
176
+
177
+ from branchkit.harness import Harness
178
+
179
+
180
+ class GreetTests(unittest.TestCase):
181
+ def test_greet_matches(self):
182
+ with Harness.start(".") as h:
183
+ result = h.must_simulate_command("hello branchkit")
184
+ self.assertEqual(result.action_type(), "myplugin.greet")
185
+ ```
186
+
187
+ It runs the `branchkit-test-harness` binary that ships inside BranchKit.app;
188
+ set `BRANCHKIT_TEST_HARNESS` to its path anywhere else.
189
+ `python3 -m unittest` runs your tests; `branchkit-cli dev test .` checks the
190
+ manifest and runs the platform's own conformance checks against the plugin.
191
+
192
+ Against the running app:
193
+
194
+ ```sh
195
+ branchkit-cli plugin install . --build # install it
196
+ branchkit-cli dev watch . # reload on save
197
+ branchkit-cli dev say "hello branchkit" --simulate # match and report, execute nothing
198
+ branchkit-cli dev plog my-plugin --since 30s # read its log
199
+ ```
200
+
201
+ ## Learn more
202
+
203
+ - **API reference:** the docstrings on every export (`help(branchkit)`), and
204
+ the type hints your editor reads.
205
+ - **Local docs:** `branchkit-cli docs path` prints the documentation bundled
206
+ with your installed BranchKit, for reading or grepping offline.
207
+ - **Worked examples:** [helloworld-py](https://github.com/branchkit/branchkit-plugin-helloworld-py)
208
+ (the scaffold), and real plugins built on the Go SDK with the same surface:
209
+ [keyboard](https://github.com/branchkit/branchkit-plugin-keyboard),
210
+ [system](https://github.com/branchkit/branchkit-plugin-system),
211
+ [windows](https://github.com/branchkit/branchkit-plugin-windows).
212
+
213
+ ## Versioning
214
+
215
+ Tags follow semver, 0.x for now: a minor release may break callers, and
216
+ CHANGELOG.md names every break with its migration. The Go
217
+ ([plugin-sdk-go](https://github.com/branchkit/plugin-sdk-go)), TypeScript
218
+ ([plugin-sdk-ts](https://github.com/branchkit/plugin-sdk-ts)) and Python SDKs
219
+ implement the same surface and are held to it by one cross-language
220
+ conformance suite.
221
+
222
+ ## Developing the SDK
223
+
224
+ ```sh
225
+ python3 -m unittest discover -s tests
226
+ ```
227
+
228
+ Files ending in `_gen.py` are generated from the platform's contracts; change
229
+ the contract, not the file.
@@ -23,17 +23,21 @@ from . import proxy as _proxy
23
23
  _proxy.install_proxy_from_env()
24
24
 
25
25
  from .plugin import (
26
+ CallTimeoutError,
26
27
  DetachedError,
27
28
  PluginCore,
28
29
  RecordingDisabledError,
29
30
  RpcCallError,
31
+ UnsupportedError,
30
32
  api_version,
31
33
  error_kind_of,
34
+ artifacts_dir,
32
35
  models_dir,
33
36
  plugin_data_dir,
34
37
  plugin_dir,
35
38
  )
36
39
  from .actor import acting_for, get_current_actor
40
+ from .origin import EventOrigin, get_current_event_origin
37
41
  from .collection import CollectionMixin, list_opts, scope_collection, scope_group
38
42
  from .collection_log import CollectionLogMixin, log_list_opts
39
43
  from .debug import DebugMixin
@@ -69,6 +73,8 @@ from .ui import (
69
73
  signal_name,
70
74
  )
71
75
  from .upstream import UpstreamClient, UpstreamResponse
76
+ from .proxy import HostRefusedError, dial
77
+ from .output import dispatch_action, say_action
72
78
  from .closed_vocab_gen import * # noqa: F401,F403 — error kinds, directives, effects
73
79
  from .contracts_gen import * # noqa: F401,F403 — method/hook/event/tag constants
74
80
  from .log_events_gen import * # noqa: F401,F403 — observability log-event names