101 lines
2.9 KiB
Python
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()
|