Rebrand the Rust client crate (coven/ → hh/, package+binary "hack-house"), README, CLI strings, and branch (coven → hack-house). Gitea repo renamed cmd-chat → hack-house to match. Crypto/server logic unchanged; selftest + golden-vector test still green, binary is now `hack-house`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
262 lines
9.2 KiB
Python
262 lines
9.2 KiB
Python
import inspect
|
|
|
|
from functools import lru_cache, partial
|
|
from os.path import abspath, dirname, realpath
|
|
|
|
from sanic import Request
|
|
from sanic.blueprints import Blueprint
|
|
from sanic.config import Config
|
|
from sanic.log import logger
|
|
from sanic.response import file, html, json
|
|
|
|
from sanic_ext.config import PRIORITY
|
|
from sanic_ext.extensions.openapi.builders import (
|
|
OperationStore,
|
|
SpecificationBuilder,
|
|
)
|
|
|
|
from ...utils.route import (
|
|
clean_route_name,
|
|
get_all_routes,
|
|
get_blueprinted_routes,
|
|
)
|
|
|
|
|
|
@lru_cache
|
|
def get_oauth2_redirect_html(version: str):
|
|
import urllib
|
|
|
|
response = urllib.request.urlopen(
|
|
f"https://cdn.jsdelivr.net/npm/swagger-ui-dist@{version}/"
|
|
"oauth2-redirect.html"
|
|
).read()
|
|
|
|
return response.decode("utf-8")
|
|
|
|
|
|
def oauth2_handler(request: Request, version: str):
|
|
return html(get_oauth2_redirect_html(version))
|
|
|
|
|
|
def blueprint_factory(config: Config):
|
|
bp = Blueprint("openapi", url_prefix=config.OAS_URL_PREFIX)
|
|
|
|
dir_path = dirname(realpath(__file__))
|
|
dir_path = abspath(dir_path + "/ui")
|
|
|
|
for ui in ("redoc", "swagger"):
|
|
if getattr(config, f"OAS_UI_{ui}".upper()):
|
|
path = getattr(config, f"OAS_PATH_TO_{ui}_HTML".upper())
|
|
uri = getattr(config, f"OAS_URI_TO_{ui}".upper())
|
|
version = getattr(config, f"OAS_UI_{ui}_VERSION".upper(), "")
|
|
html_title = getattr(config, f"OAS_UI_{ui}_HTML_TITLE".upper())
|
|
custom_css = getattr(config, f"OAS_UI_{ui}_CUSTOM_CSS".upper())
|
|
html_path = path if path else f"{dir_path}/{ui}.html"
|
|
|
|
with open(html_path) as f:
|
|
page = f.read()
|
|
|
|
def index(
|
|
request: Request, page: str, html_title: str, custom_css: str
|
|
):
|
|
prefix = (
|
|
request.app.url_for("openapi.index", _external=True)
|
|
if getattr(request.app.config, "SERVER_NAME", None)
|
|
else getattr(request.app.config, "OAS_URL_PREFIX")
|
|
).rstrip("/")
|
|
return html(
|
|
page.replace("__VERSION__", version)
|
|
.replace("__URL_PREFIX__", prefix)
|
|
.replace("__HTML_TITLE__", html_title)
|
|
.replace("__HTML_CUSTOM_CSS__", custom_css)
|
|
)
|
|
|
|
bp.add_route(
|
|
partial(
|
|
index,
|
|
page=page,
|
|
html_title=html_title,
|
|
custom_css=custom_css,
|
|
),
|
|
uri,
|
|
name=ui,
|
|
)
|
|
if config.OAS_UI_DEFAULT and config.OAS_UI_DEFAULT == ui:
|
|
bp.add_route(
|
|
partial(
|
|
index,
|
|
page=page,
|
|
html_title=html_title,
|
|
custom_css=custom_css,
|
|
),
|
|
"",
|
|
name="index",
|
|
)
|
|
|
|
if ui == "swagger":
|
|
oauth2_redirect_uri = getattr(
|
|
config, "OAS_UI_SWAGGER_OAUTH2_REDIRECT"
|
|
)
|
|
|
|
bp.add_route(
|
|
partial(oauth2_handler, version=version),
|
|
oauth2_redirect_uri,
|
|
name="oauth2-redirect",
|
|
)
|
|
|
|
@bp.get(config.OAS_URI_TO_JSON)
|
|
async def spec(request: Request):
|
|
if config.OAS_CUSTOM_FILE:
|
|
return await file(config.OAS_CUSTOM_FILE)
|
|
return json(SpecificationBuilder().build(request.app).serialize())
|
|
|
|
if config.OAS_UI_SWAGGER:
|
|
|
|
@bp.get(config.OAS_URI_TO_CONFIG)
|
|
def openapi_config(request: Request):
|
|
return json(request.app.config.SWAGGER_UI_CONFIGURATION)
|
|
|
|
@bp.before_server_start(priority=PRIORITY)
|
|
def build_spec(app, loop):
|
|
specification = SpecificationBuilder()
|
|
# --------------------------------------------------------------- #
|
|
# Blueprint Tags
|
|
# --------------------------------------------------------------- #
|
|
|
|
for blueprint_name, handler in get_blueprinted_routes(app):
|
|
operation = OperationStore()[handler]
|
|
if not operation.tags:
|
|
operation.tag(blueprint_name)
|
|
|
|
# --------------------------------------------------------------- #
|
|
# Operations
|
|
# --------------------------------------------------------------- #
|
|
for (
|
|
uri,
|
|
route_name,
|
|
route_parameters,
|
|
method_handlers,
|
|
host,
|
|
) in get_all_routes(app, bp.url_prefix):
|
|
# --------------------------------------------------------------- #
|
|
# Methods
|
|
# --------------------------------------------------------------- #
|
|
|
|
for method, _handler in method_handlers:
|
|
if (
|
|
(method == "OPTIONS" and app.config.OAS_IGNORE_OPTIONS)
|
|
or (method == "HEAD" and app.config.OAS_IGNORE_HEAD)
|
|
or method == "TRACE"
|
|
):
|
|
continue
|
|
|
|
if hasattr(_handler, "view_class"):
|
|
_handler = getattr(_handler.view_class, method.lower())
|
|
store = OperationStore()
|
|
if (
|
|
_handler not in store
|
|
and (func := getattr(_handler, "__func__", None))
|
|
and func in store
|
|
):
|
|
_handler = func
|
|
operation = store[_handler]
|
|
|
|
if operation._exclude or "openapi" in operation.tags:
|
|
continue
|
|
|
|
docstring = inspect.getdoc(_handler)
|
|
|
|
if (
|
|
docstring
|
|
and app.config.OAS_AUTODOC
|
|
and operation._allow_autodoc
|
|
):
|
|
operation.autodoc(docstring)
|
|
|
|
operation._default["operationId"] = (
|
|
f"{method.lower()}~{route_name}"
|
|
)
|
|
operation._default["summary"] = clean_route_name(route_name)
|
|
|
|
if host:
|
|
if "servers" not in operation._default:
|
|
operation._default["servers"] = []
|
|
operation._default["servers"].append({"url": f"//{host}"})
|
|
|
|
for _parameter in route_parameters:
|
|
if any(
|
|
param.fields["name"] == _parameter.name
|
|
for param in operation.parameters
|
|
):
|
|
continue
|
|
|
|
kwargs = {}
|
|
if operation._autodoc and (
|
|
parameters := operation._autodoc.get("parameters")
|
|
):
|
|
for param in parameters:
|
|
if param.pop("name", None) == _parameter.name:
|
|
kwargs["description"] = param.get(
|
|
"description"
|
|
)
|
|
kwargs["required"] = param.get("required")
|
|
if schema := param.get("schema"):
|
|
logger.warning(
|
|
f"Ignoring the schema {schema} in "
|
|
f"'{route_name}' for "
|
|
f"'{_parameter.name}'. "
|
|
"Instead of using the definition in "
|
|
"docstring definition, Sanic will use "
|
|
"the actual schema defined for this "
|
|
"parameter on the route."
|
|
)
|
|
break
|
|
|
|
operation.parameter(
|
|
_parameter.name, _parameter.cast, "path", **kwargs
|
|
)
|
|
|
|
operation._app = app
|
|
specification.operation(uri, method, operation)
|
|
|
|
add_static_info_to_spec_from_config(app, specification)
|
|
|
|
return bp
|
|
|
|
|
|
def add_static_info_to_spec_from_config(app, specification):
|
|
"""
|
|
Reads app.config and sets attributes to specification according to the
|
|
desired values.
|
|
|
|
Modifies specification in-place and returns None
|
|
"""
|
|
specification._do_describe(
|
|
getattr(app.config, "API_TITLE", "API"),
|
|
getattr(app.config, "API_VERSION", "1.0.0"),
|
|
getattr(app.config, "API_DESCRIPTION", None),
|
|
getattr(app.config, "API_TERMS_OF_SERVICE", None),
|
|
)
|
|
|
|
specification._do_license(
|
|
getattr(app.config, "API_LICENSE_NAME", None),
|
|
getattr(app.config, "API_LICENSE_URL", None),
|
|
)
|
|
|
|
specification._do_contact(
|
|
getattr(app.config, "API_CONTACT_NAME", None),
|
|
getattr(app.config, "API_CONTACT_URL", None),
|
|
getattr(app.config, "API_CONTACT_EMAIL", None),
|
|
)
|
|
|
|
schemes = getattr(app.config, "API_SCHEMES", ["http"]) or ["http"]
|
|
if isinstance(schemes, str):
|
|
schemes = [s.strip() for s in schemes.split(",")]
|
|
for scheme in schemes:
|
|
host = getattr(app.config, "API_HOST", None)
|
|
basePath = getattr(app.config, "API_BASEPATH", "")
|
|
if host is None or basePath is None:
|
|
continue
|
|
|
|
specification.url(f"{scheme}://{host}/{basePath}")
|