3lc-compute-plugin-sdk 0.2.2__py3-none-any.whl
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.dist-info/METADATA +81 -0
- 3lc_compute_plugin_sdk-0.2.2.dist-info/RECORD +28 -0
- 3lc_compute_plugin_sdk-0.2.2.dist-info/WHEEL +4 -0
- 3lc_compute_plugin_sdk-0.2.2.dist-info/licenses/LICENSE +202 -0
- tlc_plugin_sdk/__init__.py +58 -0
- tlc_plugin_sdk/asgi_app.py +103 -0
- tlc_plugin_sdk/contract/plugin-api.d.ts +429 -0
- tlc_plugin_sdk/contract.py +108 -0
- tlc_plugin_sdk/job_context.py +120 -0
- tlc_plugin_sdk/py.typed +0 -0
- tlc_plugin_sdk/shared/__init__.py +11 -0
- tlc_plugin_sdk/shared/alias_override_ui.py +175 -0
- tlc_plugin_sdk/shared/alias_ui.py +198 -0
- tlc_plugin_sdk/shared/aliases.py +227 -0
- tlc_plugin_sdk/shared/config_store.py +140 -0
- tlc_plugin_sdk/shared/config_ui.py +256 -0
- tlc_plugin_sdk/shared/data_source_routes.py +221 -0
- tlc_plugin_sdk/shared/data_source_ui.py +365 -0
- tlc_plugin_sdk/shared/generic_job.py +58 -0
- tlc_plugin_sdk/shared/images.py +350 -0
- tlc_plugin_sdk/shared/job_tracker.py +174 -0
- tlc_plugin_sdk/shared/labels.py +247 -0
- tlc_plugin_sdk/shared/modality.py +551 -0
- tlc_plugin_sdk/shared/model_storage.py +160 -0
- tlc_plugin_sdk/shared/naming.py +84 -0
- tlc_plugin_sdk/shared/ui_inject.py +63 -0
- tlc_plugin_sdk/shared/url_utils.py +112 -0
- tlc_plugin_sdk/worker.py +295 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
// Copyright 2026 3LC Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
//
|
|
4
|
+
// JS_CONTRACT 0.2 — the browser-side plugin contract.
|
|
5
|
+
//
|
|
6
|
+
// This file DECLARES the JavaScript surface a plugin's `ui.html` programs against.
|
|
7
|
+
// It does not implement it:
|
|
8
|
+
//
|
|
9
|
+
// * `PLUGIN_API`, `TlcApi`, `TlcData` are IMPLEMENTED by the 3LC Hub frontend,
|
|
10
|
+
// which builds `window.PLUGIN_API` when it mounts a plugin fragment.
|
|
11
|
+
//
|
|
12
|
+
// * `window.PluginJobs` SHIPS FROM THIS PACKAGE: it is the
|
|
13
|
+
// injectable client in `tlc_plugin_sdk.shared.job_tracker`
|
|
14
|
+
// (`JOB_TRACKER_JS`), which a plugin injects into its fragment via
|
|
15
|
+
// `inject_scripts(raw, job_tracker_script())`. It is NOT part of the host
|
|
16
|
+
// bridge — it is layered on top of `PLUGIN_API`.
|
|
17
|
+
//
|
|
18
|
+
// This declaration is the source of truth for `JS_CONTRACT` (matches
|
|
19
|
+
// `tlc_plugin_sdk.JS_CONTRACT`). It increments independently of the Python
|
|
20
|
+
// contract; bump the package version when it moves.
|
|
21
|
+
//
|
|
22
|
+
// USAGE from a plain-JS `ui.html` (no build step):
|
|
23
|
+
//
|
|
24
|
+
// /// <reference types="tlc_plugin_sdk/contract/plugin-api" />
|
|
25
|
+
// // (the IMPORT name `tlc_plugin_sdk`, not the distribution name
|
|
26
|
+
// // `3lc-plugin-sdk` — the on-disk dir is `tlc_plugin_sdk`, and a
|
|
27
|
+
// // `/// <reference types>` resolves via typeRoots, not jsconfig `paths`.)
|
|
28
|
+
// // or by relative path to this file:
|
|
29
|
+
// /// <reference path="../contract/plugin-api.d.ts" />
|
|
30
|
+
//
|
|
31
|
+
// var API = window.PLUGIN_API; // typed
|
|
32
|
+
// API.authFetch(API.getConfig('compute_service_url') + '/api/plugins/x/compute');
|
|
33
|
+
// PluginJobs.run('my-plugin', { table_url: url }, { onDone: function (job) {} });
|
|
34
|
+
|
|
35
|
+
// ── Object / Compute service method bags (TlcApi) ──────────────────────────────
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Compute-service method bag, exposed as `PLUGIN_API.compute`.
|
|
39
|
+
* Currently only `getHealth()` (GET /health).
|
|
40
|
+
*/
|
|
41
|
+
export interface TlcComputeService {
|
|
42
|
+
getHealth(): Promise<object>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Object-service method bag, exposed as `PLUGIN_API.objects`.
|
|
47
|
+
* All methods go through `authFetch` against the object-service URL; object URLs
|
|
48
|
+
* are encoded via `TlcApi.encodeObjectUrl`.
|
|
49
|
+
*/
|
|
50
|
+
export interface TlcObjectService {
|
|
51
|
+
getStatus(): Promise<object>;
|
|
52
|
+
getTableIndex(): Promise<object>;
|
|
53
|
+
getRunIndex(): Promise<object>;
|
|
54
|
+
getConfiguration(): Promise<object>;
|
|
55
|
+
getObject(objectUrl: string): Promise<object>;
|
|
56
|
+
deleteObject(objectUrl: string): Promise<Response>;
|
|
57
|
+
patchObject(objectUrl: string, patchData: object): Promise<object>;
|
|
58
|
+
reindex(force?: boolean): Promise<object>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Custom non-standard fetch options layered on top of the standard `RequestInit`. */
|
|
62
|
+
export interface PluginFetchOptions extends RequestInit {
|
|
63
|
+
/**
|
|
64
|
+
* Abort the request after this many milliseconds (default 10000). Custom,
|
|
65
|
+
* non-standard option — deleted before the underlying `fetch()` call. Ignored
|
|
66
|
+
* when the caller supplies its own `signal`.
|
|
67
|
+
*/
|
|
68
|
+
timeout?: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── TlcData (cached project/table/run indexing tables) ─────────────────────────
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The root a project lives under (the Object Service's project root or one of
|
|
75
|
+
* its scan URLs). Resolved by the frontend from `api://Configuration` plus the
|
|
76
|
+
* object URLs themselves; hosts predating JS_CONTRACT 0.2 never set it, so
|
|
77
|
+
* treat every location member as optional.
|
|
78
|
+
*/
|
|
79
|
+
export interface TlcDataLocation {
|
|
80
|
+
/** The root URL as configured or inferred (e.g. "s3://bucket/projects"). */
|
|
81
|
+
root: string;
|
|
82
|
+
/** Canonical prefix (file:// stripped, one trailing slash) — use for equality tests. */
|
|
83
|
+
key: string;
|
|
84
|
+
/** URL scheme: 'file', 's3', 'gs', ... */
|
|
85
|
+
scheme: string;
|
|
86
|
+
/** Short display name: alias name > bucket/last path segment > 'Local'. */
|
|
87
|
+
label: string;
|
|
88
|
+
/** True for the Object Service's primary project root. */
|
|
89
|
+
is_default: boolean;
|
|
90
|
+
/** True when `label` came from a configured alias. */
|
|
91
|
+
is_alias: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Per-location slice of a project's contents (a project can span several roots). */
|
|
95
|
+
export interface TlcDataProjectLocation {
|
|
96
|
+
location: TlcDataLocation;
|
|
97
|
+
table_count: number;
|
|
98
|
+
run_count: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface TlcDataProject {
|
|
102
|
+
project_name: string;
|
|
103
|
+
table_count: number;
|
|
104
|
+
run_count: number;
|
|
105
|
+
dataset_count: number;
|
|
106
|
+
last_modified: number;
|
|
107
|
+
/** Locations this project's contents resolve to. Absent on hosts predating JS_CONTRACT 0.2. */
|
|
108
|
+
locations?: TlcDataProjectLocation[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface TlcDataTable {
|
|
112
|
+
url: string;
|
|
113
|
+
project_name: string;
|
|
114
|
+
dataset_name: string;
|
|
115
|
+
table_name: string;
|
|
116
|
+
row_count: number;
|
|
117
|
+
created: string;
|
|
118
|
+
description: string;
|
|
119
|
+
type: string;
|
|
120
|
+
is_url_writable: boolean;
|
|
121
|
+
input_table_urls: string[];
|
|
122
|
+
/** Root this table lives under; null if unresolvable. Absent on hosts predating JS_CONTRACT 0.2. */
|
|
123
|
+
location?: TlcDataLocation | null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface TlcDataRun {
|
|
127
|
+
url: string;
|
|
128
|
+
project_name: string;
|
|
129
|
+
run_name: string;
|
|
130
|
+
status: string;
|
|
131
|
+
status_code: number;
|
|
132
|
+
created: string;
|
|
133
|
+
last_modified: string;
|
|
134
|
+
description: string;
|
|
135
|
+
constants: object;
|
|
136
|
+
metrics: any[];
|
|
137
|
+
is_url_writable: boolean;
|
|
138
|
+
/** Root this run lives under; null if unresolvable. Absent on hosts predating JS_CONTRACT 0.2. */
|
|
139
|
+
location?: TlcDataLocation | null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface TlcDataSummary {
|
|
143
|
+
project_count: number;
|
|
144
|
+
table_count: number;
|
|
145
|
+
run_count: number;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The global `TlcData` helper (cached indexing tables). Referenced from
|
|
150
|
+
* `PLUGIN_API.data`. Implemented by the frontend (data-helpers.js).
|
|
151
|
+
*/
|
|
152
|
+
export interface TlcData {
|
|
153
|
+
/** Fetch and cache both indexing tables; dedupes concurrent callers; refetches when stale. */
|
|
154
|
+
load(): Promise<void>;
|
|
155
|
+
/** Mark the cache stale so the next `load()` refetches; old data stays readable until replaced. */
|
|
156
|
+
invalidate(): void;
|
|
157
|
+
/** Raw rows from the cached TableIndexingTable response. */
|
|
158
|
+
allTableRows(): object[];
|
|
159
|
+
/** Raw rows from the cached RunIndexingTable response. */
|
|
160
|
+
allRunRows(): object[];
|
|
161
|
+
/**
|
|
162
|
+
* Map a run status code to a name ('completed','empty','running','collecting',
|
|
163
|
+
* 'post_processing','paused','cancelled', else 'unknown').
|
|
164
|
+
*/
|
|
165
|
+
runStatusName(statusCode: number | null | undefined): string;
|
|
166
|
+
/** Per-project rollup from both indexing tables, sorted by last_modified descending. */
|
|
167
|
+
getProjects(): TlcDataProject[];
|
|
168
|
+
/** Tables (optionally filtered by project), with table_name derived from the URL. */
|
|
169
|
+
getTables(projectName?: string): TlcDataTable[];
|
|
170
|
+
/** `getTables()` grouped by dataset_name ('(ungrouped)' when none). */
|
|
171
|
+
getTablesByDataset(projectName?: string): { [datasetName: string]: TlcDataTable[] };
|
|
172
|
+
/** Runs (optionally filtered by project), with run_name derived from the URL and status mapped. */
|
|
173
|
+
getRuns(projectName?: string): TlcDataRun[];
|
|
174
|
+
/** Dashboard summary counts. */
|
|
175
|
+
getSummary(): TlcDataSummary;
|
|
176
|
+
/**
|
|
177
|
+
* Resolve which root an object URL lives under (longest-prefix match against
|
|
178
|
+
* configured roots, falling back to inference from the URL structure).
|
|
179
|
+
* Absent on hosts predating JS_CONTRACT 0.2 — feature-detect before calling.
|
|
180
|
+
*/
|
|
181
|
+
resolveLocation?(url: string, projectName?: string): TlcDataLocation | null;
|
|
182
|
+
/**
|
|
183
|
+
* All locations relevant to this install: configured roots plus roots inferred
|
|
184
|
+
* from the loaded data. Absent on hosts predating JS_CONTRACT 0.2.
|
|
185
|
+
*/
|
|
186
|
+
getLocations?(): TlcDataLocation[];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Optional vendored third-party libs ─────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Third-party libraries pulled from `window` if the host loaded them, else `null`
|
|
193
|
+
* per key.
|
|
194
|
+
*
|
|
195
|
+
* Stability tiers (frozen contract):
|
|
196
|
+
* * `io` (socket.io client) — STABLE: the job-tracker channel rides it; the only
|
|
197
|
+
* `libs` member a plugin may depend on.
|
|
198
|
+
* * `Chart`, `cytoscape`, `html2canvas`, `PptxGenJS` — BEST-EFFORT: exposed for
|
|
199
|
+
* convenience, may be swapped/removed without a contract bump. A plugin that
|
|
200
|
+
* needs one should be prepared to vendor its own.
|
|
201
|
+
*/
|
|
202
|
+
export interface PluginLibs {
|
|
203
|
+
/** socket.io client (STABLE). */
|
|
204
|
+
io: any | null;
|
|
205
|
+
Chart: any | null;
|
|
206
|
+
html2canvas: any | null;
|
|
207
|
+
PptxGenJS: any | null;
|
|
208
|
+
cytoscape: any | null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── TlcLocation (shared location renderers) ─────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The global `TlcLocation` helper: shared renderers for project locations.
|
|
215
|
+
* Referenced from `PLUGIN_API.location`. Implemented by the frontend
|
|
216
|
+
* (location.js). Every renderer returns '' when the install has a single root
|
|
217
|
+
* (`isMultiRoot() === false`), so output can be concatenated unconditionally.
|
|
218
|
+
*/
|
|
219
|
+
export interface TlcLocationApi {
|
|
220
|
+
/** True when the install has more than one known root. */
|
|
221
|
+
isMultiRoot(): boolean;
|
|
222
|
+
/** Inline SVG string: folder glyph for 'file', cylinder for bucket schemes. */
|
|
223
|
+
iconSvg(scheme: string): string;
|
|
224
|
+
/** Small chip (icon + label, root in tooltip) for one location; '' when hidden. */
|
|
225
|
+
chipHtml(loc: TlcDataLocation | null): string;
|
|
226
|
+
/** Chip for a project rollup: its location, or "N locations"; '' when hidden. */
|
|
227
|
+
projectChipHtml(project: TlcDataProject | null): string;
|
|
228
|
+
/** Muted mono path line for project cards; '' when hidden. */
|
|
229
|
+
pathLineHtml(project: TlcDataProject | null): string;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── PLUGIN_API — the single host -> fragment bridge ────────────────────────────
|
|
233
|
+
|
|
234
|
+
/** Launch context: what the user launched the plugin against. */
|
|
235
|
+
export interface PluginContext {
|
|
236
|
+
/** Selected resource kind ('run','table',...) or `null` when launched bare. */
|
|
237
|
+
resourceType: string | null;
|
|
238
|
+
/** Selected 3LC object URLs (default `[]`). */
|
|
239
|
+
resourceUrls: string[];
|
|
240
|
+
/** Launch project name ('' when none). */
|
|
241
|
+
projectName: string;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The single host -> fragment JS contract. The frontend injects this as
|
|
246
|
+
* `window.PLUGIN_API` when it mounts a plugin fragment; a fragment should reach
|
|
247
|
+
* for nothing else. Many plugins alias it: `var API = window.PLUGIN_API`.
|
|
248
|
+
*/
|
|
249
|
+
export interface PluginApi {
|
|
250
|
+
/** Launch context (resource type/urls + project). */
|
|
251
|
+
context: PluginContext;
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The JS_CONTRACT version this host implements (e.g. "0.1"), so a fragment can
|
|
255
|
+
* feature-detect the bridge. Derived by the frontend from the installed
|
|
256
|
+
* `3lc-plugin-sdk` (`tlc_plugin_sdk.JS_CONTRACT`) — never a hardcoded literal —
|
|
257
|
+
* and surfaced via `<body data-contract-version>`. '' if the host predates it.
|
|
258
|
+
*/
|
|
259
|
+
contractVersion: string;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Return a configured URL by key. `dashboard_url` has its trailing slash
|
|
263
|
+
* stripped; `compute_service_url` is the GPU/CPU-routed service for THIS plugin;
|
|
264
|
+
* `object_service_url` comes from `TlcApi`. These three keys are the only ones
|
|
265
|
+
* recognized — any other key returns ''.
|
|
266
|
+
*/
|
|
267
|
+
getConfig(key: "dashboard_url" | "compute_service_url" | "object_service_url"): string;
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Authenticated `fetch`. Injects `Authorization` (from `TlcAuth`) and a default
|
|
271
|
+
* `Accept: application/json`; sets `Content-Type: application/json` when the body
|
|
272
|
+
* is a string. Aborts after `options.timeout` ms (default 10000) unless the
|
|
273
|
+
* caller supplies a `signal`. Rejects non-ok responses with the parsed
|
|
274
|
+
* detail/message. The most-used bridge member.
|
|
275
|
+
*/
|
|
276
|
+
authFetch(url: string, options?: PluginFetchOptions): Promise<Response>;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* `authFetch` against the compute-service base URL. `path` is joined to the
|
|
280
|
+
* compute-service root; when `requiresGpu` is given and a CPU/GPU counterpart
|
|
281
|
+
* service is configured, routes to the matching service. Most plugins instead
|
|
282
|
+
* build URLs from `getConfig('compute_service_url')` and call `authFetch`.
|
|
283
|
+
*/
|
|
284
|
+
computeFetch(path: string, options?: PluginFetchOptions, requiresGpu?: boolean): Promise<Response>;
|
|
285
|
+
|
|
286
|
+
/** Reference to `TlcApi.computeService` (currently only `getHealth()`). */
|
|
287
|
+
compute: TlcComputeService;
|
|
288
|
+
|
|
289
|
+
/** Reference to `TlcApi.objectService`. Plugins usually reach data via `authFetch`. */
|
|
290
|
+
objects: TlcObjectService;
|
|
291
|
+
|
|
292
|
+
/** Reference to the global `TlcData` helper (`null` if `TlcData` is undefined at mount). */
|
|
293
|
+
data: TlcData | null;
|
|
294
|
+
|
|
295
|
+
/** Reference to the global `TlcLocation` helper (`null` if undefined at mount). */
|
|
296
|
+
location: TlcLocationApi | null;
|
|
297
|
+
|
|
298
|
+
/** Optional vendored third-party libraries (each `null` if the host didn't load it). */
|
|
299
|
+
libs: PluginLibs;
|
|
300
|
+
|
|
301
|
+
/** The DOM element the fragment was mounted into. Plugins scope their queries to it. */
|
|
302
|
+
container: HTMLElement;
|
|
303
|
+
|
|
304
|
+
/** Navigate the host to a path (sets `window.location.href = path`). */
|
|
305
|
+
navigate(path: string): void;
|
|
306
|
+
|
|
307
|
+
/** Show a host toast notification; no-op fallback if the host's `showToast` is unavailable. */
|
|
308
|
+
showToast(message: string, type?: string): void;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Return an SVG icon string. With no id (or the current plugin's id) returns the
|
|
312
|
+
* plugin manifest's `icon_svg` when present; otherwise delegates to
|
|
313
|
+
* `TlcIcons.get(id || pluginId)`, or '' if `TlcIcons` is undefined.
|
|
314
|
+
*/
|
|
315
|
+
getIcon(id?: string): string;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── window.PluginJobs — SDK-injected job-tracker client ────────────────────────
|
|
319
|
+
|
|
320
|
+
/** The opaque generic job object delivered to `PluginJobs` handlers. */
|
|
321
|
+
export interface PluginJobUpdate {
|
|
322
|
+
/** 'queued' | 'running' | 'completed' | 'failed' | 'cancelled'. */
|
|
323
|
+
status?: string;
|
|
324
|
+
/** Job id. */
|
|
325
|
+
id?: string;
|
|
326
|
+
title?: string;
|
|
327
|
+
/** Failure message rides here on error. */
|
|
328
|
+
subtitle?: string;
|
|
329
|
+
progress?: { percent?: number; label?: string; timing?: any };
|
|
330
|
+
metrics?: Array<{ label?: string; value?: any }>;
|
|
331
|
+
run_url?: string;
|
|
332
|
+
[key: string]: any;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export interface PluginJobHandlers {
|
|
336
|
+
/** Fires on every `job_update`. */
|
|
337
|
+
onUpdate?: (job: PluginJobUpdate) => void;
|
|
338
|
+
/** Fires on terminal `completed` / `cancelled`. */
|
|
339
|
+
onDone?: (job: PluginJobUpdate) => void;
|
|
340
|
+
/** Fires on terminal `failed`. */
|
|
341
|
+
onError?: (job: PluginJobUpdate) => void;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** The parsed response of the generic `POST /api/plugins/{id}/run` route. */
|
|
345
|
+
export interface PluginRunResponse {
|
|
346
|
+
job_id?: string;
|
|
347
|
+
status?: string;
|
|
348
|
+
namespace?: string;
|
|
349
|
+
error?: string;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* The job-tracker client. SHIPS FROM `3lc-plugin-sdk`
|
|
354
|
+
* (`tlc_plugin_sdk.shared.job_tracker`, `JOB_TRACKER_JS`) — injected into a
|
|
355
|
+
* plugin's fragment via `inject_scripts(raw, job_tracker_script())`. NOT part of
|
|
356
|
+
* the host `PLUGIN_API` bridge; it is layered on top of it.
|
|
357
|
+
*/
|
|
358
|
+
export interface PluginJobsApi {
|
|
359
|
+
/**
|
|
360
|
+
* Start a job and track it on the generic `job_update` channel. Pre-subscribes
|
|
361
|
+
* on the default namespace `'/' + pluginId` (corrected to `resp.namespace` if it
|
|
362
|
+
* differs) and buffers events so a job completing before its id is known still
|
|
363
|
+
* delivers a terminal callback. Defaults `params.project_name` from
|
|
364
|
+
* `PLUGIN_API.context.projectName`. `onDone` fires on completed/cancelled,
|
|
365
|
+
* `onError` on failed. Most-used member.
|
|
366
|
+
*/
|
|
367
|
+
run(pluginId: string, params?: object, handlers?: PluginJobHandlers): Promise<PluginRunResponse>;
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* `POST {compute}/api/plugins/{pluginId}/run` with `params` as JSON. Defaults
|
|
371
|
+
* `params.project_name` from the launch context. Lower-level building block under
|
|
372
|
+
* `run()`.
|
|
373
|
+
*/
|
|
374
|
+
start(pluginId: string, params?: object): Promise<PluginRunResponse>;
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Subscribe to `job_update` for a single `jobId` on a known namespace; returns an
|
|
378
|
+
* unsubscribe function. Filters by `job.id`, fires `onDone` on completed/cancelled
|
|
379
|
+
* and `onError` on failed, then auto-unsubscribes.
|
|
380
|
+
*/
|
|
381
|
+
track(namespace: string, jobId: string, handlers?: PluginJobHandlers): () => void;
|
|
382
|
+
|
|
383
|
+
/** `POST {compute}/api/plugins/jobs/{jobId}/cancel` with body '{}'. */
|
|
384
|
+
cancel(jobId: string): Promise<{ cancelled?: boolean }>;
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Subscribe to a CUSTOM `ctx.emit()` event (not the generic `job_update`) on the
|
|
388
|
+
* plugin's namespace; returns an unsubscribe function. For rich per-job detail the
|
|
389
|
+
* flat generic schema can't carry (result payloads, loss curves). Subscribe before
|
|
390
|
+
* `run()` so the socket is connected when the event fires.
|
|
391
|
+
*/
|
|
392
|
+
on(namespace: string, event: string, handler: (payload: any) => void): () => void;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ── Ambient globals ────────────────────────────────────────────────────────────
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* `TlcApi` — the frontend API client (api-client.js). Plugins normally reach it
|
|
399
|
+
* through `PLUGIN_API` rather than directly, but it is an ambient global.
|
|
400
|
+
*/
|
|
401
|
+
export interface TlcApi {
|
|
402
|
+
computeService: TlcComputeService;
|
|
403
|
+
objectService: TlcObjectService;
|
|
404
|
+
authFetch(url: string, options?: PluginFetchOptions): Promise<Response>;
|
|
405
|
+
computeFetch(path: string, options?: PluginFetchOptions, requiresGpu?: boolean): Promise<Response>;
|
|
406
|
+
/** Resolved Object Service base URL (trailing slash stripped). */
|
|
407
|
+
readonly objectServiceUrl: string;
|
|
408
|
+
/** Resolves once compute-mode detection (GET /health -> mode/version) completes. */
|
|
409
|
+
waitForMode(): Promise<void>;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
declare global {
|
|
413
|
+
/** Injected by the frontend when a plugin fragment is mounted. */
|
|
414
|
+
const PLUGIN_API: PluginApi;
|
|
415
|
+
|
|
416
|
+
/** Injected by `3lc-plugin-sdk` (`shared.job_tracker`) into the plugin fragment. */
|
|
417
|
+
const PluginJobs: PluginJobsApi;
|
|
418
|
+
|
|
419
|
+
/** Ambient frontend API client. */
|
|
420
|
+
const TlcApi: TlcApi;
|
|
421
|
+
|
|
422
|
+
interface Window {
|
|
423
|
+
PLUGIN_API: PluginApi;
|
|
424
|
+
PluginJobs: PluginJobsApi;
|
|
425
|
+
TlcApi: TlcApi;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Copyright 2026 3LC Inc.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""The compute-service plugin contract — a behavior-only base class.
|
|
4
|
+
|
|
5
|
+
A plugin is a subclass of :class:`ComputePlugin`. The base declares the
|
|
6
|
+
*behavioral* surface the host (or an out-of-process worker) invokes; all
|
|
7
|
+
*metadata* (id, name, ui placement, gpu flag, socketio namespace, …) lives in
|
|
8
|
+
the plugin manifest (``[tool.tlc-compute]`` in ``plugin.toml`` /
|
|
9
|
+
``pyproject.toml``), the single source of truth. There is **no
|
|
10
|
+
metadata on the class** and **no** ``register()`` call at import — the host
|
|
11
|
+
discovers a plugin via its manifest ``entrypoint`` and hydrates the instance's
|
|
12
|
+
display identity (``id``/``name``/``icon``/``version``) from the card after
|
|
13
|
+
construction.
|
|
14
|
+
|
|
15
|
+
Only ``compute`` and ``get_ui_fragment`` are required (abstract); everything
|
|
16
|
+
else is optional behavior with a safe default, so a plugin implements just what
|
|
17
|
+
it needs and the host calls every hook directly against the inherited defaults.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from abc import ABC, abstractmethod
|
|
23
|
+
from typing import TYPE_CHECKING, Any
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from tlc_plugin_sdk.job_context import JobContext
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# SVG icon prefix — 16x16, currentColor, standard stroke settings.
|
|
30
|
+
# Handy when composing an icon literal for a manifest's ``icon_svg`` field:
|
|
31
|
+
# icon_svg = _ICON_SVG + '><path d="..."/></svg>'
|
|
32
|
+
_ICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"' # noqa: E501
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ComputePlugin(ABC):
|
|
36
|
+
"""Behavior-only base class for a compute-service plugin (host or venv).
|
|
37
|
+
|
|
38
|
+
Subclass and implement at least :meth:`compute` and :meth:`get_ui_fragment`.
|
|
39
|
+
The optional hooks below ship as no-op defaults, so the host can call any of
|
|
40
|
+
them directly without probing for the method first.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
id: Unique slug (e.g. ``run-insights``). Identity only, hydrated onto the
|
|
44
|
+
instance from the manifest by the host; the rest of the plugin's
|
|
45
|
+
metadata also comes from its manifest, never from the instance.
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
id: str
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def get_ui_fragment(self) -> str:
|
|
53
|
+
"""Return a self-contained HTML+JS+CSS fragment for the plugin UI."""
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def compute(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
58
|
+
"""Execute the plugin's computation and return a JSON-serializable dict."""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
# ── Optional behavior (no-op defaults) ────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
def initialise_runtime(self) -> None:
|
|
64
|
+
"""Initialise the plugin's runtime resources (runners, stores, models).
|
|
65
|
+
|
|
66
|
+
Called once after the shared GPU queue is ready. Default is a no-op.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def shutdown_runtime(self) -> None:
|
|
70
|
+
"""Tear down the plugin's runtime resources.
|
|
71
|
+
|
|
72
|
+
Must be safe to call on a plugin that was never initialised. Default is a
|
|
73
|
+
no-op.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def run_job(self, ctx: JobContext) -> None:
|
|
77
|
+
"""Run a long-running job against a host-provided context.
|
|
78
|
+
|
|
79
|
+
The plugin reports progress/metrics and polls cancellation via ``ctx``;
|
|
80
|
+
the same code runs in-process (host mode) or in a worker (venv mode).
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
NotImplementedError: The default — a plugin that streams jobs must
|
|
84
|
+
override this.
|
|
85
|
+
|
|
86
|
+
"""
|
|
87
|
+
plugin_id = getattr(self, "id", "?")
|
|
88
|
+
msg = f"Plugin '{plugin_id}' does not implement run_job()"
|
|
89
|
+
raise NotImplementedError(msg)
|
|
90
|
+
|
|
91
|
+
# Job listing, busy checks, and cancellation deliberately do NOT live on the
|
|
92
|
+
# plugin: the host owns every job's lifecycle (it started the job via run_job),
|
|
93
|
+
# so it lists, gates, and cancels. A plugin only implements run_job.
|
|
94
|
+
|
|
95
|
+
def get_route_handlers(self) -> list[Any]:
|
|
96
|
+
"""Return the plugin's custom routes as relative Litestar route handlers.
|
|
97
|
+
|
|
98
|
+
Each handler's path is **relative** to the plugin's mount point
|
|
99
|
+
``/api/plugins/{plugin_id}/`` (e.g. a ``@get("/models")`` handler serves
|
|
100
|
+
``GET /api/plugins/{plugin_id}/models``). The handlers are served by the
|
|
101
|
+
plugin's own Litestar app — invoked in-process for host plugins,
|
|
102
|
+
reverse-proxied to the worker for venv plugins (see
|
|
103
|
+
``tlc_plugin_sdk/asgi_app.py``); Litestar runs ``def`` handlers in a threadpool,
|
|
104
|
+
so a synchronous, blocking custom route does not block the event loop. The
|
|
105
|
+
reserved routes (``/run``, ``/health``, ``/ui``, ``/compute``, ``/jobs/*``)
|
|
106
|
+
are host-owned — a plugin must not define them. Empty by default.
|
|
107
|
+
"""
|
|
108
|
+
return []
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Copyright 2026 3LC Inc.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""``JobContext`` — the surface a plugin's ``run_job`` programs against.
|
|
4
|
+
|
|
5
|
+
A plugin implements ``run_job(ctx)`` and only ever touches ``ctx`` — it never
|
|
6
|
+
grabs a host queue or polls a shared ``cancel_flag``. The same object is used in
|
|
7
|
+
both ``host`` and ``venv`` modes; only the **sink** (where emitted events go) and
|
|
8
|
+
the **cancel signal** differ:
|
|
9
|
+
|
|
10
|
+
- ``venv`` mode: the worker harness gives a sink that enqueues events for the
|
|
11
|
+
streamed control-channel response, and a ``threading.Event`` set by the worker's
|
|
12
|
+
``/cancel`` endpoint.
|
|
13
|
+
- ``host`` mode: the host gives a sink that relays straight to SocketIO and a
|
|
14
|
+
cancel event tied to the host's job manager.
|
|
15
|
+
|
|
16
|
+
Import-light: stdlib only. Must not pull in the server stack.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import threading
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import TYPE_CHECKING, Any
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from collections.abc import Callable
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class JobContext:
|
|
30
|
+
"""Host-provided context a plugin uses to drive one job.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
job_id: Unique id for this job.
|
|
34
|
+
params: Job parameters (parsed request body / query).
|
|
35
|
+
state_dir: Writable per-plugin scratch dir that survives a venv
|
|
36
|
+
reinstall/reload (plugins must not write inside their package dir).
|
|
37
|
+
sink: Callable invoked with each emitted event dict.
|
|
38
|
+
cancel_event: Set by the host/worker to request cooperative cancellation.
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
job_id: str,
|
|
45
|
+
params: dict[str, Any],
|
|
46
|
+
state_dir: Path,
|
|
47
|
+
*,
|
|
48
|
+
sink: Callable[[dict[str, Any]], None],
|
|
49
|
+
cancel_event: threading.Event,
|
|
50
|
+
) -> None:
|
|
51
|
+
self.job_id = job_id
|
|
52
|
+
self.params = params or {}
|
|
53
|
+
self.state_dir = state_dir
|
|
54
|
+
self._sink = sink
|
|
55
|
+
self._cancel = cancel_event
|
|
56
|
+
|
|
57
|
+
# ── plugin-facing API ────────────────────────────────────────────────
|
|
58
|
+
@property
|
|
59
|
+
def cancelled(self) -> bool:
|
|
60
|
+
"""Whether cancellation has been requested (poll this at checkpoints)."""
|
|
61
|
+
return self._cancel.is_set()
|
|
62
|
+
|
|
63
|
+
def progress(self, *, percent: float, label: str = "", timing: dict[str, Any] | None = None) -> None:
|
|
64
|
+
"""Report progress (0-100) with an optional label and timing dict."""
|
|
65
|
+
self._emit({"event": "progress", "percent": percent, "label": label, "timing": timing})
|
|
66
|
+
|
|
67
|
+
def metric(self, label: str, value: str | float) -> None:
|
|
68
|
+
"""Report a scalar metric as a key/value card."""
|
|
69
|
+
self._emit({"event": "metric", "label": label, "value": value})
|
|
70
|
+
|
|
71
|
+
def log(self, message: str) -> None:
|
|
72
|
+
"""Emit a log line for the job."""
|
|
73
|
+
self._emit({"event": "log", "message": message})
|
|
74
|
+
|
|
75
|
+
def result(self, *, run_url: str) -> None:
|
|
76
|
+
"""Record the job's result link (e.g. the created table/run URL).
|
|
77
|
+
|
|
78
|
+
The host stores it on the generic job record so the Queue & Progress
|
|
79
|
+
panel can render it as an "open result" link; safe to call multiple times
|
|
80
|
+
(last write wins). Use this for the one canonical artifact the job
|
|
81
|
+
produced — richer per-plugin output still goes through :meth:`emit`.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
run_url: URL of the artifact the job produced.
|
|
85
|
+
|
|
86
|
+
"""
|
|
87
|
+
self._emit({"event": "result", "run_url": run_url})
|
|
88
|
+
|
|
89
|
+
def emit(self, name: str, payload: dict[str, Any] | None = None) -> None:
|
|
90
|
+
"""Emit a custom, plugin-defined event for the plugin's OWN rich UI.
|
|
91
|
+
|
|
92
|
+
The host relays it verbatim on the plugin's SocketIO namespace; the
|
|
93
|
+
generic Queue & Progress panel ignores it. Use :meth:`progress` /
|
|
94
|
+
:meth:`metric` / :meth:`log` for the generic panel, and this for
|
|
95
|
+
plugin-specific UI (e.g. a training plugin's per-epoch loss curve) — so a
|
|
96
|
+
plugin never opens its own SocketIO connection and stays host/venv
|
|
97
|
+
portable (same ``run_job`` in either mode).
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
name: Event name the plugin's UI listens for.
|
|
101
|
+
payload: JSON-serializable event body.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ValueError: If ``name`` collides with a host-reserved event
|
|
105
|
+
(``job_update``) used for the generic Queue & Progress channel.
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
if name == "job_update":
|
|
109
|
+
msg = "'job_update' is reserved for the generic job channel; choose a different event name"
|
|
110
|
+
raise ValueError(msg)
|
|
111
|
+
self._emit({"event": "custom", "name": name, "payload": payload or {}})
|
|
112
|
+
|
|
113
|
+
# ── host/worker-facing internals ─────────────────────────────────────
|
|
114
|
+
def _emit(self, event: dict[str, Any]) -> None:
|
|
115
|
+
event.setdefault("job_id", self.job_id)
|
|
116
|
+
self._sink(event)
|
|
117
|
+
|
|
118
|
+
def request_cancel(self) -> None:
|
|
119
|
+
"""Request cooperative cancellation (host/worker side)."""
|
|
120
|
+
self._cancel.set()
|
tlc_plugin_sdk/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Copyright 2026 3LC Inc.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Plugin-facing shared utilities (the SDK contract surface).
|
|
4
|
+
|
|
5
|
+
These helpers depend only on ``tlc`` + the standard library — never on the
|
|
6
|
+
host web stack (litestar/socketio) or process-global execution state (queues).
|
|
7
|
+
They are staged here so out-of-process (venv) plugin workers can import them
|
|
8
|
+
without pulling in the service. Modules that talk about ``tlc`` objects
|
|
9
|
+
(tables/runs/urls/models) are candidates to eventually graduate into ``tlc``
|
|
10
|
+
core; UI-string and job-channel helpers are hub-native and stay here.
|
|
11
|
+
"""
|