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,332 @@
1
+ // 连接页:三种连接方式(本地数据库文件 / PostgreSQL / 自定义 URL),
2
+ // 文件选择用服务器端目录浏览模态框(filepicker.js),
3
+ // 下拉统一用自定义 dropdown 组件(原生 select 弹层在嵌入式 WebView 中定位错乱)。
4
+
5
+ import { h, post, get, del, store, setConnBadge, table, msg, clear, rememberConnId } from '../api.js';
6
+ import { openFilePicker } from '../filepicker.js';
7
+ import { createDropdown } from '../dropdown.js';
8
+ const form = {
9
+ kind: 'sqlite',
10
+ db_path: '',
11
+ url: '',
12
+ pg: {
13
+ host: 'localhost',
14
+ port: 5432,
15
+ user: '',
16
+ password: '',
17
+ database: ''
18
+ },
19
+ provider: 'mimesis',
20
+ locale: 'zh_CN'
21
+ };
22
+ // 下拉组件为模块级实例:状态跨页面切换保留,render() 重建时重新挂载 el。
23
+ const kindDd = createDropdown({
24
+ value: form.kind,
25
+ options: [{
26
+ value: 'sqlite',
27
+ label: '本地数据库文件(SQLite)'
28
+ }, {
29
+ value: 'postgresql',
30
+ label: 'PostgreSQL'
31
+ }, {
32
+ value: 'url',
33
+ label: '自定义 URL'
34
+ }],
35
+ onChange: v => {
36
+ form.kind = v;
37
+ renderKind();
38
+ }
39
+ });
40
+ const providerDd = createDropdown({
41
+ value: form.provider,
42
+ options: [{
43
+ value: 'mimesis',
44
+ label: 'Mimesis'
45
+ }, {
46
+ value: 'faker',
47
+ label: 'Faker'
48
+ }, {
49
+ value: 'base',
50
+ label: '基础生成器'
51
+ }],
52
+ onChange: v => {
53
+ form.provider = v;
54
+ }
55
+ });
56
+ const localeDd = createDropdown({
57
+ value: form.locale,
58
+ options: [],
59
+ placeholder: '加载中…',
60
+ onChange: v => {
61
+ form.locale = v;
62
+ }
63
+ });
64
+ export function render() {
65
+ const root = h('div');
66
+ root.append(h('h2', {}, '数据库连接'), h('div', {
67
+ class: 'panel'
68
+ }, h('div', {
69
+ class: 'row'
70
+ }, h('label', {}, '数据库类型'), kindDd.el, h('span', {
71
+ class: 'muted',
72
+ id: 'kind-hint'
73
+ }, 'SQLite 文件(.db / .sqlite / .sqlite3)')), (() => {
74
+ const el = h('div', {
75
+ id: 'kind-body'
76
+ });
77
+ renderKind(el);
78
+ return el;
79
+ })(), h('div', {
80
+ class: 'row'
81
+ }, h('label', {}, '数据生成引擎'), providerDd.el, h('label', {}, '数据语言与地区'), localeDd.el, h('button', {
82
+ class: 'primary',
83
+ onclick: doConnect
84
+ }, '连接')), h('div', {
85
+ class: 'muted'
86
+ }, '语言与地区决定生成的姓名、地址、电话等数据的语言和格式。')), h('div', {
87
+ id: 'tables-out'
88
+ }));
89
+ loadLocales();
90
+ refreshExisting();
91
+ return root;
92
+ }
93
+ function renderKind(mountedBody) {
94
+ const body = mountedBody || document.getElementById('kind-body');
95
+ const hint = document.getElementById('kind-hint');
96
+ if (!body) {
97
+ return;
98
+ }
99
+ const setHint = text => {
100
+ if (hint) {
101
+ hint.textContent = text;
102
+ }
103
+ };
104
+ clear(body);
105
+ if (form.kind === 'sqlite') {
106
+ setHint('SQLite 文件(.db / .sqlite / .sqlite3)');
107
+ body.append(h('div', {
108
+ class: 'row'
109
+ }, h('label', {}, '数据库文件'), h('input', {
110
+ id: 'db-path-input',
111
+ class: 'grow',
112
+ spellcheck: 'false',
113
+ placeholder: '点击右侧「浏览…」选择,或直接输入绝对路径',
114
+ value: form.db_path,
115
+ oninput: e => {
116
+ form.db_path = e.target.value;
117
+ }
118
+ }), h('button', {
119
+ onclick: () => openFilePicker({
120
+ startPath: form.db_path ? form.db_path.replace(/\/[^/]*$/, '') : undefined,
121
+ onPick: p => {
122
+ form.db_path = p;
123
+ document.getElementById('db-path-input').value = p;
124
+ }
125
+ })
126
+ }, '浏览…')));
127
+ } else if (form.kind === 'postgresql') {
128
+ setHint('字段化填写连接参数(拼接为 postgresql:// URL)');
129
+ body.append(h('div', {
130
+ class: 'row'
131
+ }, h('label', {}, '主机'), h('input', {
132
+ value: form.pg.host,
133
+ oninput: e => {
134
+ form.pg.host = e.target.value;
135
+ }
136
+ }), h('label', {}, '端口'), h('input', {
137
+ type: 'number',
138
+ value: form.pg.port,
139
+ style: 'min-width:90px',
140
+ oninput: e => {
141
+ form.pg.port = +e.target.value;
142
+ }
143
+ })), h('div', {
144
+ class: 'row'
145
+ }, h('label', {}, '用户'), h('input', {
146
+ value: form.pg.user,
147
+ placeholder: 'postgres',
148
+ oninput: e => {
149
+ form.pg.user = e.target.value;
150
+ }
151
+ }), h('label', {}, '密码'), h('input', {
152
+ type: 'password',
153
+ value: form.pg.password,
154
+ oninput: e => {
155
+ form.pg.password = e.target.value;
156
+ }
157
+ }), h('label', {}, '数据库'), h('input', {
158
+ value: form.pg.database,
159
+ placeholder: 'mydb',
160
+ oninput: e => {
161
+ form.pg.database = e.target.value;
162
+ }
163
+ })), h('div', {
164
+ class: 'muted'
165
+ }, '需安装 psycopg(sqlseed[postgresql]),数据库需可达。'));
166
+ } else {
167
+ setHint('任意 SQLAlchemy URL(为未来支持的数据库预留)');
168
+ body.append(h('div', {
169
+ class: 'row'
170
+ }, h('label', {}, '连接 URL'), h('input', {
171
+ class: 'grow',
172
+ spellcheck: 'false',
173
+ placeholder: 'postgresql://user:pass@host:5432/db',
174
+ value: form.url,
175
+ oninput: e => {
176
+ form.url = e.target.value;
177
+ }
178
+ })));
179
+ }
180
+ }
181
+ async function loadLocales() {
182
+ try {
183
+ const res = await get('/api/meta/locales');
184
+ localeDd.setOptions(res.locales.map(l => ({
185
+ value: l.code,
186
+ label: l.label
187
+ })), res.default);
188
+ form.locale = res.default;
189
+ } catch {/* keep the loading placeholder; connection still works */}
190
+ }
191
+ function buildPayload() {
192
+ if (form.kind === 'sqlite') {
193
+ return {
194
+ db_path: form.db_path || null,
195
+ url: null,
196
+ provider: form.provider,
197
+ locale: form.locale
198
+ };
199
+ }
200
+ if (form.kind === 'postgresql') {
201
+ const {
202
+ host,
203
+ port,
204
+ user,
205
+ password,
206
+ database
207
+ } = form.pg;
208
+ let auth;
209
+ if (user) {
210
+ auth = `${encodeURIComponent(user)}${password ? ":" + encodeURIComponent(password) : ''}@`;
211
+ } else {
212
+ auth = '';
213
+ }
214
+ const url = `postgresql://${auth}${host}:${port}/${database}`;
215
+ return {
216
+ db_path: null,
217
+ url,
218
+ provider: form.provider,
219
+ locale: form.locale
220
+ };
221
+ }
222
+ return {
223
+ db_path: null,
224
+ url: form.url || null,
225
+ provider: form.provider,
226
+ locale: form.locale
227
+ };
228
+ }
229
+ async function doConnect() {
230
+ const out = document.getElementById('tables-out');
231
+ clear(out);
232
+ const payload = buildPayload();
233
+ if (!payload.db_path && !payload.url) {
234
+ out.append(msg(form.kind === 'sqlite' ? '请选择或输入数据库文件路径。' : '请填写连接信息。', 'warn'));
235
+ return;
236
+ }
237
+ try {
238
+ const res = await post('/api/connections', payload);
239
+ store.connId = res.conn_id;
240
+ store.target = res.target;
241
+ store.tables = res.tables;
242
+ rememberConnId(res.conn_id); // 刷新后向导可据此恢复
243
+ setConnBadge();
244
+ renderTables(out, res);
245
+ } catch (e) {
246
+ out.append(msg(`连接失败:${e.message}`));
247
+ }
248
+ }
249
+ function renderTables(out, res) {
250
+ out.append(h('div', {
251
+ class: 'msg ok'
252
+ }, `已连接 ${res.conn_id}(${res.tables.length} 张表)`), h('a', {
253
+ href: '#/workbench',
254
+ class: 'btn primary'
255
+ }, '进入工作台'), h('div', {
256
+ class: 'table-scroll'
257
+ }, table(['表名', '行数', '列数', '外键数'], res.tables.map(t => [t.name, t.row_count, t.column_count, t.foreign_keys]), {
258
+ monoCols: [0]
259
+ })));
260
+ }
261
+ async function refreshExisting() {
262
+ try {
263
+ const res = await get('/api/connections');
264
+ if (!res.connections.length) {
265
+ return;
266
+ }
267
+ const out = document.getElementById('tables-out');
268
+ if (!out || out.children.length) {
269
+ return;
270
+ }
271
+ out.append(h('h3', {}, '已有连接'), h('div', {
272
+ class: 'table-scroll'
273
+ }, table(['连接编号', '数据库', '分组', '数据生成引擎', '数据语言与地区', '操作'], res.connections.map(c => [c.conn_id, c.target, groupBadge(c), c.provider, c.locale, h('div', {
274
+ class: 'row'
275
+ }, h('button', {
276
+ class: 'small',
277
+ onclick: async () => {
278
+ try {
279
+ const detail = await get(`/api/connections/${c.conn_id}/tables`);
280
+ store.connId = c.conn_id;
281
+ store.target = detail.target;
282
+ store.tables = detail.tables;
283
+ rememberConnId(c.conn_id);
284
+ setConnBadge();
285
+ location.hash = '#/workbench';
286
+ } catch (error) {
287
+ out.append(msg(`无法打开连接:${error.message}`));
288
+ }
289
+ }
290
+ }, '进入工作台'), h('button', {
291
+ class: 'small',
292
+ onclick: async () => {
293
+ await del(`/api/connections/${c.conn_id}`);
294
+ if (store.connId === c.conn_id) {
295
+ store.connId = null;
296
+ setConnBadge();
297
+ }
298
+ location.reload();
299
+ }
300
+ }, '断开'))]))));
301
+ } catch {/* server not reachable — ignore */}
302
+ }
303
+
304
+ // 同一物理数据库的多个连接(合法且支持并发写,见 state.py 归一化说明):
305
+ // 第 1 个标记「主」,后续标记「并行 N」;同组同色,颜色由 group_key 哈希决定。
306
+ const GROUP_HUES = [212, 152, 38, 274, 350, 190]; // 与现有 accent 配色协调
307
+
308
+ function groupBadge(c) {
309
+ if (!c.group_key) {
310
+ return '—';
311
+ }
312
+ const idx = c.group_index ?? 1;
313
+ const size = c.group_size ?? 1;
314
+ if (size <= 1) {
315
+ return h('span', {
316
+ class: 'pill'
317
+ }, '唯一');
318
+ }
319
+ const hue = GROUP_HUES[hashStr(c.group_key) % GROUP_HUES.length];
320
+ const label = idx === 1 ? `主连接 · 1/${size}` : `并行 ${idx} · ${idx}/${size}`;
321
+ return h('span', {
322
+ class: 'pill group-badge',
323
+ style: `color: hsl(${hue} 70% 65%); border-color: hsl(${hue} 70% 45% / 0.55);`
324
+ }, label);
325
+ }
326
+ function hashStr(s) {
327
+ let h = 0;
328
+ for (let i = 0; i < s.length; i++) {
329
+ h = h * 31 + s.charCodeAt(i) | 0;
330
+ }
331
+ return Math.abs(h);
332
+ }