api: serve interactive OpenAPI docs at /api/docs (offline) + llms.txt
Generate docs/openapi.json (3.1, 362 operations) from the API inventory via scripts/gen_openapi.py, and serve it with a self-hosted Redoc bundle at /api/docs - no CDN, works on the air-gapped box. Also serve docs/llms.txt (a concise LLM entrypoint) at /api/docs/llms.txt. New core 'docs' blueprint; staticdocs/ excluded from the naming check (vendored minified JS).
This commit is contained in:
@@ -143,6 +143,7 @@ CORE_BLUEPRINT_NAMES = (
|
||||
'setup',
|
||||
'pluginui',
|
||||
'apitokens',
|
||||
'docs',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ from .customfields import customfields_bp
|
||||
from .setup import setup_bp
|
||||
from .pluginui import pluginui_bp
|
||||
from .apitokens import apitokens_bp
|
||||
from .docs import docs_bp
|
||||
|
||||
__all__ = [
|
||||
'auth_bp',
|
||||
@@ -48,4 +49,5 @@ __all__ = [
|
||||
'setup_bp',
|
||||
'pluginui_bp',
|
||||
'apitokens_bp',
|
||||
'docs_bp',
|
||||
]
|
||||
|
||||
61
shopdb/core/api/docs.py
Normal file
61
shopdb/core/api/docs.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Interactive API docs: self-hosted Redoc over the generated OpenAPI spec.
|
||||
|
||||
Served at /api/docs (relative to the mount). The Redoc bundle is vendored in
|
||||
staticdocs/ so this works fully offline on the air-gapped prod box - no CDN.
|
||||
The spec is docs/openapi.json in the repo (regenerate with
|
||||
scripts/gen_openapi.py after API changes).
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from flask import Blueprint, Response, send_file, url_for
|
||||
|
||||
docs_bp = Blueprint('docs', __name__)
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
# shopdb/core/api -> repo root
|
||||
_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(_HERE)))
|
||||
_SPEC = os.path.join(_REPO_ROOT, 'docs', 'openapi.json')
|
||||
_LLMS = os.path.join(_REPO_ROOT, 'docs', 'llms.txt')
|
||||
_REDOC = os.path.join(_HERE, 'staticdocs', 'redoc.standalone.js')
|
||||
|
||||
|
||||
@docs_bp.route('/', strict_slashes=False)
|
||||
def docs_index():
|
||||
"""Redoc page. url_for keeps the asset URLs correct under any mount."""
|
||||
spec_url = url_for('docs.openapi_spec')
|
||||
redoc_url = url_for('docs.redoc_js')
|
||||
page = (
|
||||
'<!doctype html><html><head>'
|
||||
'<title>ShopDB API</title>'
|
||||
'<meta charset="utf-8">'
|
||||
'<meta name="viewport" content="width=device-width, initial-scale=1">'
|
||||
'<style>body{margin:0;padding:0}</style></head><body>'
|
||||
'<redoc spec-url="%s"></redoc>'
|
||||
'<script src="%s"></script>'
|
||||
'</body></html>' % (spec_url, redoc_url)
|
||||
)
|
||||
return Response(page, mimetype='text/html')
|
||||
|
||||
|
||||
@docs_bp.route('/openapi.json')
|
||||
def openapi_spec():
|
||||
"""The generated OpenAPI 3.1 spec (machine + LLM readable)."""
|
||||
if not os.path.isfile(_SPEC):
|
||||
return Response('{"error":"openapi.json not generated"}',
|
||||
status=404, mimetype='application/json')
|
||||
return send_file(_SPEC, mimetype='application/json')
|
||||
|
||||
|
||||
@docs_bp.route('/redoc.standalone.js')
|
||||
def redoc_js():
|
||||
"""Vendored Redoc bundle (offline)."""
|
||||
return send_file(_REDOC, mimetype='application/javascript')
|
||||
|
||||
|
||||
@docs_bp.route('/llms.txt')
|
||||
def llms_txt():
|
||||
"""Concise LLM-oriented API guide."""
|
||||
if not os.path.isfile(_LLMS):
|
||||
return Response('llms.txt not found', status=404, mimetype='text/plain')
|
||||
return send_file(_LLMS, mimetype='text/plain')
|
||||
1838
shopdb/core/api/staticdocs/redoc.standalone.js
Normal file
1838
shopdb/core/api/staticdocs/redoc.standalone.js
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user