hub-core/hub_core/runtime/cli.py

101 lines
3.8 KiB
Python
Raw Normal View History

from __future__ import annotations
import argparse
import json
from importlib.resources import files
from typing import Sequence
from hub_core.mcp import HubCoreMCPServer
from hub_core.runtime.config import RuntimeSettings
def build_parser(settings: RuntimeSettings | None = None) -> argparse.ArgumentParser:
resolved = settings or RuntimeSettings.from_env()
parser = argparse.ArgumentParser(prog="hub-core", description="Hub Core runtime commands")
commands = parser.add_subparsers(dest="command", required=True)
api = commands.add_parser("api", help="Run the HTTP API and named ports")
api.add_argument("--host", default=resolved.api_host)
api.add_argument("--port", type=int, default=resolved.api_port)
mcp = commands.add_parser("mcp", help="Run the Hub Core MCP process")
mcp.add_argument("--host", default=resolved.mcp_host)
mcp.add_argument("--port", type=int, default=resolved.mcp_port)
mcp.add_argument("--transport", default=resolved.mcp_transport)
mcp.add_argument("--api-base", default=resolved.api_base)
migrate = commands.add_parser("migrate", help="Run packaged Alembic migrations")
migrate.add_argument("revision", nargs="?", default="head")
migrate.add_argument("--database-url", default=resolved.database_url)
conformance = commands.add_parser(
"conformance",
help="Run the implemented Tier 2/3 profile against an HTTP runtime",
)
conformance.add_argument("--base-url", default=resolved.api_base)
conformance.add_argument("--timeout", type=float, default=10.0)
conformance.add_argument("--json", action="store_true", dest="as_json")
return parser
def main(argv: Sequence[str] | None = None) -> int:
settings = RuntimeSettings.from_env()
args = build_parser(settings).parse_args(argv)
if args.command == "api":
_run_api(args.host, args.port)
return 0
if args.command == "mcp":
_run_mcp(args.host, args.port, args.transport, args.api_base)
return 0
if args.command == "migrate":
if not args.database_url:
raise SystemExit("hub-core migrate requires --database-url or HUB_CORE_DATABASE_URL")
_run_migrations(args.database_url, args.revision)
return 0
if args.command == "conformance":
return _run_conformance(args.base_url, args.timeout, args.as_json)
raise AssertionError(f"Unhandled command {args.command}")
def _run_api(host: str, port: int) -> None:
import uvicorn
uvicorn.run("hub_core.runtime.app:app", host=host, port=port)
def _run_mcp(host: str, port: int, transport: str, api_base: str) -> None:
server = HubCoreMCPServer(name="hub-core", api_base=api_base)
server.mcp.run(transport=transport, host=host, port=port)
def _run_migrations(database_url: str, revision: str) -> None:
from alembic import command
from alembic.config import Config
migration_root = files("hub_core.migrations")
config = Config()
config.set_main_option("script_location", str(migration_root))
config.set_main_option("sqlalchemy.url", _sync_database_url(database_url))
command.upgrade(config, revision)
def _run_conformance(base_url: str, timeout: float, as_json: bool) -> int:
import httpx
from hub_core.conformance import ConformanceHarness
with httpx.Client(base_url=base_url, timeout=timeout) as target:
report = ConformanceHarness(target).run()
if as_json:
print(json.dumps(report.to_dict(), indent=2, sort_keys=True))
else:
for check in report.checks:
print(f"{check.status.upper():4} Tier {check.tier} {check.check_id}: {check.summary}")
print(f"{report.passed_count}/{len(report.checks)} implemented checks passed")
return 0 if report.passed else 1
def _sync_database_url(database_url: str) -> str:
return database_url.replace("postgresql+asyncpg://", "postgresql+psycopg2://")