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,126 @@
1
+ // 可勾选数据库对象树(参考工具 Step2 左栏):表→列两级,
2
+ // 列节点带语义标注(外键/序列/生成器语义),点击选中列驱动右侧属性面板。
3
+
4
+ import { h, clear } from './api.js';
5
+ import { colAnnotation } from './labels.js';
6
+
7
+ /**
8
+ * @param {object} opts
9
+ * @param {Array} opts.tables - [{name, columns: ColumnInfo[], specs, fks}]
10
+ * @param {(table: string, col: string) => object|null} [opts.specResolver]
11
+ * 列标注用 spec 解析器(AI 一键配置回填后标注需反映 AI 选择而非零配置推断);
12
+ * 缺省用 t.specs。
13
+ * @param {Map<string, Set<string>>} [opts.initialSelection]
14
+ * 初始勾选状态(跨步骤持久化;AI 选择不被步骤切换重置)。缺省全选。
15
+ * @param {(table: string, col: string) => void} opts.onSelectColumn
16
+ * @param {(sel: Map<string, Set<string>>) => void} opts.onChange
17
+ */
18
+ export function createTree({
19
+ tables,
20
+ specResolver,
21
+ initialSelection,
22
+ onSelectColumn,
23
+ onChange
24
+ }) {
25
+ const expanded = new Set(tables.map(t => t.name)); // 默认全展开
26
+ const checked = initialSelection ? new Map(tables.map(t => {
27
+ const want = initialSelection.get(t.name);
28
+ return [t.name, new Set(t.columns.map(c => c.name).filter(c => want?.has(c)))];
29
+ })) : new Map(tables.map(t => [t.name, new Set(t.columns.map(c => c.name))]));
30
+ const selected = {
31
+ table: null,
32
+ col: null
33
+ };
34
+ const el = h('div', {
35
+ class: 'tree'
36
+ });
37
+ function emit() {
38
+ if (onChange) onChange(checked);
39
+ }
40
+ function render() {
41
+ clear(el);
42
+ for (const t of tables) {
43
+ const tChecked = checked.get(t.name);
44
+ const allChecked = tChecked.size === t.columns.length;
45
+ const isOpen = expanded.has(t.name);
46
+ const tRow = h('div', {
47
+ class: 'tree-row table-row',
48
+ onclick: () => {
49
+ if (isOpen) expanded.delete(t.name);else expanded.add(t.name);
50
+ render();
51
+ }
52
+ }, h('span', {
53
+ class: 'tree-arrow'
54
+ }, isOpen ? '▾' : '▸'), h('input', {
55
+ type: 'checkbox',
56
+ checked: allChecked,
57
+ onclick: e => {
58
+ e.stopPropagation();
59
+ const next = e.target.checked ? new Set(t.columns.map(c => c.name)) : new Set();
60
+ checked.set(t.name, next);
61
+ emit();
62
+ render();
63
+ }
64
+ }), h('span', {
65
+ class: 'tree-icon'
66
+ }, '▦'), h('span', {
67
+ class: 'tree-name'
68
+ }, t.name), h('span', {
69
+ class: 'muted tree-count'
70
+ }, `${tChecked.size}/${t.columns.length}`));
71
+ el.append(tRow);
72
+ if (!isOpen) continue;
73
+ renderColumns(t, tChecked);
74
+ }
75
+ function renderColumns(t, tChecked) {
76
+ for (const col of t.columns) {
77
+ const spec = specResolver ? specResolver(t.name, col.name) : t.specs?.[col.name];
78
+ const anno = colAnnotation(col, spec, t.fks);
79
+ const isSel = selected.table === t.name && selected.col === col.name;
80
+ el.append(h('div', {
81
+ class: `tree-row col-row${isSel ? ' selected' : ''}`,
82
+ onclick: e => {
83
+ e.stopPropagation();
84
+ selected.table = t.name;
85
+ selected.col = col.name;
86
+ render();
87
+ if (onSelectColumn) onSelectColumn(t.name, col.name);
88
+ }
89
+ }, h('span', {
90
+ class: 'tree-arrow'
91
+ }, ''), h('input', {
92
+ type: 'checkbox',
93
+ checked: tChecked.has(col.name),
94
+ onclick: e => {
95
+ e.stopPropagation();
96
+ if (e.target.checked) tChecked.add(col.name);else tChecked.delete(col.name);
97
+ emit();
98
+ render();
99
+ }
100
+ }), h('span', {
101
+ class: 'tree-icon col-icon'
102
+ }, '▤'), h('span', {
103
+ class: 'tree-name'
104
+ }, col.name), anno ? h('span', {
105
+ class: 'muted tree-anno'
106
+ }, `(${anno})`) : null));
107
+ }
108
+ }
109
+ }
110
+ render();
111
+ return {
112
+ el,
113
+ getSelection: () => checked,
114
+ /** 外部整体替换勾选状态(AI 一键生成配置回填用):Map<table, Set<col>> */
115
+ setSelection(sel) {
116
+ for (const t of tables) {
117
+ const cols = sel.get(t.name);
118
+ checked.set(t.name, cols ? new Set([...cols].filter(c => t.columns.some(x => x.name === c))) : new Set());
119
+ }
120
+ render();
121
+ emit();
122
+ },
123
+ getSelectedColumn: () => selected,
124
+ refresh: render
125
+ };
126
+ }
@@ -0,0 +1,33 @@
1
+ // Presentation policy mirrors workbench_ai_relations.locked_column targets.
2
+ // Explicit/effective rules take precedence over schema mappings. The server
3
+ // validates the actual schema and complete candidate before returning patches.
4
+ const populated = value => {
5
+ if (Array.isArray(value)) {
6
+ return value.length > 0;
7
+ } else if (value && typeof value === 'object') {
8
+ return Object.keys(value).length > 0;
9
+ } else {
10
+ return Boolean(value);
11
+ }
12
+ };
13
+ export function fieldAIEligibility(table, column, rule) {
14
+ const denied = (code, reason) => ({
15
+ eligible: false,
16
+ code,
17
+ reason
18
+ });
19
+ if (!table || !column?.name) return denied('unavailable', '字段结构不可用,请刷新结构后再分析。');
20
+ if (column.is_autoincrement) return denied('database_generated', '由数据库自动分配,AI 不修改该字段。');
21
+ if (column.is_primary_key || table.primary_key?.includes(column.name)) return denied('primary_key', '主键由现有规则和数据库约束管理,AI 保持原样。');
22
+ if (table.foreign_keys?.some(key => key.columns?.includes(column.name))) return denied('foreign_key', '外键从关联表取值,AI 保持引用关系。');
23
+ if (column.is_computed) return denied('computed', '由数据库表达式计算,AI 不修改该字段。');
24
+ if (populated(rule?.derive_from) || populated(rule?.expression)) return denied('derived', '已有派生规则,AI 保持原样;可在取值规则中手动调整。');
25
+ if (['faker_method', 'mimesis_method', 'native_faker_method', 'native_mimesis_method', 'native_params'].some(key => populated(rule?.[key]))) return denied('native', '已有原生引擎配置,AI 保持原样;可在取值规则中手动调整。');
26
+ const effective = rule?.generator || rule?.generator_name || table.mapping?.[column.name]?.generator_name;
27
+ if (column.default !== null && column.default !== undefined && (!effective || effective === 'skip' || effective.startsWith('__'))) return denied('database_default', '当前使用数据库 DEFAULT,AI 保持数据库默认规则。');
28
+ return {
29
+ eligible: true,
30
+ code: 'editable',
31
+ reason: ''
32
+ };
33
+ }
@@ -0,0 +1,31 @@
1
+ // One in-memory, explicitly requested trip to settings. No document edits,
2
+ // suggestions, credentials, URL payloads or browser storage belong here.
3
+ let pending=null;
4
+ const valid=connId=>Boolean(pending && pending.connId===connId
5
+ && pending.model.epoch===pending.epoch
6
+ && pending.model.lifecycleVersion===pending.lifecycle
7
+ && pending.model.schema.schema_hash===pending.schemaHash
8
+ && pending.model.schema.target_key===pending.targetKey
9
+ && pending.model.saved?.id===pending.savedId);
10
+ export function clearAIHandoff(){pending=null;}
11
+ export function rememberAIHandoff({model,connId,returnTo,context}){
12
+ pending={model,connId,returnTo,context:structuredClone(context),armed:false,
13
+ epoch:model.epoch,lifecycle:model.lifecycleVersion,schemaHash:model.schema.schema_hash,
14
+ targetKey:model.schema.target_key,savedId:model.saved?.id};
15
+ }
16
+ export function peekAIHandoff(connId){
17
+ if(!valid(connId)){pending=null;return null;}
18
+ return {returnTo:pending.returnTo};
19
+ }
20
+ export function requestAIReturn(connId){
21
+ if(!peekAIHandoff(connId))return null;
22
+ pending.armed=true;return pending.returnTo;
23
+ }
24
+ export function leaveAISettings(destinationHash){
25
+ if(!pending?.armed || pending.returnTo!==destinationHash)pending=null;
26
+ }
27
+ export function consumeAIHandoff({model,connId,returnTo}){
28
+ const context=valid(connId)&&pending.armed&&pending.model===model&&pending.returnTo===returnTo
29
+ ?structuredClone(pending.context):null;
30
+ pending=null;return context;
31
+ }
@@ -0,0 +1,116 @@
1
+ import { httpErrorMessage } from "../api.js";
2
+ const stages = new Set(['context', 'model', 'validation', 'preview']);
3
+ const invalid = () => new Error('AI 进度格式不正确,请重试。');
4
+ function responseError(detail, status) {
5
+ const message = httpErrorMessage(detail, status);
6
+ const error = new Error(message);
7
+ error.status = status;
8
+ error.detail = detail;
9
+ error.code = detail?.code;
10
+ return error;
11
+ }
12
+
13
+ // A terminal result is required: an HTTP 200 or a progress event alone never
14
+ // makes suggestions reviewable. The caller owns epoch and modal lifecycle checks.
15
+ export async function requestAISuggestions(path, request, {
16
+ signal,
17
+ onProgress
18
+ } = {}) {
19
+ const checkAbort = () => {
20
+ if (signal?.aborted) {
21
+ const error = new Error('AI 分析已取消。');
22
+ error.name = 'AbortError';
23
+ throw error;
24
+ }
25
+ };
26
+ checkAbort();
27
+ const response = await fetch(path, {
28
+ method: 'POST',
29
+ headers: {
30
+ 'Content-Type': 'application/json',
31
+ Accept: 'application/x-ndjson'
32
+ },
33
+ body: JSON.stringify(request),
34
+ signal
35
+ });
36
+ checkAbort();
37
+ const streamed = response.headers?.get('Content-Type')?.includes('application/x-ndjson');
38
+ if (!response.ok || !streamed) {
39
+ const body = await response.json().catch(() => null);
40
+ checkAbort();
41
+ if (!response.ok) throw responseError(body?.detail, response.status);
42
+ if (!body || typeof body !== 'object' || Array.isArray(body)) throw invalid();
43
+ return body;
44
+ }
45
+ if (!response.body?.getReader) throw invalid();
46
+ const reader = response.body.getReader(),
47
+ decoder = new TextDecoder();
48
+ let buffer = '';
49
+ const cancel = () => {
50
+ reader.cancel().catch(() => {});
51
+ };
52
+ signal?.addEventListener('abort', cancel, {
53
+ once: true
54
+ });
55
+ function event(line) {
56
+ let value;
57
+ try {
58
+ value = JSON.parse(line);
59
+ } catch {
60
+ throw invalid();
61
+ }
62
+ if (value?.type === 'progress' && stages.has(value.stage) && typeof value.message === 'string') {
63
+ onProgress?.({
64
+ type: 'progress',
65
+ stage: value.stage,
66
+ message: value.message
67
+ });
68
+ return null;
69
+ }
70
+ if (value?.type === 'result' && value.result && typeof value.result === 'object' && !Array.isArray(value.result)) return value;
71
+ if (value?.type === 'error' && typeof value.message === 'string') {
72
+ const error = new Error(value.message);
73
+ error.code = value.code;
74
+ throw error;
75
+ }
76
+ throw invalid();
77
+ }
78
+ try {
79
+ for (;;) {
80
+ checkAbort();
81
+ const {
82
+ value,
83
+ done
84
+ } = await reader.read();
85
+ checkAbort();
86
+ const lines = decodedLines(done, value);
87
+ const terminal = consumeLines(lines);
88
+ if (terminal) return terminal.result;
89
+ if (done) throw new Error('AI 分析连接中断,未收到完整结果,请重试。');
90
+ }
91
+ } finally {
92
+ signal?.removeEventListener('abort', cancel);
93
+ await reader.cancel().catch(() => {});
94
+ reader.releaseLock();
95
+ }
96
+ function consumeLines(lines) {
97
+ for (const line of lines) {
98
+ checkAbort();
99
+ if (!line.trim()) continue;
100
+ const terminal = event(line);
101
+ if (terminal) return terminal;
102
+ }
103
+ }
104
+ function decodedLines(done, value) {
105
+ buffer += done ? decoder.decode() : decoder.decode(value, {
106
+ stream: true
107
+ });
108
+ const lines = buffer.split('\n');
109
+ buffer = lines.pop();
110
+ if (done && buffer.trim()) {
111
+ lines.push(buffer);
112
+ buffer = '';
113
+ }
114
+ return lines;
115
+ }
116
+ }