#!/usr/bin/env python3 """Shared utilities for Settled Reach asset/connector scripts. Provides `ensure_venv`, used by the asset connectors (audio_connector, audio_batch, image_connector, trellis_connector) to re-exec into the project .venv before their third-party imports, plus the connector config helpers (`load_config`, `get_base_url`, `get_api_key`) that were formerly copy-pasted across the connectors (T-1067 rider, S-25). The former settledreach.db connection helpers were removed when the ticket/decision tooling was retired (pql migration Phase 6); planning now lives in pql (`.pql/`). """ import json import os import sys from pathlib import Path # --------------------------------------------------------------------------- # Paths # --------------------------------------------------------------------------- SCRIPT_DIR = Path(__file__).resolve().parent WORKTREE_ROOT = (SCRIPT_DIR / ".." / "..").resolve() CONFIG_PATH = SCRIPT_DIR / "config.json" # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def ensure_venv() -> None: """Re-exec into the project .venv Python if not already running there. Call this at the top of any script that uses third-party packages, before those imports. Usage:: from common import ensure_venv ensure_venv() import numpy as np # third-party import follows """ venv_python = WORKTREE_ROOT / ".venv" / "bin" / "python" # Already running inside the venv — nothing to do. if Path(sys.executable).resolve() == venv_python.resolve(): return # .venv not set up yet — fail with a helpful message. if not venv_python.exists(): print( "error: .venv not found — run `make setup-venv` first.", file=sys.stderr, ) sys.exit(1) # Re-exec into the venv Python, preserving all arguments. os.execv(str(venv_python), [str(venv_python)] + sys.argv) def load_config() -> dict: """Load the shared endpoint configuration from tooling/db/config.json.""" with open(CONFIG_PATH) as f: return json.load(f) def get_base_url(key: str, default: str) -> str: """Resolve a service base URL from config.json, with a fallback default. Usage:: base = get_base_url("trellis_url", "http://tower-of-joy:11510") """ return load_config().get(key, default) def get_api_key(env_var: str, config_key: str = "") -> str: """Get an API key from the environment — environment-only, by design. ``tooling/db/config.json`` is a *tracked* file and holds endpoints only; it must never carry secrets, so there is deliberately no config.json fallback here (the old one steered users toward committing paid API keys). ``config_key`` is retained in the signature for caller compatibility but is ignored. Prints a JSON error and exits 1 if the variable is unset — connector scripts emit machine-readable JSON on all paths. """ key = os.environ.get(env_var) if key: return key print(json.dumps({ "ok": False, "error": ( f"{env_var} not set. Export it in your shell or add it to the " "machine-local .claude/settings.local.json env block (untracked). " "Never put keys in tooling/db/config.json — it is tracked." ) }, indent=2)) sys.exit(1)