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.
Files changed (95) hide show
  1. monosmith_plugin_sdk-0.1.0/LICENSE +21 -0
  2. monosmith_plugin_sdk-0.1.0/PKG-INFO +134 -0
  3. monosmith_plugin_sdk-0.1.0/README.md +108 -0
  4. monosmith_plugin_sdk-0.1.0/pyproject.toml +68 -0
  5. monosmith_plugin_sdk-0.1.0/setup.cfg +4 -0
  6. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/__init__.py +204 -0
  7. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/build_dir/__init__.py +5 -0
  8. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/build_dir/build_dir_algorithm.py +30 -0
  9. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/cancellation/__init__.py +6 -0
  10. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/cancellation/cancel_token_model.py +75 -0
  11. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/cancellation/request_cancelled_error.py +11 -0
  12. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/ci/__init__.py +5 -0
  13. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/ci/ci_contribution_algorithm.py +57 -0
  14. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/codec/__init__.py +34 -0
  15. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/codec/encode_by_schema_algorithm.py +126 -0
  16. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/codec/encode_error.py +11 -0
  17. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/codec/encode_json_value_algorithm.py +81 -0
  18. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/codec/message_parse_error.py +10 -0
  19. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/codec/method_codec_algorithm.py +53 -0
  20. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/codec/normalize_by_schema_algorithm.py +114 -0
  21. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/codec/params_error.py +10 -0
  22. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/codec/received_message_contract.py +38 -0
  23. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/codec/schema_lookup_algorithm.py +62 -0
  24. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/codec/wire_message_mapper.py +90 -0
  25. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/concurrency/__init__.py +21 -0
  26. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/concurrency/concurrency_policy.py +44 -0
  27. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/conformance/__init__.py +7 -0
  28. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/conformance/check_generation_use_case.py +128 -0
  29. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/conformance/check_plugin_use_case.py +131 -0
  30. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/conformance/check_scheduling_use_case.py +141 -0
  31. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/conformance/conformance_report_model.py +53 -0
  32. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/conformance/find_manifests_algorithm.py +38 -0
  33. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/conformance/go_list_mapper.py +17 -0
  34. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/framing/__init__.py +7 -0
  35. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/framing/frame_message_algorithm.py +12 -0
  36. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/framing/frame_reader_store.py +64 -0
  37. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/framing/framing_error.py +11 -0
  38. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/host/__init__.py +29 -0
  39. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/host/call_interrupted_error.py +30 -0
  40. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/host/connect_plugin_use_case.py +103 -0
  41. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/host/host_options_contract.py +47 -0
  42. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/host/plugin_client_client.py +358 -0
  43. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/host/start_plugin_use_case.py +73 -0
  44. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/jsondoc/__init__.py +6 -0
  45. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/jsondoc/indent_raw_json_algorithm.py +51 -0
  46. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/jsondoc/json_document_error.py +7 -0
  47. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/jsondoc/json_document_model.py +129 -0
  48. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/jsondoc/scan_json_object_algorithm.py +165 -0
  49. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/plugin_refs/__init__.py +5 -0
  50. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/plugin_refs/plugin_ref_policy.py +31 -0
  51. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/plugin_roles/__init__.py +12 -0
  52. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/plugin_roles/plugin_role_policy.py +51 -0
  53. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/projects/__init__.py +19 -0
  54. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/projects/audit_settings_policy.py +15 -0
  55. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/projects/manifest_match_algorithm.py +46 -0
  56. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/projects/project_kind_policy.py +25 -0
  57. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/projects/publishes_policy.py +15 -0
  58. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/projects/set_toolchain_algorithm.py +21 -0
  59. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/projects/stack_plugins_algorithm.py +14 -0
  60. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/protocol/__init__.py +8 -0
  61. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/protocol/protocol_schema_contract.py +1991 -0
  62. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/protocol/protocol_types_contract.py +1350 -0
  63. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/protocol/rpc_error.py +22 -0
  64. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/py.typed +0 -0
  65. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/scaffold/__init__.py +6 -0
  66. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/scaffold/scaffold_data_mapper.py +30 -0
  67. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/scaffold/scaffold_project_use_case.py +35 -0
  68. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/__init__.py +45 -0
  69. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/capabilities_of_algorithm.py +48 -0
  70. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/claim_stdio_use_case.py +36 -0
  71. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/dispatch_request_use_case.py +72 -0
  72. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/frame_writer_store.py +27 -0
  73. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/initialize_plugin_use_case.py +29 -0
  74. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/plugin_contract.py +135 -0
  75. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/plugin_members_config.py +35 -0
  76. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/protocol_streams_contract.py +35 -0
  77. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/request_outcome_contract.py +29 -0
  78. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/request_pool_store.py +154 -0
  79. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/serve/serve_use_case.py +215 -0
  80. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/tasks/__init__.py +6 -0
  81. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/tasks/contribute_tasks_algorithm.py +28 -0
  82. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/tasks/task_algorithm.py +71 -0
  83. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/templating/__init__.py +8 -0
  84. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/templating/file_exists_error.py +15 -0
  85. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/templating/parse_template_algorithm.py +223 -0
  86. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/templating/render_template_algorithm.py +143 -0
  87. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/templating/template_error.py +13 -0
  88. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/templating/template_functions_algorithm.py +177 -0
  89. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/templating/template_syntax_contract.py +78 -0
  90. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/templating/tokenize_action_algorithm.py +81 -0
  91. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk/templating/write_templates_use_case.py +47 -0
  92. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk.egg-info/PKG-INFO +134 -0
  93. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk.egg-info/SOURCES.txt +93 -0
  94. monosmith_plugin_sdk-0.1.0/src/monosmith_plugin_sdk.egg-info/dependency_links.txt +1 -0
  95. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,5 @@
1
+ """The directory every build writes into."""
2
+
3
+ from .build_dir_algorithm import BUILD_DIR, build_dir_from_project, in_build_dir, project_build_dir
4
+
5
+ __all__ = ["BUILD_DIR", "build_dir_from_project", "in_build_dir", "project_build_dir"]
@@ -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,6 @@
1
+ """Cooperative cancellation: the token a method watches, and the error it raises when it stops."""
2
+
3
+ from .cancel_token_model import CancelToken
4
+ from .request_cancelled_error import RequestCancelledError
5
+
6
+ __all__ = ["CancelToken", "RequestCancelledError"]
@@ -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
+ """