monosmith-plugin-sdk 0.1.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.
- monosmith_plugin_sdk-0.1.0/LICENSE +21 -0
- monosmith_plugin_sdk-0.1.0/PKG-INFO +134 -0
- monosmith_plugin_sdk-0.1.0/README.md +108 -0
- monosmith_plugin_sdk-0.1.0/pyproject.toml +68 -0
- monosmith_plugin_sdk-0.1.0/setup.cfg +4 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/__init__.py +204 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/build_dir/__init__.py +5 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/build_dir/build_dir_algorithm.py +30 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/cancellation/__init__.py +6 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/cancellation/cancel_token_model.py +75 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/cancellation/request_cancelled_error.py +11 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/ci/__init__.py +5 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/ci/ci_contribution_algorithm.py +57 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/codec/__init__.py +34 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/codec/encode_by_schema_algorithm.py +126 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/codec/encode_error.py +11 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/codec/encode_json_value_algorithm.py +81 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/codec/message_parse_error.py +10 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/codec/method_codec_algorithm.py +53 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/codec/normalize_by_schema_algorithm.py +114 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/codec/params_error.py +10 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/codec/received_message_contract.py +38 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/codec/schema_lookup_algorithm.py +62 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/codec/wire_message_mapper.py +90 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/concurrency/__init__.py +21 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/concurrency/concurrency_policy.py +44 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/conformance/__init__.py +7 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/conformance/check_generation_use_case.py +128 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/conformance/check_plugin_use_case.py +131 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/conformance/check_scheduling_use_case.py +141 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/conformance/conformance_report_model.py +53 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/conformance/find_manifests_algorithm.py +38 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/conformance/go_list_mapper.py +17 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/framing/__init__.py +7 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/framing/frame_message_algorithm.py +12 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/framing/frame_reader_store.py +64 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/framing/framing_error.py +11 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/host/__init__.py +29 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/host/call_interrupted_error.py +30 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/host/connect_plugin_use_case.py +103 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/host/host_options_contract.py +47 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/host/plugin_client_client.py +358 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/host/start_plugin_use_case.py +73 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/jsondoc/__init__.py +6 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/jsondoc/indent_raw_json_algorithm.py +51 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/jsondoc/json_document_error.py +7 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/jsondoc/json_document_model.py +129 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/jsondoc/scan_json_object_algorithm.py +165 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/plugin_refs/__init__.py +5 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/plugin_refs/plugin_ref_policy.py +31 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/plugin_roles/__init__.py +12 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/plugin_roles/plugin_role_policy.py +51 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/projects/__init__.py +19 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/projects/audit_settings_policy.py +15 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/projects/manifest_match_algorithm.py +46 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/projects/project_kind_policy.py +25 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/projects/publishes_policy.py +15 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/projects/set_toolchain_algorithm.py +21 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/projects/stack_plugins_algorithm.py +14 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/protocol/__init__.py +8 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/protocol/protocol_schema_contract.py +1991 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/protocol/protocol_types_contract.py +1350 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/protocol/rpc_error.py +22 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/py.typed +0 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/scaffold/__init__.py +6 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/scaffold/scaffold_data_mapper.py +30 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/scaffold/scaffold_project_use_case.py +35 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/__init__.py +45 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/capabilities_of_algorithm.py +48 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/claim_stdio_use_case.py +36 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/dispatch_request_use_case.py +72 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/frame_writer_store.py +27 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/initialize_plugin_use_case.py +29 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/plugin_contract.py +135 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/plugin_members_config.py +35 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/protocol_streams_contract.py +35 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/request_outcome_contract.py +29 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/request_pool_store.py +154 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/serve_use_case.py +215 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/tasks/__init__.py +6 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/tasks/contribute_tasks_algorithm.py +28 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/tasks/task_algorithm.py +71 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/templating/__init__.py +8 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/templating/file_exists_error.py +15 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/templating/parse_template_algorithm.py +223 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/templating/render_template_algorithm.py +143 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/templating/template_error.py +13 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/templating/template_functions_algorithm.py +177 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/templating/template_syntax_contract.py +78 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/templating/tokenize_action_algorithm.py +81 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/templating/write_templates_use_case.py +47 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk.egg-info/PKG-INFO +134 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk.egg-info/SOURCES.txt +93 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk.egg-info/dependency_links.txt +1 -0
- monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eduardo Russo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: monosmith-plugin-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The Python SDK for writing smith plugins: the protocol's types, the serve loop, a test host, the conformance checks, templating and key-order-keeping JSON editing. No runtime dependencies.
|
|
5
|
+
Author: Eduardo Russo
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/monosmith-app/monosmith/tree/main/packages/plugin-sdk-py#readme
|
|
8
|
+
Project-URL: Documentation, https://github.com/monosmith-app/monosmith/blob/main/docs/plugins/creating/python-sdk.md
|
|
9
|
+
Project-URL: Repository, https://github.com/monosmith-app/monosmith
|
|
10
|
+
Project-URL: Issues, https://github.com/monosmith-app/monosmith/issues
|
|
11
|
+
Keywords: smith,monosmith,plugin,sdk,json-rpc
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# monosmith-plugin-sdk
|
|
28
|
+
|
|
29
|
+
The Python SDK for writing [smith](https://github.com/monosmith-app/monosmith) plugins: the
|
|
30
|
+
protocol's types, the serve loop, a host to test a plugin with, the conformance checks of
|
|
31
|
+
`smith plugin check`, `[[ ]]` templating and JSON editing that keeps key order. No runtime
|
|
32
|
+
dependencies; Python 3.10 or later; fully type-annotated.
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
pip install monosmith-plugin-sdk
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
A plugin is an object with `describe` and one method for each thing it does. The SDK frames the
|
|
39
|
+
JSON-RPC messages, answers `initialize`, derives the capabilities from the methods the object has and
|
|
40
|
+
keeps standard output for the protocol:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from monosmith_plugin_sdk import contribute_to, serve, task
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class WidgetLint:
|
|
47
|
+
def describe(self, params):
|
|
48
|
+
return {
|
|
49
|
+
"plugin": {"name": "widget-lint", "version": "0.1.0"},
|
|
50
|
+
"kind": "convention",
|
|
51
|
+
"roles": ["structure"],
|
|
52
|
+
"languages": ["widget"],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
def contribute(self, params):
|
|
56
|
+
return contribute_to(
|
|
57
|
+
params["projects"],
|
|
58
|
+
lambda project: {
|
|
59
|
+
"tasks": {"layout-check": task(["python", "check.py"], cache=True)},
|
|
60
|
+
"dependsOn": {"lint": ["layout-check"]},
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
serve(WidgetLint())
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Test it in-process, with the real handshake and framing, and run the same checks as
|
|
70
|
+
`smith plugin check`:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from monosmith_plugin_sdk import HostOptions, check_plugin, connect_in_process
|
|
74
|
+
|
|
75
|
+
with connect_in_process(WidgetLint(), HostOptions(workspace_root=str(tmp_path))) as client:
|
|
76
|
+
report = check_plugin(client)
|
|
77
|
+
assert report.ok, report.failures()
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The full guide and API reference:
|
|
81
|
+
[The Python SDK](https://github.com/monosmith-app/monosmith/blob/main/docs/plugins/creating/python-sdk.md).
|
|
82
|
+
Examples that use it:
|
|
83
|
+
[`structure-layout-py`](https://github.com/monosmith-app/monosmith/tree/main/docs/examples/plugins/structure-layout-py),
|
|
84
|
+
[`structure-scan-py`](https://github.com/monosmith-app/monosmith/tree/main/docs/examples/plugins/structure-scan-py) and
|
|
85
|
+
[`language-widget-py`](https://github.com/monosmith-app/monosmith/tree/main/docs/examples/plugins/language-widget-py).
|
|
86
|
+
|
|
87
|
+
## The protocol schema is the source of truth
|
|
88
|
+
|
|
89
|
+
The types in `src/monosmith_plugin_sdk/protocol` are not written by hand. The Go types of the protocol
|
|
90
|
+
(`packages/plugin-sdk/protocol`) are the source of truth; their JSON Schema is committed as
|
|
91
|
+
[`packages/plugin-sdk/schema/protocol.schema.json`](../plugin-sdk/schema/protocol.schema.json), and
|
|
92
|
+
`tools/generate_protocol` turns that into `protocol_types_contract.py` (a `TypedDict` per message, the
|
|
93
|
+
methods table, the error codes) and `protocol_schema_contract.py` (the schema itself, which the wire codec
|
|
94
|
+
and the host's capability check read at run time). After a change to the protocol:
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
smith run schema -p plugin-sdk # the Go side writes the schema
|
|
98
|
+
smith run generate -p plugin-sdk-py # this side writes the types
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
A test (`tests/generate_protocol`) and `smith run generate-check -p plugin-sdk-py` fail when either file
|
|
102
|
+
is stale. A test also fails when the schema has a method this SDK does not know how to serve
|
|
103
|
+
(`PLUGIN_MEMBERS`), so a new method has to be given its place in the plugin object.
|
|
104
|
+
|
|
105
|
+
## What is where
|
|
106
|
+
|
|
107
|
+
| Folder of `src/monosmith_plugin_sdk` | What it holds |
|
|
108
|
+
|---|---|
|
|
109
|
+
| `protocol` | the generated types, the schema as data, `RpcError` |
|
|
110
|
+
| `codec` | the wire form of the messages: key order, `omitempty`, sorted maps and escaping as the Go SDK writes them, so the bytes are equal; params shaped as the types promise |
|
|
111
|
+
| `framing` | `Content-Length` framing over a byte stream |
|
|
112
|
+
| `cancellation` | `CancelToken`, what a cancellable method watches |
|
|
113
|
+
| `serve` | `serve`, `serve_streams` (a bounded pool of worker threads, `cancel` and the plugin's `concurrency`), the plugin protocols, capability derivation, standard input and output claimed for the protocol |
|
|
114
|
+
| `concurrency` | what a declared `concurrency` means: one at a time by default, a count, or unlimited |
|
|
115
|
+
| `host` | `start_plugin`, `connect_plugin`, `connect_in_process`, `PluginClient`: the host side, for tests; it keeps to the plugin's concurrency and cancels a call with a `CancelToken` |
|
|
116
|
+
| `conformance` | `check_plugin`: the checks of `smith plugin check` |
|
|
117
|
+
| `templating`, `jsondoc` | ports of the Go SDK's `templating` and `jsondoc` |
|
|
118
|
+
| `tasks`, `scaffold`, `ci` | helpers to build the answers of contribute, generate and ci |
|
|
119
|
+
| `build_dir`, `plugin_roles`, `plugin_refs`, `projects` | the protocol package's small functions: build directories, roles, plugin references, project kinds, manifests |
|
|
120
|
+
|
|
121
|
+
The source is organised by vertical slices (`smith slices check` lints it); `tests/integration` holds the
|
|
122
|
+
tests that start real processes: the examples under `smith plugin check` and in a throwaway workspace, and
|
|
123
|
+
the compatibility test that a Python plugin, a TypeScript plugin and a Go plugin with the same logic answer
|
|
124
|
+
with the same bytes.
|
|
125
|
+
|
|
126
|
+
## Develop
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
smith run lint typecheck test build generate-check -p plugin-sdk-py
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The integration tests need `smith`, `go` and `node` on the path and skip themselves without them (in CI,
|
|
133
|
+
where they exist, a missing one fails the test). The package is released by smith from conventional
|
|
134
|
+
commits, like the other packages of this repository.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# monosmith-plugin-sdk
|
|
2
|
+
|
|
3
|
+
The Python SDK for writing [smith](https://github.com/monosmith-app/monosmith) plugins: the
|
|
4
|
+
protocol's types, the serve loop, a host to test a plugin with, the conformance checks of
|
|
5
|
+
`smith plugin check`, `[[ ]]` templating and JSON editing that keeps key order. No runtime
|
|
6
|
+
dependencies; Python 3.10 or later; fully type-annotated.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
pip install monosmith-plugin-sdk
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
A plugin is an object with `describe` and one method for each thing it does. The SDK frames the
|
|
13
|
+
JSON-RPC messages, answers `initialize`, derives the capabilities from the methods the object has and
|
|
14
|
+
keeps standard output for the protocol:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from monosmith_plugin_sdk import contribute_to, serve, task
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WidgetLint:
|
|
21
|
+
def describe(self, params):
|
|
22
|
+
return {
|
|
23
|
+
"plugin": {"name": "widget-lint", "version": "0.1.0"},
|
|
24
|
+
"kind": "convention",
|
|
25
|
+
"roles": ["structure"],
|
|
26
|
+
"languages": ["widget"],
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def contribute(self, params):
|
|
30
|
+
return contribute_to(
|
|
31
|
+
params["projects"],
|
|
32
|
+
lambda project: {
|
|
33
|
+
"tasks": {"layout-check": task(["python", "check.py"], cache=True)},
|
|
34
|
+
"dependsOn": {"lint": ["layout-check"]},
|
|
35
|
+
},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
serve(WidgetLint())
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Test it in-process, with the real handshake and framing, and run the same checks as
|
|
44
|
+
`smith plugin check`:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from monosmith_plugin_sdk import HostOptions, check_plugin, connect_in_process
|
|
48
|
+
|
|
49
|
+
with connect_in_process(WidgetLint(), HostOptions(workspace_root=str(tmp_path))) as client:
|
|
50
|
+
report = check_plugin(client)
|
|
51
|
+
assert report.ok, report.failures()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The full guide and API reference:
|
|
55
|
+
[The Python SDK](https://github.com/monosmith-app/monosmith/blob/main/docs/plugins/creating/python-sdk.md).
|
|
56
|
+
Examples that use it:
|
|
57
|
+
[`structure-layout-py`](https://github.com/monosmith-app/monosmith/tree/main/docs/examples/plugins/structure-layout-py),
|
|
58
|
+
[`structure-scan-py`](https://github.com/monosmith-app/monosmith/tree/main/docs/examples/plugins/structure-scan-py) and
|
|
59
|
+
[`language-widget-py`](https://github.com/monosmith-app/monosmith/tree/main/docs/examples/plugins/language-widget-py).
|
|
60
|
+
|
|
61
|
+
## The protocol schema is the source of truth
|
|
62
|
+
|
|
63
|
+
The types in `src/monosmith_plugin_sdk/protocol` are not written by hand. The Go types of the protocol
|
|
64
|
+
(`packages/plugin-sdk/protocol`) are the source of truth; their JSON Schema is committed as
|
|
65
|
+
[`packages/plugin-sdk/schema/protocol.schema.json`](../plugin-sdk/schema/protocol.schema.json), and
|
|
66
|
+
`tools/generate_protocol` turns that into `protocol_types_contract.py` (a `TypedDict` per message, the
|
|
67
|
+
methods table, the error codes) and `protocol_schema_contract.py` (the schema itself, which the wire codec
|
|
68
|
+
and the host's capability check read at run time). After a change to the protocol:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
smith run schema -p plugin-sdk # the Go side writes the schema
|
|
72
|
+
smith run generate -p plugin-sdk-py # this side writes the types
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
A test (`tests/generate_protocol`) and `smith run generate-check -p plugin-sdk-py` fail when either file
|
|
76
|
+
is stale. A test also fails when the schema has a method this SDK does not know how to serve
|
|
77
|
+
(`PLUGIN_MEMBERS`), so a new method has to be given its place in the plugin object.
|
|
78
|
+
|
|
79
|
+
## What is where
|
|
80
|
+
|
|
81
|
+
| Folder of `src/monosmith_plugin_sdk` | What it holds |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `protocol` | the generated types, the schema as data, `RpcError` |
|
|
84
|
+
| `codec` | the wire form of the messages: key order, `omitempty`, sorted maps and escaping as the Go SDK writes them, so the bytes are equal; params shaped as the types promise |
|
|
85
|
+
| `framing` | `Content-Length` framing over a byte stream |
|
|
86
|
+
| `cancellation` | `CancelToken`, what a cancellable method watches |
|
|
87
|
+
| `serve` | `serve`, `serve_streams` (a bounded pool of worker threads, `cancel` and the plugin's `concurrency`), the plugin protocols, capability derivation, standard input and output claimed for the protocol |
|
|
88
|
+
| `concurrency` | what a declared `concurrency` means: one at a time by default, a count, or unlimited |
|
|
89
|
+
| `host` | `start_plugin`, `connect_plugin`, `connect_in_process`, `PluginClient`: the host side, for tests; it keeps to the plugin's concurrency and cancels a call with a `CancelToken` |
|
|
90
|
+
| `conformance` | `check_plugin`: the checks of `smith plugin check` |
|
|
91
|
+
| `templating`, `jsondoc` | ports of the Go SDK's `templating` and `jsondoc` |
|
|
92
|
+
| `tasks`, `scaffold`, `ci` | helpers to build the answers of contribute, generate and ci |
|
|
93
|
+
| `build_dir`, `plugin_roles`, `plugin_refs`, `projects` | the protocol package's small functions: build directories, roles, plugin references, project kinds, manifests |
|
|
94
|
+
|
|
95
|
+
The source is organised by vertical slices (`smith slices check` lints it); `tests/integration` holds the
|
|
96
|
+
tests that start real processes: the examples under `smith plugin check` and in a throwaway workspace, and
|
|
97
|
+
the compatibility test that a Python plugin, a TypeScript plugin and a Go plugin with the same logic answer
|
|
98
|
+
with the same bytes.
|
|
99
|
+
|
|
100
|
+
## Develop
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
smith run lint typecheck test build generate-check -p plugin-sdk-py
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The integration tests need `smith`, `go` and `node` on the path and skip themselves without them (in CI,
|
|
107
|
+
where they exist, a missing one fails the test). The package is released by smith from conventional
|
|
108
|
+
commits, like the other packages of this repository.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "monosmith-plugin-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "The Python SDK for writing smith plugins: the protocol's types, the serve loop, a test host, the conformance checks, templating and key-order-keeping JSON editing. No runtime dependencies."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Eduardo Russo" }]
|
|
14
|
+
keywords = ["smith", "monosmith", "plugin", "sdk", "json-rpc"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Software Development :: Libraries",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
dependencies = []
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/monosmith-app/monosmith/tree/main/packages/plugin-sdk-py#readme"
|
|
31
|
+
Documentation = "https://github.com/monosmith-app/monosmith/blob/main/docs/plugins/creating/python-sdk.md"
|
|
32
|
+
Repository = "https://github.com/monosmith-app/monosmith"
|
|
33
|
+
Issues = "https://github.com/monosmith-app/monosmith/issues"
|
|
34
|
+
|
|
35
|
+
[tool.smith]
|
|
36
|
+
kind = "package"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["src"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.package-data]
|
|
42
|
+
monosmith_plugin_sdk = ["py.typed"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
46
|
+
# The tools folder (the schema generator) is a package at the project root; the tests
|
|
47
|
+
# of slices with the same file name in different folders do not clash.
|
|
48
|
+
pythonpath = ["."]
|
|
49
|
+
addopts = "--import-mode=importlib"
|
|
50
|
+
markers = ["integration: starts real processes (smith, go, the example plugins)"]
|
|
51
|
+
|
|
52
|
+
[tool.mypy]
|
|
53
|
+
python_version = "3.10"
|
|
54
|
+
strict = true
|
|
55
|
+
files = ["src", "tools", "tests"]
|
|
56
|
+
mypy_path = "src"
|
|
57
|
+
explicit_package_bases = true
|
|
58
|
+
warn_unreachable = true
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
extend = "../../ruff.toml"
|
|
62
|
+
# The SDK runs on Python 3.10 and later, whatever the workspace's own interpreter is.
|
|
63
|
+
target-version = "py310"
|
|
64
|
+
line-length = 120
|
|
65
|
+
|
|
66
|
+
[tool.ruff.format]
|
|
67
|
+
# Written by tools/generate_protocol; the drift check compares them byte for byte.
|
|
68
|
+
exclude = ["src/monosmith_plugin_sdk/protocol/protocol_*_contract.py"]
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""The Python SDK for writing smith plugins.
|
|
2
|
+
|
|
3
|
+
A plugin is an object with `describe` and one method for each thing it does. `serve` frames the
|
|
4
|
+
JSON-RPC messages, answers `initialize`, derives the capabilities from the methods the object has and
|
|
5
|
+
keeps standard output for the protocol:
|
|
6
|
+
|
|
7
|
+
from monosmith_plugin_sdk import contribute_to, serve, task
|
|
8
|
+
|
|
9
|
+
class WidgetLint:
|
|
10
|
+
def describe(self, params):
|
|
11
|
+
return {"plugin": {"name": "widget-lint", "version": "0.1.0"}, "kind": "convention",
|
|
12
|
+
"roles": ["structure"], "languages": ["widget"]}
|
|
13
|
+
|
|
14
|
+
def contribute(self, params):
|
|
15
|
+
return contribute_to(params["projects"], lambda project: {
|
|
16
|
+
"tasks": {"layout-check": task(["python", "check.py"], cache=True)},
|
|
17
|
+
"dependsOn": {"lint": ["layout-check"]},
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
serve(WidgetLint())
|
|
22
|
+
|
|
23
|
+
Test it in-process, with the real handshake and framing, and run the checks of `smith plugin check`:
|
|
24
|
+
|
|
25
|
+
from monosmith_plugin_sdk import HostOptions, check_plugin, connect_in_process
|
|
26
|
+
|
|
27
|
+
with connect_in_process(WidgetLint(), HostOptions(workspace_root=str(tmp_path))) as client:
|
|
28
|
+
report = check_plugin(client)
|
|
29
|
+
assert report.ok, report.failures()
|
|
30
|
+
|
|
31
|
+
The messages of the protocol are `TypedDict`s generated from the protocol's JSON Schema; the wire
|
|
32
|
+
form is the one the Go and TypeScript SDKs write, byte for byte. No runtime dependencies.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from . import protocol as _protocol
|
|
36
|
+
from .build_dir import BUILD_DIR, build_dir_from_project, in_build_dir, project_build_dir
|
|
37
|
+
from .cancellation import CancelToken, RequestCancelledError
|
|
38
|
+
from .ci import action_step, ci_step, credential
|
|
39
|
+
from .codec import (
|
|
40
|
+
EncodeError,
|
|
41
|
+
MessageParseError,
|
|
42
|
+
ParamsError,
|
|
43
|
+
decode_params,
|
|
44
|
+
decode_result,
|
|
45
|
+
encode_params,
|
|
46
|
+
encode_result,
|
|
47
|
+
)
|
|
48
|
+
from .concurrency import (
|
|
49
|
+
CONCURRENCY_UNLIMITED,
|
|
50
|
+
UNLIMITED_IN_FLIGHT,
|
|
51
|
+
UNLIMITED_WORKERS,
|
|
52
|
+
in_flight_limit,
|
|
53
|
+
is_valid_concurrency,
|
|
54
|
+
worker_limit,
|
|
55
|
+
)
|
|
56
|
+
from .conformance import CONFORMANCE_STACK, CheckResult, ConformanceReport, check_plugin
|
|
57
|
+
from .framing import FrameReader, FramingError, frame_message
|
|
58
|
+
from .host import (
|
|
59
|
+
DEFAULT_CALL_TIMEOUT,
|
|
60
|
+
DEFAULT_CANCEL_GRACE,
|
|
61
|
+
CallInterruptedError,
|
|
62
|
+
HostOptions,
|
|
63
|
+
PluginClient,
|
|
64
|
+
PluginStreams,
|
|
65
|
+
StartOptions,
|
|
66
|
+
connect_in_process,
|
|
67
|
+
connect_plugin,
|
|
68
|
+
start_plugin,
|
|
69
|
+
)
|
|
70
|
+
from .jsondoc import JsonDocument, JsonDocumentError
|
|
71
|
+
from .plugin_refs import conflicts_with, describe_ref, is_empty_ref, requires_language
|
|
72
|
+
from .plugin_roles import (
|
|
73
|
+
ROLES,
|
|
74
|
+
default_roles,
|
|
75
|
+
effective_roles,
|
|
76
|
+
is_additive_role,
|
|
77
|
+
is_exclusive_role,
|
|
78
|
+
is_known_role,
|
|
79
|
+
)
|
|
80
|
+
from .projects import (
|
|
81
|
+
audit_blocks,
|
|
82
|
+
audit_enabled,
|
|
83
|
+
is_manifest,
|
|
84
|
+
project_kind_directory,
|
|
85
|
+
publishes,
|
|
86
|
+
resolve_project_kind,
|
|
87
|
+
set_toolchain,
|
|
88
|
+
stack_plugins,
|
|
89
|
+
)
|
|
90
|
+
from .protocol import * # noqa: F403
|
|
91
|
+
from .scaffold import scaffold_data, scaffold_project
|
|
92
|
+
from .serve import (
|
|
93
|
+
CIContributor,
|
|
94
|
+
Contributor,
|
|
95
|
+
DeclarationEditor,
|
|
96
|
+
DependencyManager,
|
|
97
|
+
Discoverer,
|
|
98
|
+
DoctorContributor,
|
|
99
|
+
Generator,
|
|
100
|
+
LatestReader,
|
|
101
|
+
Plugin,
|
|
102
|
+
Publisher,
|
|
103
|
+
RegistryReader,
|
|
104
|
+
Remover,
|
|
105
|
+
ServeStreams,
|
|
106
|
+
Versioner,
|
|
107
|
+
capabilities_of,
|
|
108
|
+
serve,
|
|
109
|
+
serve_streams,
|
|
110
|
+
)
|
|
111
|
+
from .tasks import build_task, contribute_to, task
|
|
112
|
+
from .templating import TemplateError, TemplateFileExistsError, render_template, write_templates
|
|
113
|
+
|
|
114
|
+
__all__ = [
|
|
115
|
+
*_protocol.__all__,
|
|
116
|
+
# Writing a plugin.
|
|
117
|
+
"serve",
|
|
118
|
+
"Plugin",
|
|
119
|
+
"Discoverer",
|
|
120
|
+
"Generator",
|
|
121
|
+
"Versioner",
|
|
122
|
+
"Publisher",
|
|
123
|
+
"RegistryReader",
|
|
124
|
+
"CIContributor",
|
|
125
|
+
"DoctorContributor",
|
|
126
|
+
"Remover",
|
|
127
|
+
"DependencyManager",
|
|
128
|
+
"DeclarationEditor",
|
|
129
|
+
"LatestReader",
|
|
130
|
+
"Contributor",
|
|
131
|
+
"CancelToken",
|
|
132
|
+
"RequestCancelledError",
|
|
133
|
+
"CONCURRENCY_UNLIMITED",
|
|
134
|
+
"task",
|
|
135
|
+
"build_task",
|
|
136
|
+
"contribute_to",
|
|
137
|
+
"scaffold_project",
|
|
138
|
+
"scaffold_data",
|
|
139
|
+
"action_step",
|
|
140
|
+
"ci_step",
|
|
141
|
+
"credential",
|
|
142
|
+
"BUILD_DIR",
|
|
143
|
+
"project_build_dir",
|
|
144
|
+
"build_dir_from_project",
|
|
145
|
+
"in_build_dir",
|
|
146
|
+
"ROLES",
|
|
147
|
+
"default_roles",
|
|
148
|
+
"effective_roles",
|
|
149
|
+
"is_known_role",
|
|
150
|
+
"is_exclusive_role",
|
|
151
|
+
"is_additive_role",
|
|
152
|
+
"requires_language",
|
|
153
|
+
"conflicts_with",
|
|
154
|
+
"describe_ref",
|
|
155
|
+
"is_empty_ref",
|
|
156
|
+
"audit_enabled",
|
|
157
|
+
"audit_blocks",
|
|
158
|
+
"is_manifest",
|
|
159
|
+
"project_kind_directory",
|
|
160
|
+
"publishes",
|
|
161
|
+
"resolve_project_kind",
|
|
162
|
+
"set_toolchain",
|
|
163
|
+
"stack_plugins",
|
|
164
|
+
"render_template",
|
|
165
|
+
"write_templates",
|
|
166
|
+
"TemplateError",
|
|
167
|
+
"TemplateFileExistsError",
|
|
168
|
+
"JsonDocument",
|
|
169
|
+
"JsonDocumentError",
|
|
170
|
+
# Testing a plugin.
|
|
171
|
+
"HostOptions",
|
|
172
|
+
"StartOptions",
|
|
173
|
+
"PluginClient",
|
|
174
|
+
"PluginStreams",
|
|
175
|
+
"CallInterruptedError",
|
|
176
|
+
"DEFAULT_CALL_TIMEOUT",
|
|
177
|
+
"DEFAULT_CANCEL_GRACE",
|
|
178
|
+
"start_plugin",
|
|
179
|
+
"connect_plugin",
|
|
180
|
+
"connect_in_process",
|
|
181
|
+
"check_plugin",
|
|
182
|
+
"ConformanceReport",
|
|
183
|
+
"CheckResult",
|
|
184
|
+
"CONFORMANCE_STACK",
|
|
185
|
+
# Building blocks: the wire form of the messages, for tools that speak the protocol themselves.
|
|
186
|
+
"serve_streams",
|
|
187
|
+
"ServeStreams",
|
|
188
|
+
"capabilities_of",
|
|
189
|
+
"decode_params",
|
|
190
|
+
"decode_result",
|
|
191
|
+
"encode_params",
|
|
192
|
+
"encode_result",
|
|
193
|
+
"EncodeError",
|
|
194
|
+
"MessageParseError",
|
|
195
|
+
"ParamsError",
|
|
196
|
+
"FrameReader",
|
|
197
|
+
"FramingError",
|
|
198
|
+
"frame_message",
|
|
199
|
+
"UNLIMITED_IN_FLIGHT",
|
|
200
|
+
"UNLIMITED_WORKERS",
|
|
201
|
+
"in_flight_limit",
|
|
202
|
+
"is_valid_concurrency",
|
|
203
|
+
"worker_limit",
|
|
204
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Where every build writes its output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
BUILD_DIR = "dist"
|
|
6
|
+
"""The directory at the workspace root every build writes into.
|
|
7
|
+
|
|
8
|
+
It is smith's opinion, so all output is in one place, outside every source tree, and cleaning is
|
|
9
|
+
deleting one folder. A plugin builds a project into `project_build_dir`; `smith doctor` fails a task
|
|
10
|
+
whose outputs fall outside it.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def project_build_dir(root: str) -> str:
|
|
15
|
+
"""Where a project's build goes, workspace-relative: `dist/<project dir>` (`dist/apps/web`)."""
|
|
16
|
+
return f"{BUILD_DIR}/{root.strip('/')}"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_dir_from_project(root: str) -> str:
|
|
20
|
+
"""The same directory relative to the project (`../../dist/apps/web`).
|
|
21
|
+
|
|
22
|
+
It is what a tool configured from inside the project, or a task's `outputs`, name.
|
|
23
|
+
"""
|
|
24
|
+
trimmed = root.strip("/")
|
|
25
|
+
return "../" * len(trimmed.split("/")) + project_build_dir(trimmed)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def in_build_dir(path: str) -> bool:
|
|
29
|
+
"""Whether a workspace-relative path is inside the build directory."""
|
|
30
|
+
return path == BUILD_DIR or path.startswith(f"{BUILD_DIR}/")
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""The token that carries a cancel to the work it concerns."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
|
|
8
|
+
from .request_cancelled_error import RequestCancelledError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CancelToken:
|
|
12
|
+
"""A flag one thread sets and another watches, to stop work cooperatively.
|
|
13
|
+
|
|
14
|
+
The serve loop gives a method one for the request it is answering, and sets it when the
|
|
15
|
+
host sends `cancel` for the request (or when the plugin shuts down). A host in a test
|
|
16
|
+
passes one to `PluginClient.call` and sets it to cancel the call.
|
|
17
|
+
|
|
18
|
+
The usual loop checks it between steps, and waits on it instead of sleeping:
|
|
19
|
+
|
|
20
|
+
for path in files:
|
|
21
|
+
cancel.raise_if_cancelled()
|
|
22
|
+
scan(path)
|
|
23
|
+
if cancel.wait(0.1): # sleeps up to 0.1 s, and wakes at once on a cancel
|
|
24
|
+
return partial_result
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
self._event = threading.Event()
|
|
29
|
+
self._lock = threading.Lock()
|
|
30
|
+
self._callbacks: list[Callable[[], None]] = []
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def cancelled(self) -> bool:
|
|
34
|
+
"""Whether the work was asked to stop."""
|
|
35
|
+
return self._event.is_set()
|
|
36
|
+
|
|
37
|
+
def cancel(self) -> None:
|
|
38
|
+
"""Ask the work to stop. Calling it again does nothing."""
|
|
39
|
+
with self._lock:
|
|
40
|
+
if self._event.is_set():
|
|
41
|
+
return
|
|
42
|
+
self._event.set()
|
|
43
|
+
callbacks, self._callbacks = self._callbacks, []
|
|
44
|
+
for callback in callbacks:
|
|
45
|
+
callback()
|
|
46
|
+
|
|
47
|
+
def wait(self, timeout: float | None = None) -> bool:
|
|
48
|
+
"""Wait up to `timeout` seconds (forever for `None`); return whether it was cancelled.
|
|
49
|
+
|
|
50
|
+
A cancellable sleep: it returns early, with `True`, the moment the token is cancelled.
|
|
51
|
+
"""
|
|
52
|
+
return self._event.wait(timeout)
|
|
53
|
+
|
|
54
|
+
def raise_if_cancelled(self, message: str = "the request was cancelled") -> None:
|
|
55
|
+
"""Raise `RequestCancelledError` when the work was asked to stop."""
|
|
56
|
+
if self._event.is_set():
|
|
57
|
+
raise RequestCancelledError(message)
|
|
58
|
+
|
|
59
|
+
def on_cancel(self, callback: Callable[[], None]) -> Callable[[], None]:
|
|
60
|
+
"""Call `callback` (in the cancelling thread) when the token is cancelled, at once if it already is.
|
|
61
|
+
|
|
62
|
+
Returns a function that removes the callback again.
|
|
63
|
+
"""
|
|
64
|
+
with self._lock:
|
|
65
|
+
if not self._event.is_set():
|
|
66
|
+
self._callbacks.append(callback)
|
|
67
|
+
|
|
68
|
+
def remove() -> None:
|
|
69
|
+
with self._lock:
|
|
70
|
+
if callback in self._callbacks:
|
|
71
|
+
self._callbacks.remove(callback)
|
|
72
|
+
|
|
73
|
+
return remove
|
|
74
|
+
callback()
|
|
75
|
+
return lambda: None
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""The error a method raises to say it stopped because the request was cancelled."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RequestCancelledError(Exception):
|
|
7
|
+
"""Raised by a method that noticed its request was cancelled (`CancelToken.raise_if_cancelled`).
|
|
8
|
+
|
|
9
|
+
The serve loop answers a cancelled request that fails with any error with the code
|
|
10
|
+
`REQUEST_CANCELLED`, so raising this is a convenience, not a requirement.
|
|
11
|
+
"""
|