Files
noteflow/scripts/dev_watch_server.py

101 lines
2.9 KiB
Python

#!/usr/bin/env python3
"""Run the gRPC server with auto-reload.
Watches only the core server code (and alembic.ini) to avoid noisy directories.
"""
from __future__ import annotations
import importlib
import os
import shutil
import subprocess
import sys
from collections.abc import Callable
from pathlib import Path
from typing import Protocol, cast
class _WatchfilesPythonFilter(Protocol):
def __call__(self, change: object, path: str) -> bool: ...
def _is_truthy(value: str | None) -> bool:
if value is None:
return False
return value.strip().lower() in {"1", "true", "yes", "on"}
def _ner_enabled() -> bool:
return _is_truthy(os.getenv("NOTEFLOW_FEATURE_NER_ENABLED"))
def _ensure_spacy_model() -> None:
if not _ner_enabled():
return
try:
import importlib.util
except ModuleNotFoundError:
print("importlib unavailable; skipping model install.")
return
if importlib.util.find_spec("spacy") is None:
print("spaCy not installed; skipping model install.")
return
if importlib.util.find_spec("en_core_web_sm") is not None:
return
wheel_url = (
"https://github.com/explosion/spacy-models/releases/download/"
"en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl"
)
uv_path = shutil.which("uv")
if uv_path:
subprocess.run([uv_path, "pip", "install", wheel_url], check=False)
return
subprocess.run([sys.executable, "-m", "spacy", "download", "en_core_web_sm"], check=False)
def _load_watchfiles() -> tuple[type[_WatchfilesPythonFilter], Callable[..., object]]:
# cast required: watchfiles is optional at import time but provides the runtime types.
module = importlib.import_module("watchfiles")
python_filter = cast(type[_WatchfilesPythonFilter], module.PythonFilter)
run_process = cast(Callable[..., object], module.run_process)
return python_filter, run_process
def run_server() -> None:
"""Start the gRPC server process.
Uses os.execvp() to replace this process with the server, avoiding
nested subprocess spawning that causes zombie processes when watchfiles
restarts on file changes.
"""
try:
os.execvp(sys.executable, [sys.executable, "-m", "noteflow.grpc.server"])
except OSError as exc:
print(f"Failed to exec gRPC server: {exc}", file=sys.stderr)
raise SystemExit(1) from exc
def main() -> None:
_ensure_spacy_model()
root = Path(__file__).resolve().parents[1]
watch_paths = [root / "src" / "noteflow", root / "alembic.ini"]
existing_paths = [str(path) for path in watch_paths if path.exists()] or [
str(root / "src" / "noteflow")
]
python_filter, run_process = _load_watchfiles()
run_process(
*existing_paths,
target=run_server,
watch_filter=python_filter(),
)
if __name__ == "__main__":
main()