3lc-compute-plugin-sdk 0.3.1__tar.gz → 0.3.2__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.3.1 → 3lc_compute_plugin_sdk-0.3.2}/.github/workflows/docs.yml +4 -3
- 3lc_compute_plugin_sdk-0.3.2/1.0-READINESS.md +434 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/CHANGELOG.md +10 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/PKG-INFO +1 -1
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/docs/plugin-guide.md +152 -101
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/pyproject.toml +1 -1
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/data_source_routes.py +11 -3
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/tests/test_data_source_routes.py +8 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/uv.lock +1 -1
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/.github/workflows/ci.yml +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/.github/workflows/release.yml +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/.gitignore +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/CLAUDE.md +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/LICENSE +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/README.md +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/docs/api.md +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/docs/conf.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/docs/index.md +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/docs/package-lock.json +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/docs/package.json +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/docs/requirements.txt +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/__init__.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/asgi_app.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/contract/plugin-api.d.ts +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/contract.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/job_context.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/py.typed +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/__init__.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/alias_override_ui.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/alias_ui.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/aliases.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/config_store.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/config_ui.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/data_source_ui.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/generic_job.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/images.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/job_tracker.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/labels.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/modality.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/model_storage.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/naming.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/ui_inject.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/url_utils.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/worker.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/tests/test_import_light.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/tests/test_job_context.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/tests/test_js_contract_surface.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/tests/test_url_utils.py +0 -0
- {3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/tests/test_worker_gpu_reclaim.py +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
name: docs
|
|
2
2
|
|
|
3
3
|
# Build the Sphinx docs and publish to GitHub Pages.
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# One-time setup: repo Settings -> Pages -> Source = "GitHub Actions".
|
|
4
|
+
# Deploys automatically on every release tag, or manually from the Actions tab
|
|
5
|
+
# (`gh workflow run docs`). One-time setup: repo Settings -> Pages -> Source = "GitHub Actions".
|
|
7
6
|
on:
|
|
7
|
+
push:
|
|
8
|
+
tags: ["v*"]
|
|
8
9
|
workflow_dispatch:
|
|
9
10
|
|
|
10
11
|
# Least-privilege token for the Pages deploy.
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
# 1.0 readiness report — 3lc-compute-plugin-sdk
|
|
2
|
+
|
|
3
|
+
*Audit of SDK `main` @ `576bcd5` (0.2.0, includes JS_CONTRACT 0.2 / PR #8), host `3lc-compute` @ `bbf2aed`,
|
|
4
|
+
Hub frontend, and all 9 first-party plugins (exporter, importer, merger, splitter, image-metrics,
|
|
5
|
+
table-statistics, timm, sam3, yolo). Line numbers reference those revisions. Uncommitted working doc.*
|
|
6
|
+
|
|
7
|
+
**Framing** (per the pre-1.0 window): all consumers are in-house, so breaking is cheap *now* —
|
|
8
|
+
grep-and-fix across our own repos, coordinated via editable overlays and the frontend contract test.
|
|
9
|
+
Every breaking proposal below states who migrates and roughly how.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## The shape of the problem (executive summary)
|
|
14
|
+
|
|
15
|
+
1. **The contract that's written down is healthy; the contract that's *lived* is bigger and undeclared.**
|
|
16
|
+
The `PluginApi` top level matches the d.ts exactly (the conformance test passes), but the surface
|
|
17
|
+
plugins actually depend on includes `openTablePicker` (7 of 8 UI plugins), 13 SDK-injected `_tlc*`
|
|
18
|
+
globals (6 of 8 plugins), `CancelJob`, bare `showToast`, `cssVar`, `plugin-common.css` classes, and
|
|
19
|
+
an `allowErrorStatus` fetch option the host itself uses 13×. None of that is in the d.ts. 1.0 should
|
|
20
|
+
be the moment the declared contract and the lived contract become the same thing.
|
|
21
|
+
2. **The job-reporting API is fought by its heaviest users.** `ctx.result(run_url=)` is called by only
|
|
22
|
+
2 of 7 job plugins — all three GPU trainers smuggle their result URL through `ctx.emit`/`ctx.metric`
|
|
23
|
+
instead; failure reporting is hand-rolled (`ctx.emit("job_failed", ...)` + `raise`) in duplicate;
|
|
24
|
+
the failure message rides `PluginJobUpdate.subtitle`. These are the exact warts that freeze at 1.0.
|
|
25
|
+
3. **The version story has three axes and one consumer.** `PY_CONTRACT`/`JS_CONTRACT` are read by
|
|
26
|
+
nobody (host compares `sdk_version` only; zero plugin references); the manifest `sdk_version` the
|
|
27
|
+
SDK docstring promises is read by nobody; and `PLUGIN_API.contractVersion` is `''` in every
|
|
28
|
+
production frontend container. Consolidate to one axis and make it actually reach production.
|
|
29
|
+
4. **Live pin defect, before anything else:** the host and the plugins umbrella pin
|
|
30
|
+
`3lc-compute-plugin-sdk[shared]>=0.1.0,<0.2.0` — which *excludes the current 0.2.0 release* — so
|
|
31
|
+
three shipped 0.2 features (host-mode GPU reclaim, `/reclaim`, `/health` skew detection) are inert
|
|
32
|
+
in the resolved lock (host `pyproject.toml:24`, acknowledged at `job_manager.py:59-71`).
|
|
33
|
+
5. **Ceremony is measurable.** 8/9 plugins repeat the same `_ui_cache` + `Path(__file__)` +
|
|
34
|
+
`read_text` block; 5/9 implement `compute()` as a dead stub; script injection exists in **four**
|
|
35
|
+
idioms (two of them the exact footgun `ui_inject.py` was written to eliminate, one injection dead
|
|
36
|
+
on both ends); the config-CRUD route quartet is copy-pasted 4×; yolo reimplements
|
|
37
|
+
`PluginConfigStore` wholesale.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## A. Break at 1.0 (ranked by leverage)
|
|
42
|
+
|
|
43
|
+
### A1. Reshape job *reporting*: `result`, failure, and the error field
|
|
44
|
+
|
|
45
|
+
- **What/where:** `src/tlc_plugin_sdk/job_context.py:75-87` (`result(*, run_url)`),
|
|
46
|
+
`contract/plugin-api.d.ts` `PluginJobUpdate` (failure message documented as riding `subtitle`),
|
|
47
|
+
host translation `3lc-compute/src/tlc_compute/plugins/job_manager.py:461-479`.
|
|
48
|
+
- **Why it hurts:** the name `run_url` is wrong for half the producers (importers/exporters/mergers
|
|
49
|
+
produce *tables*), and the evidence says the API lost its users: only merger (`__init__.py:147`) and
|
|
50
|
+
importer (`:1553`) call `ctx.result`; timm ships the URL via `ctx.emit("job_completed", {..., "run_url"})`
|
|
51
|
+
(`timm/__init__.py:258-261`), yolo likewise (`:299-308`), and sam3 abuses `ctx.metric("run", str(run.url))`
|
|
52
|
+
(`:393-394`) — so the generic panel's "open result" link works for 2 of 7 job plugins. Failure has no
|
|
53
|
+
first-class API at all: timm `:266-269` and yolo `:311-313` both hand-emit `job_failed` + `raise`, and
|
|
54
|
+
the frontend reads the failure message out of `subtitle`. All of this freezes at 1.0 if untouched.
|
|
55
|
+
- **Proposed 1.0 shape:**
|
|
56
|
+
- `ctx.result(url: str, *, kind: str = "run")` (or just `url=`); keep emitting `run_url` on the wire
|
|
57
|
+
*or* rename the wire field to `result_url` in the same coordinated set.
|
|
58
|
+
- Add `error` to the generic job record/`PluginJobUpdate` (host sets it from the terminal `error`
|
|
59
|
+
event); keep `subtitle` as display text. Optionally add `ctx.fail(message)` sugar (equivalent to
|
|
60
|
+
raising, for validation paths) — that part is additive.
|
|
61
|
+
- Document `percent=-1` (indeterminate) in `JobContext.progress` itself — today it's only in the
|
|
62
|
+
guide (`docs/plugin-guide.md:530`), and importer relies on it (`:1578,1621`).
|
|
63
|
+
- **Additive or breaking:** breaking (kwarg rename + wire-field addition/rename).
|
|
64
|
+
- **Who migrates:** SDK (`job_context.py`, d.ts `PluginJobUpdate`); host (`job_manager.py:473-474`,
|
|
65
|
+
`_Job.to_generic` `:109-122`); frontend (queue-panel/cancel-job read of `run_url`/`subtitle`);
|
|
66
|
+
plugins: merger + importer one line each, then timm/yolo/sam3 *adopt* `ctx.result` and delete their
|
|
67
|
+
workarounds (a net simplification, not just a rename).
|
|
68
|
+
- **Effort:** M.
|
|
69
|
+
|
|
70
|
+
### A2. Collapse the three version axes to one, and make it reach production
|
|
71
|
+
|
|
72
|
+
- **What/where:** `src/tlc_plugin_sdk/__init__.py:39-56` (`PY_CONTRACT`, `JS_CONTRACT`),
|
|
73
|
+
`asgi_app.py:52-59` (`/health` reports all three), frontend `app.py:22-28` + `pyproject.toml:17-34`.
|
|
74
|
+
- **Why it hurts:** `PY_CONTRACT`/`JS_CONTRACT` have **zero consumers** — the host's skew check
|
|
75
|
+
compares only `sdk_version` (`3lc-compute/plugins/versioning.py:128-164`), no plugin reads them, and
|
|
76
|
+
the frontend's use of `JS_CONTRACT` is broken in production: the SDK is a dev-group-only dependency
|
|
77
|
+
(`frontend/pyproject.toml:33`), the Docker image installs only `[project.dependencies]`, so
|
|
78
|
+
**`PLUGIN_API.contractVersion === ''` in every shipped container** and all documented 0.2
|
|
79
|
+
feature-detection (`location`, `resolveLocation?`, `getLocations?`) reports "pre-0.1 host". Three
|
|
80
|
+
axes is bookkeeping we pay on every release for a distinction nothing exercises. Also: the SDK
|
|
81
|
+
docstring's claim that plugins declare `sdk_version` in their manifest (`__init__.py:32-33`) is
|
|
82
|
+
fiction — no manifest has it, no host code reads it.
|
|
83
|
+
- **Proposed 1.0 shape:** `SDK_CONTRACT_VERSION` is the one version. Delete `PY_CONTRACT`/`JS_CONTRACT`
|
|
84
|
+
exports; the d.ts header states the package version it shipped in. `/health` reports `sdk_version`
|
|
85
|
+
only. The frontend derives `contractVersion` from the *compute service* (it already calls
|
|
86
|
+
`GET /api/plugins/` and `/health`) instead of a locally-installed Python package — that fixes the
|
|
87
|
+
production hole structurally rather than by promoting the SDK to a frontend runtime dep. Fix or
|
|
88
|
+
delete the `sdk_version`-manifest docstring claim (see A6).
|
|
89
|
+
- **Additive or breaking:** breaking (removes two public constants and two `/health` fields).
|
|
90
|
+
- **Who migrates:** SDK (`__init__.py`, `asgi_app.py`, d.ts header, guide's "Two version axes"
|
|
91
|
+
section); frontend (`app.py`, `base.html:48`, `plugin-loader.js:210` → read from compute);
|
|
92
|
+
host: nothing (never consumed them). Plugins: nothing.
|
|
93
|
+
- **Effort:** M (the frontend sourcing change is the real work).
|
|
94
|
+
|
|
95
|
+
### A3. Bring the escaped browser surface into the contract; kill the bare globals
|
|
96
|
+
|
|
97
|
+
- **What/where:** frontend `static/js/table-picker.js:62` (`openTablePicker`, used by **7 of 8** UI
|
|
98
|
+
plugins), `cancel-job.js:29` (`CancelJob`, yolo+timm), `main.js:202/170` (bare `showToast`, `cssVar`);
|
|
99
|
+
SDK-injected `_tlc*` globals — 13 functions across `shared/alias_ui.py`, `alias_override_ui.py`,
|
|
100
|
+
`config_ui.py`, used by 6 of 8 plugins, none declared in the d.ts.
|
|
101
|
+
- **Why it hurts:** this is contract-shaped surface with no contract: no d.ts entry, no conformance
|
|
102
|
+
gate, no stability tier. `openTablePicker(targetInputId)` additionally couples via a global DOM id
|
|
103
|
+
(`table-picker.js:308` reaches out of the plugin container), and the host fires both `input` *and*
|
|
104
|
+
`change` events because five named plugins disagree on which to listen for (`table-picker.js:308-317`).
|
|
105
|
+
The underscore prefix on `_tlc*` says "private" while the usage pattern is "public API".
|
|
106
|
+
- **Proposed 1.0 shape:**
|
|
107
|
+
- `PLUGIN_API.pickers.table(options) → Promise<string|null>` (promise-returning, no DOM-id
|
|
108
|
+
coupling; frontend wraps the existing picker). There is no run picker today — declare only what
|
|
109
|
+
exists, add `pickers.run` later additively.
|
|
110
|
+
- `PLUGIN_API.jobs.cancelDialog(jobId, opts)` or declare `CancelJob` as-is in the d.ts; plugins
|
|
111
|
+
already use `PLUGIN_API.showToast`, so delete the bare-global usage (importer ×3).
|
|
112
|
+
- Rename the injected `_tlc*` families under one declared namespace (e.g. `window.TlcPluginUi.alias.*`,
|
|
113
|
+
`.aliasOverride.*`, `.configBar(...)`) with d.ts declarations; ship thin deprecated aliases during
|
|
114
|
+
the migration window, drop the bare `_tlc*` names at 1.0.
|
|
115
|
+
- **Additive or breaking:** additive to introduce, breaking to remove the old globals at 1.0 (that
|
|
116
|
+
removal is the point — otherwise the undeclared surface stays load-bearing forever).
|
|
117
|
+
- **Who migrates:** frontend (implement `pickers`, declare members, then delete the legacy event
|
|
118
|
+
double-fire once all plugins use the promise API); SDK (rename injected JS + d.ts declarations);
|
|
119
|
+
plugins: mechanical sweep — `openTablePicker` call sites in 7 plugins, `_tlc*` call sites in 5.
|
|
120
|
+
- **Effort:** M–L (mechanical but wide; the contract-test extension in C3 keeps it honest).
|
|
121
|
+
|
|
122
|
+
### A4. Make the SDK own the manifest schema — and fix the doc/code lies about it
|
|
123
|
+
|
|
124
|
+
- **What/where:** manifest parsing lives host-side (`3lc-compute/plugins/manifest.py:140-248`) with
|
|
125
|
+
no schema, while the SDK documents it (`docs/plugin-guide.md:104-157`). Confirmed divergences:
|
|
126
|
+
- `isolation` default: guide says `venv` is the default (`plugin-guide.md:130`); host code defaults
|
|
127
|
+
to **`host`** (`registry.py:392`) — the opposite, and the dangerous direction (in-process import).
|
|
128
|
+
- `socketio_namespace`: guide says declare-to-override (`:134-137`); host **hardcodes** `f"/{pid}"`
|
|
129
|
+
and never reads the key (`manifest.py:219-221`).
|
|
130
|
+
- `provision_extra`: guide says REQUIRED for venv (`:133,140`); host treats it as optional and
|
|
131
|
+
silently falls back to in-place resolution (`worker_spec.py:351-397`).
|
|
132
|
+
- Undocumented keys the host reads: `runtime.auth_exempt_paths`, `runtime.venv_python`,
|
|
133
|
+
`runtime.python`, `runtime.training`, `ui.min_input_count`, `ui.action_param_names`,
|
|
134
|
+
`ui.quick_action_label/description` (`manifest.py:191-216`).
|
|
135
|
+
- Documented keys nothing reads: `sdk_version`, `ui.fragment`, `display_mode = "hidden"` (no host
|
|
136
|
+
branch on it).
|
|
137
|
+
- No `max_input_count`: image-metrics encodes "1–10 tables" as `input_types = ["table"] × 10`
|
|
138
|
+
(`tlc_plugin_image_metrics/plugin.toml:13`).
|
|
139
|
+
- No reserved-id list: `id = "jobs"` (or `manifest`, `by-resource`, `install`, …) passes the id
|
|
140
|
+
pattern (`identity.py:52`) and is then unroutable under `/api/plugins`.
|
|
141
|
+
- A TOML parse error makes the plugin **vanish silently** (`manifest.py:115-121`) — contradicting
|
|
142
|
+
the stated design goal of "listed greyed-out, never vanishing".
|
|
143
|
+
- `contract.py:8-9` says the manifest is "`[tool.tlc-compute]` in plugin.toml" — all 9 real
|
|
144
|
+
manifests are bare top-level `plugin.toml`; the `[tool.tlc-compute]` form only applies to
|
|
145
|
+
`pyproject.toml`.
|
|
146
|
+
- **Why it hurts:** the manifest *is* contract surface — every key an author writes is a promise —
|
|
147
|
+
and right now the source of truth is a host file the SDK repo can't see, with drift in both
|
|
148
|
+
directions and no validation (string `priority` takes down `GET /api/plugins` wholesale,
|
|
149
|
+
`registry.py:725`).
|
|
150
|
+
- **Proposed 1.0 shape:** a typed `tlc_plugin_sdk.manifest` module (dataclass + `parse()` +
|
|
151
|
+
validation errors) that the **host imports** — SDK is already the root of the dependency graph, so
|
|
152
|
+
this is the natural home. In the same move: require `isolation` explicitly (all 9 manifests already
|
|
153
|
+
declare it — zero real migration); either implement `socketio_namespace` override or delete it from
|
|
154
|
+
the schema/docs (recommend delete — nothing has ever overridden it); make `provision_extra`
|
|
155
|
+
actually required for venv (validation error); add `max_input_count`; reserve the literal path
|
|
156
|
+
segments (`jobs`, `manifest`, `by-resource`, `venv`, …) in id validation; document the rest.
|
|
157
|
+
Also add `/venv` to the reserved-route list in `contract.py:105` (host owns
|
|
158
|
+
`GET /api/plugins/{id}/venv`; a plugin's `@get("/venv")` is silently shadowed today).
|
|
159
|
+
- **Additive or breaking:** breaking (required `isolation`, dropped dead keys, host adopts SDK parser).
|
|
160
|
+
- **Who migrates:** host (manifest.py becomes a consumer of the SDK schema — the big piece);
|
|
161
|
+
plugins: none in practice; docs: guide's manifest section becomes generated-from/checked-against
|
|
162
|
+
the schema.
|
|
163
|
+
- **Effort:** L (mostly host-side; the SDK module itself is S–M).
|
|
164
|
+
|
|
165
|
+
### A5. Prune and re-tier `shared.*` (the graduation question)
|
|
166
|
+
|
|
167
|
+
- **What/where & evidence (usage census across all 9 plugins + host):**
|
|
168
|
+
- `shared/modality.py` (551 lines — the largest shared module): plugins use exactly **one** of its
|
|
169
|
+
five public functions (`detect_modality_from_table`, 5 plugins). `detect_modality_from_url` has
|
|
170
|
+
**zero references anywhere**; `detect_modality_from_schema` and `classify_metrics_columns` are
|
|
171
|
+
internal/host-only. It is 551 lines of heuristics (7-step fallback chains, name conventions)
|
|
172
|
+
published as forever-contract.
|
|
173
|
+
- `shared/labels.py`: three plugins use one distinct function each, no overlap;
|
|
174
|
+
`candidate_label_paths`/`find_label_path`/`get_label_map` have no external callers.
|
|
175
|
+
- `shared/generic_job.epoch_progress`: 2 users (timm, yolo) — and **both** also maintain a private
|
|
176
|
+
`_timing` dict + hand-rolled elapsed/avg/ETA computation + a local `avg_epoch_s → avg_step_s`
|
|
177
|
+
remap (`yolo/__init__.py:184-191`, `timm/__init__.py:144-154`) because the helper only formats,
|
|
178
|
+
it doesn't *track*. The abstraction is at the wrong altitude.
|
|
179
|
+
- `shared/url_utils.normalize_url`: adoption ~25% (29 bypassing `tlc.Table.from_url` calls vs ~9
|
|
180
|
+
using it), and its heuristics are platform folklore — `url.startswith(("Users/", "home/"))`
|
|
181
|
+
re-adds a lost leading slash (`url_utils.py:37-39`), macOS/Linux-specific, silently wrong
|
|
182
|
+
elsewhere.
|
|
183
|
+
- `shared/model_storage.py`: 2 users (timm, yolo); `on_status: Any` where it means
|
|
184
|
+
`Callable[[str], None]`; contains an `except AttributeError` probe for a `tlc.Url` method.
|
|
185
|
+
- `shared/config_store.py`: healthy (3 users + yolo's clone proves demand) but leans on
|
|
186
|
+
`# type: ignore[attr-defined]` ×5 for the id/created/last_run envelope — the repo's own
|
|
187
|
+
CLAUDE.md bans that.
|
|
188
|
+
- **Proposed 1.0 shape:**
|
|
189
|
+
- **Graduate (stable core):** `ui_inject`, `job_tracker`, `config_store` (with a typed envelope —
|
|
190
|
+
a `PluginConfigBase` dataclass or Protocol killing the `type: ignore`s), `aliases`, `images`,
|
|
191
|
+
`labels` (public trio only), `naming`.
|
|
192
|
+
- **Trim:** delete `detect_modality_from_url` (zero users); underscore
|
|
193
|
+
`detect_modality_from_schema`, `candidate_label_paths`, `find_label_path` (internal helpers).
|
|
194
|
+
- **Mark experimental:** `modality` detection heuristics — either a `shared.experimental` namespace
|
|
195
|
+
or an explicit "best-effort, may change in minors" tier in the docstring + docs, mirroring the
|
|
196
|
+
`libs` tier precedent. Freezing 551 lines of heuristics at 1.0 is the single biggest frozen-wart
|
|
197
|
+
risk in the package.
|
|
198
|
+
- **Replace:** `epoch_progress` with an `EpochTimer` that owns the timing dict + ETA math both
|
|
199
|
+
trainers reimplement (additive add, breaking delete of `epoch_progress` — 2 trivial migrations).
|
|
200
|
+
`normalize_url` → either push proper resolution into `tlc` core or reduce it to the defensible
|
|
201
|
+
subset (absolute-ize + project-root resolution) and delete the "Users/" folklore.
|
|
202
|
+
- **Tighten:** `model_storage` signatures (`on_status: Callable[[str], None] | None`).
|
|
203
|
+
- **The `[shared]` extra:** keep the split. The pyproject "direction" note (`pyproject.toml:43`)
|
|
204
|
+
says `3lc` likely returns to base — but the frontend is a real bare-SDK consumer (it installs the
|
|
205
|
+
SDK purely for the d.ts). Recommend deleting that direction note and declaring the split
|
|
206
|
+
permanent at 1.0.
|
|
207
|
+
- **Additive or breaking:** mixed; the deletions/underscorings are breaking.
|
|
208
|
+
- **Who migrates:** essentially nobody for the deletions (zero external users, verified); timm+yolo
|
|
209
|
+
for `epoch_progress`; call-site sweep for `normalize_url` semantics (5 plugins, host insights
|
|
210
|
+
plugins).
|
|
211
|
+
- **Effort:** M.
|
|
212
|
+
|
|
213
|
+
### A6. Fix `contractVersion`'s meaning and the d.ts's own identity strings
|
|
214
|
+
|
|
215
|
+
- **What/where:** the d.ts references the **pre-rename distribution name** `3lc-plugin-sdk` in three
|
|
216
|
+
places (`plugin-api.d.ts:26, 353, 416`); the guide (`plugin-guide.md:313`) and `docs/api.md:32` say
|
|
217
|
+
`/// <reference types="3lc-compute-plugin-sdk/contract/plugin-api" />` while the d.ts header
|
|
218
|
+
(`:24-27`) explains at length why it must be the *import* name `tlc_plugin_sdk`. The guide's
|
|
219
|
+
version table (`plugin-guide.md:785-787`) still says JS_CONTRACT 0.1 / package 0.1.0. README says
|
|
220
|
+
"0.1 is the released contract line" and that the base deps include `3lc` (`README.md:46,52`) —
|
|
221
|
+
both wrong.
|
|
222
|
+
- **Why it hurts:** the contract document contradicting itself about its own name and version is
|
|
223
|
+
exactly the kind of thing a first external plugin author trips on day one.
|
|
224
|
+
- **Proposed 1.0 shape:** one pass fixing every self-reference; if A2 lands, the "Two version axes"
|
|
225
|
+
section collapses to one paragraph. Decide the `<reference>` path question once (test both forms in
|
|
226
|
+
an editor, document the winner, delete the loser).
|
|
227
|
+
- **Additive or breaking:** docs-only (bundled here because it must land *with* A2's reshape).
|
|
228
|
+
- **Effort:** S.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## B. Additive anytime (ranked)
|
|
233
|
+
|
|
234
|
+
### B1. Auto-inject the SDK client scripts at serve time (kill the injection ceremony)
|
|
235
|
+
|
|
236
|
+
- **What/where:** every job-using plugin must remember
|
|
237
|
+
`inject_scripts(raw, job_tracker_script())`. Reality check across 9 plugins: **four different
|
|
238
|
+
idioms** — `inject_scripts` (4 plugins); the literal `raw.replace("<script>", ...)` footgun that
|
|
239
|
+
`ui_inject.py:16-22` was written to eliminate (sam3 `__init__.py:50-55`); a brittle 3-line-exact
|
|
240
|
+
source-string match (importer `:1691-1694`); wholesale script-block prepending (merger `:104`,
|
|
241
|
+
splitter `:157`). Plus one **dead** injection: exporter injects `alias_override_ui_script()` into a
|
|
242
|
+
`ui.html` that never references any alias function (`exporter/__init__.py:381-386`).
|
|
243
|
+
- **Proposal:** `build_plugin_app`'s `/ui` handler (`asgi_app.py:64-66`) — and the host's in-process
|
|
244
|
+
ui path — injects `JOB_TRACKER_JS` automatically. The client is already idempotent
|
|
245
|
+
(`if (window.PluginJobs) return;`, `job_tracker.py:33`), so existing plugins keep working and can
|
|
246
|
+
delete their injection lines at leisure. Alias/config UI injection becomes a manifest opt-in
|
|
247
|
+
(`[ui] inject = ["alias", "alias_override", "config_bar"]`) or stays manual — either way documented.
|
|
248
|
+
- **Effort:** S–M. Migration: none required; cleanup sweep optional.
|
|
249
|
+
|
|
250
|
+
### B2. De-abstract `compute()`
|
|
251
|
+
|
|
252
|
+
- **What/where:** `contract.py:56-59` makes `compute()` abstract; 5 of 9 plugins implement it as a
|
|
253
|
+
dead stub (timm `:56` returns `{"status": "Use /api/plugins/timm/* endpoints"}`, etc.). The
|
|
254
|
+
`/compute` route also injects a magic `params.setdefault("url", "")` (`asgi_app.py:71`).
|
|
255
|
+
- **Proposal:** give it a default returning `{"error": "<id> does not implement compute()"}` —
|
|
256
|
+
mirroring `run_job`'s default — so `get_ui_fragment` is the only abstract. Drop the magic `url`
|
|
257
|
+
defaulting (let plugins read their own params). Deleting five stubs is optional cleanup.
|
|
258
|
+
- **Effort:** S. (The `url` defaulting change is technically behavior-breaking for any plugin relying
|
|
259
|
+
on the key existing — grep says none do.)
|
|
260
|
+
|
|
261
|
+
### B3. Ship the missing batteries the plugins keep hand-rolling
|
|
262
|
+
|
|
263
|
+
Evidence-ranked by duplication count:
|
|
264
|
+
|
|
265
|
+
| Gap | Duplication evidence | Proposal |
|
|
266
|
+
|---|---|---|
|
|
267
|
+
| HTML-escape helper in JS | identical `_esc` in **8/8** ui.html files | ship in the injected client (`TlcPluginUi.esc`) and declare it |
|
|
268
|
+
| Plugin-scoped fetch | 8/8 re-derive `getConfig('compute_service_url') + '/api/plugins/<id>/...'`; timm/yolo/sam3/image-metrics each wrap it 2–3× with per-plugin timeouts; sam3 hardcodes a `localhost:5020` fallback (`sam3/ui.html:296`) | `PLUGIN_API.pluginFetch(path, opts)` — pre-scoped to the mounted plugin's route root, with a venv-aware default timeout (timm's comment at `ui.html:474-477` documents why 10s is wrong for cold-start workers) |
|
|
269
|
+
| Config CRUD routes | the same `GET/POST /configs`, `GET /configs/{id}`, `POST /configs/{id}/delete` quartet 4× (image_metrics, timm, sam3, yolo), with **three different** dataclass↔dict marshalling answers | `shared.config_routes.make_config_routes(store) -> list[BaseRouteHandler]` — pairs with the graduated `config_store`; also removes yolo's incentive to keep its `ProjectStore` clone (97 lines) |
|
|
270
|
+
| Alias-override lifecycle | identical `apply_alias_overrides → try → finally: restore_aliases` 4×, with **three different** param keys (`_alias_overrides` popped / gotten, `alias_overrides`) | `with alias_overrides(params) as ...:` context manager + one blessed param key |
|
|
271
|
+
| Duration/ETA/log-pane UI | duration formatter 4×, timing-strip renderer 3×, log-pane appender 4×, config-picker-with-localStorage ~100 lines × 3 (only image_metrics uses `_tlcPluginConfig`) | fold into the declared injected client (per A3's namespace) |
|
|
272
|
+
| Table identity | `getattr(table, "project_name", "") or ""` ×6 across timm/yolo/image_metrics | tiny `shared.tables.table_identity(table)` helper (or a `tlc` core ask) |
|
|
273
|
+
| Task detection | `timm/task_detection.py` ≈ `yolo/task_detection.py` (same 60 lines, different tail) | `shared.modality.detect_task(table, mapping)` |
|
|
274
|
+
|
|
275
|
+
- **Effort:** M total; each row is S. All additive.
|
|
276
|
+
|
|
277
|
+
### B4. `PluginJobs` v2 — close the gap that makes GPU plugins hand-roll sockets
|
|
278
|
+
|
|
279
|
+
- **What/where:** all four plugins that need custom events (`timm`, `yolo`, `sam3`, `image_metrics`)
|
|
280
|
+
bypass `PluginJobs` entirely — hand-rolled `io(...)` connections with a copy-pasted options object
|
|
281
|
+
(`timm/ui.html:511-522`, `yolo:330-350`, `image_metrics:614-620`; sam3 connects to the **root**
|
|
282
|
+
namespace with no namespace at all, `sam3/ui.html:792-794`) and hand-rolled `POST .../run` +
|
|
283
|
+
`POST .../jobs/{id}/cancel`. The Python side achieved "a plugin never opens its own SocketIO
|
|
284
|
+
connection" (`job_context.py:97`); the JS side did not.
|
|
285
|
+
- **Proposal:** extend `PluginJobs.run(pluginId, params, handlers)` with `handlers.on = {eventName: fn}`
|
|
286
|
+
(custom `ctx.emit` events on the same managed socket, subscribed before `/run` fires — the ordering
|
|
287
|
+
footgun `PluginJobs.on` currently documents away). Add reconnection options matching what the
|
|
288
|
+
plugins standardized on. Then the four hand-rolled stacks collapse onto the client.
|
|
289
|
+
- Also: stop plugins re-stamping `job_id` into every `ctx.emit` payload (15 occurrences) — the sink
|
|
290
|
+
already `setdefault`s it (`job_context.py:115`); document that payloads arrive with `job_id`.
|
|
291
|
+
- **Effort:** M. Additive.
|
|
292
|
+
|
|
293
|
+
### B5. d.ts truth pass (declare what exists, decide what doesn't)
|
|
294
|
+
|
|
295
|
+
- Add `allowErrorStatus` to `PluginFetchOptions` (implemented at `api-client.js:172-183`, used 13×
|
|
296
|
+
by the host's own pages) — and note `authFetch`'s auth-blocking and Response-destroying error
|
|
297
|
+
behavior (`api-client.js:132-140, 182-194`).
|
|
298
|
+
- Complete `TlcLocationApi` (`rowsSpanLocations`, `shortLabel` — `location.js:38-63`) or explicitly
|
|
299
|
+
tier them out; reconcile ambient `TlcApi` (declares 6 members, implementation exports 20, and the
|
|
300
|
+
d.ts doc-comment references `encodeObjectUrl`, which it never declares).
|
|
301
|
+
- `libs.cytoscape` is **unconditionally `null`** for plugins — the vendor file is loaded only on
|
|
302
|
+
`project_detail.html:15`, never on `plugin_host.html`. It's best-effort tier, so: either load it on
|
|
303
|
+
the plugin page or delete the member at 1.0. Also decide `TlcPptx` (a full deck-builder global on
|
|
304
|
+
the plugin page with no tier at all) — declare best-effort or hide.
|
|
305
|
+
- Declare the fragment-shape contract prose in the guide: `.plugin-hero h2` rewriting
|
|
306
|
+
(`plugin-loader.js:244-254`), `[data-provisioning]` lifecycle (`:267-301`), and `plugin-common.css`
|
|
307
|
+
class stability.
|
|
308
|
+
- **Effort:** S–M. Additive.
|
|
309
|
+
|
|
310
|
+
### B6. Export the job-runner core to de-duplicate host↔worker terminal logic
|
|
311
|
+
|
|
312
|
+
- **What/where:** the terminal-event synthesis (`done/cancelled/error` + message format), GPU-reclaim
|
|
313
|
+
ordering, and the `job_update` reserved-name check exist **twice**: SDK `worker.py:158-171` /
|
|
314
|
+
`job_context.py:108-110` and host `job_manager.py:262-274, 83, 436-442` — byte-similar
|
|
315
|
+
reimplementations that must drift-proof by hand.
|
|
316
|
+
- **Proposal:** export `run_job_and_stream(plugin, job_id, params, state_dir, sink, cancel_event)`
|
|
317
|
+
from the SDK (the `_Job._run` body, made public); host mode calls it too. One implementation of the
|
|
318
|
+
terminal contract.
|
|
319
|
+
- **Effort:** M (host adopts). Additive.
|
|
320
|
+
|
|
321
|
+
### B7. Docs drift batch (everything not already inside A6)
|
|
322
|
+
|
|
323
|
+
- README: base-deps claim (`README.md:46-47` says `3lc` is base), Status section ("0.1 line").
|
|
324
|
+
- `contract.py:8-9` manifest-location claim; delete the orphaned `_ICON_SVG` (`contract.py:32` —
|
|
325
|
+
guide says "no `_ICON_SVG` import anymore", nothing imports it).
|
|
326
|
+
- `__init__.py:32-33` `sdk_version` manifest claim (per A2/A4 decision).
|
|
327
|
+
- Guide: document undocumented-but-real manifest keys (A4 list) in the interim, fix the
|
|
328
|
+
`min_input_count = 0` falsy-default footgun host-side (`manifest.py:210` — explicit 0 is silently
|
|
329
|
+
replaced), correct the isolation-default row until A4 decides it.
|
|
330
|
+
- `frontend/API.md:294-302` stale "add location in the same PR" instruction (already done).
|
|
331
|
+
- **Effort:** S.
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## C. Process & CI
|
|
336
|
+
|
|
337
|
+
### C1. Fix the live pins — today, independent of 1.0
|
|
338
|
+
|
|
339
|
+
- Host + plugins umbrella: `3lc-compute-plugin-sdk[shared]>=0.1.0,<0.2.0` excludes 0.2.0
|
|
340
|
+
(`3lc-compute/pyproject.toml:24`, `3lc-compute-plugins/pyproject.toml:23`); resolved lock is 0.1.1,
|
|
341
|
+
so host-mode GPU reclaim, worker `/reclaim`, and `/health` skew-detection are all dead in practice.
|
|
342
|
+
Bump to `>=0.2.0,<0.3.0`, re-lock, verify the contract test.
|
|
343
|
+
- Frontend: decide the production `contractVersion` story (A2's compute-derived value is the fix; the
|
|
344
|
+
dev-group-only SDK dep is fine for the d.ts + test).
|
|
345
|
+
- **Effort:** S. Do this first — it's also the dry run for the 1.0 cross-repo bump.
|
|
346
|
+
|
|
347
|
+
### C2. Mechanical additive-only enforcement (the known wishlist item — confirmed absent)
|
|
348
|
+
|
|
349
|
+
- CI job in this repo that diffs, against the last release tag: `tlc_plugin_sdk.__all__` + public
|
|
350
|
+
module symbols (a simple import-and-introspect script), the d.ts interface member sets (extend the
|
|
351
|
+
frontend's regex parser or use a real TS parser), and — once A4 lands — the manifest schema fields.
|
|
352
|
+
Removals/renames fail unless the version bump is major. This is what makes "additive-only" and the
|
|
353
|
+
post-1.0 freeze a property of the repo instead of a promise in the README.
|
|
354
|
+
- **Effort:** M.
|
|
355
|
+
|
|
356
|
+
### C3. Deepen the frontend conformance test
|
|
357
|
+
|
|
358
|
+
- `frontend/test_plugin_contract.py` diffs **top-level `PluginApi` names only**. Confirmed blind
|
|
359
|
+
spots: nested `libs`/`context` shapes, `getConfig` key set, every referenced interface
|
|
360
|
+
(`TlcData`, `TlcLocationApi`, `TlcApi`, `PluginFetchOptions`, `PluginJobsApi`), signatures, and a
|
|
361
|
+
second `window.PLUGIN_API` assignment. Dropping `libs.io` — the one STABLE lib — would pass today.
|
|
362
|
+
Extend to nested object literals and the `getConfig` key union at minimum; `PluginJobsApi` needs a
|
|
363
|
+
gate on the SDK side (its implementation and declaration live in the same wheel — a unit test
|
|
364
|
+
comparing `JOB_TRACKER_JS`'s exported names to the d.ts is cheap).
|
|
365
|
+
- **Effort:** M.
|
|
366
|
+
|
|
367
|
+
### C4. Test the contract itself in this repo
|
|
368
|
+
|
|
369
|
+
Current suite: 2 files (import-light + GPU reclaim), both good, neither about the contract. Add:
|
|
370
|
+
- `JobContext` event golden-shapes test (each method → exact wire dict; `job_id` stamping; the
|
|
371
|
+
reserved-name rejection) — this is the schema the host, frontend, and plugins all parse.
|
|
372
|
+
- Worker NDJSON protocol test (start → events → terminal; cancel; the `error` message format) —
|
|
373
|
+
doubles as the conformance suite the host can run against its in-process runner (B6).
|
|
374
|
+
- `inject_scripts` edge cases; `config_store` round-trip/migration; manifest schema round-trip
|
|
375
|
+
(post-A4).
|
|
376
|
+
- CI: add a second job with `--extra shared` so mypy actually checks `shared.*` against a real `tlc`
|
|
377
|
+
(today `tlc`/`PIL`/`torch` are `Any` in CI, which is how `on_status: Any` survives --strict).
|
|
378
|
+
- **Effort:** M.
|
|
379
|
+
|
|
380
|
+
### C5. Write the release-train checklist
|
|
381
|
+
|
|
382
|
+
- The 0.2 pin miss (C1) happened because the bump dance spans four repos with no checklist. One page
|
|
383
|
+
in this repo: bump order (SDK tag → host pin+lock → plugins pin → frontend dev pin), the contract
|
|
384
|
+
test loop, and the editable-overlay verification recipe.
|
|
385
|
+
- **Effort:** S.
|
|
386
|
+
|
|
387
|
+
### C6. State the plugin trust model explicitly
|
|
388
|
+
|
|
389
|
+
- Fragments execute with **full host origin** — `innerHTML` + script re-execution
|
|
390
|
+
(`plugin-loader.js:198, 257-261`), no iframe/shadow-DOM/CSP anywhere, `TlcAuth` (raw JWT) reachable.
|
|
391
|
+
With a catalog/install flow shipping (guide's "Publishing via a Catalog"), 1.0 must say out loud:
|
|
392
|
+
*installing a plugin is executing trusted code in the Hub's origin* — in the guide and the catalog
|
|
393
|
+
docs. Actual sandboxing is a large frontend project; the honest documented stance is the 1.0
|
|
394
|
+
deliverable, sandboxing a post-1.0 decision.
|
|
395
|
+
- **Effort:** S (doc) / L (sandboxing, out of scope).
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## Suggested one-week sequencing
|
|
400
|
+
|
|
401
|
+
Cross-repo sets are ordered so the contract test + editable overlays keep SDK, host, and frontend
|
|
402
|
+
green at every step. Breaking work lands in two coordinated waves, not a trickle.
|
|
403
|
+
|
|
404
|
+
**Day 1 — make the current state true (no breaks).**
|
|
405
|
+
C1 pin fixes across host + plugins umbrella (re-lock, contract test). B7 + A6 docs/d.ts truth batch.
|
|
406
|
+
B5 d.ts truth pass. Start C2 freeze-check script (it gates the rest of the week).
|
|
407
|
+
|
|
408
|
+
**Day 2–3 — breaking wave 1: the reporting/version core (SDK-led).**
|
|
409
|
+
A1 (`ctx.result`/error field) + A2 (version-axis collapse) + A5 prunes/underscorings + B2
|
|
410
|
+
(de-abstract `compute`) land in the SDK as `1.0.0rc1`. Same-day coordinated PRs: host
|
|
411
|
+
(`job_manager` translation + pin `>=1.0.0rc1`), frontend (error field in the panel,
|
|
412
|
+
compute-derived `contractVersion`), plugins (merger/importer `result` rename; timm/yolo/sam3 adopt
|
|
413
|
+
`ctx.result` + delete `job_failed` hand-rolls). Overlay-verify a GPU job end-to-end.
|
|
414
|
+
|
|
415
|
+
**Day 3–4 — breaking wave 2: the browser surface (frontend-led).**
|
|
416
|
+
A3: frontend implements `PLUGIN_API.pickers.table` + declares CancelJob/toast surface; SDK renames
|
|
417
|
+
`_tlc*` under the declared namespace (aliases kept for the sweep); B1 auto-injection + B4
|
|
418
|
+
`PluginJobs` v2 ship alongside. Then the mechanical plugin sweep (7 plugins × pickers, 5 × `_tlc*`,
|
|
419
|
+
4 × hand-rolled sockets → `PluginJobs`), deleting the four injection idioms and the dead exporter
|
|
420
|
+
injection as you pass through.
|
|
421
|
+
|
|
422
|
+
**Day 4–5 — the manifest + batteries.**
|
|
423
|
+
A4 `tlc_plugin_sdk.manifest` module; host adopts it (the week's biggest single PR — can trail into
|
|
424
|
+
the next week without blocking the tag if rc-gated). B3 config-route factory + alias context manager
|
|
425
|
+
+ `pluginFetch`; delete yolo's store clone and the CRUD quartets in the same sweep.
|
|
426
|
+
|
|
427
|
+
**Day 5 — lock it.**
|
|
428
|
+
C3 contract-test deepening + C4 golden-shape tests written against the *new* surface (cheaper than
|
|
429
|
+
writing them twice). C2 freeze-check turned on against `v1.0.0`. C5 checklist + C6 trust-model
|
|
430
|
+
paragraph. Tag `1.0.0`.
|
|
431
|
+
|
|
432
|
+
**Explicitly deferred (fine post-1.0, all additive):** run picker, sandboxing, `modality`
|
|
433
|
+
graduation into `tlc` core (the porting notes in `shared/images.py:13-37` are the plan of record),
|
|
434
|
+
B6 job-runner unification (safe any time).
|
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
_Nothing yet._
|
|
11
|
+
|
|
12
|
+
## [0.3.2] - 2026-08-31
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Data-source browse fields with a comma-separated `accept` (e.g. `*.yaml,*.yml`, `*.pt,*.pth`)
|
|
16
|
+
now list matching files; previously they showed none. `/browse` was matching the whole
|
|
17
|
+
`accept` string as one literal glob pattern instead of any pattern in the list.
|
|
18
|
+
([#28](https://github.com/3lc-ai/3lc-compute-plugin-sdk/pull/28))
|
|
19
|
+
|
|
10
20
|
## [0.3.1] - 2026-08-27
|
|
11
21
|
|
|
12
22
|
A patch on the 0.3 line, additive and pin-compatible (`>=0.3.0,<0.4.0` resolves it). The SDK now
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: 3lc-compute-plugin-sdk
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
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
|
|
@@ -642,143 +642,194 @@ needs to render a good, navigation-proof view.
|
|
|
642
642
|
|
|
643
643
|
---
|
|
644
644
|
|
|
645
|
-
##
|
|
645
|
+
## Styling & UI Conventions
|
|
646
646
|
|
|
647
|
-
|
|
647
|
+
A plugin's `ui.html` gets the host's entire design system for free — and the host counts on it using that system rather than reinventing it. This section is the class catalog and the rules that keep every plugin looking like one product, in every theme.
|
|
648
648
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
| `.plugin-two-col` | Sidebar + main layout |
|
|
664
|
-
| `.plugin-three-col` | Sidebar + main + preview layout |
|
|
665
|
-
| `.plugin-config-item` | Selectable list item |
|
|
666
|
-
|
|
667
|
-
Use CSS variables for colors — never hardcode:
|
|
649
|
+
### Why the shared classes are available (no import, no build step)
|
|
650
|
+
|
|
651
|
+
The frontend mounts a plugin by injecting its fragment straight into the host page — `containerEl.innerHTML = fragment` (see [How a fragment reaches the browser](#how-a-fragment-reaches-the-browser)). There is **no iframe and no shadow DOM**, so the fragment lands inside the host document and inherits its two stylesheets:
|
|
652
|
+
|
|
653
|
+
- `main.css` — the base design system (forms, buttons, cards, spinners, theme tokens)
|
|
654
|
+
- `plugin-common.css` — the plugin page scaffolding (hero, workflow, metric cards, viewer)
|
|
655
|
+
|
|
656
|
+
Write `class="btn btn-primary"` or `class="form-control"` and you get the fully themed, dark-mode-correct component with zero CSS of your own. The flip side: your fragment shares the global namespace. Prefix any bespoke class with your plugin id (`.myplugin-…`) and never restyle a shared class globally — you'd repaint every other plugin.
|
|
657
|
+
|
|
658
|
+
> **Only `main.css` and `plugin-common.css` are inherited.** Styles defined inside a host *template* — for example the `.pbadge` badges in `settings/plugins.html` — live in that page's own `<style>` block and are **not** available to a mounted fragment. Don't reach for them.
|
|
659
|
+
|
|
660
|
+
### Theme tokens — never hardcode a color
|
|
661
|
+
|
|
662
|
+
Every color, in both light and dark mode, comes from a CSS variable. Hardcoding a hex value produces a control that looks wrong the moment the user switches theme.
|
|
668
663
|
|
|
669
664
|
```css
|
|
670
|
-
color: var(--text);
|
|
671
|
-
color: var(--text-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
665
|
+
color: var(--text); /* primary text */
|
|
666
|
+
color: var(--text-secondary); /* secondary text */
|
|
667
|
+
color: var(--text-muted); /* de-emphasized / helper text */
|
|
668
|
+
color: var(--accent); /* accent (light mode #2a4a61) */
|
|
669
|
+
background: var(--bg); /* recessed page ground */
|
|
670
|
+
background: var(--bg-card); /* ELEVATED surface (raised cards) */
|
|
671
|
+
border-color: var(--border);
|
|
672
|
+
border-color: var(--border-light);
|
|
676
673
|
```
|
|
677
674
|
|
|
678
|
-
|
|
675
|
+
`--bg` and `--bg-card` are the pair that matters most: `--bg-card` is an *elevated* surface, `--bg` the *recessed* page ground. In dark mode they are visibly different depths, and swapping them inverts every affordance on the page (see **Cards**, below). Also available: `--accent-light` (tinted wash), `--error` / `--danger`, and the `--badge-<color>-bg` / `--badge-<color>-text` pairs. Never use the legacy teal `#5a9aad` — `var(--accent)` is the accent.
|
|
679
676
|
|
|
680
|
-
|
|
677
|
+
### Forms
|
|
681
678
|
|
|
682
|
-
|
|
683
|
-
|---|---|---|---|---|
|
|
684
|
-
| `importer` | sidebar | Data Ops | — | Import data (YOLO, COCO, Folder, CSV, Unlabeled) |
|
|
685
|
-
| `exporter` | sidebar | Data Ops | — | Export tables to CSV, XLSX, YOLO, COCO |
|
|
686
|
-
| `splitter` | sidebar | Data Ops | — | Split tables into train/val/test sets |
|
|
687
|
-
| `merger` | sidebar | Data Ops | — | Merge 2 tables by column join |
|
|
688
|
-
| `image-metrics` | sidebar | Data Ops | GPU | Image quality metrics (brightness, sharpness, noise, etc.) |
|
|
689
|
-
| `yolo` | sidebar | AI Tools | GPU | Ultralytics YOLO training + metrics collection |
|
|
690
|
-
| `sam3` | sidebar | AI Tools | GPU | Auto-labeling with SAM3/GroundingDINO |
|
|
691
|
-
| `timm` | sidebar | AI Tools | GPU | Image classification with timm models |
|
|
692
|
-
| `table-statistics` | hidden | Analysis | — | Per-column stats & image thumbnails (API-only) |
|
|
693
|
-
| `run-insights` | action | Analysis | — | Run statistics, health scores, per-class metrics |
|
|
694
|
-
| `table-insights` | action | Analysis | — | GT-only data quality analysis (bbox sizes, balance, etc.) |
|
|
679
|
+
Wrap each field in `.form-group`; label it with `.form-label`; use `.form-control` for text inputs and `<textarea>`, `.form-select` for `<select>`; add `.form-help` for a line of helper text. For multi-field layouts, `.plugin-form-grid` (2-col) and `.plugin-form-grid-3` (3-col) are responsive grids.
|
|
695
680
|
|
|
696
|
-
|
|
681
|
+
```html
|
|
682
|
+
<div class="form-group">
|
|
683
|
+
<label class="form-label required">Dataset name</label>
|
|
684
|
+
<input type="text" class="form-control" required placeholder="my-dataset">
|
|
685
|
+
<div class="form-help">Lowercase, no spaces.</div>
|
|
686
|
+
</div>
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
- `.form-label.required` appends a red `*`.
|
|
690
|
+
- Placeholders render dimmed + italic automatically.
|
|
691
|
+
- **Required-field cue — worth adopting:** put both `required` **and** a `placeholder` on a required text field. While the field is empty (its placeholder still showing) the host paints it with a muted red wash (`:required:placeholder-shown`), which clears the instant the user types. A free "you still need to fill this in" signal — no JS.
|
|
692
|
+
|
|
693
|
+
### Buttons
|
|
694
|
+
|
|
695
|
+
Use `.btn` plus one variant. Never define your own button styles.
|
|
697
696
|
|
|
698
|
-
|
|
697
|
+
| Class | Use |
|
|
698
|
+
|---|---|
|
|
699
|
+
| `.btn` | base — required on every button |
|
|
700
|
+
| `.btn-primary` | the one main action (Run, Import) |
|
|
701
|
+
| `.btn-secondary` | secondary action |
|
|
702
|
+
| `.btn-ghost` | low-emphasis / borderless |
|
|
703
|
+
| `.btn-danger` | destructive |
|
|
704
|
+
| `.btn-plugin` | accent-styled plugin action |
|
|
705
|
+
| `.btn-sm` / `.btn-lg` | size modifiers, combine with the above |
|
|
706
|
+
|
|
707
|
+
### Page scaffolding (`plugin-common.css`)
|
|
699
708
|
|
|
700
|
-
|
|
701
|
-
|
|
709
|
+
| Class | Purpose |
|
|
710
|
+
|---|---|
|
|
711
|
+
| `.plugin-page` | max-width page container — wrap the whole fragment in it |
|
|
712
|
+
| `.plugin-hero` | intro banner (accent-tinted); put `<h2>` + `<p>` inside |
|
|
713
|
+
| `.plugin-hero-badge` | informational "what this does" tile — flat, recessed, **non-interactive** |
|
|
714
|
+
| `.plugin-workflow` / `.plugin-workflow-step` | numbered step strip (`.num` child is the circle) |
|
|
715
|
+
| `.plugin-param-group` / `.plugin-param-group-label` | titled section inside a card |
|
|
716
|
+
| `.plugin-form-grid` / `.plugin-form-grid-3` | 2- and 3-column responsive form layouts |
|
|
717
|
+
| `.plugin-action-bar` | submit/cancel row |
|
|
718
|
+
| `.plugin-config-item` | selectable list item |
|
|
719
|
+
| `.plugin-metric-card` (+ `-label` / `-value`) | scalar metric tile |
|
|
720
|
+
| `.plugin-progress-wrap` + `.plugin-progress-bar` | progress bar |
|
|
721
|
+
| `.plugin-log-area` | monospace log output |
|
|
722
|
+
| `.plugin-two-col` / `.plugin-three-col` | sidebar + main (+ preview) layouts |
|
|
723
|
+
| `.plugin-viewer-card` (+ `-header` / `-toolbar` / `-viewport` / `-footer`) | image viewer shell |
|
|
724
|
+
| `.spinner` / `.spinner-lg` | loading spinner |
|
|
702
725
|
|
|
703
|
-
###
|
|
726
|
+
### Cards: interactive vs. informational — the one distinction to get right
|
|
704
727
|
|
|
705
|
-
|
|
728
|
+
Two kinds of tile look superficially similar but must read differently:
|
|
729
|
+
|
|
730
|
+
- **Interactive / selectable** — something the user clicks or picks (a config item, a selectable tile). These sit on the **raised** surface: `--bg-card` + a drop shadow, with a `.selected` / `.active` state. The generic `.card` / `.card-header` / `.card-title` / `.card-body` panel is the standard raised container for config forms.
|
|
731
|
+
- **Informational** — pure display, not clickable (the hero "what this does" badges). These stay **flat and recessed**: use `.plugin-hero-badge`, which the host styles on `--bg` with **no shadow**. That flatness is exactly what marks it as "not a button".
|
|
732
|
+
|
|
733
|
+
Get these backwards and a static badge invites a click it won't answer. The mistake is nearly invisible in light mode and obvious in dark mode, where `--bg-card` is a distinctly elevated surface — so **verify your plugin in dark mode.**
|
|
734
|
+
|
|
735
|
+
> **Don't inline surface styles.** `.plugin-hero-badge` carries its whole look, so write `class="plugin-hero-badge"` and nothing else. Several plugins once inlined `style="background:var(--bg-card);box-shadow:…"` on it, and because inline styles outrank the host stylesheet, those badges kept the raised, clickable look the class was written to remove — the exact dark-mode bug above. Keep background, elevation, and color out of your `style=` attributes and let the class do the work. (`.plugin-hero-badge-card` is a deprecated alias kept only for backward compatibility — prefer `.plugin-hero-badge` alone.)
|
|
736
|
+
|
|
737
|
+
### Page structure
|
|
738
|
+
|
|
739
|
+
A sidebar plugin UI follows this shape:
|
|
706
740
|
|
|
707
741
|
```html
|
|
708
|
-
<style
|
|
742
|
+
<style>
|
|
743
|
+
/* Plugin-specific rules only — layout for your own elements. Everything
|
|
744
|
+
visual (surface, color, elevation) comes from the shared classes. */
|
|
745
|
+
.myplugin-badges { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 12px; }
|
|
746
|
+
</style>
|
|
709
747
|
|
|
710
|
-
<div class="plugin-page
|
|
711
|
-
<div class="plugin-hero">
|
|
748
|
+
<div class="plugin-page">
|
|
749
|
+
<div class="plugin-hero">
|
|
712
750
|
<h2>🎯 Plugin Name</h2>
|
|
713
751
|
<p>Short description of what this plugin does.</p>
|
|
714
752
|
|
|
715
|
-
<!--
|
|
716
|
-
<div
|
|
717
|
-
<div class="plugin-hero-badge">
|
|
718
|
-
<div class="plugin-hero-badge">
|
|
719
|
-
<div class="plugin-hero-badge">
|
|
753
|
+
<!-- Informational badges — flat, non-interactive -->
|
|
754
|
+
<div class="myplugin-badges">
|
|
755
|
+
<div class="plugin-hero-badge">Fast</div>
|
|
756
|
+
<div class="plugin-hero-badge">No config</div>
|
|
757
|
+
<div class="plugin-hero-badge">Exports CSV</div>
|
|
720
758
|
</div>
|
|
721
759
|
|
|
722
|
-
<!--
|
|
760
|
+
<!-- Numbered workflow (optional) -->
|
|
723
761
|
<div class="plugin-workflow">
|
|
724
|
-
<
|
|
725
|
-
<
|
|
726
|
-
<
|
|
727
|
-
<span class="plugin-workflow-arrow">→</span>
|
|
728
|
-
<span class="plugin-workflow-step">3. Results</span>
|
|
729
|
-
</div>
|
|
730
|
-
</div>
|
|
731
|
-
|
|
732
|
-
<!-- Config bar (for plugins with saved configs) -->
|
|
733
|
-
<div class="card" style="margin-bottom:12px;padding:10px 14px">
|
|
734
|
-
<div style="display:flex;align-items:center;gap:8px">
|
|
735
|
-
<select id="config-select">...</select>
|
|
736
|
-
<button class="btn btn-sm">Load</button>
|
|
737
|
-
<button class="btn btn-sm btn-primary">Save</button>
|
|
762
|
+
<div class="plugin-workflow-step"><span class="num">1</span> Configure</div>
|
|
763
|
+
<div class="plugin-workflow-step"><span class="num">2</span> Run</div>
|
|
764
|
+
<div class="plugin-workflow-step"><span class="num">3</span> Results</div>
|
|
738
765
|
</div>
|
|
739
766
|
</div>
|
|
740
767
|
|
|
741
768
|
<!-- Main form -->
|
|
742
|
-
<div class="card"
|
|
743
|
-
<div class="
|
|
744
|
-
|
|
745
|
-
<div class="plugin-
|
|
746
|
-
|
|
769
|
+
<div class="card">
|
|
770
|
+
<div class="card-header"><span class="card-title">Configuration</span></div>
|
|
771
|
+
<div class="card-body">
|
|
772
|
+
<div class="plugin-param-group">
|
|
773
|
+
<div class="plugin-param-group-label">Source</div>
|
|
774
|
+
<div class="form-group">
|
|
775
|
+
<label class="form-label required">Table name</label>
|
|
776
|
+
<input id="myplugin-name" type="text" class="form-control" required placeholder="my-table">
|
|
777
|
+
<div class="form-help">The table to analyze.</div>
|
|
778
|
+
</div>
|
|
779
|
+
</div>
|
|
780
|
+
<div class="plugin-action-bar">
|
|
781
|
+
<button id="myplugin-run" class="btn btn-primary">Run</button>
|
|
782
|
+
<span class="spinner" style="display:none"></span>
|
|
747
783
|
</div>
|
|
748
|
-
</div>
|
|
749
|
-
|
|
750
|
-
<div class="plugin-action-bar">
|
|
751
|
-
<button class="btn btn-primary">Run</button>
|
|
752
|
-
<span class="spinner" style="display:none"></span>
|
|
753
784
|
</div>
|
|
754
785
|
</div>
|
|
755
786
|
|
|
756
787
|
<!-- Results area (appears after execution) -->
|
|
757
|
-
<div id="results" class="card" style="display:none
|
|
758
|
-
</div>
|
|
788
|
+
<div id="myplugin-results" class="card" style="display:none"></div>
|
|
759
789
|
</div>
|
|
760
790
|
```
|
|
761
791
|
|
|
762
|
-
### Visual
|
|
792
|
+
### Visual rules
|
|
793
|
+
|
|
794
|
+
1. **Container** — wrap the fragment in `.plugin-page`.
|
|
795
|
+
2. **Hero** — `.plugin-hero` with icon + title + description, optionally three `.plugin-hero-badge` tiles.
|
|
796
|
+
3. **Accent** — `var(--accent)`; never the legacy teal `#5a9aad`.
|
|
797
|
+
4. **Cards** — group content in `.card`; raised for interactive, flat `.plugin-hero-badge` for informational.
|
|
798
|
+
5. **Forms** — `.form-group` + `.form-control` / `.form-select`, laid out with `.plugin-form-grid` where useful.
|
|
799
|
+
6. **Buttons** — `.btn` + one variant; the primary action gets `.btn-primary`.
|
|
800
|
+
7. **Spinners** — `<span class="spinner"></span>` (defined in `main.css`).
|
|
801
|
+
8. **Toasts** — `PLUGIN_API.showToast(msg, 'success'|'error'|'info')` for feedback.
|
|
802
|
+
9. **Dark mode** — must work; only `var(--*)` colors, and confirm the raised/flat card distinction actually reads.
|
|
803
|
+
|
|
804
|
+
### What NOT to do
|
|
805
|
+
|
|
806
|
+
- Don't hardcode colors — always a `var(--…)` token.
|
|
807
|
+
- Don't inline surface/elevation styles (`background`, `box-shadow`) on shared classes — let the class do it.
|
|
808
|
+
- Don't define custom button, input, or card styles — the shared classes exist.
|
|
809
|
+
- Don't restyle a shared class globally, and prefix your own classes with the plugin id.
|
|
810
|
+
- Don't reach for host-template-only classes like `.pbadge` — they aren't inherited.
|
|
811
|
+
- Don't create custom modal/dialog implementations — use `.card` with show/hide.
|
|
812
|
+
- Don't use `position: fixed` — it breaks the plugin container.
|
|
813
|
+
- Don't add font sizes below 10px or custom scrollbars.
|
|
814
|
+
- Don't skip a dark-mode check.
|
|
763
815
|
|
|
764
|
-
|
|
765
|
-
2. **Hero section**: Always include `.plugin-hero` with icon + title + description + 3 feature badges.
|
|
766
|
-
3. **Color**: Use `var(--accent)` (#2a4a61) as the accent. Never use teal (#5a9aad).
|
|
767
|
-
4. **Cards**: Wrap all content sections in `.card`. Use `margin-bottom:12px` between cards.
|
|
768
|
-
5. **Forms**: Use `.plugin-form-grid` (2-col) or `.plugin-form-grid-3` (3-col) for form layouts.
|
|
769
|
-
6. **Buttons**: Primary actions get `.btn-primary`. Secondary actions get plain `.btn`.
|
|
770
|
-
7. **Spinners**: Use `<span class="spinner"></span>` (defined in main.css).
|
|
771
|
-
8. **Toasts**: Use `PLUGIN_API.showToast(msg, 'success'|'error'|'info')` for feedback.
|
|
772
|
-
9. **Config bar**: Plugins with saved configurations (YOLO, SAM3, timm) should have a config select/save/delete bar below the hero.
|
|
773
|
-
10. **Dark mode**: All CSS must work in dark mode. Use `var(--text)`, `var(--bg)`, `var(--border)`, etc. Never hardcode colors.
|
|
816
|
+
---
|
|
774
817
|
|
|
775
|
-
|
|
818
|
+
## Existing Plugins Reference
|
|
776
819
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
820
|
+
| Plugin ID | display_mode | Section | GPU | Description |
|
|
821
|
+
|---|---|---|---|---|
|
|
822
|
+
| `importer` | sidebar | Data Ops | — | Import data (YOLO, COCO, Folder, CSV, Unlabeled) |
|
|
823
|
+
| `exporter` | sidebar | Data Ops | — | Export tables to CSV, XLSX, YOLO, COCO |
|
|
824
|
+
| `splitter` | sidebar | Data Ops | — | Split tables into train/val/test sets |
|
|
825
|
+
| `merger` | sidebar | Data Ops | — | Merge 2 tables by column join |
|
|
826
|
+
| `image-metrics` | sidebar | Data Ops | GPU | Image quality metrics (brightness, sharpness, noise, etc.) |
|
|
827
|
+
| `yolo` | sidebar | AI Tools | GPU | Ultralytics YOLO training + metrics collection |
|
|
828
|
+
| `sam3` | sidebar | AI Tools | GPU | Auto-labeling with SAM3/GroundingDINO |
|
|
829
|
+
| `timm` | sidebar | AI Tools | GPU | Image classification with timm models |
|
|
830
|
+
| `table-statistics` | hidden | Analysis | — | Per-column stats & image thumbnails (API-only) |
|
|
831
|
+
| `run-insights` | action | Analysis | — | Run statistics, health scores, per-class metrics |
|
|
832
|
+
| `table-insights` | action | Analysis | — | GT-only data quality analysis (bbox sizes, balance, etc.) |
|
|
782
833
|
|
|
783
834
|
---
|
|
784
835
|
|
|
@@ -964,7 +1015,7 @@ If your entry doesn't show up after that, check that the tester's compute-servic
|
|
|
964
1015
|
- [ ] Job progress follows the generic schema (no plugin-specific fields in frontend)
|
|
965
1016
|
- [ ] If GPU-bound: `requires_gpu = true` in `[runtime]`; long work is `run_job(ctx)` — never grab a queue
|
|
966
1017
|
- [ ] If creating tables from images: registers URL aliases via `tlc_plugin_sdk/shared/aliases.py` + `tlc_plugin_sdk/shared/alias_ui.py` (inject with `inject_scripts()`)
|
|
967
|
-
- [ ] UI follows the page structure from "UI
|
|
1018
|
+
- [ ] UI follows the page structure and card conventions from "Styling & UI Conventions" above
|
|
968
1019
|
- [ ] Hero section with icon, title, description, and 3 feature badges
|
|
969
1020
|
- [ ] Config bar if plugin has saved configurations
|
|
970
1021
|
- [ ] Dark mode works correctly (no hardcoded colors)
|
|
@@ -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.3.
|
|
10
|
+
version = "0.3.2"
|
|
11
11
|
description = "Public Python SDK for building 3LC compute-service plugins."
|
|
12
12
|
requires-python = ">=3.10"
|
|
13
13
|
readme = "README.md"
|
|
@@ -176,7 +176,9 @@ def data_source_route_handlers() -> list[BaseRouteHandler]:
|
|
|
176
176
|
|
|
177
177
|
Query params:
|
|
178
178
|
path: Directory to list. Empty or ``~`` opens the first allowed root.
|
|
179
|
-
glob: Optional glob pattern to filter files
|
|
179
|
+
glob: Optional glob pattern(s) to filter files, comma-separated for more than
|
|
180
|
+
one (e.g. ``*.yaml`` or ``*.yaml,*.yml`` — matches the widget's ``accept``
|
|
181
|
+
config verbatim). A file is kept if it matches *any* pattern in the list.
|
|
180
182
|
show_hidden: Whether to include dotfiles (default ``false``).
|
|
181
183
|
purpose: ``input`` (default) or ``output``. In ``output`` mode the response
|
|
182
184
|
additionally reports writability (see below); ``input`` mode skips that
|
|
@@ -196,7 +198,9 @@ def data_source_route_handlers() -> list[BaseRouteHandler]:
|
|
|
196
198
|
|
|
197
199
|
roots = allowed_browse_roots()
|
|
198
200
|
raw_path = request.query_params.get("path", "").strip()
|
|
199
|
-
|
|
201
|
+
# Comma-separated, matching the widget's "accept" config (e.g. "*.yaml,*.yml") —
|
|
202
|
+
# a bare fnmatch against the joined string would only match a literal comma.
|
|
203
|
+
glob_patterns = [g.strip() for g in request.query_params.get("glob", "").split(",") if g.strip()]
|
|
200
204
|
show_hidden = request.query_params.get("show_hidden", "false").lower() == "true"
|
|
201
205
|
purpose = request.query_params.get("purpose", "input")
|
|
202
206
|
want_writable = purpose == "output"
|
|
@@ -224,7 +228,11 @@ def data_source_route_handlers() -> list[BaseRouteHandler]:
|
|
|
224
228
|
for entry in sorted(os.scandir(expanded), key=lambda e: (not e.is_dir(), e.name.lower())):
|
|
225
229
|
if not show_hidden and entry.name.startswith("."):
|
|
226
230
|
continue
|
|
227
|
-
if
|
|
231
|
+
if (
|
|
232
|
+
glob_patterns
|
|
233
|
+
and not entry.is_dir()
|
|
234
|
+
and not any(fnmatch.fnmatch(entry.name, pattern) for pattern in glob_patterns)
|
|
235
|
+
):
|
|
228
236
|
continue
|
|
229
237
|
try:
|
|
230
238
|
stat = entry.stat()
|
{3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/tests/test_data_source_routes.py
RENAMED
|
@@ -100,6 +100,14 @@ class TestBrowseConfinement:
|
|
|
100
100
|
body = _browse(client, path=str(root / "sub"), glob="*.yaml")
|
|
101
101
|
assert [e["name"] for e in body["entries"]] == ["table.yaml"]
|
|
102
102
|
|
|
103
|
+
def test_glob_accepts_comma_separated_patterns(self, client: TestClient[Litestar], root: Path) -> None:
|
|
104
|
+
# Matches the widget's "accept" config verbatim (e.g. "*.yaml,*.yml") — a file
|
|
105
|
+
# is kept if it matches ANY pattern in the list, not the joined string literally.
|
|
106
|
+
body = _browse(client, path=str(root / "sub"), glob="*.yaml,*.yml")
|
|
107
|
+
assert [e["name"] for e in body["entries"]] == ["table.yaml"]
|
|
108
|
+
body = _browse(client, path=str(root), glob="*.csv,*.json")
|
|
109
|
+
assert [e["name"] for e in body["entries"]] == ["sub", "data.csv"]
|
|
110
|
+
|
|
103
111
|
|
|
104
112
|
class TestBrowseAccessibility:
|
|
105
113
|
"""Each dir entry is tagged with whether it can actually be opened for listing."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/__init__.py
RENAMED
|
File without changes
|
{3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/asgi_app.py
RENAMED
|
File without changes
|
|
File without changes
|
{3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/contract.py
RENAMED
|
File without changes
|
{3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/job_context.py
RENAMED
|
File without changes
|
|
File without changes
|
{3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/alias_ui.py
RENAMED
|
File without changes
|
{3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/aliases.py
RENAMED
|
File without changes
|
|
File without changes
|
{3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/config_ui.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/images.py
RENAMED
|
File without changes
|
|
File without changes
|
{3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/labels.py
RENAMED
|
File without changes
|
{3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/modality.py
RENAMED
|
File without changes
|
|
File without changes
|
{3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/naming.py
RENAMED
|
File without changes
|
{3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/ui_inject.py
RENAMED
|
File without changes
|
{3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/src/tlc_plugin_sdk/shared/url_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/tests/test_js_contract_surface.py
RENAMED
|
File without changes
|
|
File without changes
|
{3lc_compute_plugin_sdk-0.3.1 → 3lc_compute_plugin_sdk-0.3.2}/tests/test_worker_gpu_reclaim.py
RENAMED
|
File without changes
|