mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 23:07:03 +01:00
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Has been cancelled
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Has been cancelled
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Has been cancelled
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Has been cancelled
Python tests / test (push) Has been cancelled
repo-contract / repo contract (push) Has been cancelled
Shell tests / shell-tests (push) Has been cancelled
Shell tests / lib-tests (push) Has been cancelled
Shell tests / arch-tests (push) Has been cancelled
Engine, fixtures and reference solutions moved to kuhyx/utils sql_drills (tag sql-drills-v0.1.0); the CLI keeps its commands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWYGW5NZL5uVpnqmNKo
118 lines
3.5 KiB
Python
118 lines
3.5 KiB
Python
"""CLI: ``python3 -m python_pkg.sql_drills start|stop|list|show|check``.
|
|
|
|
A thin wrapper over the shared ``sql_drills`` package (kuhyx/utils).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import importlib
|
|
import logging
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
import sql_drills as sd
|
|
|
|
_LOG = logging.getLogger(__name__)
|
|
|
|
|
|
def _heartbeat() -> None:
|
|
"""Tell the optional focus tracker that practice is happening.
|
|
|
|
Soft dependency: absent module -> silent no-op; a tracker error is logged
|
|
and never changes the drill's result or exit code.
|
|
"""
|
|
try:
|
|
tracker = importlib.import_module("focus_project")
|
|
except ImportError:
|
|
return
|
|
try:
|
|
tracker.record_activity("sql_drills", "heartbeat")
|
|
except OSError, ValueError:
|
|
_LOG.exception("focus tracker heartbeat failed")
|
|
|
|
|
|
def _out(line: str) -> None:
|
|
sys.stdout.write(f"{line}\n")
|
|
|
|
|
|
def _err(line: str) -> None:
|
|
sys.stderr.write(f"{line}\n")
|
|
|
|
|
|
def _build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(prog="sql_drills", description=__doc__)
|
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
sub.add_parser("start", help="start the MySQL 8.0.33 container and wait")
|
|
sub.add_parser("stop", help="remove the container")
|
|
sub.add_parser("list", help="list drills in priority order")
|
|
show = sub.add_parser("show", help="print task, schema, expected sample")
|
|
show.add_argument("drill", help="drill id or number, e.g. 03")
|
|
check = sub.add_parser("check", help="grade a .sql file against a drill")
|
|
check.add_argument("drill", help="drill id or number, e.g. 03")
|
|
check.add_argument("sql_file", type=Path)
|
|
return parser
|
|
|
|
|
|
def _render_show(drill: sd.Drill) -> str:
|
|
tail = "## Query to predict" if drill.query else "## Expected output (sample)"
|
|
body = drill.query or "\n".join("\t".join(r) for r in drill.expected_preview)
|
|
return (
|
|
f"{drill.task_md.strip()}\n\n## Schema\n\n{drill.schema_sql.strip()}\n\n"
|
|
f"{tail}\n\n{body}\n"
|
|
)
|
|
|
|
|
|
def _check(drill_id: str, sql_file: Path) -> int:
|
|
if not sql_file.is_file():
|
|
_out(f"query file not found: {sql_file}")
|
|
return 2
|
|
res = sd.run(drill_id, sql_file.read_text())
|
|
if res.error is not None and not res.match:
|
|
_out(f"SQL error: {res.error}")
|
|
return 2
|
|
if res.match:
|
|
_out(f"PASS {drill_id} ({len(res.rows)} rows)")
|
|
return 0
|
|
_out(
|
|
f"FAIL {drill_id}: expected {len(res.expected_rows)} rows, got {len(res.rows)}"
|
|
)
|
|
for row, col, want, got in res.cell_diff:
|
|
_out(f"line {row + 1} cell {col + 1}: expected {want!r}, actual {got!r}")
|
|
return 1
|
|
|
|
|
|
def _dispatch(args: argparse.Namespace) -> int:
|
|
if args.cmd == "start":
|
|
sd.ensure_running()
|
|
_out(f"{sd.CONTAINER} ready ({sd.IMAGE})")
|
|
elif args.cmd == "stop":
|
|
sd.stop()
|
|
_out(f"{sd.CONTAINER} removed")
|
|
elif args.cmd == "list":
|
|
for meta in sd.list_drills():
|
|
_out(f"{meta.id} {meta.title}")
|
|
elif args.cmd == "show":
|
|
_heartbeat()
|
|
sys.stdout.write(_render_show(sd.get_drill(args.drill)))
|
|
else:
|
|
_heartbeat()
|
|
return _check(args.drill, args.sql_file)
|
|
return 0
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
"""Entry point; returns the process exit code."""
|
|
args = _build_parser().parse_args(argv)
|
|
try:
|
|
return _dispatch(args)
|
|
except sd.UnknownDrillError as exc:
|
|
_err(exc.args[0])
|
|
except sd.EngineError as exc:
|
|
_err(f"error: {exc}")
|
|
return 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|