import html import json from datetime import datetime from glob import glob import importlib.util from pathlib import Path import re import shutil import sys from fastapi import FastAPI # CDN pins for the spec viewers. SWAGGER_CSS = "https://unpkg.com/swagger-ui-dist@5.18.2/swagger-ui.css" SWAGGER_JS = "https://unpkg.com/swagger-ui-dist@5.18.2/swagger-ui-bundle.js" ELEMENTS_CSS = "https://unpkg.com/@stoplight/elements@9.0.24/styles.min.css" ELEMENTS_JS = "https://unpkg.com/@stoplight/elements@9.0.24/web-components.min.js" def version_sort_key(v: str) -> tuple: """Sort versions semver-ish (e.g. 'v4.1.0.7'); newest last when sorted ascending.""" parts = re.findall(r"\d+", v) return tuple(int(p) for p in parts) if parts else (0,) def embed_json(spec: dict) -> str: """Serialize `spec` for safe embedding in a `` would break out of the block; escape the slash. `\/` is a valid # JSON string escape and `` never appears in JSON outside a string. return json.dumps(spec, indent=2, ensure_ascii=False).replace("", "<\\/") def dynamic_import(mod_path: Path) -> FastAPI: """Import an `app.py` located at mod_path and return its `app` FastAPI instance. The subject's own directory is temporarily prepended to sys.path so that sibling modules (e.g. `schemas`) resolve correctly. """ mod_name = f"subject_{mod_path.parent.parent.name}_{mod_path.parent.name}" spec = importlib.util.spec_from_file_location(mod_name, mod_path) if spec is None or spec.loader is None: raise ImportError(f"Could not load module from {mod_path}") module = importlib.util.module_from_spec(spec) # Ensure sibling imports (e.g. `import schemas`) resolve. parent = str(mod_path.parent) sys.path.insert(0, parent) try: sys.modules[mod_name] = module spec.loader.exec_module(module) finally: if parent in sys.path: sys.path.remove(parent) sys.modules.pop(mod_name, None) if not hasattr(module, "app") or not isinstance(module.app, FastAPI): raise AttributeError(f"{mod_path} does not expose a FastAPI `app` instance") return module.app def render_viewers(version_dir: Path, subject: str, version: str, spec: dict) -> None: """Write swagger.html and elements.html for one spec, embedding the JSON inline.""" spec_json = embed_json(spec) title = f"{subject} {version}" swagger_html = f"""