3lc-compute-plugin-sdk 0.2.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.
Files changed (46) hide show
  1. 3lc_compute_plugin_sdk-0.2.2/.github/workflows/ci.yml +44 -0
  2. 3lc_compute_plugin_sdk-0.2.2/.github/workflows/docs.yml +68 -0
  3. 3lc_compute_plugin_sdk-0.2.2/.github/workflows/release.yml +93 -0
  4. 3lc_compute_plugin_sdk-0.2.2/.gitignore +25 -0
  5. 3lc_compute_plugin_sdk-0.2.2/CHANGELOG.md +78 -0
  6. 3lc_compute_plugin_sdk-0.2.2/CLAUDE.md +184 -0
  7. 3lc_compute_plugin_sdk-0.2.2/LICENSE +202 -0
  8. 3lc_compute_plugin_sdk-0.2.2/PKG-INFO +82 -0
  9. 3lc_compute_plugin_sdk-0.2.2/README.md +56 -0
  10. 3lc_compute_plugin_sdk-0.2.2/docs/api.md +87 -0
  11. 3lc_compute_plugin_sdk-0.2.2/docs/conf.py +111 -0
  12. 3lc_compute_plugin_sdk-0.2.2/docs/index.md +28 -0
  13. 3lc_compute_plugin_sdk-0.2.2/docs/package-lock.json +633 -0
  14. 3lc_compute_plugin_sdk-0.2.2/docs/package.json +10 -0
  15. 3lc_compute_plugin_sdk-0.2.2/docs/plugin-guide.md +939 -0
  16. 3lc_compute_plugin_sdk-0.2.2/docs/requirements.txt +9 -0
  17. 3lc_compute_plugin_sdk-0.2.2/pyproject.toml +129 -0
  18. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/__init__.py +58 -0
  19. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/asgi_app.py +103 -0
  20. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/contract/plugin-api.d.ts +429 -0
  21. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/contract.py +108 -0
  22. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/job_context.py +120 -0
  23. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/py.typed +0 -0
  24. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/__init__.py +11 -0
  25. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/alias_override_ui.py +175 -0
  26. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/alias_ui.py +198 -0
  27. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/aliases.py +227 -0
  28. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/config_store.py +140 -0
  29. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/config_ui.py +256 -0
  30. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/data_source_routes.py +221 -0
  31. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/data_source_ui.py +365 -0
  32. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/generic_job.py +58 -0
  33. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/images.py +350 -0
  34. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/job_tracker.py +174 -0
  35. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/labels.py +247 -0
  36. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/modality.py +551 -0
  37. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/model_storage.py +160 -0
  38. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/naming.py +84 -0
  39. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/ui_inject.py +63 -0
  40. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/url_utils.py +112 -0
  41. 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/worker.py +295 -0
  42. 3lc_compute_plugin_sdk-0.2.2/tests/test_data_source_routes.py +153 -0
  43. 3lc_compute_plugin_sdk-0.2.2/tests/test_import_light.py +43 -0
  44. 3lc_compute_plugin_sdk-0.2.2/tests/test_url_utils.py +61 -0
  45. 3lc_compute_plugin_sdk-0.2.2/tests/test_worker_gpu_reclaim.py +149 -0
  46. 3lc_compute_plugin_sdk-0.2.2/uv.lock +1900 -0
