sqlseed-web 0.2.4__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.
Files changed (87) hide show
  1. sqlseed_web/AGENTS.md +106 -0
  2. sqlseed_web/__init__.py +24 -0
  3. sqlseed_web/__main__.py +8 -0
  4. sqlseed_web/_application.py +205 -0
  5. sqlseed_web/ai_settings.py +290 -0
  6. sqlseed_web/api.py +1102 -0
  7. sqlseed_web/app.py +26 -0
  8. sqlseed_web/managed_worker.py +184 -0
  9. sqlseed_web/operation_errors.py +42 -0
  10. sqlseed_web/plugin_environment.py +231 -0
  11. sqlseed_web/plugin_management.py +322 -0
  12. sqlseed_web/plugin_process.py +131 -0
  13. sqlseed_web/runtime_lifecycle.py +130 -0
  14. sqlseed_web/runtime_session.py +97 -0
  15. sqlseed_web/settings_environment.py +368 -0
  16. sqlseed_web/sqlite_target.py +86 -0
  17. sqlseed_web/state.py +348 -0
  18. sqlseed_web/static/AGENTS.md +180 -0
  19. sqlseed_web/static/ai.css +57 -0
  20. sqlseed_web/static/configs.css +50 -0
  21. sqlseed_web/static/date-picker.css +230 -0
  22. sqlseed_web/static/disclosure.css +149 -0
  23. sqlseed_web/static/graph-clarity.css +103 -0
  24. sqlseed_web/static/index.html +29 -0
  25. sqlseed_web/static/js/api.js +228 -0
  26. sqlseed_web/static/js/app.js +97 -0
  27. sqlseed_web/static/js/dropdown.js +432 -0
  28. sqlseed_web/static/js/filepicker.js +203 -0
  29. sqlseed_web/static/js/genform.js +1066 -0
  30. sqlseed_web/static/js/labels.js +195 -0
  31. sqlseed_web/static/js/pages/browse.js +209 -0
  32. sqlseed_web/static/js/pages/configs.js +424 -0
  33. sqlseed_web/static/js/pages/connect.js +332 -0
  34. sqlseed_web/static/js/pages/heal.js +395 -0
  35. sqlseed_web/static/js/pages/meta.js +110 -0
  36. sqlseed_web/static/js/pages/runs.js +293 -0
  37. sqlseed_web/static/js/pages/settings.js +942 -0
  38. sqlseed_web/static/js/pages/wizard.js +751 -0
  39. sqlseed_web/static/js/pages/workbench.js +3123 -0
  40. sqlseed_web/static/js/tree.js +126 -0
  41. sqlseed_web/static/js/workbench/ai-eligibility.js +33 -0
  42. sqlseed_web/static/js/workbench/ai-handoff.js +31 -0
  43. sqlseed_web/static/js/workbench/ai-stream.js +116 -0
  44. sqlseed_web/static/js/workbench/ai.js +888 -0
  45. sqlseed_web/static/js/workbench/connection.js +508 -0
  46. sqlseed_web/static/js/workbench/date-picker.js +445 -0
  47. sqlseed_web/static/js/workbench/dependency-view.js +119 -0
  48. sqlseed_web/static/js/workbench/editor.js +1236 -0
  49. sqlseed_web/static/js/workbench/focus.js +11 -0
  50. sqlseed_web/static/js/workbench/graph-layout.js +332 -0
  51. sqlseed_web/static/js/workbench/graph.js +970 -0
  52. sqlseed_web/static/js/workbench/guidance.js +29 -0
  53. sqlseed_web/static/js/workbench/model.js +124 -0
  54. sqlseed_web/static/js/workbench/plugin-management.js +512 -0
  55. sqlseed_web/static/js/workbench/preview-scroll-layout.js +94 -0
  56. sqlseed_web/static/js/workbench/preview.js +572 -0
  57. sqlseed_web/static/js/workbench/provider-guide.js +33 -0
  58. sqlseed_web/static/js/workbench/recovery.js +28 -0
  59. sqlseed_web/static/js/workbench/scroll-lock.js +26 -0
  60. sqlseed_web/static/js/workbench/session.js +174 -0
  61. sqlseed_web/static/js/workbench/table-data.js +186 -0
  62. sqlseed_web/static/js/workbench/ui.js +262 -0
  63. sqlseed_web/static/navigation.css +92 -0
  64. sqlseed_web/static/preview.css +29 -0
  65. sqlseed_web/static/runs.css +53 -0
  66. sqlseed_web/static/scrollbars.css +42 -0
  67. sqlseed_web/static/settings.css +108 -0
  68. sqlseed_web/static/style.css +3382 -0
  69. sqlseed_web/static/table-data.css +27 -0
  70. sqlseed_web/static/workbench.css +509 -0
  71. sqlseed_web/supervised_plugins.py +173 -0
  72. sqlseed_web/supervisor.py +238 -0
  73. sqlseed_web/workbench.py +381 -0
  74. sqlseed_web/workbench_ai.py +887 -0
  75. sqlseed_web/workbench_ai_relations.py +285 -0
  76. sqlseed_web/workbench_ai_stream.py +172 -0
  77. sqlseed_web/workbench_data.py +163 -0
  78. sqlseed_web/workbench_execution.py +199 -0
  79. sqlseed_web/workbench_runtime.py +1218 -0
  80. sqlseed_web/workbench_schema.py +277 -0
  81. sqlseed_web/workbench_store.py +458 -0
  82. sqlseed_web/worker_control.py +192 -0
  83. sqlseed_web-0.2.4.dist-info/METADATA +105 -0
  84. sqlseed_web-0.2.4.dist-info/RECORD +87 -0
  85. sqlseed_web-0.2.4.dist-info/WHEEL +4 -0
  86. sqlseed_web-0.2.4.dist-info/entry_points.txt +2 -0
  87. sqlseed_web-0.2.4.dist-info/licenses/LICENSE +679 -0
