Spaces:
Runtime error
Runtime error
| """scripts/_lib/cli.py — shared scaffolding for the `odysseus-*` CLIs. | |
| Each top-level CLI imports a few helpers from here so they don't | |
| have to redefine the same `_quiet_logs` / `_emit` / `_fail` / | |
| parents-parser pattern. Usage: | |
| from scripts._lib.cli import quiet_logs, emit, fail, common_parser, run | |
| quiet_logs() | |
| try: | |
| from core.database import SessionLocal, Note # or whatever | |
| quiet_logs() | |
| except ModuleNotFoundError as e: | |
| fail(f"{e}\\nhint: run from repo root with venv active.", code=2) | |
| def cmd_list(args): | |
| ... | |
| def build_parser(): | |
| p = common_parser("odysseus-foo", "Description.") | |
| sub = p.add_subparsers(dest="cmd", required=True) | |
| pl = sub.add_parser("list", parents=[p._common_parents[0]]) | |
| pl.set_defaults(func=cmd_list) | |
| return p | |
| if __name__ == "__main__": | |
| sys.exit(run(build_parser())) | |
| The `--pretty` flag, repo-root-on-sys.path, and clean exit on | |
| KeyboardInterrupt / unexpected exception are all handled centrally. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import logging | |
| import os | |
| import sys | |
| from pathlib import Path | |
| # Make repo root importable. Tools are invoked as `scripts/odysseus-foo` | |
| # from any cwd; we want `from core.database import ...` to work. | |
| REPO_ROOT = Path(__file__).resolve().parent.parent.parent | |
| if str(REPO_ROOT) not in sys.path: | |
| sys.path.insert(0, str(REPO_ROOT)) | |
| def quiet_logs() -> None: | |
| """Force the root logger down to WARNING (overridable via | |
| LOG_LEVEL=...). Call once before importing app modules and again | |
| *after* — some submodules call `logging.basicConfig` during their | |
| own import and re-raise the level to INFO.""" | |
| level_name = os.environ.get("LOG_LEVEL", "WARNING").upper() | |
| level = getattr(logging, level_name, logging.WARNING) | |
| root = logging.getLogger() | |
| root.setLevel(level) | |
| for handler in root.handlers: | |
| handler.setLevel(level) | |
| def emit(obj, args) -> None: | |
| """Write JSON to stdout. Pretty-print if `--pretty` was passed or | |
| stdout is a TTY. Uses `default=str` so SQLAlchemy datetimes etc. | |
| serialize cleanly.""" | |
| pretty = getattr(args, "pretty", False) or sys.stdout.isatty() | |
| json.dump( | |
| obj, sys.stdout, | |
| indent=2 if pretty else None, | |
| default=str, | |
| ensure_ascii=False, | |
| ) | |
| sys.stdout.write("\n") | |
| def fail(msg: str, code: int = 1) -> "None": | |
| """Print an error to stderr and exit non-zero. Doesn't return.""" | |
| sys.stderr.write(f"error: {msg}\n") | |
| sys.exit(code) | |
| VERSION = "0.1.0" # bumped centrally; every odysseus-* CLI reports this | |
| def common_parser(prog: str, description: str = "") -> argparse.ArgumentParser: | |
| """Return a top-level parser with `--pretty` and `--version` already | |
| wired up, and a stashed `_common_parents` list each subcommand should | |
| reuse via `parents=[...]` so the same flag works before AND after | |
| the subcommand name.""" | |
| common = argparse.ArgumentParser(add_help=False) | |
| common.add_argument("--pretty", action="store_true", | |
| help="Pretty-print JSON output") | |
| p = argparse.ArgumentParser(prog=prog, description=description, parents=[common]) | |
| p.add_argument("--version", action="version", version=f"%(prog)s {VERSION}") | |
| p._common_parents = [common] # consumed by callers when building sub-parsers | |
| return p | |
| def run(parser: argparse.ArgumentParser, argv=None) -> int: | |
| """Parse args, dispatch to `args.func(args)`, return an exit code. | |
| Catches KeyboardInterrupt (→ 130) and uncaught exceptions (→ 1) | |
| with a friendly stderr message. | |
| Intercepts `--version` / `-V` before argparse can complain about the | |
| missing required subcommand — `argparse.required=True` on the | |
| subparsers fires first otherwise.""" | |
| raw_argv = sys.argv[1:] if argv is None else list(argv) | |
| if any(a in ("--version", "-V") for a in raw_argv): | |
| sys.stdout.write(f"{parser.prog} {VERSION}\n") | |
| return 0 | |
| args = parser.parse_args(argv) | |
| try: | |
| args.func(args) | |
| except KeyboardInterrupt: | |
| sys.stderr.write("interrupted\n") | |
| return 130 | |
| except SystemExit: | |
| raise | |
| except Exception as e: | |
| fail(str(e)) | |
| return 0 | |