Files
Timmy-time-dashboard/scripts/dev_server.py
Kimi Agent 3c10da489b
All checks were successful
Tests / lint (push) Successful in 3s
Tests / test (push) Successful in 1m9s
fix: enhance tox dev environment (port, banner, reload) (#386)
Co-authored-by: Kimi Agent <kimi@timmy.local>
Co-committed-by: Kimi Agent <kimi@timmy.local>
2026-03-19 01:08:49 -04:00

170 lines
4.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Timmy Time — Development server launcher.
Satisfies tox -e dev criteria:
- Graceful port selection (finds next free port if default is taken)
- Clickable links to dashboard and other web GUIs
- Status line: backend inference source, version, git commit, smoke tests
- Auto-reload on code changes (delegates to uvicorn --reload)
Usage: python scripts/dev_server.py [--port PORT]
"""
import argparse
import datetime
import os
import socket
import subprocess
import sys
DEFAULT_PORT = 8000
MAX_PORT_ATTEMPTS = 10
OLLAMA_DEFAULT = "http://localhost:11434"
def _port_free(port: int) -> bool:
"""Return True if the TCP port is available on localhost."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.bind(("0.0.0.0", port))
return True
except OSError:
return False
def _find_port(start: int) -> int:
"""Return *start* if free, otherwise probe up to MAX_PORT_ATTEMPTS higher."""
for offset in range(MAX_PORT_ATTEMPTS):
candidate = start + offset
if _port_free(candidate):
return candidate
raise RuntimeError(
f"No free port found in range {start}{start + MAX_PORT_ATTEMPTS - 1}"
)
def _git_info() -> str:
"""Return short commit hash + timestamp, or 'unknown'."""
try:
sha = subprocess.check_output(
["git", "rev-parse", "--short", "HEAD"],
stderr=subprocess.DEVNULL,
text=True,
).strip()
ts = subprocess.check_output(
["git", "log", "-1", "--format=%ci"],
stderr=subprocess.DEVNULL,
text=True,
).strip()
return f"{sha} ({ts})"
except Exception:
return "unknown"
def _project_version() -> str:
"""Read version from pyproject.toml without importing toml libs."""
pyproject = os.path.join(os.path.dirname(__file__), "..", "pyproject.toml")
try:
with open(pyproject) as f:
for line in f:
if line.strip().startswith("version"):
# version = "1.0.0"
return line.split("=", 1)[1].strip().strip('"').strip("'")
except Exception:
pass
return "unknown"
def _ollama_url() -> str:
return os.environ.get("OLLAMA_URL", OLLAMA_DEFAULT)
def _smoke_ollama(url: str) -> str:
"""Quick connectivity check against Ollama."""
import urllib.request
import urllib.error
try:
req = urllib.request.Request(url, method="GET")
with urllib.request.urlopen(req, timeout=3):
return "ok"
except Exception:
return "unreachable"
def _print_banner(port: int) -> None:
version = _project_version()
git = _git_info()
ollama_url = _ollama_url()
ollama_status = _smoke_ollama(ollama_url)
hr = "" * 62
print(flush=True)
print(f" {hr}")
print(f" ┃ Timmy Time — Development Server")
print(f" {hr}")
print()
print(f" Dashboard: http://localhost:{port}")
print(f" API docs: http://localhost:{port}/docs")
print(f" Health: http://localhost:{port}/health")
print()
print(f" ── Status ──────────────────────────────────────────────")
print(f" Backend: {ollama_url} [{ollama_status}]")
print(f" Version: {version}")
print(f" Git commit: {git}")
print(f" {hr}")
print(flush=True)
def main() -> None:
parser = argparse.ArgumentParser(description="Timmy dev server")
parser.add_argument(
"--port",
type=int,
default=DEFAULT_PORT,
help=f"Preferred port (default: {DEFAULT_PORT})",
)
args = parser.parse_args()
port = _find_port(args.port)
if port != args.port:
print(f" ⚠ Port {args.port} in use — using {port} instead")
_print_banner(port)
# Set PYTHONPATH so `timmy` CLI inside the tox venv resolves to this source.
src_dir = os.path.join(os.path.dirname(__file__), "..", "src")
os.environ["PYTHONPATH"] = os.path.abspath(src_dir)
# Launch uvicorn with auto-reload
cmd = [
sys.executable,
"-m",
"uvicorn",
"dashboard.app:app",
"--reload",
"--host",
"0.0.0.0",
"--port",
str(port),
"--reload-dir",
os.path.abspath(src_dir),
"--reload-include",
"*.html",
"--reload-include",
"*.css",
"--reload-include",
"*.js",
"--reload-exclude",
".claude",
]
try:
subprocess.run(cmd, check=True)
except KeyboardInterrupt:
print("\n Shutting down dev server.")
if __name__ == "__main__":
main()