feat: add hub runtime and extension contract
This commit is contained in:
parent
fce19f193f
commit
7e1ec03f0c
44 changed files with 3875 additions and 84 deletions
100
hub_core/runtime/cli.py
Normal file
100
hub_core/runtime/cli.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
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://")
|
||||
Loading…
Add table
Add a link
Reference in a new issue