@@ -0,0 +1,44 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main]
6
+ push:
7
+ branches: [main]
8
+ workflow_call: # reused as the gate by release.yml
9
+
10
+ concurrency:
11
+ group: ci-${{ github.ref }}
12
+ cancel-in-progress: true
13
+
14
+ jobs:
15
+ lint-type-test:
16
+ name: ruff · mypy · pytest
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - name: Install uv
22
+ uses: astral-sh/setup-uv@v5
23
+ with:
24
+ version: "0.8.15"
25
+ enable-cache: true
26
+ cache-dependency-glob: "uv.lock"
27
+
28
+ - name: Install dependencies
29
+ run: uv sync --python 3.12 --dev --locked
30
+
31
+ - name: Ruff lint
32
+ run: uv run --python 3.12 ruff check .
33
+
34
+ - name: Ruff format check
35
+ run: uv run --python 3.12 ruff format --check .
36
+
37
+ - name: mypy (strict)
38
+ run: uv run --python 3.12 mypy src/
39
+
40
+ - name: pytest
41
+ run: uv run --python 3.12 pytest -q
42
+
43
+ - name: Minimize uv cache
44
+ run: uv cache prune --ci
@@ -0,0 +1,68 @@
1
+ name: docs
2
+
3
+ # Build the Sphinx docs and publish to GitHub Pages.
4
+ # Manual-only by choice: run from the Actions tab (or `gh workflow run docs`)
5
+ # when the docs should go out — pushes to main do NOT redeploy.
6
+ # One-time setup: repo Settings -> Pages -> Source = "GitHub Actions".
7
+ on:
8
+ workflow_dispatch:
9
+
10
+ # Least-privilege token for the Pages deploy.
11
+ permissions:
12
+ contents: read
13
+ pages: write
14
+ id-token: write
15
+
16
+ # Serialize deploys; don't cancel an in-flight one.
17
+ concurrency:
18
+ group: pages
19
+ cancel-in-progress: false
20
+
21
+ jobs:
22
+ build-deploy:
23
+ name: build & deploy
24
+ runs-on: ubuntu-latest
25
+ environment:
26
+ name: github-pages
27
+ url: ${{ steps.deploy.outputs.page_url }}
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+
31
+ - name: Install uv
32
+ uses: astral-sh/setup-uv@v5
33
+ with:
34
+ version: "0.8.15"
35
+ enable-cache: true
36
+ cache-dependency-glob: "uv.lock"
37
+
38
+ # sphinx-js drives TypeDoc to document the browser contract (.d.ts);
39
+ # both are Node tools pinned in docs/package.json.
40
+ - name: Install Node
41
+ uses: actions/setup-node@v4
42
+ with:
43
+ node-version: "20"
44
+ cache: npm
45
+ cache-dependency-path: docs/package-lock.json
46
+
47
+ - name: Install docs Node toolchain (TypeDoc)
48
+ run: npm --prefix docs ci
49
+
50
+ # Install the package + deps so autodoc can import tlc_plugin_sdk,
51
+ # then overlay the docs toolchain and build.
52
+ - name: Install dependencies
53
+ run: uv sync --python 3.12 --locked
54
+
55
+ - name: Build docs
56
+ run: >
57
+ uv run --python 3.12 --with-requirements docs/requirements.txt
58
+ sphinx-build -b html -W --keep-going docs docs/_build/html
59
+
60
+ - uses: actions/configure-pages@v5
61
+
62
+ - uses: actions/upload-pages-artifact@v3
63
+ with:
64
+ path: docs/_build/html
65
+
66
+ - name: Deploy to GitHub Pages
67
+ id: deploy
68
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,93 @@
1
+ name: Release
2
+
3
+ # Tag (`v*`) pushes publish to public PyPI via Trusted Publishing (GitHub OIDC, no secrets).
4
+ # To cut a release: bump `version` in pyproject.toml (regenerate uv.lock), update CHANGELOG.md,
5
+ # commit, then push a tag `vX.Y.Z` that matches the version.
6
+ #
7
+ # Manual (workflow_dispatch) runs publish build-numbered prereleases to the legacy CloudRepo
8
+ # index (pypi.3lc.ai) — kept as a grace-period fallback; retire that job (and the CLOUDREPO_*
9
+ # secrets/vars on the "release" environment) once nothing consumes the CloudRepo index. The
10
+ # split deliberately keeps build-numbered versions OFF PyPI: `X.Y.Z.N` sorts above `X.Y.Z`,
11
+ # so a prerelease build would win over the real release in `>=X.Y,<X.Y+1`-style pins.
12
+
13
+ on:
14
+ push:
15
+ tags: ["v*"]
16
+ workflow_dispatch:
17
+
18
+ jobs:
19
+ # Gate the release on the full CI suite (ruff · mypy · pytest).
20
+ test:
21
+ uses: ./.github/workflows/ci.yml
22
+ secrets: inherit
23
+
24
+ # Tag (`v*`) pushes → the real release, to public PyPI.
25
+ publish-pypi:
26
+ if: github.event_name == 'push'
27
+ needs: test
28
+ runs-on: ubuntu-latest
29
+ environment: release
30
+ permissions:
31
+ contents: read
32
+ id-token: write # OIDC token for PyPI Trusted Publishing
33
+ steps:
34
+ - uses: actions/checkout@v4
35
+
36
+ - name: Install uv
37
+ uses: astral-sh/setup-uv@v5
38
+ with:
39
+ version: "0.8.15"
40
+
41
+ - name: Verify tag matches pyproject version
42
+ run: |
43
+ PKG=$(uv version --output-format json | jq -r '.version')
44
+ TAG="${GITHUB_REF_NAME#v}"
45
+ echo "pyproject=$PKG tag=$TAG"
46
+ [ "$PKG" = "$TAG" ] || { echo "::error::tag $GITHUB_REF_NAME ($TAG) != pyproject version $PKG"; exit 1; }
47
+
48
+ # --no-sources strips [tool.uv.sources] so the released wheel never bakes in a dev
49
+ # index/path override (the published artifact resolves deps from PyPI).
50
+ - name: Build
51
+ run: uv build --no-sources
52
+
53
+ - name: Publish to PyPI
54
+ run: uv publish --trusted-publishing always
55
+
56
+ # Manual (workflow_dispatch) runs → build-numbered prereleases to the legacy CloudRepo index.
57
+ publish-cloudrepo-prerelease:
58
+ if: github.event_name == 'workflow_dispatch'
59
+ needs: test
60
+ runs-on: ubuntu-latest
61
+ environment: release
62
+ env:
63
+ UV_PUBLISH_URL: ${{ vars.CLOUDREPO_PUBLISH_URL }}
64
+ UV_PUBLISH_USERNAME: ${{ secrets.CLOUDREPO_USERNAME }}
65
+ UV_PUBLISH_PASSWORD: ${{ secrets.CLOUDREPO_PASSWORD }}
66
+ steps:
67
+ - uses: actions/checkout@v4
68
+
69
+ - name: Install uv
70
+ uses: astral-sh/setup-uv@v5
71
+ with:
72
+ version: "0.8.15"
73
+
74
+ - name: Verify the 'release' environment is configured
75
+ run: |
76
+ : "${UV_PUBLISH_URL:?set the CLOUDREPO_PUBLISH_URL variable on the 'release' environment}"
77
+ : "${UV_PUBLISH_USERNAME:?set the CLOUDREPO_USERNAME secret on the 'release' environment}"
78
+ : "${UV_PUBLISH_PASSWORD:?set the CLOUDREPO_PASSWORD secret on the 'release' environment}"
79
+
80
+ # A 4th version segment keeps every prerelease publish unique (CloudRepo has overwrites
81
+ # disabled) while sorting above the base version, so `<X.Y+1`-style pins still resolve it
82
+ # — unlike `.devN` (sorts below) or `+localN` (rejected on upload).
83
+ - name: Stamp build-numbered version
84
+ run: |
85
+ BASE=$(uv version --output-format json | jq -r '.version')
86
+ uv version "${BASE}.${GITHUB_RUN_NUMBER}"
87
+ echo "prerelease build version: ${BASE}.${GITHUB_RUN_NUMBER}"
88
+
89
+ - name: Build
90
+ run: uv build --no-sources
91
+
92
+ - name: Publish to CloudRepo
93
+ run: uv publish
@@ -0,0 +1,25 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+
9
+ # Environments
10
+ .venv/
11
+ venv/
12
+
13
+ # Tooling caches
14
+ .mypy_cache/
15
+ .ruff_cache/
16
+ .pytest_cache/
17
+
18
+ # Docs build output
19
+ docs/_build/
20
+ docs/node_modules/
21
+
22
+ # Editors / OS
23
+ .vscode/
24
+ .idea/
25
+ .DS_Store
@@ -0,0 +1,78 @@
1
+ # Changelog
2
+
3
+ All notable changes to `3lc-compute-plugin-sdk` are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ Nothing yet.
11
+
12
+ ## [0.2.2] - 2026-08-18
13
+
14
+ ### Added
15
+ - Shared data-source picker (`shared.data_source_ui`) and its browse/upload route handlers
16
+ (`shared.data_source_routes`): plugins mount one consistent widget for choosing a local
17
+ path or uploading a file, instead of each fragment rolling its own (#11).
18
+
19
+ ### Changed
20
+ - **Distribution moved to PyPI**: `3lc-compute-plugin-sdk` is now published to public
21
+ [PyPI](https://pypi.org/project/3lc-compute-plugin-sdk/) via Trusted Publishing; the private
22
+ CloudRepo index (pypi.3lc.ai) is no longer needed to install the SDK. Manual prerelease
23
+ builds keep publishing to CloudRepo for a grace period (#13).
24
+ - The `[shared]` extra's `3lc` dependency resolves from public PyPI (its home since the 3.2
25
+ rust release), so the SDK no longer pins a custom package index (#12).
26
+
27
+ ### Security
28
+ - The data-source `/browse` route is confined to operator-configured roots
29
+ (`TLC_DATA_SOURCE_ROOTS`, `os.pathsep`-separated; default: the service user's home).
30
+ Every requested path is realpath-resolved before the containment check, so `..`
31
+ segments and symlinks pointing outside a root are denied rather than followed, and
32
+ the widget's breadcrumb stops at the root instead of offering the whole filesystem (#11).
33
+ - The data-source `/upload-temp` route strips directory components from the
34
+ client-supplied filename (closing a path traversal where a `../…` name chose the
35
+ write location), lands each upload in its own private temp directory so same-named
36
+ uploads never clobber each other, and rejects bodies larger than
37
+ `TLC_DATA_SOURCE_MAX_UPLOAD_MB` (default 512) (#11).
38
+
39
+ ## [0.2.1] - 2026-08-13
40
+
41
+ ### Added
42
+ - Tilde expansion and local-path normalization helpers in `shared.url_utils`
43
+ (`normalize_local_path`, `normalize_url`): user-supplied paths expand `~` and resolve
44
+ to absolute form at every ingress, so exported files and stored URLs never carry a
45
+ user-relative path (#10).
46
+
47
+ ## [0.2.0] - 2026-08-05
48
+
49
+ ### Added
50
+ - JS_CONTRACT 0.2: project locations (`TlcLocation`) are available to plugin UI fragments
51
+ through `PLUGIN_API`, so plugins can resolve and present where a project's data lives (#8).
52
+ - The plugin worker reports the SDK contract version from its `/health` endpoint, letting the
53
+ host enforce compatibility floors precisely instead of guessing (#6).
54
+
55
+ ### Changed
56
+ - The plugin worker releases cached GPU memory after every job, so a finished GPU job no longer
57
+ pins CUDA memory that other plugins need (#7).
58
+
59
+ ## [0.1.1] - 2026-07-03
60
+
61
+ ### Changed
62
+ - **Distribution renamed**: `3lc-plugin-sdk` is now published as `3lc-compute-plugin-sdk`
63
+ (the import name `tlc_plugin_sdk` is unchanged). Update your dependency declarations.
64
+
65
+ ### Fixed
66
+ - Regenerated `uv.lock` for the renamed distribution; release builds append the run number as a
67
+ build counter.
68
+
69
+ ## [0.1.0] - 2026-06-30
70
+
71
+ First public release of the plugin SDK for the 3LC compute service.
72
+
73
+ ### Added
74
+ - The plugin contract: `ComputePlugin`, `JobContext`, and the `PLUGIN_API` browser bridge
75
+ (JS_CONTRACT) that plugin UI fragments use to talk to the host.
76
+ - Slim base package with a `[shared]` extra for common plugin dependencies.
77
+ - Apache-2.0 license, public CI, and a documentation site including the plugin author guide and
78
+ a rendered browser-contract reference.
@@ -0,0 +1,184 @@
1
+ # 3lc-compute-plugin-sdk — agent & contributor orientation
2
+
3
+ This repo is the **public Python plugin SDK** for the 3LC compute service: the import-light
4
+ contract a plugin programs against. The compute-service host that discovers and runs plugins lives
5
+ in a separate repository and is **not** a dependency of this SDK.
6
+
7
+ ## What this package is (and is not)
8
+
9
+ - **Is:** the contract surface — `ComputePlugin`, `JobContext`, the worker entrypoint, and the
10
+ `tlc_plugin_sdk.shared.*` helpers. Distribution `3lc-compute-plugin-sdk`, import `tlc_plugin_sdk`.
11
+ - **Is not:** the host. The compute service (`3lc-compute` / import `tlc_compute`) is a separate
12
+ package that *discovers and runs* plugins. It is not a dependency of this SDK and its source is
13
+ not here.
14
+
15
+ ## The invariants — do not break these
16
+
17
+ 1. **No back-edge to the host.** `tlc_plugin_sdk` must never import `tlc_compute` (or anything
18
+ host-side). The SDK is the root of the dependency graph; the host depends on the SDK, never the
19
+ reverse. The whole point — a plugin built against just this wheel runs in its own isolated venv.
20
+ 2. **Import-light.** Importing `tlc_plugin_sdk` must not eagerly pull the server stack *or the data
21
+ plane*. `litestar` and `uvicorn` are base deps but are imported **lazily** (only when a worker
22
+ actually serves); `tlc` is an optional extra imported lazily only by `shared.*`; and the
23
+ host-only SocketIO server must not be importable from here at all. `tests/test_import_light.py`
24
+ enforces this (guards `litestar`/`socketio`/`uvicorn`/`tlc`) — keep it green.
25
+ 3. **Dependencies stay minimal.** Base = `uvicorn` + `litestar` only. `3lc` is an optional extra
26
+ (`3lc-compute-plugin-sdk[shared]`, named for the `shared.*` module it unlocks) used solely by those
27
+ helpers — the contract core needs no data plane, so light consumers (e.g. the Hub frontend)
28
+ install the bare SDK. Adding a base dep widens what every plugin venv must install — justify it.
29
+ (Direction: as `shared.*` graduates into the core, `3lc` likely returns to base with `[shared]`
30
+ kept as a no-op alias.)
31
+ 4. **The contract is published — every public symbol is forever-ish.** The 0.x line is
32
+ additive-only (see README → Status): additions are the safe move; reshaping
33
+ `JobContext`/`ComputePlugin` waits for a major bump. The version is the contract version
34
+ (see below).
35
+
36
+ ## Versioning
37
+
38
+ `SDK_CONTRACT_VERSION` is read from this package's own version via `importlib.metadata` — one
39
+ source of truth (`[project] version` in `pyproject.toml`). Bump it (SemVer) when the contract
40
+ changes; the 0.x line is additive-only (README → Status). Plugins pin a range
41
+ (`3lc-compute-plugin-sdk>=X,<Y`); the host implements a range. Don't reintroduce a
42
+ separately-maintained version constant.
43
+
44
+ ## Where the rest of the context lives
45
+
46
+ The author guide and API reference travel with this repo (`docs/plugin-guide.md`, `docs/api.md`)
47
+ and are published at <https://3lc-ai.github.io/3lc-compute-plugin-sdk/>. That guide is the canonical,
48
+ self-contained source for building a plugin against this contract — including how isolation works
49
+ and how to port an existing plugin.
50
+
51
+ ## Dev setup
52
+
53
+ `3lc` resolves from the public 3LC releases index (`[tool.uv.index]` in `pyproject.toml`). If
54
+ your platform lacks a prebuilt wheel there, override locally (uncommitted) with an editable
55
+ path source.
56
+
57
+ ```bash
58
+ uv sync # installs the SDK + dev tools (ruff, mypy, pytest)
59
+ uv run ruff check .
60
+ uv run mypy src/
61
+ uv run pytest
62
+ ```
63
+
64
+ ## Conventions
65
+
66
+ Python 3.10+, uv, Hatchling, Litestar, Ruff (line-length 120), mypy `--strict` clean (no
67
+ `# type: ignore`, no `Any` used to silence the checker). Google-style docstrings. Copyright header
68
+ on new files.
69
+
70
+ ## Building a new plugin (agent guide)
71
+
72
+ Reference for building a plugin against this SDK. Read `docs/plugin-guide.md` first — this
73
+ section is the condensed working order.
74
+
75
+ ### Before you start
76
+
77
+ 1. Read the full plugin guide (`docs/plugin-guide.md`).
78
+ 2. Read an existing plugin closest to what you're building — the open first-party plugins live
79
+ in [`3lc-compute-plugins`](https://github.com/3lc-ai/3lc-compute-plugins) under
80
+ `src/tlc_plugin_<name>/`, and the GPU training/labeling plugins in their own repos
81
+ (`3lc-compute-plugin-timm` / `-sam3` / `-yolo`):
82
+ - **Form → execute → result**: `tlc_plugin_exporter` or `tlc_plugin_importer`
83
+ - **Config + GPU job + SocketIO**: sam3, yolo, or timm
84
+ - **Inline data display (no standalone page)**: `tlc_plugin_table_statistics`
85
+ - **Action on selected resources**: `tlc_plugin_merger`
86
+ 3. Read `src/tlc_plugin_sdk/contract.py` — `ComputePlugin` is an `abc.ABC` with two abstract
87
+ methods (`get_ui_fragment()`, `compute()`) and an `id` attribute the host stamps from the
88
+ manifest. The optional hooks (`run_job`, `initialise_runtime`, `shutdown_runtime`,
89
+ `get_route_handlers`) ship as no-op defaults, so you override only what you need. There is
90
+ no `register()` and **no** `get_active_jobs`/`cancel_job` — the host owns job listing and
91
+ cancellation.
92
+ 4. Read `tlc_plugin_exporter`'s `plugin.toml` + `__init__.py` — the canonical manifest +
93
+ subclass exemplar.
94
+
95
+ ### Step-by-step
96
+
97
+ 1. **Create the package**: the standalone `tlc_plugin_<name>/` shape from the guide's
98
+ "Create the plugin directory" section.
99
+ 2. **Create `plugin.toml`** (the manifest) with all metadata: top-level identity/version,
100
+ `[ui]`, and `[runtime]` (including `entrypoint = "pkg.module:ClassName"`). Copy the shape
101
+ from `tlc_plugin_exporter`.
102
+ 3. **Create `__init__.py`** with a `ComputePlugin` subclass:
103
+ - Subclass `ComputePlugin` (from `tlc_plugin_sdk`); no metadata attributes, no `register()`
104
+ - `get_ui_fragment()` reading from `ui.html`
105
+ - `get_route_handlers()` returning your route handlers (optional)
106
+ - Any optional hooks you need (`run_job` for long-running tasks, `initialise_runtime`,
107
+ `shutdown_runtime`)
108
+ 4. **Create `ui.html`** following the guide's UI Consistency section.
109
+ 5. **Create `routes.py`** if you need custom endpoints (e.g. config CRUD).
110
+ 6. **If you have a long-running task**: implement `run_job(ctx)` and set `requires_gpu` in
111
+ `[runtime]`. Reference: `tlc_plugin_image_metrics` (simplest), the training plugins.
112
+
113
+ ### Code patterns
114
+
115
+ **Config store** (if the plugin has saved configurations):
116
+ - Use the shared `PluginConfigStore` from `tlc_plugin_sdk.shared.config_store`
117
+ (generic over your config dataclass; JSON persistence under the plugin's config dir).
118
+ - Pair it with `config_ui_script()` from `tlc_plugin_sdk.shared.config_ui` for the config bar
119
+ (inject via `inject_scripts()`). See the sam3 / timm plugins.
120
+
121
+ **Long-running jobs** (training, inference, multi-step import):
122
+ - Implement `run_job(ctx)` — the host owns the queue, GPU/CPU slot, progress fan-out,
123
+ listing, and cancellation.
124
+ - Drive the generic Queue & Progress panel via `ctx.progress`/`ctx.metric`/`ctx.result`;
125
+ poll `ctx.cancelled` at checkpoints. The same `run_job` runs in `host` or `venv` mode.
126
+ - Do **not** hand-roll a queue, job store, or job-schema translation — all host-provided.
127
+
128
+ **Routes** (custom REST):
129
+ - A module-level `get_route_handlers()` returning bare `@get`/`@post` handlers with
130
+ **relative** paths (no `Controller`, no `/api/plugins` prefix, `sync_to_thread=True` for
131
+ blocking work). The plugin class delegates to it. They resolve under
132
+ `/api/plugins/<plugin-id>/...` via the host's per-plugin app + catch-all.
133
+ - CRUD for configs: relative `GET /configs`, `POST /configs`, `GET /configs/{id}`,
134
+ `POST /configs/{id}/delete`.
135
+ - Do **not** add `GET /queue` / `POST /cancel/{id}` / a job-start route — long-running work
136
+ goes through the generic `POST /api/plugins/{id}/run`, `GET /api/plugins/jobs`, and
137
+ `POST /api/plugins/jobs/{job_id}/cancel`. Custom routes carry only genuinely
138
+ plugin-specific surface (config CRUD, metadata lookups, column detection, …).
139
+
140
+ **Error response convention:**
141
+ - Pre-validation errors (bad input): return `{"error": "description"}`
142
+ - Execution results (job completed): return `{"success": true/false, "message": "..."}`
143
+ - Not found: raise `HTTPException(status_code=404)`
144
+
145
+ **URL aliases** (if the plugin creates tables from image folders):
146
+ - `from tlc_plugin_sdk.shared.aliases import register_alias`; call
147
+ `register_alias(project_name, image_folder, alias_token)` after table creation.
148
+ - Shared UI component: `from tlc_plugin_sdk.shared.alias_ui import alias_ui_script`; inject
149
+ into the fragment with `inject_scripts()` (never `str.replace` — see
150
+ `tlc_plugin_sdk/shared/ui_inject.py` for why).
151
+ - In the UI: `_tlcAliasSettingsHtml(prefix, project, folder)` renders the form;
152
+ `_tlcBindAliasToggle(prefix)` + `_tlcBindAliasAutoUpdate(prefix, projectInputId,
153
+ folderInputId)` bind it; `_tlcGetAliasValues(prefix)` at submit time; after programmatic
154
+ form fills call `_tlcSyncAliasFromForm(prefix, projectId, folderId)`.
155
+
156
+ **SocketIO** (if real-time updates are needed):
157
+ - `socketio_namespace` is optional — it defaults to `/<plugin-id>`, which the host
158
+ auto-registers at startup; declare it in `[runtime]` only to override.
159
+ - Prefer the `window.PluginJobs` client over a hand-rolled socket.
160
+ - Use `ctx.emit(name, payload)` only for telemetry the generic schema can't express
161
+ (e.g. a loss curve); never re-emit the generic lifecycle by hand.
162
+
163
+ ### Common mistakes to avoid
164
+
165
+ - Don't add a `register()` call or metadata class attributes — the manifest is the only
166
+ metadata source
167
+ - Don't add plugin-specific logic to the frontend (templates, JS modules)
168
+ - Don't use bare `fetch()` in the UI — always use `PLUGIN_API.authFetch()`
169
+ - Don't hardcode colors — use CSS variables
170
+ - Don't create new shared utilities for one-off operations
171
+ - Don't duplicate the alias UI HTML/JS — use `tlc_plugin_sdk/shared/alias_ui.py`, injected
172
+ at serve time via `inject_scripts()`
173
+ - Don't grab a GPU queue or push a closure — implement `run_job(ctx)`
174
+ - Don't implement `get_active_jobs`/`cancel_job` or add `/queue`/`/cancel` routes — host-owned
175
+ - Don't `async`-define `run_job` — it runs synchronously on a host/worker thread; poll
176
+ `ctx.cancelled`
177
+
178
+ ### Testing
179
+
180
+ - Co-locate tests with the plugin (its own `tests/`).
181
+ - Test both success and error paths for all endpoints.
182
+ - For `run_job(ctx)`, drive it with a fake `JobContext` (a recording sink + a
183
+ `threading.Event` for cancel) and assert the emitted `progress`/`metric`/`result`
184
+ events — no GPU or queue needed.