@@ -0,0 +1,424 @@
1
+ import { h, get, api, store, safeTargetLabel } from '../api.js';
2
+ import { button, modal, download, icon } from '../workbench/ui.js';
3
+ let root, list, notice, count, search, currentFilter, connectionHint;
4
+ let records = [],
5
+ query = '',
6
+ onlyCurrent = false,
7
+ scope = null,
8
+ version = 0,
9
+ sequence = 0,
10
+ dialog = null;
11
+ const draftPath = id => `/api/workbench/drafts/${encodeURIComponent(id)}`;
12
+ const workbenchLink = (label, suffix, primary = false, glyph = null) => h('a', {
13
+ href: `#/workbench?${suffix}`,
14
+ class: `btn wb-button${primary ? ' primary' : ''}`
15
+ }, glyph ? icon(glyph) : null, label);
16
+ export function render() {
17
+ version++;
18
+ records = [];
19
+ scope = null;
20
+ query = '';
21
+ onlyCurrent = Boolean(store.connId);
22
+ notice = h('p', {
23
+ class: 'config-notice wb-notice',
24
+ role: 'status',
25
+ 'aria-live': 'polite'
26
+ });
27
+ count = h('p', {
28
+ class: 'config-count muted'
29
+ });
30
+ list = h('section', {
31
+ class: 'config-list',
32
+ role: 'list',
33
+ 'aria-label': '已保存配置'
34
+ });
35
+ search = h('input', {
36
+ type: 'search',
37
+ value: '',
38
+ 'aria-label': '查找配置',
39
+ placeholder: '配置名称或数据库',
40
+ oninput: () => {
41
+ query = search.value;
42
+ drawList();
43
+ }
44
+ });
45
+ currentFilter = h('input', {
46
+ type: 'checkbox',
47
+ checked: onlyCurrent,
48
+ 'aria-label': '仅当前数据库',
49
+ onchange: () => {
50
+ onlyCurrent = currentFilter.checked && Boolean(store.connId);
51
+ return refresh();
52
+ }
53
+ });
54
+ connectionHint = h('p', {
55
+ class: 'config-filter-hint muted'
56
+ });
57
+ root = h('div', {
58
+ class: 'page configs-page'
59
+ }, h('header', {
60
+ class: 'heading'
61
+ }, h('div', {}, h('h1', {}, '配置管理'), h('p', {
62
+ class: 'subtitle'
63
+ }, '保存、复用和管理生成规则,打开配置后在工作台编辑。')), h('div', {
64
+ class: 'heading-actions'
65
+ }, workbenchLink('导入配置', 'new=1&import=1', false, 'upload'), workbenchLink('新建配置', 'new=1', true))), h('section', {
66
+ class: 'config-toolbar',
67
+ 'aria-label': '查找和筛选配置'
68
+ }, h('label', {
69
+ class: 'config-search'
70
+ }, h('span', {}, '查找配置'), search), h('div', {
71
+ class: 'config-scope'
72
+ }, h('label', {
73
+ class: 'config-filter'
74
+ }, currentFilter, '仅当前数据库'), connectionHint), button('刷新列表', () => refresh(), {
75
+ glyph: 'refresh'
76
+ })), notice, count, list);
77
+ updateFilter();
78
+ return root;
79
+ }
80
+ export function mount() {
81
+ window.addEventListener('sqlseed:connection-changed', connectionChanged);
82
+ return refresh();
83
+ }
84
+ export function unmount() {
85
+ version++;
86
+ sequence++;
87
+ dialog?.close();
88
+ dialog = null;
89
+ window.removeEventListener('sqlseed:connection-changed', connectionChanged);
90
+ }
91
+ function updateFilter() {
92
+ if (!store.connId) {
93
+ onlyCurrent = false;
94
+ }
95
+ currentFilter.disabled = !store.connId;
96
+ currentFilter.checked = onlyCurrent;
97
+ connectionHint.textContent = store.connId ? `当前:${safeTargetLabel(store.target)}` : '连接数据库后可按当前库筛选';
98
+ }
99
+ function connectionChanged() {
100
+ updateFilter();
101
+ return refresh();
102
+ }
103
+ async function refresh(message = '') {
104
+ const expected = version,
105
+ request = ++sequence;
106
+ const target = onlyCurrent && store.connId ? `?conn_id=${encodeURIComponent(store.connId)}` : '';
107
+ if (target !== scope) {
108
+ records = [];
109
+ scope = target;
110
+ list.replaceChildren(h('p', {
111
+ class: 'config-empty'
112
+ }, '正在读取配置…'));
113
+ }
114
+ list.setAttribute('aria-busy', 'true');
115
+ notice.textContent = message;
116
+ const current = () => expected === version && request === sequence;
117
+ try {
118
+ const response = await get(`/api/workbench/drafts${target}`);
119
+ if (!current()) {
120
+ return;
121
+ }
122
+ records = Array.isArray(response) ? response : response.drafts || [];
123
+ drawList();
124
+ } catch (error) {
125
+ if (!current()) {
126
+ return;
127
+ }
128
+ notice.textContent = `无法读取配置:${error.message}。请重试刷新列表。`;
129
+ if (!records.length) {
130
+ list.replaceChildren(h('p', {
131
+ class: 'config-empty'
132
+ }, '配置暂时无法读取,已保存内容不会因此丢失。'));
133
+ }
134
+ } finally {
135
+ if (current()) {
136
+ list.setAttribute('aria-busy', 'false');
137
+ }
138
+ }
139
+ }
140
+ function savedTime(value) {
141
+ const date = new Date(typeof value === 'number' ? value * 1000 : value);
142
+ if (value == null || value === '' || !Number.isFinite(date.getTime())) {
143
+ return '保存时间未知';
144
+ }
145
+ return date.toLocaleString('zh-CN', {
146
+ year: 'numeric',
147
+ month: '2-digit',
148
+ day: '2-digit',
149
+ hour: '2-digit',
150
+ minute: '2-digit',
151
+ hourCycle: 'h23'
152
+ });
153
+ }
154
+ function drawList() {
155
+ const term = query.trim().toLocaleLowerCase();
156
+ const visible = records.filter(record => `${record.name} ${record.target_label}`.toLocaleLowerCase().includes(term));
157
+ count.textContent = `${visible.length} 份配置${term ? " · 共 " + records.length + " 份" : ''}`;
158
+ if (!visible.length) {
159
+ list.replaceChildren(h('div', {
160
+ class: 'config-empty'
161
+ }, h('h2', {}, term ? '没有匹配的配置' : '还没有保存的配置'), h('p', {}, term ? '尝试其他名称,或关闭“仅当前数据库”查看更多配置。' : '在工作台选择表并保存规则,下次可以直接打开复用。'), term ? null : workbenchLink('新建配置', 'new=1', true)));
162
+ return;
163
+ }
164
+ list.replaceChildren(...visible.map(record => {
165
+ const document = record.document || {},
166
+ tables = Array.isArray(document.tables) ? document.tables : [];
167
+ const planned = tables.every(table => Number.isInteger(table.count) && table.count >= 0) ? tables.reduce((sum, table) => sum + table.count, 0).toLocaleString() : '待设置';
168
+ const provider = {
169
+ base: 'Base',
170
+ faker: 'Faker',
171
+ mimesis: 'Mimesis'
172
+ }[document.provider] || document.provider || '待设置';
173
+ return h('article', {
174
+ class: 'config-card',
175
+ role: 'listitem',
176
+ 'data-config-id': record.id
177
+ }, h('div', {
178
+ class: 'config-card-main'
179
+ }, h('div', {
180
+ class: 'config-card-heading'
181
+ }, h('h2', {}, record.name), h('span', {
182
+ class: 'config-revision'
183
+ }, `v${record.revision}`)), h('p', {
184
+ class: 'config-target'
185
+ }, icon('database'), record.target_label || '数据库信息未记录'), h('p', {
186
+ class: 'config-facts'
187
+ }, `${tables.length} 张表 · ${planned} 行 · ${provider} · ${document.locale || '语言待设置'}`), h('p', {
188
+ class: 'config-updated muted'
189
+ }, `最后保存 ${savedTime(record.updated_at)}`)), h('div', {
190
+ class: 'config-card-actions',
191
+ role: 'group',
192
+ 'aria-label': `${record.name} 的操作`
193
+ }, workbenchLink('打开', `draft=${encodeURIComponent(record.id)}`), button('复制', () => editName(record, true)), button('重命名', () => editName(record, false)), button('导出', () => exportConfig(record), {
194
+ glyph: 'download'
195
+ }), button('删除', () => deleteConfig(record), {
196
+ class: 'config-delete'
197
+ })));
198
+ }));
199
+ }
200
+ function createDialog(title) {
201
+ const page = version;
202
+ let closed = false;
203
+ const owned = modal(title, {
204
+ onClose: () => {
205
+ closed = true;
206
+ if (dialog === owned) {
207
+ dialog = null;
208
+ }
209
+ }
210
+ });
211
+ dialog = owned;
212
+ owned.el.classList.add('config-dialog');
213
+ return {
214
+ owned,
215
+ pageCurrent: () => page === version,
216
+ current: () => page === version && !closed && dialog === owned
217
+ };
218
+ }
219
+ function publish(type, detail) {
220
+ window.dispatchEvent(new CustomEvent(type, {
221
+ detail
222
+ }));
223
+ }
224
+ async function mutationFailure(error, context, alert, confirm) {
225
+ if (!context.current()) {
226
+ return;
227
+ }
228
+ if ([404, 409].includes(error.status)) {
229
+ alert.textContent = error.status === 409 ? '配置已被其他操作更新。列表已刷新,请取消后核对最新版本再重试。' : '配置已不存在。请关闭窗口,在更新后的列表中重新选择。';
230
+ confirm.disabled = true;
231
+ await refresh();
232
+ } else {
233
+ alert.textContent = `未能确认操作结果:${error.message}。请刷新列表核对后再重试。`;
234
+ }
235
+ }
236
+ function editName(record, copy) {
237
+ const context = createDialog(copy ? '复制配置' : '重命名配置'),
238
+ {
239
+ owned
240
+ } = context;
241
+ const input = h('input', {
242
+ type: 'text',
243
+ value: copy ? `${record.name.slice(0, 197)} 副本` : record.name,
244
+ maxlength: 200,
245
+ 'aria-label': '配置名称',
246
+ onkeydown: event => {
247
+ if (event.key === 'Enter') {
248
+ event.preventDefault();
249
+ return submit();
250
+ }
251
+ }
252
+ });
253
+ const alert = h('p', {
254
+ role: 'alert',
255
+ class: 'config-error'
256
+ });
257
+ const confirm = button(copy ? '创建副本' : '保存名称', submit, {
258
+ primary: true
259
+ });
260
+ let pending = false;
261
+ owned.body.append(h('p', {
262
+ class: 'muted'
263
+ }, copy ? '复制已保存的完整规则,原配置保持不变。' : '只修改配置名称,生成规则保持不变。'), h('label', {
264
+ class: 'config-name-field'
265
+ }, h('span', {}, '配置名称'), input), alert);
266
+ owned.actions.append(button('取消', owned.close), confirm);
267
+ input.focus();
268
+ async function submit() {
269
+ if (pending || confirm.disabled || !context.current()) {
270
+ return;
271
+ }
272
+ const name = input.value.trim();
273
+ if (!name) {
274
+ alert.textContent = '请输入配置名称。';
275
+ input.focus();
276
+ return;
277
+ }
278
+ if (name.length > 200) {
279
+ alert.textContent = '配置名称不能超过 200 个字符。';
280
+ return;
281
+ }
282
+ pending = true;
283
+ confirm.disabled = true;
284
+ input.disabled = true;
285
+ alert.textContent = '';
286
+ let failed = null;
287
+ try {
288
+ const saved = await api(`${draftPath(record.id)}${copy ? '/copy' : ''}`, {
289
+ method: copy ? 'POST' : 'PATCH',
290
+ body: JSON.stringify({
291
+ revision: record.revision,
292
+ name
293
+ })
294
+ });
295
+ if (!copy) {
296
+ publish('sqlseed:draft-renamed', {
297
+ id: saved.id,
298
+ revision: saved.revision,
299
+ name: saved.name
300
+ });
301
+ }
302
+ if (!context.pageCurrent()) {
303
+ return;
304
+ }
305
+ if (context.current()) {
306
+ owned.close();
307
+ }
308
+ await refresh(copy ? `已创建“${saved.name}”,可以打开继续编辑。` : `已重命名为“${saved.name}”。`);
309
+ } catch (error) {
310
+ failed = error;
311
+ await mutationFailure(error, context, alert, confirm);
312
+ } finally {
313
+ pending = false;
314
+ if (context.current()) {
315
+ input.disabled = false;
316
+ confirm.disabled = [404, 409].includes(failed?.status);
317
+ }
318
+ }
319
+ }
320
+ }
321
+ function deleteConfig(record) {
322
+ const context = createDialog('删除配置'),
323
+ {
324
+ owned
325
+ } = context;
326
+ const alert = h('p', {
327
+ role: 'alert',
328
+ class: 'config-error'
329
+ }),
330
+ cancel = button('取消', owned.close);
331
+ let pending = false;
332
+ const confirm = button('删除配置', async () => {
333
+ if (pending || confirm.disabled || !context.current()) {
334
+ return;
335
+ }
336
+ pending = true;
337
+ confirm.disabled = true;
338
+ alert.textContent = '';
339
+ let failed = null;
340
+ try {
341
+ await api(`${draftPath(record.id)}?revision=${record.revision}`, {
342
+ method: 'DELETE'
343
+ });
344
+ publish('sqlseed:draft-deleted', {
345
+ id: record.id,
346
+ revision: record.revision
347
+ });
348
+ if (!context.pageCurrent()) {
349
+ return;
350
+ }
351
+ if (context.current()) {
352
+ owned.close();
353
+ }
354
+ await refresh(`已删除“${record.name}”。运行记录和数据库数据均保留。`);
355
+ } catch (error) {
356
+ failed = error;
357
+ await mutationFailure(error, context, alert, confirm);
358
+ } finally {
359
+ pending = false;
360
+ if (context.current()) {
361
+ confirm.disabled = [404, 409].includes(failed?.status);
362
+ }
363
+ }
364
+ }, {
365
+ class: 'config-delete-confirm'
366
+ });
367
+ owned.body.append(h('p', {}, '删除此配置?'), h('strong', {
368
+ class: 'config-delete-name'
369
+ }, record.name), h('p', {
370
+ class: 'muted'
371
+ }, `数据库:${record.target_label} · 已保存版本 v${record.revision}`), h('p', {}, '只删除这份已保存的配置。运行记录、已提交的运行快照与数据库中的数据都会保留,正在执行的任务继续运行。'), alert);
372
+ owned.actions.append(cancel, confirm);
373
+ cancel.focus();
374
+ }
375
+ function exportConfig(record) {
376
+ const context = createDialog('导出配置'),
377
+ {
378
+ owned
379
+ } = context;
380
+ const alert = h('p', {
381
+ role: 'alert',
382
+ class: 'config-error'
383
+ }),
384
+ result = h('p', {
385
+ role: 'status',
386
+ 'aria-live': 'polite'
387
+ });
388
+ const actions = ['JSON', 'YAML'].map(format => button(`下载 ${format}`, () => saveFile(format), {
389
+ glyph: 'download'
390
+ }));
391
+ let pending = false;
392
+ owned.body.append(h('strong', {}, record.name), h('p', {}, '导出服务器上已保存的生成规则。文件可在工作台导入,不包含数据库连接凭据。'), alert, result);
393
+ owned.actions.append(button('关闭', owned.close), ...actions);
394
+ async function saveFile(format) {
395
+ if (pending || !context.current()) {
396
+ return;
397
+ }
398
+ pending = true;
399
+ alert.textContent = '';
400
+ actions.forEach(action => {
401
+ action.disabled = true;
402
+ });
403
+ try {
404
+ const saved = await get(`${draftPath(record.id)}/export`);
405
+ if (!context.current()) {
406
+ return;
407
+ }
408
+ const name = String(saved.name || 'sqlseed').replace(/[^\p{L}\p{N}._-]+/gu, '_').slice(0, 80) || 'sqlseed';
409
+ download(`${name}.${format === 'JSON' ? 'json' : 'yaml'}`, format === 'JSON' ? JSON.stringify(saved.json, null, 2) : saved.yaml, format === 'JSON' ? 'application/json' : 'application/yaml');
410
+ result.textContent = `已导出 v${saved.revision} 的 ${format} 文件。`;
411
+ } catch (error) {
412
+ if (context.current()) {
413
+ alert.textContent = `导出失败:${error.message}`;
414
+ }
415
+ } finally {
416
+ pending = false;
417
+ if (context.current()) {
418
+ actions.forEach(action => {
419
+ action.disabled = false;
420
+ });
421
+ }
422
+ }
423
+ }
424
+ }