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,175 @@
|
|
|
1
|
+
# Copyright 2026 3LC Inc.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
r"""Shared UI component for URL alias overrides.
|
|
4
|
+
|
|
5
|
+
Generates the HTML + JS block that plugins embed in their UI fragments.
|
|
6
|
+
This ensures a consistent alias override UI across every plugin that
|
|
7
|
+
consumes existing 3LC tables.
|
|
8
|
+
|
|
9
|
+
The override section is automatically hidden when all alias paths are
|
|
10
|
+
already local to the compute service (i.e. accessible on disk).
|
|
11
|
+
|
|
12
|
+
Usage in a plugin's ``get_ui_fragment()``::
|
|
13
|
+
|
|
14
|
+
from tlc_plugin_sdk.shared.alias_override_ui import alias_override_ui_script
|
|
15
|
+
from tlc_plugin_sdk.shared.ui_inject import inject_scripts
|
|
16
|
+
|
|
17
|
+
raw = Path("ui.html").read_text()
|
|
18
|
+
html = inject_scripts(raw, alias_override_ui_script())
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
# fmt: off
|
|
24
|
+
ALIAS_OVERRIDE_UI_JS = (
|
|
25
|
+
"// \u2500\u2500 Shared URL Alias Override UI \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"
|
|
26
|
+
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"
|
|
27
|
+
"function _tlcAliasOverrideHtml(idPrefix) {\n"
|
|
28
|
+
" var s = 'margin-top:12px;padding:12px;border:1px solid var(--border)';\n"
|
|
29
|
+
" s += ';border-radius:6px;background:var(--bg)';\n"
|
|
30
|
+
" var html = '<div class=\"tlc-alias-override\" id=\"' + idPrefix"
|
|
31
|
+
" + '-alias-override-container\" style=\"' + s + '\">';\n"
|
|
32
|
+
" html += '<label style=\"display:flex;align-items:center;gap:6px;"
|
|
33
|
+
"font-size:12px;font-weight:600;cursor:pointer\">';\n"
|
|
34
|
+
" html += '<input type=\"checkbox\" id=\"' + idPrefix"
|
|
35
|
+
" + '-alias-override-enabled\"> Override URL Aliases';\n"
|
|
36
|
+
" html += '</label>';\n"
|
|
37
|
+
" html += '<div style=\"margin:4px 0 8px 0;font-size:11px;"
|
|
38
|
+
"color:var(--text-muted)\">';\n"
|
|
39
|
+
" html += 'Redirect alias paths to local storage for faster I/O "
|
|
40
|
+
"\u2014 ';\n"
|
|
41
|
+
" html += 'e.g. use a local SSD copy instead of S3. ';\n"
|
|
42
|
+
" html += '<a href=\"https://docs.3lc.ai/3lc/latest/user-guide/"
|
|
43
|
+
"sharing.html#alias-best-practices\" ';\n"
|
|
44
|
+
" html += 'target=\"_blank\" style=\"color:var(--accent)\">"
|
|
45
|
+
"Learn more</a>';\n"
|
|
46
|
+
" html += '</div>';\n"
|
|
47
|
+
" html += '<div id=\"' + idPrefix + '-alias-override-fields\""
|
|
48
|
+
" style=\"display:none\">';\n"
|
|
49
|
+
" html += '<div id=\"' + idPrefix + '-alias-override-list\""
|
|
50
|
+
" style=\"font-size:11px;color:var(--text-muted)\">"
|
|
51
|
+
"Select a table to see its aliases</div>';\n"
|
|
52
|
+
" html += '</div>';\n"
|
|
53
|
+
" html += '</div>';\n"
|
|
54
|
+
" return html;\n"
|
|
55
|
+
"}\n"
|
|
56
|
+
"\n"
|
|
57
|
+
"function _tlcBindAliasOverrideToggle(idPrefix) {\n"
|
|
58
|
+
" var cb = document.getElementById(idPrefix + '-alias-override-enabled');\n"
|
|
59
|
+
" var fields = document.getElementById(idPrefix + '-alias-override-fields');\n"
|
|
60
|
+
" if (!cb || !fields) return;\n"
|
|
61
|
+
" cb.addEventListener('change', function() {\n"
|
|
62
|
+
" fields.style.display = cb.checked ? '' : 'none';\n"
|
|
63
|
+
" });\n"
|
|
64
|
+
"}\n"
|
|
65
|
+
"\n"
|
|
66
|
+
"function _tlcFetchAndPopulateOverrides(idPrefix, tableUrl, savedOverrides) {\n"
|
|
67
|
+
" var list = document.getElementById(idPrefix + '-alias-override-list');\n"
|
|
68
|
+
" var container = document.getElementById(idPrefix + '-alias-override-container');\n"
|
|
69
|
+
" if (!list) return;\n"
|
|
70
|
+
" if (!tableUrl) {\n"
|
|
71
|
+
" list.innerHTML = '<span style=\"font-size:11px;color:var(--text-muted)\">"
|
|
72
|
+
"Select a table to see its aliases</span>';\n"
|
|
73
|
+
" if (container) container.style.display = '';\n"
|
|
74
|
+
" return;\n"
|
|
75
|
+
" }\n"
|
|
76
|
+
" list.innerHTML = '<span style=\"font-size:11px;color:var(--text-muted)\">"
|
|
77
|
+
"Loading aliases...</span>';\n"
|
|
78
|
+
" var _aliasPath = '/api/aliases/for-table?url='"
|
|
79
|
+
" + encodeURIComponent(tableUrl);\n"
|
|
80
|
+
" window.PLUGIN_API.computeFetch(_aliasPath)\n"
|
|
81
|
+
" .then(function(r) { return r.json(); })\n"
|
|
82
|
+
" .then(function(data) {\n"
|
|
83
|
+
" var aliases = data.aliases || [];\n"
|
|
84
|
+
" if ((data.all_local || aliases.length === 0) && !savedOverrides) {\n"
|
|
85
|
+
" if (container) container.style.display = 'none';\n"
|
|
86
|
+
" return;\n"
|
|
87
|
+
" }\n"
|
|
88
|
+
" if (container) container.style.display = '';\n"
|
|
89
|
+
" var html = '<div style=\"display:grid;grid-template-columns:auto 1fr 1fr;"
|
|
90
|
+
"gap:6px 8px;align-items:center\">';\n"
|
|
91
|
+
" html += '<div style=\"font-weight:600;font-size:10px\">Token</div>';\n"
|
|
92
|
+
" html += '<div style=\"font-weight:600;font-size:10px\">Current Path</div>';\n"
|
|
93
|
+
" html += '<div style=\"font-weight:600;font-size:10px\">Override Path</div>';\n"
|
|
94
|
+
" for (var i = 0; i < aliases.length; i++) {\n"
|
|
95
|
+
" var a = aliases[i];\n"
|
|
96
|
+
" html += '<div style=\"font-family:monospace;font-size:11px;"
|
|
97
|
+
"white-space:nowrap\"><' + a.token + '></div>';\n"
|
|
98
|
+
" html += '<div style=\"font-size:11px;color:var(--text-muted);"
|
|
99
|
+
"overflow:hidden;text-overflow:ellipsis;white-space:nowrap\""
|
|
100
|
+
" title=\"' + (a.current_path || '') + '\">' + (a.current_path || '"
|
|
101
|
+
"<em>unresolved</em>') + '</div>';\n"
|
|
102
|
+
" html += '<input type=\"text\" class=\"form-control\" style=\"font-size:11px\"';\n"
|
|
103
|
+
" html += ' data-alias-token=\"' + a.token + '\"';\n"
|
|
104
|
+
" html += ' data-alias-original=\"' + (a.current_path || '') + '\"';\n"
|
|
105
|
+
" html += ' id=\"' + idPrefix + '-alias-override-' + a.token + '\"';\n"
|
|
106
|
+
" html += ' placeholder=\"Leave empty to keep current\">';\n"
|
|
107
|
+
" }\n"
|
|
108
|
+
" html += '</div>';\n"
|
|
109
|
+
" list.innerHTML = html;\n"
|
|
110
|
+
" if (savedOverrides) _tlcRestoreAliasOverrides(idPrefix, savedOverrides);\n"
|
|
111
|
+
" })\n"
|
|
112
|
+
" .catch(function(err) {\n"
|
|
113
|
+
" list.innerHTML = '<span style=\"color:var(--danger,red);"
|
|
114
|
+
"font-size:11px\">Failed to load aliases</span>';\n"
|
|
115
|
+
" console.error('Alias override fetch error:', err);\n"
|
|
116
|
+
" });\n"
|
|
117
|
+
"}\n"
|
|
118
|
+
"\n"
|
|
119
|
+
"function _tlcRestoreAliasOverrides(idPrefix, saved) {\n"
|
|
120
|
+
" if (!saved || !saved.overrides || !saved.overrides.length) return;\n"
|
|
121
|
+
" var cb = document.getElementById(idPrefix + '-alias-override-enabled');\n"
|
|
122
|
+
" var fields = document.getElementById(idPrefix + '-alias-override-fields');\n"
|
|
123
|
+
" if (saved.enabled && cb) {\n"
|
|
124
|
+
" cb.checked = true;\n"
|
|
125
|
+
" if (fields) fields.style.display = '';\n"
|
|
126
|
+
" }\n"
|
|
127
|
+
" for (var i = 0; i < saved.overrides.length; i++) {\n"
|
|
128
|
+
" var ov = saved.overrides[i];\n"
|
|
129
|
+
" var input = document.getElementById(\n"
|
|
130
|
+
" idPrefix + '-alias-override-' + ov.token);\n"
|
|
131
|
+
" if (input && ov.path) input.value = ov.path;\n"
|
|
132
|
+
" }\n"
|
|
133
|
+
"}\n"
|
|
134
|
+
"\n"
|
|
135
|
+
"function _tlcGetAliasOverrides(idPrefix) {\n"
|
|
136
|
+
" var cb = document.getElementById(idPrefix + '-alias-override-enabled');\n"
|
|
137
|
+
" var enabled = cb ? cb.checked : false;\n"
|
|
138
|
+
" var overrides = [];\n"
|
|
139
|
+
" if (enabled) {\n"
|
|
140
|
+
" var inputs = document.querySelectorAll(\n"
|
|
141
|
+
" '#' + idPrefix + '-alias-override-list input[data-alias-token]');\n"
|
|
142
|
+
" for (var i = 0; i < inputs.length; i++) {\n"
|
|
143
|
+
" var val = inputs[i].value.trim();\n"
|
|
144
|
+
" if (val) {\n"
|
|
145
|
+
" overrides.push({\n"
|
|
146
|
+
" token: inputs[i].getAttribute('data-alias-token'),\n"
|
|
147
|
+
" path: val\n"
|
|
148
|
+
" });\n"
|
|
149
|
+
" }\n"
|
|
150
|
+
" }\n"
|
|
151
|
+
" }\n"
|
|
152
|
+
" return {enabled: enabled, overrides: overrides};\n"
|
|
153
|
+
"}\n"
|
|
154
|
+
)
|
|
155
|
+
# fmt: on
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def alias_override_ui_script() -> str:
|
|
159
|
+
"""Return the shared alias override UI JavaScript block.
|
|
160
|
+
|
|
161
|
+
Include this once in a ``<script>`` tag. Then call:
|
|
162
|
+
|
|
163
|
+
- ``_tlcAliasOverrideHtml(prefix)`` to render HTML
|
|
164
|
+
- ``_tlcBindAliasOverrideToggle(prefix)`` after inserting the HTML
|
|
165
|
+
- ``_tlcFetchAndPopulateOverrides(prefix, tableUrl, savedOverrides)``
|
|
166
|
+
- ``_tlcGetAliasOverrides(prefix)`` at submit time
|
|
167
|
+
|
|
168
|
+
The override section is automatically hidden when all alias paths
|
|
169
|
+
are local to the compute service filesystem.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
JavaScript source string.
|
|
173
|
+
|
|
174
|
+
"""
|
|
175
|
+
return ALIAS_OVERRIDE_UI_JS
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Copyright 2026 3LC Inc.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
r"""Shared UI component for URL alias settings.
|
|
4
|
+
|
|
5
|
+
Generates the HTML + JS block that plugins embed in their UI fragments.
|
|
6
|
+
This ensures a consistent alias UI across every plugin that creates
|
|
7
|
+
3LC tables.
|
|
8
|
+
|
|
9
|
+
Usage in a plugin's ``get_ui_fragment()``::
|
|
10
|
+
|
|
11
|
+
from tlc_plugin_sdk.shared.alias_ui import alias_ui_script
|
|
12
|
+
from tlc_plugin_sdk.shared.ui_inject import inject_scripts
|
|
13
|
+
|
|
14
|
+
raw = Path("ui.html").read_text()
|
|
15
|
+
html = inject_scripts(raw, alias_ui_script())
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
# The JS helper functions are defined once and shared by all plugins.
|
|
21
|
+
# Each plugin calls _tlcAliasSettingsHtml(idPrefix, projectValue, folderValue)
|
|
22
|
+
# to render the alias section, and _tlcGetAliasValues(idPrefix) to read values.
|
|
23
|
+
|
|
24
|
+
# fmt: off
|
|
25
|
+
ALIAS_UI_JS = (
|
|
26
|
+
"// ── Shared URL Alias UI ─────────────────────────────────\n"
|
|
27
|
+
"(function(){var st=document.createElement('style');st.textContent="
|
|
28
|
+
"'.tlc-tip{position:relative}'"
|
|
29
|
+
"+'.tlc-tip .tlc-tip-text{display:none;position:absolute;bottom:calc(100% + 8px);"
|
|
30
|
+
"left:50%;transform:translateX(-50%);width:260px;padding:8px 10px;"
|
|
31
|
+
"background:var(--bg-card,#1a2332);color:var(--text-muted,#94a3b8);"
|
|
32
|
+
"font-size:11px;font-weight:400;line-height:1.5;border-radius:6px;"
|
|
33
|
+
"border:1px solid var(--border,#2a3a4a);box-shadow:0 4px 12px rgba(0,0,0,.3);"
|
|
34
|
+
"z-index:1000;pointer-events:none;white-space:normal}'"
|
|
35
|
+
"+'.tlc-tip:hover .tlc-tip-text{display:block}';"
|
|
36
|
+
"document.head.appendChild(st)})();\n"
|
|
37
|
+
"\n"
|
|
38
|
+
"function _tlcDefaultAliasToken(projectName) {\n"
|
|
39
|
+
" var token = projectName.toUpperCase()"
|
|
40
|
+
".replace(/[^A-Z0-9]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');\n"
|
|
41
|
+
" if (!token || !/^[A-Z]/.test(token)) token = 'PROJECT_' + token;\n"
|
|
42
|
+
" return token;\n"
|
|
43
|
+
"}\n"
|
|
44
|
+
"\n"
|
|
45
|
+
"function _tlcAliasSettingsHtml(idPrefix, projectValue, folderValue) {\n"
|
|
46
|
+
" var token = projectValue ? _tlcDefaultAliasToken(projectValue) : '';\n"
|
|
47
|
+
" var s = 'margin-top:12px;padding:10px 12px;border:1px solid var(--border)';\n"
|
|
48
|
+
" s += ';border-radius:6px;background:var(--bg)';\n"
|
|
49
|
+
" var html = '<div class=\"tlc-alias-settings\" style=\"' + s + '\">';\n"
|
|
50
|
+
" html += '<div style=\"display:flex;align-items:center;gap:6px\">';\n"
|
|
51
|
+
" html += '<label style=\"display:flex;align-items:center;gap:6px;"
|
|
52
|
+
"font-size:12px;font-weight:600;cursor:pointer;flex:1\">';\n"
|
|
53
|
+
" html += '<input type=\"checkbox\" id=\"' + idPrefix"
|
|
54
|
+
" + '-alias-enabled\" checked> Register URL Alias';\n"
|
|
55
|
+
" html += '<span class=\"tlc-tip\" style=\"display:inline-flex;align-items:center;"
|
|
56
|
+
"justify-content:center;width:15px;height:15px;border-radius:50%;"
|
|
57
|
+
"background:var(--border);color:var(--text-muted);font-size:10px;"
|
|
58
|
+
"font-weight:700;cursor:help;flex-shrink:0;position:relative\">"
|
|
59
|
+
"?<span class=\"tlc-tip-text\">URL aliases make image paths portable "
|
|
60
|
+
"across machines. Instead of hardcoding absolute paths, the table "
|
|
61
|
+
"stores <ALIAS>/images \\u2014 so the same table works on any "
|
|
62
|
+
"machine where the alias is configured. Recommended for all new "
|
|
63
|
+
"tables.</span></span>';\n"
|
|
64
|
+
" html += '</label>';\n"
|
|
65
|
+
" html += '<button type=\"button\" id=\"' + idPrefix + '-alias-toggle\" "
|
|
66
|
+
"style=\"background:none;border:none;color:var(--text-muted);cursor:pointer;"
|
|
67
|
+
"font-size:10px;padding:2px 6px\" "
|
|
68
|
+
"onclick=\"var f=document.getElementById(\\'' + idPrefix + '-alias-fields\\');"
|
|
69
|
+
"var open=f.style.display!==\\'none\\';f.style.display=open?\\'none\\':\\'\\';"
|
|
70
|
+
"this.textContent=open?\\'Details ▸\\':\\'Details ▾\\'\">Details ▸</button>';\n"
|
|
71
|
+
" html += '</div>';\n"
|
|
72
|
+
" html += '<div id=\"' + idPrefix + '-alias-fields\" style=\"display:none;"
|
|
73
|
+
"margin-top:8px\">';\n"
|
|
74
|
+
" html += '<div style=\"display:grid;"
|
|
75
|
+
"grid-template-columns:1fr 1fr;gap:8px\">';\n"
|
|
76
|
+
" html += '<div class=\"form-group\" style=\"margin-bottom:0\">';\n"
|
|
77
|
+
" html += '<label class=\"form-label\" style=\"font-size:11px\">"
|
|
78
|
+
"Alias Token</label>';\n"
|
|
79
|
+
" html += '<input type=\"text\" id=\"' + idPrefix + '-alias-token\"';\n"
|
|
80
|
+
" html += ' class=\"form-control\" style=\"font-size:11px\"';\n"
|
|
81
|
+
" html += ' value=\"' + (token || '') + '\"';\n"
|
|
82
|
+
" html += ' placeholder=\"PROJECT_NAME\">';\n"
|
|
83
|
+
" html += '<div style=\"font-size:10px;color:var(--text-muted);"
|
|
84
|
+
"margin-top:2px\">';\n"
|
|
85
|
+
" html += 'Must be UPPER_SNAKE_CASE (A-Z, 0-9, _)</div>';\n"
|
|
86
|
+
" html += '</div>';\n"
|
|
87
|
+
" html += '<div class=\"form-group\" style=\"margin-bottom:0\">';\n"
|
|
88
|
+
" html += '<label class=\"form-label\" style=\"font-size:11px\">"
|
|
89
|
+
"Alias Folder</label>';\n"
|
|
90
|
+
" html += '<input type=\"text\" id=\"' + idPrefix + '-alias-folder\"';\n"
|
|
91
|
+
" html += ' class=\"form-control\" style=\"font-size:11px\"';\n"
|
|
92
|
+
" html += ' value=\"' + (folderValue || '') + '\"';\n"
|
|
93
|
+
" html += ' placeholder=\"Auto-detected from input\">';\n"
|
|
94
|
+
" html += '<div style=\"font-size:10px;color:var(--text-muted);"
|
|
95
|
+
"margin-top:2px\">';\n"
|
|
96
|
+
" html += 'Folder that the alias points to</div>';\n"
|
|
97
|
+
" html += '</div>';\n"
|
|
98
|
+
" html += '</div>';\n"
|
|
99
|
+
" html += '</div>';\n"
|
|
100
|
+
" html += '</div>';\n"
|
|
101
|
+
" return html;\n"
|
|
102
|
+
"}\n"
|
|
103
|
+
"\n"
|
|
104
|
+
"function _tlcBindAliasToggle(idPrefix) {\n"
|
|
105
|
+
" var cb = document.getElementById(idPrefix + '-alias-enabled');\n"
|
|
106
|
+
" var fields = document.getElementById(idPrefix + '-alias-fields');\n"
|
|
107
|
+
" if (!cb || !fields) return;\n"
|
|
108
|
+
" cb.addEventListener('change', function() {\n"
|
|
109
|
+
" fields.style.display = cb.checked ? '' : 'none';\n"
|
|
110
|
+
" document.getElementById(idPrefix + '-alias-toggle')"
|
|
111
|
+
".style.display = cb.checked ? '' : 'none';\n"
|
|
112
|
+
" });\n"
|
|
113
|
+
"}\n"
|
|
114
|
+
"\n"
|
|
115
|
+
"function _tlcBindAliasAutoUpdate("
|
|
116
|
+
"idPrefix, projectInputId, folderInputId) {\n"
|
|
117
|
+
" var projInput = document.getElementById(projectInputId);\n"
|
|
118
|
+
" var tokenInput = document.getElementById("
|
|
119
|
+
"idPrefix + '-alias-token');\n"
|
|
120
|
+
" var folderInput = folderInputId"
|
|
121
|
+
" ? document.getElementById(folderInputId) : null;\n"
|
|
122
|
+
" var aliasFolderInput = document.getElementById("
|
|
123
|
+
"idPrefix + '-alias-folder');\n"
|
|
124
|
+
" if (projInput && tokenInput) {\n"
|
|
125
|
+
" projInput.addEventListener('input', function() {\n"
|
|
126
|
+
" if (!tokenInput.dataset.userEdited)\n"
|
|
127
|
+
" tokenInput.value = _tlcDefaultAliasToken(projInput.value);\n"
|
|
128
|
+
" });\n"
|
|
129
|
+
" tokenInput.addEventListener('input', function() {\n"
|
|
130
|
+
" tokenInput.dataset.userEdited = '1';\n"
|
|
131
|
+
" });\n"
|
|
132
|
+
" }\n"
|
|
133
|
+
" if (folderInput && aliasFolderInput) {\n"
|
|
134
|
+
" folderInput.addEventListener('input', function() {\n"
|
|
135
|
+
" if (!aliasFolderInput.dataset.userEdited)\n"
|
|
136
|
+
" aliasFolderInput.value = folderInput.value;\n"
|
|
137
|
+
" });\n"
|
|
138
|
+
" aliasFolderInput.addEventListener('input', function() {\n"
|
|
139
|
+
" aliasFolderInput.dataset.userEdited = '1';\n"
|
|
140
|
+
" });\n"
|
|
141
|
+
" }\n"
|
|
142
|
+
"}\n"
|
|
143
|
+
"\n"
|
|
144
|
+
"function _tlcSyncAliasFromForm("
|
|
145
|
+
"idPrefix, projectInputId, folderInputId) {\n"
|
|
146
|
+
" var projEl = document.getElementById(projectInputId);\n"
|
|
147
|
+
" var tokenEl = document.getElementById(idPrefix + '-alias-token');\n"
|
|
148
|
+
" var folderEl = folderInputId"
|
|
149
|
+
" ? document.getElementById(folderInputId) : null;\n"
|
|
150
|
+
" var aliasFolderEl = document.getElementById("
|
|
151
|
+
"idPrefix + '-alias-folder');\n"
|
|
152
|
+
" if (projEl && tokenEl && !tokenEl.dataset.userEdited) {\n"
|
|
153
|
+
" tokenEl.value = projEl.value"
|
|
154
|
+
" ? _tlcDefaultAliasToken(projEl.value) : '';\n"
|
|
155
|
+
" }\n"
|
|
156
|
+
" if (folderEl && aliasFolderEl"
|
|
157
|
+
" && !aliasFolderEl.dataset.userEdited) {\n"
|
|
158
|
+
" aliasFolderEl.value = folderEl.value || '';\n"
|
|
159
|
+
" }\n"
|
|
160
|
+
"}\n"
|
|
161
|
+
"\n"
|
|
162
|
+
"function _tlcSetAliasRoot(idPrefix, rootPath) {\n"
|
|
163
|
+
" var el = document.getElementById(idPrefix + '-alias-folder');\n"
|
|
164
|
+
" if (el && rootPath) {\n"
|
|
165
|
+
" el.value = rootPath;\n"
|
|
166
|
+
" el.dataset.userEdited = '1';\n"
|
|
167
|
+
" }\n"
|
|
168
|
+
"}\n"
|
|
169
|
+
"\n"
|
|
170
|
+
"function _tlcGetAliasValues(idPrefix) {\n"
|
|
171
|
+
" var cb = document.getElementById(idPrefix + '-alias-enabled');\n"
|
|
172
|
+
" return {\n"
|
|
173
|
+
" alias_enabled: cb ? cb.checked : true,\n"
|
|
174
|
+
" alias_token: (document.getElementById(\n"
|
|
175
|
+
" idPrefix + '-alias-token') || {}).value || '',\n"
|
|
176
|
+
" alias_folder: (document.getElementById(\n"
|
|
177
|
+
" idPrefix + '-alias-folder') || {}).value || '',\n"
|
|
178
|
+
" };\n"
|
|
179
|
+
"}\n"
|
|
180
|
+
)
|
|
181
|
+
# fmt: on
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def alias_ui_script() -> str:
|
|
185
|
+
"""Return the shared alias UI JavaScript block.
|
|
186
|
+
|
|
187
|
+
Include this once in a ``<script>`` tag. Then call:
|
|
188
|
+
|
|
189
|
+
- ``_tlcAliasSettingsHtml(prefix, project, folder)`` to render HTML
|
|
190
|
+
- ``_tlcBindAliasToggle(prefix)`` after inserting the HTML
|
|
191
|
+
- ``_tlcBindAliasAutoUpdate(prefix, projectInputId, folderInputId)``
|
|
192
|
+
- ``_tlcGetAliasValues(prefix)`` at submit time
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
JavaScript source string.
|
|
196
|
+
|
|
197
|
+
"""
|
|
198
|
+
return ALIAS_UI_JS
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# Copyright 2026 3LC Inc.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Shared URL alias utilities for plugins.
|
|
4
|
+
|
|
5
|
+
Two concerns:
|
|
6
|
+
|
|
7
|
+
1. **Registration** — when creating a new table, register a persistent project
|
|
8
|
+
alias so image paths use a portable ``<TOKEN>`` prefix.
|
|
9
|
+
2. **Override** — when consuming an existing table, temporarily override an
|
|
10
|
+
alias so ``<TOKEN>`` resolves to a fast local path (e.g. SSD) instead of
|
|
11
|
+
the default (e.g. S3). Overrides are session-scoped and never persisted.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _sanitize_token(name: str) -> str:
|
|
25
|
+
"""Convert a project name to a valid alias token.
|
|
26
|
+
|
|
27
|
+
Alias tokens must match ``[A-Z][A-Z0-9_]*``. We upper-case the input,
|
|
28
|
+
replace non-alphanumeric characters with underscores, collapse runs of
|
|
29
|
+
underscores, and ensure it starts with a letter.
|
|
30
|
+
"""
|
|
31
|
+
token = re.sub(r"[^A-Z0-9]", "_", name.upper())
|
|
32
|
+
token = re.sub(r"_+", "_", token).strip("_")
|
|
33
|
+
if not token or not token[0].isalpha():
|
|
34
|
+
token = "PROJECT_" + token
|
|
35
|
+
return token
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def default_alias_token(project_name: str) -> str:
|
|
39
|
+
"""Generate a default alias token from a project name.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
project_name: Human-readable project name (e.g. "My COCO Dataset").
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
A valid alias token like ``MY_COCO_DATASET``.
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
return _sanitize_token(project_name)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def register_alias(
|
|
52
|
+
project_name: str,
|
|
53
|
+
image_folder: str,
|
|
54
|
+
alias_token: str | None = None,
|
|
55
|
+
) -> dict[str, Any]:
|
|
56
|
+
"""Register a project URL alias for an image folder.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
project_name: The 3LC project that owns the alias.
|
|
60
|
+
image_folder: Absolute path to the image root folder.
|
|
61
|
+
alias_token: Override token name. If *None*, one is derived from
|
|
62
|
+
*project_name* via :func:`default_alias_token`.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Dict with ``token`` and ``path`` that were registered, or ``error``
|
|
66
|
+
on failure.
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
import tlc
|
|
70
|
+
|
|
71
|
+
token = alias_token or default_alias_token(project_name)
|
|
72
|
+
# Expand ~ before persisting — an alias stored with a literal tilde would
|
|
73
|
+
# poison every future table that resolves through it.
|
|
74
|
+
path = os.path.expanduser(image_folder.strip())
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
# Track whether a session alias for this token already existed, so the
|
|
78
|
+
# caller knows whether it created one and should clean it up afterwards.
|
|
79
|
+
# Aliases are a single flat namespace in 3.x, so the public alias
|
|
80
|
+
# snapshot is the thing to compare against.
|
|
81
|
+
existed = f"<{token}>" in tlc.url.get_registered_url_aliases()
|
|
82
|
+
|
|
83
|
+
# 1. Persist the alias in the project config.
|
|
84
|
+
tlc.helpers.ProjectHelper.register_project_url_alias(
|
|
85
|
+
token=token,
|
|
86
|
+
path=path,
|
|
87
|
+
project_name=project_name,
|
|
88
|
+
)
|
|
89
|
+
# 2. Also register as a session alias so it is active for the current
|
|
90
|
+
# process when the SDK encodes image paths.
|
|
91
|
+
tlc.url.register_url_alias(token=token, path=path, force=True)
|
|
92
|
+
logger.info("Registered alias <%s> → %s for project %r", token, path, project_name)
|
|
93
|
+
return {"token": token, "path": path, "primary_created": not existed}
|
|
94
|
+
except Exception:
|
|
95
|
+
logger.exception("Failed to register alias <%s> → %s", token, path)
|
|
96
|
+
return {"error": f"Failed to register alias <{token}> → {path}"}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# Alias override (for plugins that consume existing tables)
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
_ALIAS_TOKEN_RE = re.compile(r"<([A-Z][A-Z0-9_]*)>")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_table_aliases(table_url: str) -> list[dict[str, str]]:
|
|
107
|
+
"""Discover which URL aliases a table uses.
|
|
108
|
+
|
|
109
|
+
Loads the table, reads image-path columns from the first row, and
|
|
110
|
+
returns every alias token that appears together with its current
|
|
111
|
+
resolved path.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
table_url: 3LC table URL.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
List of ``{"token": "MY_DATA", "current_path": "/data/images", "is_local": true}``.
|
|
118
|
+
|
|
119
|
+
"""
|
|
120
|
+
import tlc
|
|
121
|
+
|
|
122
|
+
table = tlc.Table.from_url(table_url)
|
|
123
|
+
all_aliases = tlc.url.get_registered_url_aliases() # {"<TOKEN>": "/path", ...}
|
|
124
|
+
|
|
125
|
+
# Collect alias tokens referenced by the table
|
|
126
|
+
found_tokens: set[str] = set()
|
|
127
|
+
|
|
128
|
+
# Check input_url (creation source, often has alias)
|
|
129
|
+
input_url = str(getattr(table, "input_url", "")) or ""
|
|
130
|
+
for m in _ALIAS_TOKEN_RE.finditer(input_url):
|
|
131
|
+
found_tokens.add(m.group(1))
|
|
132
|
+
|
|
133
|
+
# Check a sample row from URL columns.
|
|
134
|
+
from tlc_plugin_sdk.shared.url_utils import get_url_column_names
|
|
135
|
+
|
|
136
|
+
url_col_names = get_url_column_names(table)
|
|
137
|
+
|
|
138
|
+
if url_col_names and len(table) > 0:
|
|
139
|
+
try:
|
|
140
|
+
row = table[0]
|
|
141
|
+
for col in url_col_names:
|
|
142
|
+
val = str(row.get(col, ""))
|
|
143
|
+
for m in _ALIAS_TOKEN_RE.finditer(val):
|
|
144
|
+
found_tokens.add(m.group(1))
|
|
145
|
+
except Exception:
|
|
146
|
+
logger.debug("Could not scan first row for alias tokens", exc_info=True)
|
|
147
|
+
|
|
148
|
+
# Also scan the table URL itself
|
|
149
|
+
for m in _ALIAS_TOKEN_RE.finditer(str(table.url)):
|
|
150
|
+
found_tokens.add(m.group(1))
|
|
151
|
+
|
|
152
|
+
# Build result with current resolved paths
|
|
153
|
+
result: list[dict[str, Any]] = []
|
|
154
|
+
for token in sorted(found_tokens):
|
|
155
|
+
key = f"<{token}>"
|
|
156
|
+
path = all_aliases.get(key, "")
|
|
157
|
+
if not path:
|
|
158
|
+
# Try get_alias_path as fallback
|
|
159
|
+
path = tlc.url.get_alias_path(token) or ""
|
|
160
|
+
result.append({
|
|
161
|
+
"token": token,
|
|
162
|
+
"current_path": path,
|
|
163
|
+
"is_local": bool(path) and os.path.isdir(path),
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def apply_alias_overrides(overrides: list[dict[str, str]]) -> list[dict[str, str]]:
|
|
170
|
+
"""Temporarily override alias paths for the current session.
|
|
171
|
+
|
|
172
|
+
Uses ``tlc.url.register_url_alias`` (session-only, not persisted) so that
|
|
173
|
+
``<TOKEN>`` resolves to a different path during processing.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
overrides: List of ``{"token": "TOKEN", "path": "/local/fast/path"}``.
|
|
177
|
+
Entries with empty *path* are skipped.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
List of ``{"token": "TOKEN", "original_path": "/original/path"}``
|
|
181
|
+
needed by :func:`restore_aliases` to undo the overrides.
|
|
182
|
+
|
|
183
|
+
"""
|
|
184
|
+
import tlc
|
|
185
|
+
|
|
186
|
+
originals: list[dict[str, str]] = []
|
|
187
|
+
for entry in overrides:
|
|
188
|
+
token = entry.get("token", "").strip()
|
|
189
|
+
new_path = entry.get("path", "").strip()
|
|
190
|
+
if not token or not new_path:
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
# Save original path before overriding
|
|
194
|
+
original = tlc.url.get_alias_path(token) or ""
|
|
195
|
+
if new_path == original:
|
|
196
|
+
continue # No change needed
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
tlc.url.register_url_alias(token=token, path=new_path, force=True)
|
|
200
|
+
originals.append({"token": token, "original_path": original})
|
|
201
|
+
logger.info("Override alias <%s>: %s → %s", token, original, new_path)
|
|
202
|
+
except Exception:
|
|
203
|
+
logger.exception("Failed to override alias <%s>", token)
|
|
204
|
+
|
|
205
|
+
return originals
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def restore_aliases(originals: list[dict[str, str]]) -> None:
|
|
209
|
+
"""Restore aliases to their original paths after an override.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
originals: List returned by :func:`apply_alias_overrides`.
|
|
213
|
+
|
|
214
|
+
"""
|
|
215
|
+
import tlc
|
|
216
|
+
|
|
217
|
+
for entry in originals:
|
|
218
|
+
token = entry.get("token", "")
|
|
219
|
+
original_path = entry.get("original_path", "")
|
|
220
|
+
try:
|
|
221
|
+
if original_path:
|
|
222
|
+
tlc.url.register_url_alias(token=token, path=original_path, force=True)
|
|
223
|
+
else:
|
|
224
|
+
tlc.url.unregister_url_alias(token=token)
|
|
225
|
+
logger.info("Restored alias <%s> → %s", token, original_path or "(unregistered)")
|
|
226
|
+
except Exception:
|
|
227
|
+
logger.exception("Failed to restore alias <%s>", token)
|