From 17078127b8cba5a4148987e6a1c879ef5cd6b002 Mon Sep 17 00:00:00 2001 From: Robert Halter Date: Fri, 20 Mar 2026 21:15:20 +0100 Subject: [PATCH] readd docs link --- src/apiup/cli.py | 4 ++- src/apiup/server.py | 61 +++++++++++++++++++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/src/apiup/cli.py b/src/apiup/cli.py index 246b9cc..a5186b7 100644 --- a/src/apiup/cli.py +++ b/src/apiup/cli.py @@ -75,6 +75,8 @@ def main() -> None: print(f" Spec : {cfg.spec}") print(f" Mode : {cfg.mode}") print(f" Listen: http://{cfg.host}:{cfg.port}") + print(f" Docs : http://{cfg.host}:{cfg.port}/docs") + print(f" Spec : http://{cfg.host}:{cfg.port}/spec.json") print(f" Routes: {len(routes)}") print() for r in routes: @@ -84,7 +86,7 @@ def main() -> None: # ── Build + serve ──────────────────────────────────────────────────────── from apiup.server import build_mock_app, serve - app = build_mock_app(routes, spec) + app = build_mock_app(routes, spec, cfg.spec) serve(app, host=cfg.host, port=cfg.port) diff --git a/src/apiup/server.py b/src/apiup/server.py index aac0d0b..e63c624 100644 --- a/src/apiup/server.py +++ b/src/apiup/server.py @@ -7,36 +7,56 @@ import json import re import sys +from pathlib import Path from typing import Any from apiup.mock import extract_mock_response from apiup.spec import Route try: - from litestar import Litestar + from litestar import Litestar, get from litestar.handlers import HTTPRouteHandler from litestar.response import Response as LitestarResponse except ImportError: # pragma: no cover Litestar = None # type: ignore[assignment,misc] HTTPRouteHandler = None # type: ignore[assignment,misc] LitestarResponse = None # type: ignore[assignment] + get = None # type: ignore[assignment] -# OpenAPI {param} → Litestar {param:str} _PARAM_RE = re.compile(r"\{(\w+)\}") - -# Status codes that must not have a response body _NO_BODY_CODES = frozenset({204, 304}) +_SWAGGER_HTML = """ + + + apiup — Swagger UI + + + + + +
+ + + +""" + def _openapi_path_to_litestar(path: str) -> str: - """Convert /skills/{skillId} → /skills/{skillId:str}.""" return _PARAM_RE.sub(r"{\1:str}", path) def _make_handler(body: Any, status_code: int) -> Any: - """Return a handler closed over body+status — no mutable defaults.""" if status_code in _NO_BODY_CODES: - # No-body response — return None, Litestar sends empty 204/304 + async def _handler() -> None: return None else: @@ -52,8 +72,8 @@ async def _handler() -> Any: # type: ignore[misc] return _handler -def build_mock_app(routes: list[Route], spec: dict[str, Any]) -> Any: - """Return a Litestar ASGI app with one handler per spec route.""" +def build_mock_app(routes: list[Route], spec: dict[str, Any], spec_path: str) -> Any: + """Return a Litestar ASGI app with mock handlers + swagger UI.""" if Litestar is None: print( "ERROR: litestar not installed.\nRun: pip install 'litestar[standard]'", @@ -61,7 +81,27 @@ def build_mock_app(routes: list[Route], spec: dict[str, Any]) -> Any: ) sys.exit(2) - handlers = [] + # ── built-in routes ──────────────────────────────────────────────────── + spec_bytes: bytes = Path(spec_path).read_bytes() + + @get("/spec.json", media_type="application/json") + async def serve_spec() -> LitestarResponse: # type: ignore[return] + return LitestarResponse( + content=spec_bytes, + status_code=200, + media_type="application/json", + ) + + @get("/docs", media_type="text/html") + async def serve_docs() -> LitestarResponse: # type: ignore[return] + return LitestarResponse( + content=_SWAGGER_HTML.encode(), + status_code=200, + media_type="text/html", + ) + + # ── mock route handlers ──────────────────────────────────────────────── + handlers: list[Any] = [serve_spec, serve_docs] for route in routes: status_code, body = extract_mock_response(route.responses, spec) @@ -79,7 +119,6 @@ def build_mock_app(routes: list[Route], spec: dict[str, Any]) -> Any: def serve(app: Any, host: str, port: int) -> None: - """Start uvicorn with the given app.""" try: import uvicorn except ImportError: