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.
- 3lc_compute_plugin_sdk-0.2.2/.github/workflows/ci.yml +44 -0
- 3lc_compute_plugin_sdk-0.2.2/.github/workflows/docs.yml +68 -0
- 3lc_compute_plugin_sdk-0.2.2/.github/workflows/release.yml +93 -0
- 3lc_compute_plugin_sdk-0.2.2/.gitignore +25 -0
- 3lc_compute_plugin_sdk-0.2.2/CHANGELOG.md +78 -0
- 3lc_compute_plugin_sdk-0.2.2/CLAUDE.md +184 -0
- 3lc_compute_plugin_sdk-0.2.2/LICENSE +202 -0
- 3lc_compute_plugin_sdk-0.2.2/PKG-INFO +82 -0
- 3lc_compute_plugin_sdk-0.2.2/README.md +56 -0
- 3lc_compute_plugin_sdk-0.2.2/docs/api.md +87 -0
- 3lc_compute_plugin_sdk-0.2.2/docs/conf.py +111 -0
- 3lc_compute_plugin_sdk-0.2.2/docs/index.md +28 -0
- 3lc_compute_plugin_sdk-0.2.2/docs/package-lock.json +633 -0
- 3lc_compute_plugin_sdk-0.2.2/docs/package.json +10 -0
- 3lc_compute_plugin_sdk-0.2.2/docs/plugin-guide.md +939 -0
- 3lc_compute_plugin_sdk-0.2.2/docs/requirements.txt +9 -0
- 3lc_compute_plugin_sdk-0.2.2/pyproject.toml +129 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/__init__.py +58 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/asgi_app.py +103 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/contract/plugin-api.d.ts +429 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/contract.py +108 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/job_context.py +120 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/py.typed +0 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/__init__.py +11 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/alias_override_ui.py +175 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/alias_ui.py +198 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/aliases.py +227 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/config_store.py +140 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/config_ui.py +256 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/data_source_routes.py +221 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/data_source_ui.py +365 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/generic_job.py +58 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/images.py +350 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/job_tracker.py +174 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/labels.py +247 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/modality.py +551 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/model_storage.py +160 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/naming.py +84 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/ui_inject.py +63 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/shared/url_utils.py +112 -0
- 3lc_compute_plugin_sdk-0.2.2/src/tlc_plugin_sdk/worker.py +295 -0
- 3lc_compute_plugin_sdk-0.2.2/tests/test_data_source_routes.py +153 -0
- 3lc_compute_plugin_sdk-0.2.2/tests/test_import_light.py +43 -0
- 3lc_compute_plugin_sdk-0.2.2/tests/test_url_utils.py +61 -0
- 3lc_compute_plugin_sdk-0.2.2/tests/test_worker_gpu_reclaim.py +149 -0
- 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.
|