3lc-compute-plugin-sdk 0.2.3__tar.gz → 0.3.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.
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/.github/workflows/release.yml +27 -1
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/CHANGELOG.md +62 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/CLAUDE.md +10 -7
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/PKG-INFO +13 -10
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/README.md +12 -9
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/docs/api.md +7 -13
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/docs/plugin-guide.md +87 -40
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/pyproject.toml +1 -1
- 3lc_compute_plugin_sdk-0.3.0/src/tlc_plugin_sdk/__init__.py +42 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/asgi_app.py +17 -9
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/contract/plugin-api.d.ts +105 -27
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/contract.py +29 -24
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/job_context.py +48 -10
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/shared/job_tracker.py +22 -2
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/shared/labels.py +11 -9
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/shared/modality.py +7 -27
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/worker.py +5 -2
- 3lc_compute_plugin_sdk-0.3.0/tests/test_job_context.py +139 -0
- 3lc_compute_plugin_sdk-0.3.0/tests/test_js_contract_surface.py +50 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/uv.lock +1 -1
- 3lc_compute_plugin_sdk-0.2.3/src/tlc_plugin_sdk/__init__.py +0 -58
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/.github/workflows/ci.yml +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/.github/workflows/docs.yml +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/.gitignore +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/LICENSE +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/docs/conf.py +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/docs/index.md +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/docs/package-lock.json +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/docs/package.json +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/docs/requirements.txt +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/py.typed +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/shared/__init__.py +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/shared/alias_override_ui.py +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/shared/alias_ui.py +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/shared/aliases.py +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/shared/config_store.py +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/shared/config_ui.py +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/shared/data_source_routes.py +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/shared/data_source_ui.py +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/shared/generic_job.py +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/shared/images.py +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/shared/model_storage.py +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/shared/naming.py +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/shared/ui_inject.py +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/shared/url_utils.py +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/tests/test_data_source_routes.py +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/tests/test_import_light.py +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/tests/test_url_utils.py +0 -0
- {3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/tests/test_worker_gpu_reclaim.py +0 -0
|
@@ -28,7 +28,7 @@ jobs:
|
|
|
28
28
|
runs-on: ubuntu-latest
|
|
29
29
|
environment: release
|
|
30
30
|
permissions:
|
|
31
|
-
contents:
|
|
31
|
+
contents: write # create the GitHub Release (step below)
|
|
32
32
|
id-token: write # OIDC token for PyPI Trusted Publishing
|
|
33
33
|
steps:
|
|
34
34
|
- uses: actions/checkout@v4
|
|
@@ -53,6 +53,32 @@ jobs:
|
|
|
53
53
|
- name: Publish to PyPI
|
|
54
54
|
run: uv publish --trusted-publishing always
|
|
55
55
|
|
|
56
|
+
# Publish the tagged version's changelog section as a GitHub Release (curated notes,
|
|
57
|
+
# not GitHub's commit list). The Release stays editable after publish for touch-up.
|
|
58
|
+
- name: Extract changelog section
|
|
59
|
+
id: changelog
|
|
60
|
+
run: |
|
|
61
|
+
VER="${GITHUB_REF_NAME#v}"
|
|
62
|
+
awk -v v="$VER" '$0 ~ "^## \\[" v "\\]" {f=1; next} /^## \[/ && f {exit} f' \
|
|
63
|
+
CHANGELOG.md > notes.md
|
|
64
|
+
echo "ver=$VER" >> "$GITHUB_OUTPUT"
|
|
65
|
+
{ echo "notes<<EOF"; cat notes.md; echo EOF; } >> "$GITHUB_OUTPUT"
|
|
66
|
+
|
|
67
|
+
- name: Create GitHub Release
|
|
68
|
+
uses: softprops/action-gh-release@v2
|
|
69
|
+
with:
|
|
70
|
+
tag_name: ${{ github.ref_name }}
|
|
71
|
+
name: ${{ github.ref_name }}
|
|
72
|
+
body: |
|
|
73
|
+
${{ steps.changelog.outputs.notes }}
|
|
74
|
+
|
|
75
|
+
## Installation
|
|
76
|
+
```bash
|
|
77
|
+
pip install 3lc-compute-plugin-sdk==${{ steps.changelog.outputs.ver }}
|
|
78
|
+
```
|
|
79
|
+
draft: false
|
|
80
|
+
generate_release_notes: false
|
|
81
|
+
|
|
56
82
|
# Manual (workflow_dispatch) runs → build-numbered prereleases to the legacy CloudRepo index.
|
|
57
83
|
publish-cloudrepo-prerelease:
|
|
58
84
|
if: github.event_name == 'workflow_dispatch'
|
|
@@ -9,6 +9,68 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
Nothing yet.
|
|
11
11
|
|
|
12
|
+
## [0.3.0] - 2026-08-25
|
|
13
|
+
|
|
14
|
+
A coordinated contract wave: breaking **for plugin authors** but **not on the wire** — the
|
|
15
|
+
worker→host NDJSON events are unchanged, so a host keeps running a plugin venv still on 0.2.x
|
|
16
|
+
(skew badge only). The whole first-party fleet re-pins `>=0.3.0,<0.4.0` in the same release.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- `JobContext.fail(message)` raises `JobFailed` (exported from `tlc_plugin_sdk`); the worker
|
|
20
|
+
reports it as the terminal `error` event with the **bare** message, while any other exception
|
|
21
|
+
keeps its `TypeName: …` prefix.
|
|
22
|
+
- `PluginJobs.list(pluginId?)` — `GET {compute}/api/plugins/jobs`, filtered client-side by
|
|
23
|
+
`plugin_id`. Use it to **seed a fragment on mount** from the durable host job list (the
|
|
24
|
+
fragment is torn down on navigation and `job_update` is live-only). Declared in the `.d.ts`.
|
|
25
|
+
- `PluginJobs.connect(namespace)` — warm the namespace socket on mount. `track()`/`on()`
|
|
26
|
+
connect lazily and SocketIO does not replay events to a not-yet-connected client, so a
|
|
27
|
+
fragment listening for custom events from the first second (or re-attaching to a job it did
|
|
28
|
+
not start) calls this first; `run()` already connects for you.
|
|
29
|
+
- The host's `/ui` handler now **auto-injects** the `PluginJobs` client into every fragment; the
|
|
30
|
+
client is idempotent, so a plugin that still injects it by hand keeps working.
|
|
31
|
+
- `ComputePlugin.compute()` ships a default (returns an error dict), so only `get_ui_fragment()`
|
|
32
|
+
stays abstract.
|
|
33
|
+
- `.d.ts` truth pass: `PluginJobUpdate.error`, `PluginFetchOptions.allowErrorStatus`,
|
|
34
|
+
`PluginJobsApi.list`, `TlcLocationApi.rowsSpanLocations`/`shortLabel`, and a new
|
|
35
|
+
**Legacy globals** ambient block (`openTablePicker`/`closeTablePicker`/`CancelJob`/`cssVar`
|
|
36
|
+
and the injected `_tlc*` helpers).
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
- **One contract axis.** `PY_CONTRACT` and `JS_CONTRACT` are removed; `SDK_CONTRACT_VERSION`
|
|
40
|
+
(the package version) is the whole contract, compared on `MAJOR.MINOR`. `/health` reports
|
|
41
|
+
`sdk_version` only (no `py_contract`/`js_contract`).
|
|
42
|
+
- `JobContext.result` takes a **positional** `url` (`ctx.result(url)`); it opens the Open
|
|
43
|
+
button — a run *or* a table URL. The `run_url=` keyword is gone; the emitted `result` event
|
|
44
|
+
still carries `run_url` (wire unchanged).
|
|
45
|
+
- `JobContext.progress` documents `percent=-1` as indeterminate.
|
|
46
|
+
|
|
47
|
+
### Removed
|
|
48
|
+
- `shared.modality.detect_modality_from_url` (zero users). `detect_modality_from_schema`,
|
|
49
|
+
`classify_metrics_columns`, `labels.candidate_label_paths`, and `labels.find_label_path`
|
|
50
|
+
are now internal (underscore-prefixed). Public alternatives: `detect_modality_from_table`
|
|
51
|
+
for modality; `get_label_map` / `get_label_names` / `find_label_column` for labels.
|
|
52
|
+
`classify_metrics_columns` was a plugin-specific column-name heuristic with no public
|
|
53
|
+
replacement — inline it (its one known user did).
|
|
54
|
+
- The orphan `_ICON_SVG` constant; the dead `params.setdefault("url", "")` in the `/compute`
|
|
55
|
+
handler.
|
|
56
|
+
|
|
57
|
+
### Migration
|
|
58
|
+
- `ctx.result(run_url=x)` → `ctx.result(x)`.
|
|
59
|
+
- Validation failures: raise, or `ctx.fail("message")` for a clean user-facing card.
|
|
60
|
+
- Reading `tlc_plugin_sdk.PY_CONTRACT` / `JS_CONTRACT`, or a worker's `/health`
|
|
61
|
+
`py_contract`/`js_contract`: use `SDK_CONTRACT_VERSION` / `sdk_version` and compare on
|
|
62
|
+
`MAJOR.MINOR`.
|
|
63
|
+
- `shared.modality.detect_modality_from_url(url)` → load the table yourself and call
|
|
64
|
+
`detect_modality_from_table(table)`. `classify_metrics_columns(columns)` → copy the
|
|
65
|
+
heuristic into your plugin.
|
|
66
|
+
- A fragment that kept its own always-connected socket for custom events: switch to
|
|
67
|
+
`PluginJobs.on(namespace, event, handler)` and call `PluginJobs.connect(namespace)` on mount
|
|
68
|
+
so nothing emitted before the first `track()` is missed.
|
|
69
|
+
- Fragments may delete their manual `inject_scripts(raw, job_tracker_script())` — the host
|
|
70
|
+
injects `PluginJobs` now (harmless to keep).
|
|
71
|
+
- The manifest never declared the SocketIO namespace or `sdk_version`; both were dead and are
|
|
72
|
+
documented as such. The namespace is host-derived as `/<plugin-id>`.
|
|
73
|
+
|
|
12
74
|
## [0.2.3] - 2026-08-21
|
|
13
75
|
|
|
14
76
|
### Changed
|
|
@@ -83,10 +83,11 @@ section is the condensed working order.
|
|
|
83
83
|
- **Config + GPU job + SocketIO**: sam3, yolo, or timm
|
|
84
84
|
- **Inline data display (no standalone page)**: `tlc_plugin_table_statistics`
|
|
85
85
|
- **Action on selected resources**: `tlc_plugin_merger`
|
|
86
|
-
3. Read `src/tlc_plugin_sdk/contract.py` — `ComputePlugin` is an `abc.ABC` with
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
`get_route_handlers`) ship as
|
|
86
|
+
3. Read `src/tlc_plugin_sdk/contract.py` — `ComputePlugin` is an `abc.ABC` with one abstract
|
|
87
|
+
method (`get_ui_fragment()`) and an `id` attribute the host stamps from the manifest.
|
|
88
|
+
`compute()` and the other hooks (`run_job`, `initialise_runtime`, `shutdown_runtime`,
|
|
89
|
+
`get_route_handlers`) ship as defaults (`compute()` reports that the plugin doesn't
|
|
90
|
+
implement it), so you override only what you need. There is
|
|
90
91
|
no `register()` and **no** `get_active_jobs`/`cancel_job` — the host owns job listing and
|
|
91
92
|
cancellation.
|
|
92
93
|
4. Read `tlc_plugin_exporter`'s `plugin.toml` + `__init__.py` — the canonical manifest +
|
|
@@ -154,9 +155,11 @@ section is the condensed working order.
|
|
|
154
155
|
form fills call `_tlcSyncAliasFromForm(prefix, projectId, folderId)`.
|
|
155
156
|
|
|
156
157
|
**SocketIO** (if real-time updates are needed):
|
|
157
|
-
-
|
|
158
|
-
|
|
159
|
-
- Prefer the `window.PluginJobs` client over a hand-rolled socket.
|
|
158
|
+
- The namespace is host-derived as `/<plugin-id>` and auto-registered at startup; the manifest
|
|
159
|
+
does not (and never did) declare it.
|
|
160
|
+
- Prefer the `window.PluginJobs` client over a hand-rolled socket. It connects a namespace
|
|
161
|
+
lazily; call `PluginJobs.connect('/<plugin-id>')` on mount if the fragment needs custom
|
|
162
|
+
events from the first second (`run()` connects for you).
|
|
160
163
|
- Use `ctx.emit(name, payload)` only for telemetry the generic schema can't express
|
|
161
164
|
(e.g. a loss curve); never re-emit the generic lifecycle by hand.
|
|
162
165
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: 3lc-compute-plugin-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Public Python SDK for building 3LC compute-service plugins.
|
|
5
5
|
Project-URL: Homepage, https://github.com/3lc-ai/3lc-compute-plugin-sdk
|
|
6
6
|
Project-URL: Repository, https://github.com/3lc-ai/3lc-compute-plugin-sdk
|
|
@@ -44,9 +44,10 @@ pip install 3lc-compute-plugin-sdk # import name: tlc_plugin_sdk
|
|
|
44
44
|
|
|
45
45
|
## What it gives you
|
|
46
46
|
|
|
47
|
-
- **`ComputePlugin`** — the base class you subclass. Implement `
|
|
48
|
-
job and lifecycle hooks ship as
|
|
49
|
-
metadata lives in its `
|
|
47
|
+
- **`ComputePlugin`** — the base class you subclass. Implement `get_ui_fragment` (and `compute`
|
|
48
|
+
if you expose a synchronous endpoint); job and lifecycle hooks ship as safe defaults. There is
|
|
49
|
+
no `register()` to call — a plugin's metadata lives in its `plugin.toml` manifest (or a
|
|
50
|
+
`[tool.tlc-compute]` table in `pyproject.toml`), and the Hub discovers it from there.
|
|
50
51
|
- **`JobContext`** — the surface a long-running job programs against: `progress` / `metric` /
|
|
51
52
|
`log` / `result` for the generic job panel, `emit` for your plugin's own rich UI, and
|
|
52
53
|
cooperative `cancelled`.
|
|
@@ -63,10 +64,10 @@ format, custom routes, the job model, UI fragment, checklist).
|
|
|
63
64
|
|
|
64
65
|
## The contract version
|
|
65
66
|
|
|
66
|
-
`tlc_plugin_sdk.SDK_CONTRACT_VERSION` is this package's own version — one source of truth
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
`tlc_plugin_sdk.SDK_CONTRACT_VERSION` is this package's own version — one source of truth, one
|
|
68
|
+
contract axis (both the Python and the browser surface). A plugin pins the SDK
|
|
69
|
+
(`3lc-compute-plugin-sdk>=X,<Y`); the compute service and frontend implement a range and compare
|
|
70
|
+
compatibility on `MAJOR.MINOR`. Versions are SemVer; see **Status** below for the 0.x stability
|
|
70
71
|
stance.
|
|
71
72
|
|
|
72
73
|
## How it fits
|
|
@@ -79,7 +80,9 @@ therefore portable across service versions and runs in its own isolated environm
|
|
|
79
80
|
|
|
80
81
|
## Status
|
|
81
82
|
|
|
82
|
-
**0.
|
|
83
|
-
|
|
83
|
+
**0.3 is the current contract line.** Within 0.x the contract still evolves — mostly additively,
|
|
84
|
+
but a coordinated breaking change may land on a MINOR bump while the fleet re-pins in lockstep (0.3
|
|
85
|
+
did: `JobContext.result` took a positional `url`, and the `PY_CONTRACT`/`JS_CONTRACT` axes
|
|
86
|
+
collapsed into `SDK_CONTRACT_VERSION`). Anything reshaping the core still waits for a major bump. In
|
|
84
87
|
the browser bridge, `PLUGIN_API.libs.io` is a stable part of the contract; the other bundled
|
|
85
88
|
libs are best-effort (see the guide).
|
|
@@ -18,9 +18,10 @@ pip install 3lc-compute-plugin-sdk # import name: tlc_plugin_sdk
|
|
|
18
18
|
|
|
19
19
|
## What it gives you
|
|
20
20
|
|
|
21
|
-
- **`ComputePlugin`** — the base class you subclass. Implement `
|
|
22
|
-
job and lifecycle hooks ship as
|
|
23
|
-
metadata lives in its `
|
|
21
|
+
- **`ComputePlugin`** — the base class you subclass. Implement `get_ui_fragment` (and `compute`
|
|
22
|
+
if you expose a synchronous endpoint); job and lifecycle hooks ship as safe defaults. There is
|
|
23
|
+
no `register()` to call — a plugin's metadata lives in its `plugin.toml` manifest (or a
|
|
24
|
+
`[tool.tlc-compute]` table in `pyproject.toml`), and the Hub discovers it from there.
|
|
24
25
|
- **`JobContext`** — the surface a long-running job programs against: `progress` / `metric` /
|
|
25
26
|
`log` / `result` for the generic job panel, `emit` for your plugin's own rich UI, and
|
|
26
27
|
cooperative `cancelled`.
|
|
@@ -37,10 +38,10 @@ format, custom routes, the job model, UI fragment, checklist).
|
|
|
37
38
|
|
|
38
39
|
## The contract version
|
|
39
40
|
|
|
40
|
-
`tlc_plugin_sdk.SDK_CONTRACT_VERSION` is this package's own version — one source of truth
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
`tlc_plugin_sdk.SDK_CONTRACT_VERSION` is this package's own version — one source of truth, one
|
|
42
|
+
contract axis (both the Python and the browser surface). A plugin pins the SDK
|
|
43
|
+
(`3lc-compute-plugin-sdk>=X,<Y`); the compute service and frontend implement a range and compare
|
|
44
|
+
compatibility on `MAJOR.MINOR`. Versions are SemVer; see **Status** below for the 0.x stability
|
|
44
45
|
stance.
|
|
45
46
|
|
|
46
47
|
## How it fits
|
|
@@ -53,7 +54,9 @@ therefore portable across service versions and runs in its own isolated environm
|
|
|
53
54
|
|
|
54
55
|
## Status
|
|
55
56
|
|
|
56
|
-
**0.
|
|
57
|
-
|
|
57
|
+
**0.3 is the current contract line.** Within 0.x the contract still evolves — mostly additively,
|
|
58
|
+
but a coordinated breaking change may land on a MINOR bump while the fleet re-pins in lockstep (0.3
|
|
59
|
+
did: `JobContext.result` took a positional `url`, and the `PY_CONTRACT`/`JS_CONTRACT` axes
|
|
60
|
+
collapsed into `SDK_CONTRACT_VERSION`). Anything reshaping the core still waits for a major bump. In
|
|
58
61
|
the browser bridge, `PLUGIN_API.libs.io` is a stable part of the contract; the other bundled
|
|
59
62
|
libs are best-effort (see the guide).
|
|
@@ -13,29 +13,23 @@ package's own docstrings.
|
|
|
13
13
|
:members:
|
|
14
14
|
|
|
15
15
|
.. autodata:: tlc_plugin_sdk.SDK_CONTRACT_VERSION
|
|
16
|
-
|
|
17
|
-
.. autodata:: tlc_plugin_sdk.PY_CONTRACT
|
|
18
|
-
|
|
19
|
-
.. autodata:: tlc_plugin_sdk.JS_CONTRACT
|
|
20
16
|
```
|
|
21
17
|
|
|
22
|
-
`SDK_CONTRACT_VERSION` is the
|
|
23
|
-
against (`3lc-compute-plugin-sdk>=X,<Y`)
|
|
24
|
-
|
|
25
|
-
surface (`ComputePlugin` / `JobContext`), `JS_CONTRACT` the browser surface
|
|
18
|
+
`SDK_CONTRACT_VERSION` is the one contract axis: this package's own version — the
|
|
19
|
+
dependency *pin* a plugin resolves against (`3lc-compute-plugin-sdk>=X,<Y`) and the version
|
|
20
|
+
of both the Python surface (`ComputePlugin` / `JobContext`) and the browser surface
|
|
26
21
|
(`PLUGIN_API` / `PluginJobs` / `TlcData`, declared in
|
|
27
|
-
`tlc_plugin_sdk/contract/plugin-api.d.ts`).
|
|
28
|
-
|
|
22
|
+
`tlc_plugin_sdk/contract/plugin-api.d.ts`). The host and frontend implement a range and
|
|
23
|
+
compare compatibility on `MAJOR.MINOR`. See `docs/plugin-guide.md` → "Version & Compatibility".
|
|
29
24
|
|
|
30
25
|
The `.d.ts` ships in the wheel at
|
|
31
26
|
`<site-packages>/tlc_plugin_sdk/contract/plugin-api.d.ts`; a plain-JS `ui.html` references
|
|
32
27
|
it with `/// <reference types="3lc-compute-plugin-sdk/contract/plugin-api" />`.
|
|
33
28
|
|
|
34
|
-
## The browser contract
|
|
29
|
+
## The browser contract
|
|
35
30
|
|
|
36
31
|
The browser surface a plugin's `ui.html` programs against — `PLUGIN_API`, `PluginJobs`,
|
|
37
|
-
and the `TlcData` helper — is declared in `plugin-api.d.ts
|
|
38
|
-
`JS_CONTRACT`.
|
|
32
|
+
and the `TlcData` helper — is declared in `plugin-api.d.ts`.
|
|
39
33
|
|
|
40
34
|
```{eval-rst}
|
|
41
35
|
.. automodule documents every exported symbol in the file, so new contract
|
|
@@ -36,7 +36,7 @@ Browser Compute Service (port 5020)
|
|
|
36
36
|
│ │ + ComputePlugin subclass││
|
|
37
37
|
│ │ ComputePlugin (ABC) ││
|
|
38
38
|
│ │ ├── get_ui_fragment() ││ ← abstract
|
|
39
|
-
│ │ ├── compute() ││ ←
|
|
39
|
+
│ │ ├── compute() ││ ← override (default)
|
|
40
40
|
│ │ ├── id ││ ← host-stamped
|
|
41
41
|
│ │ ├── run_job(ctx) ││ ← override (default)
|
|
42
42
|
│ │ └── get_route_handlers()││ ← override (default)
|
|
@@ -106,7 +106,7 @@ description = "Analyzes table data quality."
|
|
|
106
106
|
version = "1.0.0"
|
|
107
107
|
min_service_version = "0.1.0" # Minimum compute service version required
|
|
108
108
|
icon = "🔍" # Fallback emoji
|
|
109
|
-
# 16x16 SVG, inline in the manifest
|
|
109
|
+
# 16x16 SVG, inline in the manifest:
|
|
110
110
|
icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="5"/><path d="M12 12l3 3"/></svg>'
|
|
111
111
|
|
|
112
112
|
[ui]
|
|
@@ -126,12 +126,21 @@ isolation = "venv" # "venv" is the only value (and the default
|
|
|
126
126
|
entrypoint = "tlc_plugin_my_plugin:MyPlugin" # "pkg.module:ClassName"
|
|
127
127
|
requires_gpu = false # drives GPU vs CPU classification
|
|
128
128
|
provision_extra = "my-plugin" # umbrella extra the host provisions: `uv sync --extra <this>`
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
# socketio_namespace = "/my-plugin"
|
|
129
|
+
# The plugin's SocketIO namespace is host-derived as "/<plugin-id>" and registered at
|
|
130
|
+
# startup — it is NOT declarable in the manifest (a plugin emits via ctx; the host owns
|
|
131
|
+
# the transport).
|
|
133
132
|
```
|
|
134
133
|
|
|
134
|
+
**Other keys the host reads** (all optional, read without importing the plugin):
|
|
135
|
+
|
|
136
|
+
- `[runtime]`: `auth_exempt_paths` (relative subpaths served without auth, scoped to the
|
|
137
|
+
plugin's own subtree), `training` (marks a training plugin), `python` / `venv_python`
|
|
138
|
+
(pin the interpreter the plugin's venv is built with).
|
|
139
|
+
- `[ui]`: `min_input_count` (minimum selected resources an `action` plugin needs — defaults to
|
|
140
|
+
`len(input_types)`; set `0` explicitly to require none), `action_param_names` (query params
|
|
141
|
+
passed through from the action launch), `quick_action_label` / `quick_action_description`
|
|
142
|
+
(dashboard quick-action copy).
|
|
143
|
+
|
|
135
144
|
`runtime.provision_extra` is **required for every plugin distributed through an umbrella**:
|
|
136
145
|
it names the extra the host installs with `uv sync --extra <that-value>`. For first-party
|
|
137
146
|
plugins this is a per-plugin extra in the `3lc-compute-plugins` umbrella `pyproject.toml`.
|
|
@@ -145,11 +154,12 @@ on the CPU queue. Both are host-owned; the plugin never picks a queue or names a
|
|
|
145
154
|
### 3. Implement the plugin object
|
|
146
155
|
|
|
147
156
|
A plugin is a **subclass of `ComputePlugin`** (imported from `tlc_plugin_sdk`) —
|
|
148
|
-
there is no `register()` call. You must implement the
|
|
149
|
-
`get_ui_fragment()
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
directly.
|
|
157
|
+
there is no `register()` call. You must implement the one abstract method,
|
|
158
|
+
`get_ui_fragment()`; `id` is hydrated onto the instance from the manifest by the host.
|
|
159
|
+
Everything else — `compute()`, custom routes, jobs, lifecycle hooks — ships as a safe
|
|
160
|
+
default on the base, so you override only what you need and the host calls every hook
|
|
161
|
+
directly. (`compute()`'s default returns an error dict; implement it only if you expose a
|
|
162
|
+
synchronous `GET /compute` endpoint.)
|
|
153
163
|
|
|
154
164
|
```python
|
|
155
165
|
"""My Plugin — does something useful with tables."""
|
|
@@ -299,10 +309,10 @@ startup.
|
|
|
299
309
|
> var API = window.PLUGIN_API; // now typed
|
|
300
310
|
> ```
|
|
301
311
|
>
|
|
302
|
-
> That file
|
|
303
|
-
>
|
|
304
|
-
> `window.PluginJobs` **ships from this
|
|
305
|
-
>
|
|
312
|
+
> That file declares the browser-side contract — versioned by the single
|
|
313
|
+
> `SDK_CONTRACT_VERSION` (see "Version & Compatibility" below). The 3LC Hub frontend
|
|
314
|
+
> **implements** `PLUGIN_API` when it mounts a fragment; `window.PluginJobs` **ships from this
|
|
315
|
+
> package** (auto-injected by the host, layered on top of the bridge, not part of it).
|
|
306
316
|
|
|
307
317
|
### How a fragment reaches the browser
|
|
308
318
|
|
|
@@ -351,7 +361,7 @@ PLUGIN_API = {
|
|
|
351
361
|
objects: TlcApi.objectService, // Object service methods
|
|
352
362
|
authFetch: TlcApi.authFetch, // fetch() with auth headers
|
|
353
363
|
data: TlcData, // Cached data (projects, tables, runs)
|
|
354
|
-
location: TlcLocation, // Location renderers (chips/labels for project roots) —
|
|
364
|
+
location: TlcLocation, // Location renderers (chips/labels for project roots) — SDK 0.2+
|
|
355
365
|
|
|
356
366
|
computeFetch: TlcApi.computeFetch, // authFetch joined to the compute-service base URL
|
|
357
367
|
|
|
@@ -388,7 +398,7 @@ PLUGIN_API = {
|
|
|
388
398
|
- **`compute` / `objects` / `data` / `computeFetch` / `navigate` / `getIcon` / `container`** are
|
|
389
399
|
part of the declared surface but rarely used directly by `ui.html` (plugins reach data through
|
|
390
400
|
`authFetch`); they are documented in the `.d.ts` for completeness.
|
|
391
|
-
- **`location`** (
|
|
401
|
+
- **`location`** (SDK 0.2+) exposes the host's shared location renderers (`TlcLocationApi`):
|
|
392
402
|
chips and labels for the project roots / scan URLs that tables, runs, and projects from
|
|
393
403
|
`PLUGIN_API.data` resolve to (their `location` / `locations` fields, also 0.2). Every renderer
|
|
394
404
|
returns `''` on single-root installs, so output can be concatenated unconditionally. Feature-detect
|
|
@@ -473,7 +483,7 @@ isolation = "venv"
|
|
|
473
483
|
entrypoint = "tlc_plugin_my_gpu_plugin:MyGpuPlugin"
|
|
474
484
|
requires_gpu = true # → routed through the shared GPU queue (1 at a time)
|
|
475
485
|
provision_extra = "my-gpu-plugin" # venv deps installed via `uv sync --extra <this>`
|
|
476
|
-
#
|
|
486
|
+
# SocketIO namespace is host-derived as "/my-gpu-plugin" — not declarable here
|
|
477
487
|
```
|
|
478
488
|
|
|
479
489
|
GPU jobs are serialized — only one runs at a time across every GPU plugin (YOLO, SAM3,
|
|
@@ -498,8 +508,9 @@ class MyGpuPlugin(ComputePlugin):
|
|
|
498
508
|
ctx.progress(percent=100 * i / n, label=f"batch {i}/{n}")
|
|
499
509
|
ctx.metric("loss", 0.042) # key/value card on the generic panel
|
|
500
510
|
|
|
501
|
-
ctx.result(
|
|
502
|
-
# Raise to fail the job
|
|
511
|
+
ctx.result(created_table_url) # the one "open result" link (run or table URL)
|
|
512
|
+
# Raise to fail the job (or ctx.fail("message") for a clean, user-facing message) —
|
|
513
|
+
# the host records the error and ends the stream.
|
|
503
514
|
```
|
|
504
515
|
|
|
505
516
|
`JobContext` surface:
|
|
@@ -513,7 +524,8 @@ class MyGpuPlugin(ComputePlugin):
|
|
|
513
524
|
| `ctx.progress(*, percent, label="", timing=None)` | Generic progress bar. `percent=-1` = indeterminate. `timing` = `{elapsed_s, eta_s, avg_step_s, step_label}`. |
|
|
514
525
|
| `ctx.metric(label, value)` | Scalar metric card on the generic panel. |
|
|
515
526
|
| `ctx.log(message)` | A log line for the job. |
|
|
516
|
-
| `ctx.result(
|
|
527
|
+
| `ctx.result(url)` | The canonical result link the Open button opens — a run *or* a table URL (last write wins). |
|
|
528
|
+
| `ctx.fail(message)` | Fail the job with a clean, user-facing message (raises `JobFailed`; reported verbatim, no type prefix). |
|
|
517
529
|
| `ctx.emit(name, payload)` | A **custom** event for the plugin's OWN rich UI (see below). |
|
|
518
530
|
|
|
519
531
|
**The host owns listing and cancellation — there is nothing to implement.** Because the
|
|
@@ -539,21 +551,21 @@ can be a second consumer of that same channel for a richer, tailored view — it
|
|
|
539
551
|
bespoke events** for the generic lifecycle (queued → running → done, %, result link,
|
|
540
552
|
metrics).
|
|
541
553
|
|
|
542
|
-
**The `window.PluginJobs` client** is a global the
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
554
|
+
**The `window.PluginJobs` client** is a global the **host auto-injects** into every fragment
|
|
555
|
+
(its `/ui` handler prepends `job_tracker_script()`), so you can call it directly — no manual
|
|
556
|
+
injection needed. It is idempotent (`if (window.PluginJobs) return;`), so an older plugin that
|
|
557
|
+
still injects it by hand via `inject_scripts(raw, job_tracker_script())` keeps working. It
|
|
558
|
+
starts a job and tracks it over the generic channel:
|
|
546
559
|
|
|
547
560
|
```javascript
|
|
548
|
-
// The host registers the namespace automatically
|
|
549
|
-
// declare socketio_namespace in plugin.toml only to override it.
|
|
561
|
+
// The host registers the namespace automatically as "/<plugin-id>".
|
|
550
562
|
PluginJobs.run('my-plugin', { table_url: url }, {
|
|
551
563
|
onUpdate: function (job) {
|
|
552
564
|
// generic schema: job.status, job.progress.percent/label, job.metrics[]
|
|
553
565
|
setProgress(job.progress.percent, job.progress.label);
|
|
554
566
|
},
|
|
555
567
|
onDone: function (job) { showResult(job.run_url); },
|
|
556
|
-
onError: function (job) { showError(job.
|
|
568
|
+
onError: function (job) { showError(job.error); }, // failure message on job.error
|
|
557
569
|
});
|
|
558
570
|
```
|
|
559
571
|
|
|
@@ -562,6 +574,12 @@ the client subscribing still delivers its terminal event. (`PluginJobs.start/tra
|
|
|
562
574
|
are the lower-level pieces.) The separate frontend's generic panel polls the same schema
|
|
563
575
|
independently.
|
|
564
576
|
|
|
577
|
+
One timing rule to know: `track()` and `on()` open the namespace socket **lazily**, on first
|
|
578
|
+
use, and SocketIO does not replay server→client events to a client that was not yet
|
|
579
|
+
connected. `run()` connects before it posts, so the common path is safe — but a fragment that
|
|
580
|
+
listens for custom events from the first second, or re-attaches to a job it did not start
|
|
581
|
+
(seed-on-mount), should warm the socket on mount: `PluginJobs.connect('/my-plugin')`.
|
|
582
|
+
|
|
565
583
|
**Custom events** — `ctx.emit(name, payload)` is reserved for telemetry the generic schema
|
|
566
584
|
**can't** express (e.g. a training plugin's per-epoch loss curve). The host relays it
|
|
567
585
|
verbatim on the plugin's namespace; the generic panel ignores it. The name `job_update` is
|
|
@@ -585,6 +603,38 @@ socket.on('epoch_metrics', function (d) { lossChart.push(d.epoch, d.loss); });
|
|
|
585
603
|
|
|
586
604
|
---
|
|
587
605
|
|
|
606
|
+
## The job page is a launcher — the Queue is the durable view
|
|
607
|
+
|
|
608
|
+
A plugin fragment is **torn down on navigation**: the host remounts it via `innerHTML`, so
|
|
609
|
+
its JS state and any live SocketIO subscription are gone the moment the user leaves and comes
|
|
610
|
+
back. The generic **Queue & Progress** panel is the opposite — it is host-owned, polls
|
|
611
|
+
`GET /api/plugins/jobs`, and survives navigation. So treat your fragment as the place a job is
|
|
612
|
+
*launched and configured*, and the generic Queue card as the place a job is *watched*.
|
|
613
|
+
|
|
614
|
+
Two obligations follow:
|
|
615
|
+
|
|
616
|
+
**1. Seed on mount from the durable job list.** `job_update` is live-only — a long job already
|
|
617
|
+
running when the fragment remounts has no event to catch until it next progresses, so the
|
|
618
|
+
fragment would show an empty form over a job that is very much alive. On mount, ask the host
|
|
619
|
+
what's running and render a compact running-state instead:
|
|
620
|
+
|
|
621
|
+
```js
|
|
622
|
+
PluginJobs.list('<id>').then(function (jobs) {
|
|
623
|
+
var live = jobs.filter(function (j) { return j.status === 'queued' || j.status === 'running'; });
|
|
624
|
+
if (live.length) renderRunningState(live[0]); // compact "job running — watch it in the Queue"
|
|
625
|
+
});
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
**2. Keep the generic card meaningful.** If you emit custom progress for your own rich UI
|
|
629
|
+
(`ctx.emit`), keep `ctx.progress(percent, label, timing)` current *too* — the generic card is
|
|
630
|
+
the durable view, and a job that only drives a custom channel looks stalled there. Report the
|
|
631
|
+
one result the Open button opens with **`ctx.result(url)`** (a run or a table URL), and report
|
|
632
|
+
failures by **raising** (or `ctx.fail("message")` for a clean, user-facing message — it lands
|
|
633
|
+
on the card's `error`, without a `TypeError:`-style prefix). That is all the generic Queue
|
|
634
|
+
needs to render a good, navigation-proof view.
|
|
635
|
+
|
|
636
|
+
---
|
|
637
|
+
|
|
588
638
|
## CSS Conventions
|
|
589
639
|
|
|
590
640
|
Plugin UI fragments inherit all CSS from `main.css` (via `base.html`) and `plugin-common.css` (via `plugin_host.html`). Use these shared patterns:
|
|
@@ -758,24 +808,21 @@ plugins are unaffected either way.
|
|
|
758
808
|
|
|
759
809
|
All versions use **SemVer** (`MAJOR.MINOR.PATCH`).
|
|
760
810
|
|
|
761
|
-
### Two version
|
|
811
|
+
### Two kinds of version — never conflate them
|
|
762
812
|
|
|
763
813
|
There are **two independent** kinds of "version" in play. Keep them separate:
|
|
764
814
|
|
|
765
|
-
**(a) CONTRACT
|
|
766
|
-
|
|
815
|
+
**(a) CONTRACT — what a plugin *programs against*.** Pinned at build/install time via the
|
|
816
|
+
`3lc-compute-plugin-sdk` dependency. There is **one contract axis**, `tlc_plugin_sdk.SDK_CONTRACT_VERSION`:
|
|
767
817
|
|
|
768
818
|
| Constant | Covers | Value |
|
|
769
819
|
|----------|--------|-------|
|
|
770
|
-
| `SDK_CONTRACT_VERSION` | the
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
capability can only exist in a shipped wheel). **Bump the package version when *either* moves.**
|
|
777
|
-
A plugin that needs a newer capability raises its `3lc-compute-plugin-sdk` floor; it can also
|
|
778
|
-
feature-detect at runtime by reading `tlc_plugin_sdk.PY_CONTRACT` / `JS_CONTRACT`.
|
|
820
|
+
| `SDK_CONTRACT_VERSION` | the whole contract — the Python surface (`ComputePlugin` / `JobContext` / `shared.*`) *and* the browser surface (`PLUGIN_API` / `PluginJobs` / `TlcData`, see `plugin-api.d.ts`); also the dependency *pin* (`3lc-compute-plugin-sdk>=X,<Y`) | = package version (e.g. `0.3.0`) |
|
|
821
|
+
|
|
822
|
+
It is this package's own version — one source of truth. A plugin that needs a newer capability
|
|
823
|
+
raises its `3lc-compute-plugin-sdk` floor; the host and frontend implement a range and compare
|
|
824
|
+
compatibility on `MAJOR.MINOR`. (Earlier 0.x lines split this into separate `PY_CONTRACT` /
|
|
825
|
+
`JS_CONTRACT` markers; those were removed in 0.3 — there is one axis now.)
|
|
779
826
|
|
|
780
827
|
**(b) SERVICE compatibility — what a plugin *runs against*.** Negotiated at *runtime*, not pinned.
|
|
781
828
|
The compute-service and frontend version independently (separate repos); a plugin declares floors
|
|
@@ -7,7 +7,7 @@ name = "3lc-compute-plugin-sdk"
|
|
|
7
7
|
# The plugin-contract (ABI) version. This IS tlc_plugin_sdk.SDK_CONTRACT_VERSION
|
|
8
8
|
# (read back via importlib.metadata) — one source of truth. SemVer MAJOR.MINOR.PATCH;
|
|
9
9
|
# the 0.x line is additive-only (see README → Status).
|
|
10
|
-
version = "0.
|
|
10
|
+
version = "0.3.0"
|
|
11
11
|
description = "Public Python SDK for building 3LC compute-service plugins."
|
|
12
12
|
requires-python = ">=3.10"
|
|
13
13
|
readme = "README.md"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Copyright 2026 3LC Inc.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Import-light plugin SDK — the contract surface a plugin programs against.
|
|
4
|
+
|
|
5
|
+
This is the public import path for the plugin contract. Importing it must **not**
|
|
6
|
+
pull in the heavy server stack (litestar, python-socketio, uvicorn, the queues,
|
|
7
|
+
discovery): a ``venv``-mode plugin installs only this surface, not the full
|
|
8
|
+
service. The ``tests/test_import_light.py`` test enforces the boundary.
|
|
9
|
+
|
|
10
|
+
A plugin subclasses :class:`ComputePlugin` and implements at least
|
|
11
|
+
``compute``/``get_ui_fragment``; the optional job/lifecycle hooks ship as no-op
|
|
12
|
+
defaults. There is no ``register()`` to call — metadata lives in the plugin
|
|
13
|
+
manifest, and the host discovers the plugin via its manifest ``entrypoint``.
|
|
14
|
+
|
|
15
|
+
The contract is **defined here**, in :mod:`tlc_plugin_sdk.contract`, so it lives
|
|
16
|
+
next to :class:`JobContext` and the venv worker on the import-light side of the SDK
|
|
17
|
+
boundary — nothing in this package pulls in the server stack. This is the standalone
|
|
18
|
+
public ``3lc-compute-plugin-sdk`` distribution: the light base a venv plugin installs (the
|
|
19
|
+
host ``3lc-compute`` depends on it, never the reverse). See ``CLAUDE.md``.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from importlib.metadata import PackageNotFoundError
|
|
25
|
+
from importlib.metadata import version as _pkg_version
|
|
26
|
+
|
|
27
|
+
from tlc_plugin_sdk.contract import ComputePlugin
|
|
28
|
+
from tlc_plugin_sdk.job_context import JobContext, JobFailed
|
|
29
|
+
|
|
30
|
+
# The plugin contract version — one axis, one source of truth: this package's own
|
|
31
|
+
# version (the ``[project] version`` in pyproject), read via importlib.metadata rather
|
|
32
|
+
# than a separately maintained constant. It IS the contract a plugin programs against —
|
|
33
|
+
# both the Python surface (``ComputePlugin`` / ``JobContext`` / ``shared.*``) and the
|
|
34
|
+
# browser surface (``PLUGIN_API`` / ``PluginJobs`` / ``TlcData``, declared in
|
|
35
|
+
# ``contract/plugin-api.d.ts``) — and the thing a plugin pins (``>=X,<Y``). The host and
|
|
36
|
+
# frontend implement a range and compare compatibility on ``MAJOR.MINOR``.
|
|
37
|
+
try:
|
|
38
|
+
SDK_CONTRACT_VERSION = _pkg_version("3lc-compute-plugin-sdk")
|
|
39
|
+
except PackageNotFoundError: # running from a raw checkout that was never installed
|
|
40
|
+
SDK_CONTRACT_VERSION = "0.0.0"
|
|
41
|
+
|
|
42
|
+
__all__ = ["SDK_CONTRACT_VERSION", "ComputePlugin", "JobContext", "JobFailed"]
|
{3lc_compute_plugin_sdk-0.2.3 → 3lc_compute_plugin_sdk-0.3.0}/src/tlc_plugin_sdk/asgi_app.py
RENAMED
|
@@ -26,6 +26,9 @@ from typing import TYPE_CHECKING, Any
|
|
|
26
26
|
|
|
27
27
|
from litestar import Litestar, Request, get
|
|
28
28
|
|
|
29
|
+
from tlc_plugin_sdk.shared.job_tracker import JOB_TRACKER_JS
|
|
30
|
+
from tlc_plugin_sdk.shared.ui_inject import inject_scripts
|
|
31
|
+
|
|
29
32
|
if TYPE_CHECKING:
|
|
30
33
|
from litestar.handlers import BaseRouteHandler
|
|
31
34
|
|
|
@@ -37,31 +40,36 @@ def _generic_handlers(plugin: ComputePlugin) -> list[BaseRouteHandler]:
|
|
|
37
40
|
|
|
38
41
|
@get("/health", sync_to_thread=False)
|
|
39
42
|
def health() -> dict[str, Any]:
|
|
40
|
-
#
|
|
43
|
+
# ``sdk_version`` is the worker half of a handshake: a venv worker imports its
|
|
41
44
|
# *own* install of this SDK, so the host cannot know which contract is live inside
|
|
42
|
-
# the venv unless the worker says so. The host compares
|
|
43
|
-
# flags skew on the plugin card.
|
|
44
|
-
|
|
45
|
-
from tlc_plugin_sdk import JS_CONTRACT, PY_CONTRACT, SDK_CONTRACT_VERSION
|
|
45
|
+
# the venv unless the worker says so. The host compares it against its own on
|
|
46
|
+
# MAJOR.MINOR and flags skew on the plugin card. One contract axis, one field.
|
|
47
|
+
from tlc_plugin_sdk import SDK_CONTRACT_VERSION
|
|
46
48
|
|
|
47
49
|
return {
|
|
48
50
|
"ok": True,
|
|
49
51
|
"plugin": getattr(plugin, "id", "?"),
|
|
50
52
|
"sdk_version": SDK_CONTRACT_VERSION,
|
|
51
|
-
"py_contract": PY_CONTRACT,
|
|
52
|
-
"js_contract": JS_CONTRACT,
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
# def + sync_to_thread: get_ui_fragment()/compute() are synchronous and may do
|
|
56
56
|
# blocking work, so Litestar runs them in a threadpool, off the event loop.
|
|
57
57
|
@get("/ui", media_type="text/html", sync_to_thread=True)
|
|
58
58
|
def ui() -> str:
|
|
59
|
-
|
|
59
|
+
# Auto-inject the PluginJobs client so every fragment can drive the generic job
|
|
60
|
+
# channel without a manual inject_scripts() call. The client is idempotent
|
|
61
|
+
# (``if (window.PluginJobs) return;``), so a plugin that still injects it by hand
|
|
62
|
+
# is harmless. A fragment with no inline <script> has nothing to drive PluginJobs
|
|
63
|
+
# from — inject_scripts() raises there, so serve it unchanged.
|
|
64
|
+
raw = plugin.get_ui_fragment()
|
|
65
|
+
try:
|
|
66
|
+
return inject_scripts(raw, JOB_TRACKER_JS)
|
|
67
|
+
except ValueError:
|
|
68
|
+
return raw
|
|
60
69
|
|
|
61
70
|
@get("/compute", sync_to_thread=True)
|
|
62
71
|
def compute(request: Request[Any, Any, Any]) -> dict[str, Any]:
|
|
63
72
|
params: dict[str, Any] = dict(request.query_params)
|
|
64
|
-
params.setdefault("url", "")
|
|
65
73
|
return plugin.compute(params)
|
|
66
74
|
|
|
67
75
|
return [health, ui, compute]
|