Skip to content

Commit 355aced

Browse files
committed
fix: resolve CodeFlow/SonarCloud quality issues
- signal.alarm: use vars(signal) dict lookup instead of type: ignore - server.py: extract _build_ai_config() to eliminate code duplication - analyzer.py: extract _extract_tool_call_result() to reduce complexity - config.py: add explicit AIBackend type annotation - _model_selector.py: remove unnecessary pass statement - _create_demo_db.py: fix redefined-variable-type - quickstart.py: use with for urlopen, narrow exception types, reduce complexity, fix f-string
1 parent 93d1a76 commit 355aced

7 files changed

Lines changed: 126 additions & 100 deletions

File tree

plugins/mcp-server-sqlseed/src/mcp_server_sqlseed/server.py

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,33 @@
2525
_MAX_YAML_CONFIG_SIZE = 256 * 1024
2626

2727

28+
def _build_ai_config(
29+
db_path: str,
30+
model: str | None,
31+
backend: str | None,
32+
) -> tuple[AIConfig, dict[str, Any] | None]:
33+
"""Build an AIConfig with Gemma 4 defaults, validating db_path and backend.
34+
35+
Returns (config, None) on success, or (config, error_dict) on failure.
36+
"""
37+
if not _AI_AVAILABLE:
38+
return AIConfig(), {"error": "sqlseed-ai plugin not installed. Install with: pip install sqlseed-ai"}
39+
40+
db_path = _validate_db_path(db_path)
41+
42+
ai_config = AIConfig.from_env()
43+
if model:
44+
ai_config.model = model
45+
if backend:
46+
try:
47+
ai_config.backend = AIBackend(backend)
48+
except ValueError:
49+
return ai_config, {"error": f"Invalid backend: {backend}. Use: google_ai_studio, ollama, openai_compat"}
50+
ai_config.resolve_model()
51+
52+
return ai_config, None
53+
54+
2855
def _validate_db_path(db_path: str) -> str:
2956
resolved = Path(db_path).resolve()
3057
valid_exts = (".db", ".sqlite", ".sqlite3")
@@ -206,25 +233,14 @@ def sqlseed_gemma4_analyze(
206233
Supported backends: google_ai_studio (default), ollama, openai_compat.
207234
Supported models: gemma-4-26b-it (default), gemma-4-31b-it, gemma-4-4b-it, gemma-4-2b-it.
208235
"""
209-
if not _AI_AVAILABLE:
210-
return {"error": "sqlseed-ai plugin not installed. Install with: pip install sqlseed-ai"}
236+
ai_config, err = _build_ai_config(db_path, model, backend)
237+
if err is not None:
238+
return err
211239

212-
db_path = _validate_db_path(db_path)
213240
with DataOrchestrator(db_path) as orch:
214241
_validate_table_name(table_name, orch.get_table_names())
215242
schema_ctx = orch.get_schema_context(table_name)
216243

217-
# Build AIConfig with Gemma 4 defaults
218-
ai_config = AIConfig.from_env()
219-
if model:
220-
ai_config.model = model
221-
if backend:
222-
try:
223-
ai_config.backend = AIBackend(backend)
224-
except ValueError:
225-
return {"error": f"Invalid backend: {backend}. Use: google_ai_studio, ollama, openai_compat"}
226-
ai_config.resolve_model()
227-
228244
analyzer = SchemaAnalyzer(config=ai_config)
229245
result = analyzer.analyze_table_from_ctx(**_serialize_schema_context(schema_ctx))
230246

@@ -259,22 +275,11 @@ def sqlseed_gemma4_agent_fill(
259275
The agent uses Gemma 4's tool use to understand schema semantics and
260276
produce appropriate data generation rules automatically.
261277
"""
262-
if not _AI_AVAILABLE:
263-
return {"error": "sqlseed-ai plugin not installed. Install with: pip install sqlseed-ai"}
264-
265-
db_path = _validate_db_path(db_path)
278+
ai_config, err = _build_ai_config(db_path, model, backend)
279+
if err is not None:
280+
return err
266281

267282
# Step 1: AI analysis with self-correction
268-
ai_config = AIConfig.from_env()
269-
if model:
270-
ai_config.model = model
271-
if backend:
272-
try:
273-
ai_config.backend = AIBackend(backend)
274-
except ValueError:
275-
return {"error": f"Invalid backend: {backend}. Use: google_ai_studio, ollama, openai_compat"}
276-
ai_config.resolve_model()
277-
278283
analyzer = SchemaAnalyzer(config=ai_config)
279284
refiner = AiConfigRefiner(analyzer, db_path)
280285

plugins/sqlseed-ai/src/sqlseed_ai/_model_selector.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,3 @@ def select_next_free_model(failed_model: str) -> str | None:
9393

9494
def clear_cache() -> None:
9595
"""Legacy compat: no-op, Gemma models don't need cache."""
96-
pass

plugins/sqlseed-ai/src/sqlseed_ai/analyzer.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,26 @@ def _call_llm_once(self, messages: list[dict[str, str]]) -> dict[str, Any]:
343343
return {}
344344
return self._parse_json_response(content)
345345

346+
def _extract_tool_call_result(self, choice: Any) -> dict[str, Any] | None:
347+
"""Extract the analyze_schema result from a tool call choice."""
348+
if not choice.message.tool_calls:
349+
return None
350+
for tool_call in choice.message.tool_calls:
351+
if tool_call.function.name == "analyze_schema":
352+
args_str = tool_call.function.arguments
353+
if args_str:
354+
try:
355+
result: dict[str, Any] | None = json.loads(args_str)
356+
logger.info(
357+
"Gemma 4 native function calling succeeded",
358+
tool="analyze_schema",
359+
model=self._config.model if self._config else "unknown",
360+
)
361+
return result
362+
except json.JSONDecodeError:
363+
logger.debug("Failed to parse tool call arguments", args=args_str[:200])
364+
return None
365+
346366
def _try_tool_calling(self, client: Any, kwargs: dict[str, Any]) -> dict[str, Any] | None:
347367
"""Attempt Gemma 4 native function calling.
348368
@@ -365,22 +385,9 @@ def _try_tool_calling(self, client: Any, kwargs: dict[str, Any]) -> dict[str, An
365385

366386
choice = response.choices[0]
367387

368-
# Check if the model made a tool call
369-
if choice.message.tool_calls:
370-
for tool_call in choice.message.tool_calls:
371-
if tool_call.function.name == "analyze_schema":
372-
args_str = tool_call.function.arguments
373-
if args_str:
374-
try:
375-
result: dict[str, Any] | None = json.loads(args_str)
376-
logger.info(
377-
"Gemma 4 native function calling succeeded",
378-
tool="analyze_schema",
379-
model=self._config.model if self._config else "unknown",
380-
)
381-
return result
382-
except json.JSONDecodeError:
383-
logger.debug("Failed to parse tool call arguments", args=args_str[:200])
388+
result = self._extract_tool_call_result(choice)
389+
if result is not None:
390+
return result
384391

385392
# If no tool call was made but we have text content, parse it
386393
if choice.message.content:

plugins/sqlseed-ai/src/sqlseed_ai/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def from_env(cls) -> AIConfig:
7070
timeout = float(timeout_str) if timeout_str else 60.0
7171

7272
# Resolve backend
73-
backend = AIBackend.GOOGLE_AI_STUDIO # default
73+
backend: AIBackend = AIBackend.GOOGLE_AI_STUDIO
7474
if backend_str == "lm_studio":
7575
backend = AIBackend.LM_STUDIO
7676
elif backend_str == "ollama":

scripts/_create_demo_db.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Create demo database with 3 tables for quickstart script."""
2+
23
from __future__ import annotations
34

45
import sqlite3
@@ -7,8 +8,8 @@
78

89

910
def main() -> None:
10-
db_path = sys.argv[1] if len(sys.argv) > 1 else "quickstart_demo.db"
11-
db_path = Path(db_path)
11+
db_path_str = sys.argv[1] if len(sys.argv) > 1 else "quickstart_demo.db"
12+
db_path = Path(db_path_str)
1213
if db_path.exists():
1314
db_path.unlink()
1415

scripts/quickstart.py

Lines changed: 62 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
python scripts/quickstart.py [--backend lm_studio|ollama|google] [--model MODEL_NAME]
66
python scripts/quickstart.py --skip-install # Skip pip install, use current env
77
"""
8+
89
from __future__ import annotations
910

1011
import argparse
1112
import os
1213
import subprocess
1314
import sys
15+
import urllib.error
1416
import urllib.request
1517
from pathlib import Path
1618

@@ -27,18 +29,18 @@ def run(cmd: list[str], check: bool = True) -> subprocess.CompletedProcess[str]:
2729
def check_lm_studio() -> bool:
2830
"""Check if LM Studio is running."""
2931
try:
30-
r = urllib.request.urlopen("http://127.0.0.1:1234/v1/models", timeout=3)
31-
return r.status == 200
32-
except Exception:
32+
with urllib.request.urlopen("http://127.0.0.1:1234/v1/models", timeout=3) as r:
33+
return r.status == 200
34+
except (urllib.error.URLError, OSError):
3335
return False
3436

3537

3638
def check_ollama() -> bool:
3739
"""Check if Ollama is running."""
3840
try:
39-
r = urllib.request.urlopen("http://localhost:11434/api/tags", timeout=3)
40-
return r.status == 200
41-
except Exception:
41+
with urllib.request.urlopen("http://localhost:11434/api/tags", timeout=3) as r:
42+
return r.status == 200
43+
except (urllib.error.URLError, OSError):
4244
return False
4345

4446

@@ -47,6 +49,53 @@ def sqlseed_cmd(python: str) -> list[str]:
4749
return [python, "-c", "from sqlseed.cli.main import cli; cli()", "--"]
4850

4951

52+
def _fill_data(python: str) -> None:
53+
"""Fill the demo database with sample data using sqlseed CLI."""
54+
cmd = sqlseed_cmd(python)
55+
for table, count in [("users", 500), ("projects", 200), ("orders", 1000)]:
56+
run([*cmd, "fill", str(DB_PATH), "-t", table, "-n", str(count)])
57+
58+
print()
59+
print(" Data fill complete!")
60+
61+
62+
def _run_ai_analysis_step(args: argparse.Namespace, python: str) -> None:
63+
"""Run the Gemma 4 AI schema analysis step."""
64+
print("[5/5] Gemma 4 AI Schema Analysis...")
65+
print(f" Backend: {args.backend}")
66+
print(f" Model: {args.model}")
67+
print()
68+
69+
os.environ["SQLSEED_AI_BACKEND"] = args.backend
70+
os.environ["SQLSEED_AI_MODEL"] = args.model
71+
72+
ai_ok = False
73+
if args.backend == "lm_studio":
74+
if check_lm_studio():
75+
ai_ok = True
76+
else:
77+
print(" Skipped: LM Studio is not running")
78+
print(" Please start LM Studio and load a Gemma 4 model")
79+
print(" Download: https://lmstudio.ai/")
80+
elif args.backend == "ollama":
81+
if check_ollama():
82+
ai_ok = True
83+
else:
84+
print(" Skipped: Ollama is not running")
85+
print(" Example: ollama pull gemma4:4b")
86+
elif args.backend == "google":
87+
if os.environ.get("GOOGLE_API_KEY"):
88+
ai_ok = True
89+
else:
90+
print(" Skipped: GOOGLE_API_KEY not set")
91+
print(" Example: export GOOGLE_API_KEY=your-key")
92+
93+
if ai_ok:
94+
output_yaml = str(PROJECT_ROOT / "projects_config.yaml")
95+
cmd = sqlseed_cmd(python)
96+
run([*cmd, "ai-suggest", str(DB_PATH), "-t", "projects", "-o", output_yaml, "--timeout", "300"])
97+
98+
5099
def main() -> None:
51100
parser = argparse.ArgumentParser(description="GemmaSQLSeed one-click setup")
52101
parser.add_argument(
@@ -107,48 +156,10 @@ def main() -> None:
107156

108157
# ── Step 4: Fill data ────────────────────────────────────────────
109158
print("[4/5] Filling data (zero-config)...")
110-
111-
cmd = sqlseed_cmd(python)
112-
for table, count in [("users", 500), ("projects", 200), ("orders", 1000)]:
113-
run(cmd + ["fill", str(DB_PATH), "-t", table, "-n", str(count)])
114-
115-
print()
116-
print(" Data fill complete!")
159+
_fill_data(python)
117160

118161
# ── Step 5: Gemma 4 AI analysis ──────────────────────────────────
119-
print("[5/5] Gemma 4 AI Schema Analysis...")
120-
print(f" Backend: {args.backend}")
121-
print(f" Model: {args.model}")
122-
print()
123-
124-
os.environ["SQLSEED_AI_BACKEND"] = args.backend
125-
os.environ["SQLSEED_AI_MODEL"] = args.model
126-
127-
ai_ok = False
128-
if args.backend == "lm_studio":
129-
if check_lm_studio():
130-
ai_ok = True
131-
else:
132-
print(" Skipped: LM Studio is not running")
133-
print(" Please start LM Studio and load a Gemma 4 model")
134-
print(" Download: https://lmstudio.ai/")
135-
elif args.backend == "ollama":
136-
if check_ollama():
137-
ai_ok = True
138-
else:
139-
print(" Skipped: Ollama is not running")
140-
print(" Example: ollama pull gemma4:4b")
141-
elif args.backend == "google":
142-
if os.environ.get("GOOGLE_API_KEY"):
143-
ai_ok = True
144-
else:
145-
print(" Skipped: GOOGLE_API_KEY not set")
146-
print(" Example: export GOOGLE_API_KEY=your-key")
147-
148-
if ai_ok:
149-
output_yaml = str(PROJECT_ROOT / "projects_config.yaml")
150-
cmd = sqlseed_cmd(python)
151-
run(cmd + ["ai-suggest", str(DB_PATH), "-t", "projects", "-o", output_yaml, "--timeout", "300"])
162+
_run_ai_analysis_step(args, python)
152163

153164
# ── Done ─────────────────────────────────────────────────────────
154165
print()
@@ -157,10 +168,11 @@ def main() -> None:
157168
print("=" * 50)
158169
print()
159170
print(f" Database: {DB_PATH}")
160-
print(f" Preview: python -c \"from sqlseed.cli.main import cli; cli()\" -- preview {DB_PATH} -t users -n 5")
161-
print(f" Inspect: python -c \"from sqlseed.cli.main import cli; cli()\" -- inspect {DB_PATH} --show-mapping")
162-
print(f" AI Suggest: python -c \"from sqlseed.cli.main import cli; cli()\" -- ai-suggest {DB_PATH} -t users -o config.yaml")
163-
print(f" MCP Server: mcp-server-sqlseed")
171+
cli_call = 'python -c "from sqlseed.cli.main import cli; cli()" --'
172+
print(f" Preview: {cli_call} preview {DB_PATH} -t users -n 5")
173+
print(f" Inspect: {cli_call} inspect {DB_PATH} --show-mapping")
174+
print(f" AI Suggest: {cli_call} ai-suggest {DB_PATH} -t users -o config.yaml")
175+
print(" MCP Server: mcp-server-sqlseed")
164176
print()
165177

166178

src/sqlseed/cli/main.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -495,13 +495,15 @@ def ai_suggest(
495495
old_handler: Any = None
496496
if hasattr(signal, "SIGALRM"):
497497
old_handler = signal.signal(signal.SIGALRM, lambda _s, _f: _sigalrm_handler(total_timeout))
498-
signal.alarm(int(total_timeout)) # type: ignore[attr-defined,unused-ignore]
498+
_alarm_fn = vars(signal)["alarm"]
499+
_alarm_fn(int(total_timeout))
499500

500501
try:
501502
result = _run_ai_analysis(analyzer, db_path, table, verify, max_retries, no_cache)
502503
finally:
503504
if hasattr(signal, "SIGALRM"):
504-
signal.alarm(0) # type: ignore[attr-defined,unused-ignore]
505+
_alarm_fn = vars(signal)["alarm"]
506+
_alarm_fn(0)
505507
if old_handler is not None:
506508
signal.signal(signal.SIGALRM, old_handler)
507509

0 commit comments

Comments
 (0)