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,381 @@
1
+ """HTTP contracts for the persistent, offline generation workbench."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+ from contextlib import contextmanager
7
+ from http import HTTPStatus
8
+ from typing import Annotated, Any
9
+
10
+ import yaml
11
+ from fastapi import APIRouter, HTTPException, Query
12
+ from fastapi.encoders import jsonable_encoder
13
+ from pydantic import BaseModel, ConfigDict, Field
14
+
15
+ from sqlseed_web.state import ConnectionBusyError, state
16
+ from sqlseed_web.workbench_runtime import (
17
+ WorkbenchError,
18
+ check_document,
19
+ export_document,
20
+ normalize_document,
21
+ parse_document,
22
+ plan_execution,
23
+ public_error,
24
+ start_run,
25
+ )
26
+ from sqlseed_web.workbench_schema import _target_identity, generator_catalog, inspect_connection
27
+ from sqlseed_web.workbench_store import RevisionConflict, get_store
28
+
29
+ router = APIRouter(prefix="/api/workbench", tags=["workbench"])
30
+
31
+
32
+ class DocumentRequest(BaseModel):
33
+ model_config = ConfigDict(extra="forbid")
34
+ conn_id: str
35
+ document: dict[str, Any]
36
+
37
+
38
+ class DraftRequest(DocumentRequest):
39
+ name: str = Field(min_length=1, max_length=200)
40
+ schema_hash: str
41
+ view_state: dict[str, Any] = Field(default_factory=dict)
42
+
43
+
44
+ class DraftUpdateRequest(DraftRequest):
45
+ revision: int = Field(ge=1)
46
+
47
+
48
+ class DraftNameRequest(BaseModel):
49
+ model_config = ConfigDict(extra="forbid")
50
+ revision: int = Field(ge=1, strict=True)
51
+ name: str = Field(min_length=1, max_length=200)
52
+
53
+
54
+ class CheckRequest(DocumentRequest):
55
+ schema_hash: str
56
+ count: int = Field(default=3, ge=1, le=100)
57
+
58
+
59
+ class ParseRequest(BaseModel):
60
+ model_config = ConfigDict(extra="forbid")
61
+ conn_id: str
62
+ text: str = Field(max_length=2 * 1024 * 1024)
63
+
64
+
65
+ class RunRequest(BaseModel):
66
+ model_config = ConfigDict(extra="forbid")
67
+ conn_id: str
68
+ draft_id: str
69
+ revision: int = Field(ge=1)
70
+ schema_hash: str
71
+ config_hash: str
72
+ execution: dict[str, Any] | None = None
73
+ plan_hash: str = Field(default="", max_length=128)
74
+
75
+
76
+ @contextmanager
77
+ def _request_errors() -> Iterator[None]:
78
+ try:
79
+ yield
80
+ except HTTPException:
81
+ raise
82
+ except WorkbenchError as exc:
83
+ raise HTTPException(exc.status, detail={"code": exc.code, "message": public_error(exc)}) from exc
84
+ except ConnectionBusyError as exc:
85
+ raise HTTPException(409, detail={"code": "connection_busy", "message": public_error(exc)}) from exc
86
+ except RevisionConflict as exc:
87
+ raise HTTPException(409, detail={"code": "conflict", "message": public_error(exc)}) from exc
88
+ except KeyError as exc:
89
+ raise HTTPException(404, detail={"code": "not_found", "message": public_error(exc)}) from exc
90
+ except Exception as exc:
91
+ raise HTTPException(422, detail={"code": "request_failed", "message": public_error(exc)}) from exc
92
+
93
+
94
+ @router.get(
95
+ "/connections/{conn_id}/schema",
96
+ responses={
97
+ 404: {"description": HTTPStatus(404).phrase},
98
+ 409: {"description": HTTPStatus(409).phrase},
99
+ 422: {"description": HTTPStatus(422).phrase},
100
+ },
101
+ )
102
+ def connection_schema(conn_id: str) -> dict[str, Any]:
103
+ with _request_errors(), state.connection_operation(conn_id) as conn:
104
+ return inspect_connection(conn)
105
+
106
+
107
+ @router.get(
108
+ "/generators",
109
+ responses={
110
+ 404: {"description": HTTPStatus(404).phrase},
111
+ 409: {"description": HTTPStatus(409).phrase},
112
+ 422: {"description": HTTPStatus(422).phrase},
113
+ },
114
+ )
115
+ def generators() -> dict[str, Any]:
116
+ with _request_errors():
117
+ return generator_catalog()
118
+
119
+
120
+ @router.get(
121
+ "/drafts",
122
+ responses={
123
+ 404: {"description": HTTPStatus(404).phrase},
124
+ 409: {"description": HTTPStatus(409).phrase},
125
+ 422: {"description": HTTPStatus(422).phrase},
126
+ },
127
+ )
128
+ def list_drafts(conn_id: str | None = None) -> list[dict[str, Any]]:
129
+ with _request_errors():
130
+ target_key = None
131
+ if conn_id:
132
+ # Filtering saved metadata needs only connection identity, not a
133
+ # live schema scan or the orchestrator's generation lock.
134
+ conn = state.get_connection(conn_id)
135
+ target_key, _ = _target_identity(conn)
136
+ return get_store().list_drafts(target_key=target_key)
137
+
138
+
139
+ def _save_draft(body: DraftRequest, draft_id: str | None = None, revision: int | None = None) -> dict[str, Any]:
140
+ with _request_errors(), state.connection_operation(body.conn_id) as conn:
141
+ schema = inspect_connection(conn)
142
+ if schema["schema_hash"] != body.schema_hash:
143
+ raise WorkbenchError("schema 已变化,请刷新后保存", code="schema_changed", status=409)
144
+ return get_store().save_draft(
145
+ {
146
+ "name": body.name.strip(),
147
+ "document": normalize_document(conn, body.document),
148
+ "schema_hash": body.schema_hash,
149
+ "view_state": body.view_state,
150
+ "target_key": schema["target_key"],
151
+ "target_label": schema["target_label"],
152
+ },
153
+ draft_id=draft_id,
154
+ expected_revision=revision,
155
+ )
156
+
157
+
158
+ @router.post(
159
+ "/drafts",
160
+ responses={
161
+ 404: {"description": HTTPStatus(404).phrase},
162
+ 409: {"description": HTTPStatus(409).phrase},
163
+ 422: {"description": HTTPStatus(422).phrase},
164
+ },
165
+ )
166
+ def create_draft(body: DraftRequest) -> dict[str, Any]:
167
+ return _save_draft(body)
168
+
169
+
170
+ @router.get(
171
+ "/drafts/{draft_id}",
172
+ responses={
173
+ 404: {"description": HTTPStatus(404).phrase},
174
+ 409: {"description": HTTPStatus(409).phrase},
175
+ 422: {"description": HTTPStatus(422).phrase},
176
+ },
177
+ )
178
+ def get_draft(draft_id: str) -> dict[str, Any]:
179
+ with _request_errors():
180
+ return get_store().get_draft(draft_id)
181
+
182
+
183
+ @router.put(
184
+ "/drafts/{draft_id}",
185
+ responses={
186
+ 404: {"description": HTTPStatus(404).phrase},
187
+ 409: {"description": HTTPStatus(409).phrase},
188
+ 422: {"description": HTTPStatus(422).phrase},
189
+ },
190
+ )
191
+ def update_draft(draft_id: str, body: DraftUpdateRequest) -> dict[str, Any]:
192
+ return _save_draft(body, draft_id, body.revision)
193
+
194
+
195
+ @router.patch(
196
+ "/drafts/{draft_id}",
197
+ responses={
198
+ 404: {"description": HTTPStatus(404).phrase},
199
+ 409: {"description": HTTPStatus(409).phrase},
200
+ 422: {"description": HTTPStatus(422).phrase},
201
+ },
202
+ )
203
+ def rename_draft(draft_id: str, body: DraftNameRequest) -> dict[str, Any]:
204
+ with _request_errors():
205
+ return get_store().rename_draft(draft_id, body.name, expected_revision=body.revision)
206
+
207
+
208
+ @router.post(
209
+ "/drafts/{draft_id}/copy",
210
+ responses={
211
+ 404: {"description": HTTPStatus(404).phrase},
212
+ 409: {"description": HTTPStatus(409).phrase},
213
+ 422: {"description": HTTPStatus(422).phrase},
214
+ },
215
+ )
216
+ def copy_draft(draft_id: str, body: DraftNameRequest) -> dict[str, Any]:
217
+ with _request_errors():
218
+ return get_store().copy_draft(draft_id, body.name, expected_revision=body.revision)
219
+
220
+
221
+ @router.delete(
222
+ "/drafts/{draft_id}",
223
+ responses={
224
+ 404: {"description": HTTPStatus(404).phrase},
225
+ 409: {"description": HTTPStatus(409).phrase},
226
+ 422: {"description": HTTPStatus(422).phrase},
227
+ },
228
+ )
229
+ def delete_draft(draft_id: str, revision: Annotated[int, Query(ge=1)]) -> dict[str, Any]:
230
+ with _request_errors():
231
+ return get_store().delete_draft(draft_id, expected_revision=revision)
232
+
233
+
234
+ @router.get(
235
+ "/drafts/{draft_id}/export",
236
+ responses={
237
+ 404: {"description": HTTPStatus(404).phrase},
238
+ 409: {"description": HTTPStatus(409).phrase},
239
+ 422: {"description": HTTPStatus(422).phrase},
240
+ },
241
+ )
242
+ def export_draft(draft_id: str) -> dict[str, Any]:
243
+ """Export the saved document without reopening a business database."""
244
+ with _request_errors():
245
+ record = get_store().get_draft(draft_id)
246
+ return {
247
+ "id": record["id"],
248
+ "name": record["name"],
249
+ "revision": record["revision"],
250
+ "json": record["document"],
251
+ "yaml": yaml.safe_dump(record["document"], sort_keys=False, allow_unicode=True),
252
+ }
253
+
254
+
255
+ @router.post(
256
+ "/parse",
257
+ responses={
258
+ 404: {"description": HTTPStatus(404).phrase},
259
+ 409: {"description": HTTPStatus(409).phrase},
260
+ 422: {"description": HTTPStatus(422).phrase},
261
+ },
262
+ )
263
+ def parse(body: ParseRequest) -> dict[str, Any]:
264
+ with _request_errors(), state.connection_operation(body.conn_id) as conn:
265
+ return {"document": parse_document(conn, body.text)}
266
+
267
+
268
+ @router.post(
269
+ "/export",
270
+ responses={
271
+ 404: {"description": HTTPStatus(404).phrase},
272
+ 409: {"description": HTTPStatus(409).phrase},
273
+ 422: {"description": HTTPStatus(422).phrase},
274
+ },
275
+ )
276
+ def export(body: DocumentRequest) -> dict[str, Any]:
277
+ with _request_errors(), state.connection_operation(body.conn_id) as conn:
278
+ return export_document(conn, body.document)
279
+
280
+
281
+ def _check(body: CheckRequest, *, include_preview: bool) -> dict[str, Any]:
282
+ with _request_errors(), state.connection_operation(body.conn_id) as conn:
283
+ result = check_document(conn, body.document, body.schema_hash, count=body.count, preview=include_preview)
284
+ encoded: dict[str, Any] = jsonable_encoder(result, custom_encoder={bytes: lambda value: value.hex()})
285
+ return encoded
286
+
287
+
288
+ @router.post(
289
+ "/check",
290
+ responses={
291
+ 404: {"description": HTTPStatus(404).phrase},
292
+ 409: {"description": HTTPStatus(409).phrase},
293
+ 422: {"description": HTTPStatus(422).phrase},
294
+ },
295
+ )
296
+ def check(body: CheckRequest) -> dict[str, Any]:
297
+ return _check(body, include_preview=False)
298
+
299
+
300
+ @router.post(
301
+ "/preview",
302
+ responses={
303
+ 404: {"description": HTTPStatus(404).phrase},
304
+ 409: {"description": HTTPStatus(409).phrase},
305
+ 422: {"description": HTTPStatus(422).phrase},
306
+ },
307
+ )
308
+ def preview(body: CheckRequest) -> dict[str, Any]:
309
+ return _check(body, include_preview=True)
310
+
311
+
312
+ @router.post(
313
+ "/execution-plan",
314
+ responses={
315
+ 404: {"description": HTTPStatus(404).phrase},
316
+ 409: {"description": HTTPStatus(409).phrase},
317
+ 422: {"description": HTTPStatus(422).phrase},
318
+ },
319
+ )
320
+ def execution_plan(body: RunRequest) -> dict[str, Any]:
321
+ with _request_errors():
322
+ return plan_execution(
323
+ body.conn_id,
324
+ body.draft_id,
325
+ body.revision,
326
+ body.schema_hash,
327
+ body.config_hash,
328
+ execution=body.execution,
329
+ registry=state,
330
+ store=get_store(),
331
+ )
332
+
333
+
334
+ @router.post(
335
+ "/runs",
336
+ status_code=202,
337
+ responses={
338
+ 404: {"description": HTTPStatus(404).phrase},
339
+ 409: {"description": HTTPStatus(409).phrase},
340
+ 422: {"description": HTTPStatus(422).phrase},
341
+ },
342
+ )
343
+ def create_run(body: RunRequest) -> dict[str, Any]:
344
+ with _request_errors():
345
+ return start_run(
346
+ body.conn_id,
347
+ body.draft_id,
348
+ body.revision,
349
+ body.schema_hash,
350
+ body.config_hash,
351
+ execution=body.execution,
352
+ plan_hash=body.plan_hash,
353
+ registry=state,
354
+ store=get_store(),
355
+ )
356
+
357
+
358
+ @router.get(
359
+ "/runs",
360
+ responses={
361
+ 404: {"description": HTTPStatus(404).phrase},
362
+ 409: {"description": HTTPStatus(409).phrase},
363
+ 422: {"description": HTTPStatus(422).phrase},
364
+ },
365
+ )
366
+ def list_runs(limit: Annotated[int, Query(ge=1, le=200)] = 50) -> list[dict[str, Any]]:
367
+ with _request_errors():
368
+ return get_store().list_runs(limit=limit)
369
+
370
+
371
+ @router.get(
372
+ "/runs/{run_id}",
373
+ responses={
374
+ 404: {"description": HTTPStatus(404).phrase},
375
+ 409: {"description": HTTPStatus(409).phrase},
376
+ 422: {"description": HTTPStatus(422).phrase},
377
+ },
378
+ )
379
+ def get_run(run_id: str) -> dict[str, Any]:
380
+ with _request_errors():
381
+ return get_store().get_run(run_id)