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.
- sqlseed_web/AGENTS.md +106 -0
- sqlseed_web/__init__.py +24 -0
- sqlseed_web/__main__.py +8 -0
- sqlseed_web/_application.py +205 -0
- sqlseed_web/ai_settings.py +290 -0
- sqlseed_web/api.py +1102 -0
- sqlseed_web/app.py +26 -0
- sqlseed_web/managed_worker.py +184 -0
- sqlseed_web/operation_errors.py +42 -0
- sqlseed_web/plugin_environment.py +231 -0
- sqlseed_web/plugin_management.py +322 -0
- sqlseed_web/plugin_process.py +131 -0
- sqlseed_web/runtime_lifecycle.py +130 -0
- sqlseed_web/runtime_session.py +97 -0
- sqlseed_web/settings_environment.py +368 -0
- sqlseed_web/sqlite_target.py +86 -0
- sqlseed_web/state.py +348 -0
- sqlseed_web/static/AGENTS.md +180 -0
- sqlseed_web/static/ai.css +57 -0
- sqlseed_web/static/configs.css +50 -0
- sqlseed_web/static/date-picker.css +230 -0
- sqlseed_web/static/disclosure.css +149 -0
- sqlseed_web/static/graph-clarity.css +103 -0
- sqlseed_web/static/index.html +29 -0
- sqlseed_web/static/js/api.js +228 -0
- sqlseed_web/static/js/app.js +97 -0
- sqlseed_web/static/js/dropdown.js +432 -0
- sqlseed_web/static/js/filepicker.js +203 -0
- sqlseed_web/static/js/genform.js +1066 -0
- sqlseed_web/static/js/labels.js +195 -0
- sqlseed_web/static/js/pages/browse.js +209 -0
- sqlseed_web/static/js/pages/configs.js +424 -0
- sqlseed_web/static/js/pages/connect.js +332 -0
- sqlseed_web/static/js/pages/heal.js +395 -0
- sqlseed_web/static/js/pages/meta.js +110 -0
- sqlseed_web/static/js/pages/runs.js +293 -0
- sqlseed_web/static/js/pages/settings.js +942 -0
- sqlseed_web/static/js/pages/wizard.js +751 -0
- sqlseed_web/static/js/pages/workbench.js +3123 -0
- sqlseed_web/static/js/tree.js +126 -0
- sqlseed_web/static/js/workbench/ai-eligibility.js +33 -0
- sqlseed_web/static/js/workbench/ai-handoff.js +31 -0
- sqlseed_web/static/js/workbench/ai-stream.js +116 -0
- sqlseed_web/static/js/workbench/ai.js +888 -0
- sqlseed_web/static/js/workbench/connection.js +508 -0
- sqlseed_web/static/js/workbench/date-picker.js +445 -0
- sqlseed_web/static/js/workbench/dependency-view.js +119 -0
- sqlseed_web/static/js/workbench/editor.js +1236 -0
- sqlseed_web/static/js/workbench/focus.js +11 -0
- sqlseed_web/static/js/workbench/graph-layout.js +332 -0
- sqlseed_web/static/js/workbench/graph.js +970 -0
- sqlseed_web/static/js/workbench/guidance.js +29 -0
- sqlseed_web/static/js/workbench/model.js +124 -0
- sqlseed_web/static/js/workbench/plugin-management.js +512 -0
- sqlseed_web/static/js/workbench/preview-scroll-layout.js +94 -0
- sqlseed_web/static/js/workbench/preview.js +572 -0
- sqlseed_web/static/js/workbench/provider-guide.js +33 -0
- sqlseed_web/static/js/workbench/recovery.js +28 -0
- sqlseed_web/static/js/workbench/scroll-lock.js +26 -0
- sqlseed_web/static/js/workbench/session.js +174 -0
- sqlseed_web/static/js/workbench/table-data.js +186 -0
- sqlseed_web/static/js/workbench/ui.js +262 -0
- sqlseed_web/static/navigation.css +92 -0
- sqlseed_web/static/preview.css +29 -0
- sqlseed_web/static/runs.css +53 -0
- sqlseed_web/static/scrollbars.css +42 -0
- sqlseed_web/static/settings.css +108 -0
- sqlseed_web/static/style.css +3382 -0
- sqlseed_web/static/table-data.css +27 -0
- sqlseed_web/static/workbench.css +509 -0
- sqlseed_web/supervised_plugins.py +173 -0
- sqlseed_web/supervisor.py +238 -0
- sqlseed_web/workbench.py +381 -0
- sqlseed_web/workbench_ai.py +887 -0
- sqlseed_web/workbench_ai_relations.py +285 -0
- sqlseed_web/workbench_ai_stream.py +172 -0
- sqlseed_web/workbench_data.py +163 -0
- sqlseed_web/workbench_execution.py +199 -0
- sqlseed_web/workbench_runtime.py +1218 -0
- sqlseed_web/workbench_schema.py +277 -0
- sqlseed_web/workbench_store.py +458 -0
- sqlseed_web/worker_control.py +192 -0
- sqlseed_web-0.2.4.dist-info/METADATA +105 -0
- sqlseed_web-0.2.4.dist-info/RECORD +87 -0
- sqlseed_web-0.2.4.dist-info/WHEEL +4 -0
- sqlseed_web-0.2.4.dist-info/entry_points.txt +2 -0
- sqlseed_web-0.2.4.dist-info/licenses/LICENSE +679 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""Read-only facts about the interpreter serving this Web application."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import importlib.util
|
|
7
|
+
import inspect
|
|
8
|
+
import platform
|
|
9
|
+
import shlex
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from functools import lru_cache
|
|
16
|
+
from importlib import metadata
|
|
17
|
+
from typing import Any, Literal
|
|
18
|
+
|
|
19
|
+
from fastapi import APIRouter, Request
|
|
20
|
+
|
|
21
|
+
_AI_CONFIG_MODULE = "sqlseed_ai.config"
|
|
22
|
+
|
|
23
|
+
router = APIRouter(prefix="/api/settings", tags=["settings"])
|
|
24
|
+
|
|
25
|
+
# The pre-0.2.4 release line lacks the workbench/runtime interfaces. Include
|
|
26
|
+
# development builds from this release line for source and wheel validation.
|
|
27
|
+
AI_INSTALL_REQUIREMENT = "sqlseed-ai>=0.2.4.dev0"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class _ComponentInfo:
|
|
32
|
+
category: Literal["application", "extension", "provider"]
|
|
33
|
+
requirement: Literal["required", "optional", "builtin"]
|
|
34
|
+
dependency_ids: tuple[str, ...]
|
|
35
|
+
description: str
|
|
36
|
+
install_requirement: str | None
|
|
37
|
+
guidance: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# These relationships describe the displayed components, not every transitive Python dependency.
|
|
41
|
+
_COMPONENTS = {
|
|
42
|
+
"core": _ComponentInfo(
|
|
43
|
+
"application",
|
|
44
|
+
"required",
|
|
45
|
+
("faker",),
|
|
46
|
+
"离线生成、约束处理与数据库写入",
|
|
47
|
+
"sqlseed",
|
|
48
|
+
"当前应用必需;Faker 是 Core 的必需依赖。",
|
|
49
|
+
),
|
|
50
|
+
"web": _ComponentInfo(
|
|
51
|
+
"application",
|
|
52
|
+
"required",
|
|
53
|
+
("core",),
|
|
54
|
+
"当前浏览器工作台",
|
|
55
|
+
"sqlseed-web",
|
|
56
|
+
"当前 Web 应用;安装时会同时安装 Core。",
|
|
57
|
+
),
|
|
58
|
+
"ai": _ComponentInfo(
|
|
59
|
+
"extension",
|
|
60
|
+
"optional",
|
|
61
|
+
("core", "cli"),
|
|
62
|
+
"根据表结构与业务说明提出规则建议",
|
|
63
|
+
"sqlseed-web[ai]",
|
|
64
|
+
"可选 AI 扩展;安装时会同时安装 Core 和 CLI。",
|
|
65
|
+
),
|
|
66
|
+
"cli": _ComponentInfo(
|
|
67
|
+
"extension",
|
|
68
|
+
"optional",
|
|
69
|
+
("core",),
|
|
70
|
+
"在终端配置、预览和生成数据",
|
|
71
|
+
"sqlseed-cli",
|
|
72
|
+
"基础 Web 功能无需 CLI;安装 AI 扩展时会一并安装。",
|
|
73
|
+
),
|
|
74
|
+
"mcp": _ComponentInfo(
|
|
75
|
+
"extension",
|
|
76
|
+
"optional",
|
|
77
|
+
("core",),
|
|
78
|
+
"供 MCP 客户端调用规则生成与填充能力",
|
|
79
|
+
"mcp-server-sqlseed",
|
|
80
|
+
"供 MCP 客户端使用;依赖 Core,无需 AI 或 CLI。",
|
|
81
|
+
),
|
|
82
|
+
"base": _ComponentInfo(
|
|
83
|
+
"provider",
|
|
84
|
+
"builtin",
|
|
85
|
+
("core",),
|
|
86
|
+
"内置基础数据与语义占位值",
|
|
87
|
+
None,
|
|
88
|
+
"随 Core 内置,无需单独安装。",
|
|
89
|
+
),
|
|
90
|
+
"faker": _ComponentInfo(
|
|
91
|
+
"provider",
|
|
92
|
+
"required",
|
|
93
|
+
(),
|
|
94
|
+
"姓名、地址等本地化测试数据",
|
|
95
|
+
"Faker>=30.0",
|
|
96
|
+
"sqlseed 的必需依赖,随 sqlseed 安装。",
|
|
97
|
+
),
|
|
98
|
+
"mimesis": _ComponentInfo(
|
|
99
|
+
"provider",
|
|
100
|
+
"optional",
|
|
101
|
+
(),
|
|
102
|
+
"另一种本地化数据实现,可按场景选择",
|
|
103
|
+
"sqlseed[mimesis]",
|
|
104
|
+
"按需安装的生成引擎;未安装时仍可使用 Base 和 Faker。",
|
|
105
|
+
),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class _Installer:
|
|
111
|
+
tool: Literal["pip", "uv"] | None
|
|
112
|
+
python_executable: str
|
|
113
|
+
tool_executable: str | None
|
|
114
|
+
shell: Literal["posix", "powershell"]
|
|
115
|
+
|
|
116
|
+
def command(self, action: Literal["install", "check"], requirement: str | None = None) -> str | None:
|
|
117
|
+
"""Build a shell-quoted command for the serving interpreter without executing it."""
|
|
118
|
+
if self.tool is None or not self.python_executable or (action == "install" and requirement is None):
|
|
119
|
+
return None
|
|
120
|
+
if self.tool == "pip":
|
|
121
|
+
arguments = [self.python_executable, "-m", "pip", action]
|
|
122
|
+
elif self.tool_executable is not None:
|
|
123
|
+
arguments = [self.tool_executable, "pip", action, "--python", self.python_executable]
|
|
124
|
+
else:
|
|
125
|
+
return None
|
|
126
|
+
if requirement is not None:
|
|
127
|
+
arguments.append(requirement)
|
|
128
|
+
if self.shell == "powershell":
|
|
129
|
+
return "& " + " ".join("'" + argument.replace("'", "''") + "'" for argument in arguments)
|
|
130
|
+
return shlex.join(arguments)
|
|
131
|
+
|
|
132
|
+
def public_info(self) -> dict[str, Any]:
|
|
133
|
+
"""Describe the detected tool and target interpreter for the settings UI."""
|
|
134
|
+
return {
|
|
135
|
+
"tool": self.tool,
|
|
136
|
+
"shell": self.shell,
|
|
137
|
+
"python_executable": self.python_executable,
|
|
138
|
+
"available": self.tool is not None,
|
|
139
|
+
"message": (
|
|
140
|
+
"命令针对当前 Web 使用的 Python 解释器;完成后重启 Web 服务。"
|
|
141
|
+
if self.tool is not None
|
|
142
|
+
else "未检测到可用的 pip 或 uv。请使用创建此 Python 环境的工具安装或修复组件,完成后重启 Web 服务。"
|
|
143
|
+
),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@lru_cache(maxsize=32)
|
|
148
|
+
def _probe_version(arguments: tuple[str, ...], time_slot: int) -> bool:
|
|
149
|
+
"""Cache bounded, read-only version probes for at most 30 seconds."""
|
|
150
|
+
try:
|
|
151
|
+
result = subprocess.run(
|
|
152
|
+
list(arguments),
|
|
153
|
+
stdin=subprocess.DEVNULL,
|
|
154
|
+
stdout=subprocess.DEVNULL,
|
|
155
|
+
stderr=subprocess.DEVNULL,
|
|
156
|
+
timeout=1,
|
|
157
|
+
check=False,
|
|
158
|
+
)
|
|
159
|
+
except (OSError, subprocess.SubprocessError):
|
|
160
|
+
return False
|
|
161
|
+
return result.returncode == 0
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _installer() -> _Installer:
|
|
165
|
+
executable = sys.executable
|
|
166
|
+
shell: Literal["posix", "powershell"] = "powershell" if sys.platform == "win32" else "posix"
|
|
167
|
+
time_slot = int(time.monotonic() // 30)
|
|
168
|
+
if executable:
|
|
169
|
+
try:
|
|
170
|
+
has_pip = importlib.util.find_spec("pip") is not None
|
|
171
|
+
except (ImportError, ValueError):
|
|
172
|
+
has_pip = False
|
|
173
|
+
if has_pip and _probe_version((executable, "-m", "pip", "--version"), time_slot):
|
|
174
|
+
return _Installer("pip", executable, None, shell)
|
|
175
|
+
uv = shutil.which("uv")
|
|
176
|
+
if uv and _probe_version((uv, "--version"), time_slot):
|
|
177
|
+
return _Installer("uv", executable, uv, shell)
|
|
178
|
+
return _Installer(None, executable, None, shell)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class ComponentMetadataError(RuntimeError):
|
|
182
|
+
"""An installed distribution has unreadable or invalid metadata."""
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _component_version(distribution: str) -> str:
|
|
186
|
+
"""Normalize metadata-provider failures before capability decisions."""
|
|
187
|
+
try:
|
|
188
|
+
version = metadata.version(distribution)
|
|
189
|
+
if not isinstance(version, str) or not version.strip():
|
|
190
|
+
raise ValueError("Distribution version is missing")
|
|
191
|
+
except metadata.PackageNotFoundError:
|
|
192
|
+
raise
|
|
193
|
+
except Exception as exc:
|
|
194
|
+
raise ComponentMetadataError(f"Unable to read distribution metadata: {distribution}") from exc
|
|
195
|
+
return version
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _load_component(distribution: str, module: str) -> None:
|
|
199
|
+
"""Optional module initialization has the same ImportError contract as missing code."""
|
|
200
|
+
try:
|
|
201
|
+
importlib.import_module(module)
|
|
202
|
+
if distribution == "sqlseed-ai":
|
|
203
|
+
_require_ai_contract()
|
|
204
|
+
except Exception as exc:
|
|
205
|
+
raise ImportError(f"Unable to initialize component: {distribution}") from exc
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def ai_import_failure() -> dict[str, Any]:
|
|
209
|
+
"""Describe a caught AI import failure without exposing dependency exception text."""
|
|
210
|
+
status = "import_error"
|
|
211
|
+
try:
|
|
212
|
+
_component_version("sqlseed-ai")
|
|
213
|
+
except metadata.PackageNotFoundError:
|
|
214
|
+
status = "not_installed"
|
|
215
|
+
except ComponentMetadataError:
|
|
216
|
+
status = "import_error"
|
|
217
|
+
installer = _installer()
|
|
218
|
+
message = (
|
|
219
|
+
"尚未安装 AI 插件,AI 服务检测与规则分析不可用。请在设置的插件页安装 AI;手动配置与生成仍可使用。"
|
|
220
|
+
if status == "not_installed"
|
|
221
|
+
else "AI 插件加载异常或版本不兼容,AI 服务检测与规则分析不可用。请在插件页查看修复指引,确认有可安装的兼容版本后再卸载重装。"
|
|
222
|
+
)
|
|
223
|
+
return {
|
|
224
|
+
"available": False,
|
|
225
|
+
"code": "ai_unavailable",
|
|
226
|
+
"component_id": "ai",
|
|
227
|
+
"recovery_action": "install" if status == "not_installed" else "repair",
|
|
228
|
+
"availability_status": status,
|
|
229
|
+
"message": message,
|
|
230
|
+
"reason": message,
|
|
231
|
+
"installer": installer.public_info(),
|
|
232
|
+
"install_command": installer.command("install", "sqlseed-web[ai]"),
|
|
233
|
+
"repair_command": installer.command("check"),
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _require_ai_contract() -> None:
|
|
238
|
+
"""Check interfaces without creating clients, reading settings or calling a model."""
|
|
239
|
+
config = importlib.import_module(_AI_CONFIG_MODULE).AIConfig
|
|
240
|
+
if not {"tool_calling_protocol", "log_llm_interactions"}.issubset(config.model_fields):
|
|
241
|
+
raise ImportError("AI configuration interface is incompatible")
|
|
242
|
+
analyzer = importlib.import_module("sqlseed_ai.analyzer").SchemaAnalyzer
|
|
243
|
+
inspect.signature(analyzer.call_llm).bind(None, [], stage="workbench-suggestions")
|
|
244
|
+
runtime = importlib.import_module("sqlseed_ai.runtime")
|
|
245
|
+
if not all(
|
|
246
|
+
callable(getattr(runtime, factory, None))
|
|
247
|
+
for factory in ("build_ai_config", "build_llm_client", "build_heal_orchestrator")
|
|
248
|
+
):
|
|
249
|
+
raise ImportError("AI runtime interface is incompatible")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def package_availability(distribution: str, module: str, *, metadata_only: bool = False) -> dict[str, Any]:
|
|
253
|
+
"""Web capability requires installed metadata as well as importable code.
|
|
254
|
+
|
|
255
|
+
An old sys.modules cache or editable source path inherited by spawn cannot
|
|
256
|
+
re-enable a distribution that the user has explicitly uninstalled.
|
|
257
|
+
"""
|
|
258
|
+
installed = metadata_valid = True
|
|
259
|
+
try:
|
|
260
|
+
version = _component_version(distribution)
|
|
261
|
+
except metadata.PackageNotFoundError:
|
|
262
|
+
version, installed, metadata_valid = None, False, False
|
|
263
|
+
except ComponentMetadataError:
|
|
264
|
+
version, metadata_valid = None, False
|
|
265
|
+
available = False
|
|
266
|
+
if metadata_valid and not metadata_only:
|
|
267
|
+
try:
|
|
268
|
+
_load_component(distribution, module)
|
|
269
|
+
available = True
|
|
270
|
+
except ImportError:
|
|
271
|
+
available = False
|
|
272
|
+
if not installed:
|
|
273
|
+
status = "not_installed"
|
|
274
|
+
elif metadata_only:
|
|
275
|
+
status = "installed"
|
|
276
|
+
elif available:
|
|
277
|
+
status = "available"
|
|
278
|
+
else:
|
|
279
|
+
status = "import_error"
|
|
280
|
+
messages = {
|
|
281
|
+
"installed": "已安装;服务恢复后验证运行状态",
|
|
282
|
+
"available": "当前 Python 环境已安装且可导入",
|
|
283
|
+
"import_error": "已安装,但加载失败或版本不兼容;请在插件页查看修复指引",
|
|
284
|
+
"not_installed": "当前 Python 环境未安装;请在插件页安装后使用",
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
"version": version,
|
|
288
|
+
"installed": installed,
|
|
289
|
+
"available": available,
|
|
290
|
+
"status": status,
|
|
291
|
+
"message": messages[status],
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def require_ai_available() -> None:
|
|
296
|
+
if not package_availability("sqlseed-ai", _AI_CONFIG_MODULE)["available"]:
|
|
297
|
+
raise ImportError("AI component is unavailable")
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def provider_availability() -> dict[str, dict[str, Any]]:
|
|
301
|
+
return {
|
|
302
|
+
"base": package_availability("sqlseed", "sqlseed.generators.base_provider"),
|
|
303
|
+
"faker": package_availability("Faker", "faker"),
|
|
304
|
+
"mimesis": package_availability("mimesis", "mimesis"),
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _package(
|
|
309
|
+
identifier: str,
|
|
310
|
+
name: str,
|
|
311
|
+
distribution: str,
|
|
312
|
+
module: str,
|
|
313
|
+
installer: _Installer,
|
|
314
|
+
*,
|
|
315
|
+
metadata_only: bool = False,
|
|
316
|
+
) -> dict[str, Any]:
|
|
317
|
+
info = _COMPONENTS[identifier]
|
|
318
|
+
availability = package_availability(distribution, module, metadata_only=metadata_only)
|
|
319
|
+
status = availability["status"]
|
|
320
|
+
guidance = info.guidance
|
|
321
|
+
if status == "import_error":
|
|
322
|
+
guidance += " 加载异常,请检查运行 Web 的 Python 环境依赖,修复后重启服务。"
|
|
323
|
+
elif status == "not_installed":
|
|
324
|
+
if info.requirement in {"required", "builtin"}:
|
|
325
|
+
guidance = "必需依赖缺失,请修复运行 Web 的 Python 环境。" + guidance
|
|
326
|
+
else:
|
|
327
|
+
guidance += " 如需使用,请在运行 Web 的 Python 环境中安装,然后重启服务。"
|
|
328
|
+
return {
|
|
329
|
+
"id": identifier,
|
|
330
|
+
"name": name,
|
|
331
|
+
"distribution": distribution,
|
|
332
|
+
**availability,
|
|
333
|
+
"category": info.category,
|
|
334
|
+
"requirement": info.requirement,
|
|
335
|
+
"dependency_ids": list(info.dependency_ids),
|
|
336
|
+
"description": info.description,
|
|
337
|
+
"install_command": installer.command("install", info.install_requirement),
|
|
338
|
+
"repair_command": installer.command("check"),
|
|
339
|
+
"guidance": guidance,
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@router.get("/environment")
|
|
344
|
+
def environment(request: Request) -> dict[str, Any]:
|
|
345
|
+
"""Inspect installed metadata and imports; never query a package registry."""
|
|
346
|
+
installer = _installer()
|
|
347
|
+
metadata_only = request.app.state.metadata_only
|
|
348
|
+
return {
|
|
349
|
+
"inspection": "metadata_only" if metadata_only else "metadata_and_imports",
|
|
350
|
+
"installer": installer.public_info(),
|
|
351
|
+
"python": {"version": platform.python_version(), "implementation": platform.python_implementation()},
|
|
352
|
+
"packages": [
|
|
353
|
+
_package("core", "Core", "sqlseed", "sqlseed", installer, metadata_only=metadata_only),
|
|
354
|
+
_package("web", "Web", "sqlseed-web", "sqlseed_web.app", installer, metadata_only=metadata_only),
|
|
355
|
+
_package("ai", "AI", "sqlseed-ai", _AI_CONFIG_MODULE, installer, metadata_only=metadata_only),
|
|
356
|
+
_package("cli", "CLI", "sqlseed-cli", "sqlseed_cli.main", installer, metadata_only=metadata_only),
|
|
357
|
+
_package(
|
|
358
|
+
"mcp", "MCP", "mcp-server-sqlseed", "mcp_server_sqlseed.server", installer, metadata_only=metadata_only
|
|
359
|
+
),
|
|
360
|
+
],
|
|
361
|
+
"providers": [
|
|
362
|
+
_package(
|
|
363
|
+
"base", "Base", "sqlseed", "sqlseed.generators.base_provider", installer, metadata_only=metadata_only
|
|
364
|
+
),
|
|
365
|
+
_package("faker", "Faker", "Faker", "faker", installer, metadata_only=metadata_only),
|
|
366
|
+
_package("mimesis", "Mimesis", "mimesis", "mimesis", installer, metadata_only=metadata_only),
|
|
367
|
+
],
|
|
368
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Shared SQLite target identity using the adapter's actual DBAPI URI semantics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Literal
|
|
8
|
+
from urllib.parse import parse_qs, quote, unquote, urlsplit
|
|
9
|
+
from urllib.request import url2pathname
|
|
10
|
+
|
|
11
|
+
from sqlalchemy.dialects.sqlite.pysqlite import SQLiteDialect_pysqlite
|
|
12
|
+
from sqlalchemy.engine import Dialect, make_url
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class SQLiteTarget:
|
|
17
|
+
"""A file, a named shared memory database, or one private connection."""
|
|
18
|
+
|
|
19
|
+
kind: Literal["sqlite", "sqlite-memory", "sqlite-shared-memory"]
|
|
20
|
+
value: str
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def key(self) -> str:
|
|
24
|
+
"""Retain existing admission keys while sharing their interpretation."""
|
|
25
|
+
return self.value if self.kind == "sqlite" else f"{self.kind}:{self.value}"
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def label(self) -> str:
|
|
29
|
+
if self.kind == "sqlite":
|
|
30
|
+
return self.value
|
|
31
|
+
if self.kind == "sqlite-shared-memory":
|
|
32
|
+
return f"SQLite 共享内存:{self.value}"
|
|
33
|
+
return "SQLite :memory:"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _sqlite_uri_parts(filename: str) -> tuple[str, dict[str, list[str]]]:
|
|
37
|
+
"""Decode URI components only after rejecting SQLite/Python parsing ambiguities."""
|
|
38
|
+
# urlsplit strips these raw controls, whereas SQLite retains them.
|
|
39
|
+
# Require percent encoding rather than identifying a different file.
|
|
40
|
+
if any(control in filename for control in "\t\r\n"):
|
|
41
|
+
raise ValueError("SQLite URI 含原始控制字符;请对文件名中的 TAB、CR、LF 使用百分号编码")
|
|
42
|
+
uri = urlsplit(filename)
|
|
43
|
+
try:
|
|
44
|
+
filename = unquote(uri.path, errors="strict")
|
|
45
|
+
query = parse_qs(uri.query, keep_blank_values=True, errors="strict")
|
|
46
|
+
except UnicodeDecodeError as exc:
|
|
47
|
+
# Replacement decoding would collapse distinct byte filenames.
|
|
48
|
+
raise ValueError("SQLite URI 使用了无效 UTF-8 编码;请使用有效 UTF-8 文件名和参数") from exc
|
|
49
|
+
# SQLite truncates URI strings at decoded NUL; Python does not. An
|
|
50
|
+
# apparent file target can otherwise become a memory database or VFS.
|
|
51
|
+
if "\x00" in filename or any("\x00" in text for key, values in query.items() for text in (key, *values)):
|
|
52
|
+
raise ValueError("SQLite URI 不支持 NUL 字符;请检查路径和查询参数中的百分号编码")
|
|
53
|
+
if "vfs" in query:
|
|
54
|
+
raise ValueError("Web 暂不支持 SQLite 自定义 VFS;请使用默认 VFS 的文件或内存连接")
|
|
55
|
+
return filename, query
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def sqlite_target(target: str, conn_id: str) -> SQLiteTarget | None:
|
|
59
|
+
"""Resolve only SQLite; do not confuse URI options with ordinary filenames.
|
|
60
|
+
|
|
61
|
+
The core adapter wraps non-URL strings in ``sqlite:///`` too. SQLAlchemy
|
|
62
|
+
separates sqlite3 options (uri, timeout, etc.) from SQLite filename options;
|
|
63
|
+
interpreting ``url.database`` alone therefore describes the wrong target.
|
|
64
|
+
Memory names are literal SQLite names, not filesystem paths. Private memory
|
|
65
|
+
and temporary databases must remain scoped to the registered connection.
|
|
66
|
+
"""
|
|
67
|
+
url = make_url(target if "://" in target else f"sqlite:///{target}")
|
|
68
|
+
if url.get_backend_name() != "sqlite":
|
|
69
|
+
return None
|
|
70
|
+
dialect: Dialect = SQLiteDialect_pysqlite()
|
|
71
|
+
args, options = dialect.create_connect_args(url)
|
|
72
|
+
if (filename := str(args[0])) in {"", ":memory:"}:
|
|
73
|
+
return SQLiteTarget("sqlite-memory", conn_id)
|
|
74
|
+
if options.get("uri") and filename.startswith("file:"):
|
|
75
|
+
filename, query = _sqlite_uri_parts(filename)
|
|
76
|
+
if not filename:
|
|
77
|
+
return SQLiteTarget("sqlite-memory", conn_id)
|
|
78
|
+
if filename == ":memory:" or query.get("mode") == ["memory"]:
|
|
79
|
+
if query.get("cache") == ["shared"]:
|
|
80
|
+
return SQLiteTarget("sqlite-shared-memory", filename)
|
|
81
|
+
return SQLiteTarget("sqlite-memory", conn_id)
|
|
82
|
+
# Convert URI drive syntax (/C:/...) to a native Windows filename.
|
|
83
|
+
# Re-encode the validated text so literal percent sequences are not
|
|
84
|
+
# decoded twice by url2pathname; keep drive colons visible to it.
|
|
85
|
+
filename = url2pathname(quote(filename, safe="/:"))
|
|
86
|
+
return SQLiteTarget("sqlite", str(Path(filename).resolve()))
|