mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 22:27:01 +01:00
cli.py 345 -> 222 and _verifier.py 347 -> 228, via _cli_checks.py and _verifier_support.py. The cli seam cut straight through code covered by its per-file-ignores. Rather than copy S607 and PERF203 onto a second entry, both are fixed: systemctl is resolved with shutil.which, and the readiness poll no longer carries a try/except in its body now that the probe is its own function. Both ignores are deleted from meta/pyproject.toml and ruff passes without them - the repo has two fewer suppressions than before. The which() fix adds a systemctl-is-None branch, so the pass count goes 216 -> 217: one new test for it, no existing test touched. Coverage is back at 100% by covering the branch, not excluding it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
176 lines
5.4 KiB
Python
176 lines
5.4 KiB
Python
"""Ollama readiness and plan-freshness checks for the CLI commands.
|
|
|
|
Split out of :mod:`python_pkg.code_tutor.cli` to keep it under the 250-line
|
|
cap. ``cli`` imports all three entry points back, because the command tests
|
|
patch them on ``cli``; ``requests``, ``subprocess`` and ``time`` travel here
|
|
with the code that calls them, and the Ollama tests patch them on this module.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import time
|
|
from typing import TYPE_CHECKING, cast
|
|
|
|
import requests
|
|
|
|
from python_pkg.code_tutor._analyzer import extract_items
|
|
from python_pkg.code_tutor._deps import codebase_fingerprint
|
|
from python_pkg.code_tutor._plan_builder import build_plan
|
|
from python_pkg.code_tutor._progress import save_plan
|
|
|
|
if TYPE_CHECKING:
|
|
from rich.console import Console
|
|
|
|
from python_pkg.code_tutor._progress import PlanData
|
|
|
|
_OLLAMA_API = "http://localhost:11434/api/tags"
|
|
_OLLAMA_START_TIMEOUT = 30
|
|
|
|
|
|
def _ollama_reachable() -> bool:
|
|
"""Return True when the Ollama API answers.
|
|
|
|
Keeping the probe in its own function is what lets the readiness poll below
|
|
stay a plain loop with no try/except in its body.
|
|
"""
|
|
try:
|
|
requests.get(_OLLAMA_API, timeout=2)
|
|
except requests.exceptions.RequestException:
|
|
return False
|
|
return True
|
|
|
|
|
|
def _start_ollama_service(console: Console) -> bool:
|
|
"""Run ``systemctl start ollama``. Returns False when the call fails.
|
|
|
|
Args:
|
|
console: Rich console for status messages.
|
|
"""
|
|
systemctl = shutil.which("systemctl")
|
|
if systemctl is None:
|
|
console.print("[red]systemctl not found.[/red]")
|
|
return False
|
|
try:
|
|
subprocess.run(
|
|
[systemctl, "start", "ollama"],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
except subprocess.CalledProcessError as exc:
|
|
console.print(
|
|
f"[red]systemctl start ollama failed: {exc.stderr.decode().strip()}[/red]"
|
|
)
|
|
return False
|
|
return True
|
|
|
|
|
|
def _ensure_ollama_running(console: Console) -> bool:
|
|
"""Start the Ollama systemd service if it is not already reachable.
|
|
|
|
Tries ``systemctl start ollama`` and polls the API for up to
|
|
``_OLLAMA_START_TIMEOUT`` seconds.
|
|
|
|
Args:
|
|
console: Rich console for status messages.
|
|
|
|
Returns:
|
|
True when Ollama is reachable, False after timeout.
|
|
"""
|
|
if _ollama_reachable():
|
|
return True
|
|
|
|
console.print("[yellow]Ollama not running -- starting via systemctl...[/yellow]")
|
|
if not _start_ollama_service(console):
|
|
return False
|
|
|
|
deadline = time.monotonic() + _OLLAMA_START_TIMEOUT
|
|
while time.monotonic() < deadline:
|
|
if _ollama_reachable():
|
|
console.print("[green]Ollama is up.[/green]")
|
|
return True
|
|
time.sleep(1)
|
|
|
|
console.print("[red]Ollama did not become ready in time.[/red]")
|
|
return False
|
|
|
|
|
|
def _ensure_fresh_plan(codebase: Path, plan: PlanData, console: Console) -> PlanData:
|
|
"""Rebuild *plan* if the codebase source files have changed since it was created.
|
|
|
|
Compares the plan's stored fingerprint against the current one. When they
|
|
differ, re-runs the full analyze pipeline, saves the new plan to disk, and
|
|
returns it. Plans without a stored fingerprint (created before this feature)
|
|
are returned unchanged.
|
|
|
|
Args:
|
|
codebase: Root directory of the codebase.
|
|
plan: Plan dict loaded from ``plan.json``.
|
|
console: Rich console for status messages.
|
|
|
|
Returns:
|
|
The original plan when up-to-date, or a freshly built ``PlanData`` when
|
|
the fingerprint has changed.
|
|
"""
|
|
saved = plan.get("source_fingerprint", "")
|
|
if not saved:
|
|
return plan
|
|
current = codebase_fingerprint(codebase)
|
|
if current == saved:
|
|
return plan
|
|
|
|
console.print(
|
|
"[yellow]Plan is stale -- source files changed. Rebuilding...[/yellow]"
|
|
)
|
|
items = extract_items(codebase)
|
|
if not items:
|
|
console.print("[red]No extractable items found -- keeping existing plan.[/red]")
|
|
return plan
|
|
|
|
new_plan = build_plan(codebase, items)
|
|
save_plan(codebase, new_plan)
|
|
n = new_plan["total_items"]
|
|
console.print(f"[green]Plan rebuilt: {n} items.[/green]")
|
|
return cast("PlanData", new_plan)
|
|
|
|
|
|
def _find_codebase_for_file(file: Path) -> Path | None:
|
|
"""Search all saved plans and return the codebase that contains *file*.
|
|
|
|
Args:
|
|
file: Absolute path to the source file.
|
|
|
|
Returns:
|
|
The codebase ``Path`` whose plan contains *file*, or ``None``.
|
|
"""
|
|
config_root = Path.home() / ".config" / "code_tutor"
|
|
if not config_root.exists():
|
|
return None
|
|
for plan_file in sorted(config_root.glob("*/plan.json")):
|
|
result = _check_plan_file(plan_file, file)
|
|
if result is not None:
|
|
return result
|
|
return None
|
|
|
|
|
|
def _check_plan_file(plan_file: Path, file: Path) -> Path | None:
|
|
"""Return the codebase path from *plan_file* if it contains *file*, else None.
|
|
|
|
Args:
|
|
plan_file: Path to a ``plan.json`` file.
|
|
file: Absolute path to look up.
|
|
|
|
Returns:
|
|
The codebase ``Path`` when *file* is relative to it, or ``None``.
|
|
"""
|
|
try:
|
|
data = json.loads(plan_file.read_text(encoding="utf-8"))
|
|
codebase = Path(str(data.get("codebase_path", "")))
|
|
file.relative_to(codebase)
|
|
except (ValueError, KeyError, OSError):
|
|
return None
|
|
return codebase
|