Something went wrong. Try again.
OpenAPI docs for public facing APIs of multiple Indian apps
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195import htmlimport jsonfrom datetime import datetimefrom glob import globimport importlib.utilfrom pathlib import Pathimport reimport shutilimport 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 `<script type="application/json">` block.""" # Only `</script>` 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"""<!DOCTYPE html><html><head> <meta charset="utf-8"> <title>{html.escape(title)} - Swagger UI</title> <link rel="stylesheet" href="{SWAGGER_CSS}"></head><body> <div id="swagger-ui"></div> <script type="application/json" id="spec">{spec_json}</script> <script src="{SWAGGER_JS}"></script> <script> SwaggerUIBundle({{ spec: JSON.parse(document.getElementById('spec').textContent), dom_id: '#swagger-ui', deepLinking: true, }}); </script></body></html>""" (version_dir / "swagger.html").write_text(swagger_html)
elements_html = f"""<!doctype html><html lang="en"><head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <title>{html.escape(title)} - Elements</title> <script src="{ELEMENTS_JS}"></script> <link rel="stylesheet" href="{ELEMENTS_CSS}"></head><body> <elements-api apiDescriptionUrl="openapi.json" router="hash" layout="responsive" ></elements-api></body></html>""" (version_dir / "elements.html").write_text(elements_html)
def build_index(out_dir: Path, specs: dict[str, list[str]]) -> None: """Write a plain old-school index.html listing apps, versions, and viewer links.""" rows: list[str] = [] for subject in sorted(specs): versions = sorted(specs[subject], key=version_sort_key, reverse=True) rows.append(f' <li><a href="{html.escape(subject)}/">{html.escape(subject)}</a>') rows.append(" <ul>") for i, v in enumerate(versions): latest = " (latest)" if i == 0 else "" base = f"{html.escape(subject)}/{html.escape(v)}" rows.append(f' <li>{html.escape(v)}{latest}') rows.append(" <ul>") rows.append(f' <li><a href="{base}/swagger.html">Swagger</a></li>') rows.append(f' <li><a href="{base}/elements.html">Elements</a></li>') rows.append(f' <li><a href="{base}/openapi.json">Raw JSON</a></li>') rows.append(" </ul>") rows.append(" </li>") rows.append(" </ul>") rows.append(" </li>")
page = f"""<!DOCTYPE html><html><head> <title>OpenAPI Specs</title></head><body> <h1>OpenAPI Specs</h1> <hr> <ul>{chr(10).join(rows)} </ul> <hr> <address>Generated by Tangled Spindle - {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - <a href="https://tangled.org/nkmason.dev/oa-specs">Source</a></address></body></html>""" (out_dir / "index.html").write_text(page)
def main() -> None: out_dir = Path("specs") # Start from a clean slate so stale specs/views never linger. if out_dir.exists(): shutil.rmtree(out_dir) out_dir.mkdir() generated: dict[str, list[str]] = {}
# Layout: subjects/<subject>/<version>/app.py for app_path in sorted(glob("subjects/*/*/app.py")): mod_path = Path(app_path) subject = mod_path.parent.parent.name dir_version = mod_path.parent.name
print(f"Generating OpenAPI spec for {subject} @ {dir_version} ...") app = dynamic_import(mod_path)
# The directory version must match what the app declares, otherwise # the two sources of truth will silently drift. app_version = app.version if app_version != dir_version: raise ValueError( f"Version mismatch for {subject}: directory says " f"'{dir_version}' but app.version says '{app_version}'" )
version_dir = out_dir / subject / dir_version version_dir.mkdir(parents=True, exist_ok=True)
openapi = app.openapi() (version_dir / "openapi.json").write_text( json.dumps(openapi, indent=2, ensure_ascii=False) ) render_viewers(version_dir, subject, dir_version, openapi) print(f" -> wrote {version_dir}/openapi.json, swagger.html, elements.html")
generated.setdefault(subject, []).append(dir_version)
build_index(out_dir, generated) print(f" -> wrote {out_dir / 'index.html'}")
if __name__ == "__main__": main()