chore: rename project coven → hack-house ⛧

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>
This commit is contained in:
leetcrypt
2026-05-30 13:29:14 -07:00
parent 82a04f3e12
commit bb1d662ee1
2730 changed files with 712933 additions and 46 deletions
@@ -0,0 +1,93 @@
import inspect
import warnings
import yaml
class OpenAPIDocstringParser:
def __init__(self, docstring: str):
"""
Args:
docstring (str): docstring of function to be parsed
"""
if docstring is None:
docstring = ""
self.docstring = inspect.cleandoc(docstring)
def to_openAPI_2(self) -> dict:
"""
Returns:
json style dict: dict to be read for the path by swagger 2.0 UI
"""
raise NotImplementedError()
def to_openAPI_3(self) -> dict:
"""
Returns:
json style dict: dict to be read for the path by swagger 3.0.0 UI
"""
raise NotImplementedError()
class YamlStyleParametersParser(OpenAPIDocstringParser):
def _parse_no_yaml(self, doc: str) -> dict:
"""
Args:
doc (str): section of doc before yaml, or full section of doc
Returns:
json style dict: dict to be read for the path by swagger UI
"""
# clean again in case further indentation can be removed,
# usually this do nothing...
doc = inspect.cleandoc(doc)
if len(doc) == 0:
return {}
lines = doc.split("\n")
if len(lines) == 1:
return {"summary": lines[0]}
else:
summary = lines.pop(0)
# remove empty lines at the beginning of the description
while len(lines) and lines[0].strip() == "":
lines.pop(0)
if len(lines) == 0:
return {"summary": summary}
else:
# use html tag to preserve linebreaks
return {"summary": summary, "description": "<br>".join(lines)}
def _parse_yaml(self, doc: str) -> dict:
"""
Args:
doc (str): section of doc detected as openapi yaml
Returns:
json style dict: dict to be read for the path by swagger UI
Warns:
UserWarning if the yaml couldn't be parsed
"""
try:
return yaml.safe_load(doc)
except Exception as e:
warnings.warn(f"error parsing openAPI yaml, ignoring it. ({e})")
return {}
def _parse_all(self) -> dict:
if "openapi:\n" not in self.docstring:
return self._parse_no_yaml(self.docstring)
predoc, yamldoc = self.docstring.split("openapi:\n", 1)
conf = self._parse_no_yaml(predoc)
conf.update(self._parse_yaml(yamldoc))
return conf
def to_openAPI_2(self) -> dict:
return self._parse_all()
def to_openAPI_3(self) -> dict:
return self._parse_all()
@@ -0,0 +1,261 @@
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}")
@@ -0,0 +1,446 @@
from __future__ import annotations
from collections import defaultdict
from collections.abc import Sequence
from typing import TYPE_CHECKING, Optional, Union, cast
from sanic_ext.extensions.openapi.constants import (
SecuritySchemeAuthorization,
SecuritySchemeLocation,
SecuritySchemeType,
)
from ...utils.route import remove_nulls, remove_nulls_from_kwargs
from .autodoc import YamlStyleParametersParser
from .definitions import (
Any,
Components,
Contact,
ExternalDocumentation,
Flows,
Info,
License,
OpenAPI,
Operation,
Parameter,
PathItem,
RequestBody,
Response,
SecurityRequirement,
SecurityScheme,
Server,
Tag,
)
if TYPE_CHECKING:
from sanic import Sanic
class OperationBuilder:
summary: str
description: str
operationId: str
requestBody: RequestBody
externalDocs: ExternalDocumentation
tags: list[str]
security: list[Any]
parameters: list[Parameter]
responses: dict[str, Response]
callbacks: list[str] # TODO
deprecated: bool = False
def __init__(self):
self.tags = []
self.security = []
self.parameters = []
self.responses = {}
self._default = {}
self._autodoc = None
self._exclude = False
self._allow_autodoc = True
self._app: Optional[Sanic] = None
def name(self, value: str):
self.operationId = value
def describe(self, summary: str = None, description: str = None):
if summary:
self.summary = summary
if description:
self.description = description
def document(self, url: str, description: str = None):
self.externalDocs = ExternalDocumentation.make(url, description)
def tag(self, *args: str):
for arg in args:
if isinstance(arg, Tag):
arg = arg.fields["name"]
self.tags.append(arg)
def deprecate(self):
self.deprecated = True
def body(self, content: Any, **kwargs):
self.requestBody = RequestBody.make(content, **kwargs)
def parameter(
self, name: str, schema: Any, location: str = "query", **kwargs
):
self.parameters.append(
Parameter.make(name, schema, location, **kwargs)
)
def response(
self, status, content: Any = None, description: str = None, **kwargs
):
response = Response.make(content, description, **kwargs)
if status in self.responses:
self.responses[status]._fields["content"].update(
response.fields["content"]
)
else:
self.responses[status] = response
def secured(self, *args, **kwargs):
if not kwargs and len(args) == 1 and isinstance(args[0], dict):
items = args[0]
else:
items = {**{v: [] for v in args}, **kwargs}
gates = {}
for name, params in items.items():
gate = name.__name__ if isinstance(name, type) else name
gates[gate] = params
self.security.append(gates)
def disable_autodoc(self):
self._allow_autodoc = False
def build(self):
operation_dict = self._build_merged_dict()
if "responses" not in operation_dict:
# todo -- look into more consistent default response format
operation_dict["responses"] = {"default": {"description": "OK"}}
return Operation(**operation_dict)
def _build_merged_dict(self):
defined_dict = self.__dict__.copy()
autodoc_dict = self._autodoc or {}
default_dict = self._default
merged_dict = {}
for d in (default_dict, autodoc_dict, defined_dict):
cleaned = {
k: v for k, v in d.items() if v and not k.startswith("_")
}
merged_dict.update(cleaned)
return merged_dict
def autodoc(self, docstring: str):
y = YamlStyleParametersParser(docstring)
self._autodoc = y.to_openAPI_3()
def exclude(self, flag: bool = True):
self._exclude = flag
class OperationStore(defaultdict):
_singleton = None
def __new__(cls) -> Any:
if not cls._singleton:
cls._singleton = super().__new__(cls)
return cls._singleton
def __init__(self):
super().__init__(OperationBuilder)
@classmethod
def reset(cls):
cls._singleton = None
class SpecificationBuilder:
_urls: list[str]
_title: str
_version: str
_description: Optional[str]
_terms: Optional[str]
_contact: Contact
_license: License
_paths: dict[str, dict[str, OperationBuilder]]
_tags: dict[str, Tag]
_security: list[SecurityRequirement]
_components: dict[str, Any]
_servers: list[Server]
# _components: ComponentsBuilder
# deliberately not included
_singleton: Optional[SpecificationBuilder] = None
def __new__(cls) -> SpecificationBuilder:
if not cls._singleton:
cls._singleton = super().__new__(cls)
cls._setup_instance(cls._singleton)
return cast(SpecificationBuilder, cls._singleton)
@classmethod
def _setup_instance(cls, instance):
instance._components = defaultdict(dict)
instance._contact = None
instance._description = None
instance._external = None
instance._license = None
instance._paths = defaultdict(dict)
instance._servers = []
instance._tags = {}
instance._security = []
instance._terms = None
instance._title = None
instance._urls = []
instance._version = None
@classmethod
def reset(cls):
cls._singleton = None
@property
def tags(self):
return self._tags
@property
def security(self):
return self._security
def url(self, value: str):
self._urls.append(value)
def describe(
self,
title: str,
version: str,
description: Optional[str] = None,
terms: Optional[str] = None,
):
self._title = title
self._version = version
self._description = description
self._terms = terms
def _do_describe(
self,
title: str,
version: str,
description: Optional[str] = None,
terms: Optional[str] = None,
):
if any([self._title, self._version, self._description, self._terms]):
return
self.describe(title, version, description, terms)
def tag(self, name: str, description: Optional[str] = None, **kwargs):
self._tags[name] = Tag(name, description=description, **kwargs)
def external(self, url: str, description: Optional[str] = None, **kwargs):
self._external = ExternalDocumentation(url, description=description)
def secured(
self,
name: str = None,
value: Optional[Union[str, Sequence[str]]] = None,
):
if value is None:
value = []
elif isinstance(value, str):
value = [value]
else:
value = list(value)
self._security.append(SecurityRequirement(name=name, value=value))
def contact(self, name: str = None, url: str = None, email: str = None):
kwargs = remove_nulls_from_kwargs(name=name, url=url, email=email)
self._contact = Contact(**kwargs)
def _do_contact(
self, name: str = None, url: str = None, email: str = None
):
if self._contact:
return
self.contact(name, url, email)
def license(self, name: str = None, url: str = None):
if name is not None:
self._license = License(name, url=url)
def _do_license(self, name: str = None, url: str = None):
if self._license:
return
self.license(name, url)
def operation(self, path: str, method: str, operation: OperationBuilder):
for _tag in operation.tags:
if _tag in self._tags.keys():
continue
self._tags[_tag] = Tag(_tag)
self._paths[path][method.lower()] = operation
def add_component(self, location: str, name: str, obj: Any):
self._components[location].update({name: obj})
def has_component(self, location: str, name: str) -> bool:
return name in self._components.get(location, {})
def add_security_scheme(
self,
ident: str,
type: Union[str, SecuritySchemeType],
*,
bearer_format: Optional[str] = None,
description: Optional[str] = None,
flows: Optional[Union[Flows, dict[str, Any]]] = None,
location: Union[
str, SecuritySchemeLocation
] = SecuritySchemeLocation.HEADER,
name: str = "authorization",
openid_connect_url: Optional[str] = None,
scheme: Union[
str, SecuritySchemeAuthorization
] = SecuritySchemeAuthorization.BEARER,
):
if isinstance(type, str):
type = SecuritySchemeType(type)
if isinstance(location, str):
location = SecuritySchemeLocation(location)
kwargs: dict[str, Any] = {"type": type, "description": description}
if type is SecuritySchemeType.API_KEY:
kwargs["location"] = location
kwargs["name"] = name
elif type is SecuritySchemeType.HTTP:
kwargs["scheme"] = scheme
kwargs["bearerFormat"] = bearer_format
elif type is SecuritySchemeType.OAUTH2:
kwargs["flows"] = flows
elif type is SecuritySchemeType.OPEN_ID_CONNECT:
kwargs["openIdConnectUrl"] = openid_connect_url
self.add_component(
"securitySchemes",
ident,
SecurityScheme(**kwargs),
) # type: ignore
def raw(self, data):
if "info" in data:
self.describe(
data["info"].get("title"),
data["info"].get("version"),
data["info"].get("description"),
data["info"].get("terms"),
)
if "servers" in data:
for server in data["servers"]:
self._servers.append(Server(**server))
if "paths" in data:
self._paths.update(data["paths"])
if "components" in data:
for location, component in data["components"].items():
self._components[location].update(component)
if "security" in data:
for security in data["security"]:
if not security:
self.secured()
else:
for key, value in security.items():
self.secured(key, value)
if "tags" in data:
for tag in data["tags"]:
self.tag(**tag)
if "externalDocs" in data:
self.external(**data["externalDocs"])
def build(self, app: Sanic) -> OpenAPI:
info = self._build_info()
paths = self._build_paths(app)
tags = self._build_tags()
security = self._build_security()
url_servers = getattr(self, "_urls", None)
servers = self._servers
existing = [
server.fields["url"].strip("/") for server in self._servers
]
if url_servers is not None:
for url_server in url_servers:
if url_server.strip("/") not in existing:
servers.append(Server(url=url_server))
components = (
Components(**self._components) if self._components else None
)
return OpenAPI(
info,
paths,
tags=tags,
servers=servers,
security=security,
components=components,
externalDocs=self._external,
)
def _build_info(self) -> Info:
kwargs = remove_nulls(
{
"description": self._description,
"termsOfService": self._terms,
"license": self._license,
"contact": self._contact,
},
deep=False,
)
return Info(self._title, self._version, **kwargs)
def _build_tags(self):
return [self._tags[k] for k in self._tags]
def _build_paths(self, app: Sanic) -> dict:
paths = {}
for path, operations in self._paths.items():
paths[path] = PathItem(
**{
k: v if isinstance(v, dict) else v.build()
for k, v in operations.items()
if isinstance(v, dict) or v._app is app
}
)
return paths
def _build_security(self):
return [
(
{sec.fields["name"]: sec.fields["value"]}
if sec.fields["name"] is not None
else {}
)
for sec in self.security
]
@@ -0,0 +1,36 @@
from enum import Enum, auto
class BaseEnum(Enum):
def _generate_next_value_(name, start, count, last_values):
parts = name.split("_")
return parts[0].lower() + "".join(part.title() for part in parts[1:])
class SecuritySchemeType(BaseEnum):
API_KEY = auto()
HTTP = auto()
OAUTH2 = auto()
OPEN_ID_CONNECT = auto()
class SecuritySchemeLocation(BaseEnum):
QUERY = auto()
HEADER = auto()
COOKIE = auto()
class SecuritySchemeAuthorization(BaseEnum):
def _generate_next_value_(name, start, count, last_values):
return name.title()
BASIC = auto()
BEARER = auto()
DIGEST = auto()
HOBA = "HOBA"
MUTUAL = auto()
NEGOTIATE = auto()
OAUTH = "OAuth"
SCRAM_SHA_1 = "SCRAM-SHA-1"
SCRAM_SHA_256 = "SCRAM-SHA-256"
VAPID = "vapid"
@@ -0,0 +1,455 @@
"""
Classes defined from the OpenAPI 3.0 specifications.
I.e., the objects described https://swagger.io/docs/specification
"""
from __future__ import annotations
from inspect import isclass
from typing import (
Any,
Literal,
Optional,
Union,
get_type_hints,
)
from sanic.exceptions import SanicException
from sanic_ext.utils.typing import (
contains_annotations,
is_msgspec,
is_pydantic,
)
from .types import Definition, Schema
class Reference(Schema):
def __init__(self, value):
super().__init__(**{"$ref": value})
def guard(self, fields: dict[str, Any]):
return fields
class Contact(Definition):
name: str
url: str
email: str
class License(Definition):
name: str
url: str
def __init__(self, name: str, **kwargs):
super().__init__(name=name, **kwargs)
class Info(Definition):
title: str
description: str
termsOfService: str
contact: Contact
license: License
version: str
def __init__(self, title: str, version: str, **kwargs):
super().__init__(title=title, version=version, **kwargs)
class Example(Definition):
summary: str
description: str
value: Any
externalValue: str
def __init__(self, value: Any = None, **kwargs):
super().__init__(value=value, **kwargs)
@staticmethod
def make(value: Any, **kwargs):
return Example(Schema.make(value), **kwargs)
@staticmethod
def external(value: Any, **kwargs):
return Example(externalValue=value, **kwargs)
class MediaType(Definition):
schema: Schema
example: Any
def __init__(self, schema: Union[Schema, dict[str, Any]], **kwargs):
if isinstance(schema, dict) and contains_annotations(schema):
schema = Schema.make(schema)
super().__init__(schema=schema, **kwargs)
@staticmethod
def make(value: Any):
if isinstance(value, dict):
kwargs = {}
if "schema" in value:
kwargs = {**value}
value = kwargs.pop("schema")
return MediaType(value, **kwargs)
# See https://github.com/sanic-org/sanic-ext/issues/152
# The following lines will automatically inject pydantic models as
# components if that feature is desired. Until that decision is made
# this commented out code will remain.
# elif isclass(value) and is_pydantic(value):
# return MediaType(Component(value))
return MediaType(Schema.make(value))
@staticmethod
def all(content: Any):
media_types = (
content if isinstance(content, dict) else {"*/*": content or {}}
)
return {x: MediaType.make(v) for x, v in media_types.items()}
class Response(Definition):
content: Union[Any, dict[str, Union[Any, MediaType]]]
description: Optional[str]
status: Union[Literal["default"], int]
__nullable__ = None
__ignore__ = ["status"]
def __init__(
self,
content: Optional[Union[Any, dict[str, Union[Any, MediaType]]]] = None,
status: Union[Literal["default"], int] = "default",
description: Optional[str] = None,
**kwargs,
):
super().__init__(
content=content,
status=status,
description=description,
**kwargs,
)
@staticmethod
def make(content, description: Optional[str] = None, **kwargs):
if not description:
description = "Default Response"
return Response(
MediaType.all(content), description=description, **kwargs
)
class RequestBody(Definition):
description: Optional[str]
required: Optional[bool]
content: Union[Any, dict[str, Union[Any, MediaType]]]
__nullable__ = None
def __init__(
self,
content: Union[Any, dict[str, Union[Any, MediaType]]],
required: Optional[bool] = None,
description: Optional[str] = None,
**kwargs,
):
"""Can be initialized with content in one of a few ways:
RequestBody(SomeModel)
RequestBody({"application/json": SomeModel})
RequestBody({"application/json": {"name": str}})
"""
super().__init__(
content=content,
required=required,
description=description,
**kwargs,
)
@staticmethod
def make(content: Any, **kwargs):
return RequestBody(MediaType.all(content), **kwargs)
class ExternalDocumentation(Definition):
url: str
description: str
__nullable__ = None
def __init__(self, url: str, description=None):
super().__init__(url=url, description=description)
@staticmethod
def make(url: str, description: Optional[str] = None):
return ExternalDocumentation(url, description)
class Header(Definition):
name: str
description: str
externalDocs: ExternalDocumentation
def __init__(self, url: str, description=None):
super().__init__(url=url, description=description)
@staticmethod
def make(url: str, description: Optional[str] = None):
return Header(url, description)
class Parameter(Definition):
name: str
schema: Union[type, Schema]
location: str
description: Optional[str]
required: Optional[bool]
deprecated: Optional[bool]
allowEmptyValue: Optional[bool]
__nullable__ = None
def __init__(
self,
name: str,
schema: Union[type, Schema] = str,
location: str = "query",
description: Optional[str] = None,
required: Optional[bool] = None,
deprecated: Optional[bool] = None,
allowEmptyValue: Optional[bool] = None,
**kwargs,
):
super().__init__(
name=name,
schema=schema,
location=location,
description=description,
required=required,
deprecated=deprecated,
allowEmptyValue=allowEmptyValue,
**kwargs,
)
@property
def fields(self):
values = super().fields
if "location" in values:
values["in"] = values.pop("location")
return values
@staticmethod
def make(name: str, schema: type, location: str, **kwargs):
if location == "path" and "required" not in kwargs:
kwargs["required"] = True
return Parameter(name, Schema.make(schema), location, **kwargs)
class Operation(Definition):
tags: list[str]
summary: str
description: str
operationId: str
requestBody: RequestBody
externalDocs: ExternalDocumentation
parameters: list[Parameter]
responses: dict[str, Response]
security: dict[str, list[str]]
callbacks: list[str] # TODO
deprecated: bool
servers: list[dict[str, str]]
class PathItem(Definition):
summary: str
description: str
get: Operation
put: Operation
post: Operation
delete: Operation
options: Operation
head: Operation
patch: Operation
trace: Operation
class Flow(Definition):
authorizationUrl: str
tokenUrl: str
refreshUrl: str
scopes: dict[str, str]
class Flows(Definition):
implicit: Flow
password: Flow
clientCredentials: Flow
authorizationCode: Flow
class SecurityRequirement(Definition):
name: str
value: list[str]
class SecurityScheme(Definition):
type: str
bearerFormat: str
description: str
flows: Flows
location: str
name: str
openIdConnectUrl: str
scheme: str
__nullable__ = None
def __init__(self, type: str, **kwargs):
super().__init__(type=type, **kwargs)
@property
def fields(self):
values = super().fields
if "location" in values:
values["in"] = values.pop("location")
return values
@staticmethod
def make(_type: str, cls: type, **kwargs):
params: dict[str, Any] = getattr(cls, "__dict__", {})
return SecurityScheme(_type, **params, **kwargs)
class ServerVariable(Definition):
default: str
description: str
enum: list[str]
def __init__(self, default: str, **kwargs):
super().__init__(default=default, **kwargs)
class Server(Definition):
url: str
description: str
variables: dict[str, ServerVariable]
__nullable__ = None
def __init__(
self,
url: str,
description: Optional[str] = None,
variables: Optional[dict[str, Any]] = None,
):
super().__init__(
url=url, description=description, variables=variables or {}
)
class Tag(Definition):
name: str
description: str
externalDocs: ExternalDocumentation
def __init__(self, name: str, **kwargs):
super().__init__(name=name, **kwargs)
class Components(Definition):
# This class is not being used in sanic-openapi right now, but the
# definition is kept here to keep in close accordance with the openapi
# spec, in case it is desired to be added later.
schemas: dict[str, Schema]
responses: dict[str, Response]
parameters: dict[str, Parameter]
examples: dict[str, Example]
requestBodies: dict[str, RequestBody]
headers: dict[str, Header]
securitySchemes: dict[str, SecurityScheme]
links: dict[str, Schema] # TODO
callbacks: dict[str, Schema] # TODO
def Component(
obj: Any, *, name: str = "", field: str = "schemas"
) -> Reference:
hints = get_type_hints(Components)
if field not in hints:
raise AttributeError(
f"Unknown field '{field}'. Must be a valid field per OAS3 "
"requirements. See "
"https://swagger.io/specification/#components-object."
)
if not isclass(obj) and not name:
raise SanicException(
f"Components {obj} must be created with a declared name"
)
if not name:
name = obj.__name__
from sanic_ext.extensions.openapi.builders import SpecificationBuilder
spec = SpecificationBuilder()
refval = f"#/components/{field}/{name}"
ref = Reference(refval)
if not spec.has_component(field, name):
prop_info = hints[field]
type_ = prop_info.__args__[1]
if is_msgspec(obj):
import msgspec
_, definitions = msgspec.json.schema_components(
[obj], ref_template="#/components/schemas/{name}"
)
if definitions:
for key, value in definitions.items():
spec.add_component(field, key, value)
elif is_pydantic(obj):
try:
schema = obj.schema
except AttributeError:
schema = obj.__pydantic_model__.schema
component = schema(ref_template="#/components/schemas/{model}")
definitions = component.pop("definitions", None)
if definitions:
for key, value in definitions.items():
spec.add_component(field, key, value)
spec.add_component(field, name, component)
else:
component = (
type_.make(obj) if hasattr(type_, "make") else type_(obj)
)
spec.add_component(field, name, component)
return ref
class OpenAPI(Definition):
openapi: str
info: Info
servers: list[Server]
paths: dict[str, PathItem]
components: Components
security: dict[str, SecurityScheme]
tags: list[Tag]
externalDocs: ExternalDocumentation
def __init__(self, info: Info, paths: dict[str, PathItem], **kwargs):
use = {k: v for k, v in kwargs.items() if v is not None}
super().__init__(openapi="3.0.3", info=info, paths=paths, **use)
@@ -0,0 +1,43 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from sanic_ext.extensions.openapi.builders import SpecificationBuilder
from ..base import Extension
from .blueprint import blueprint_factory
if TYPE_CHECKING:
from sanic_ext import Extend
class OpenAPIExtension(Extension):
name = "openapi"
def startup(self, bootstrap: Extend) -> None:
if self.app.config.OAS:
self.bp = blueprint_factory(self.app.config)
self.app.blueprint(self.bp)
bootstrap._openapi = SpecificationBuilder()
def label(self):
if self.app.config.OAS:
return self._make_url()
return ""
def included(self):
return self.config.OAS
def _make_url(self):
name = f"{self.bp.name}.index"
_server = (
None
if "SERVER_NAME" in self.app.config
else self.app.serve_location
) or None
_external = bool(_server) or "SERVER_NAME" in self.app.config
return (
f"{self.app.url_for(name, _external=_external, _server=_server)}"
)
@@ -0,0 +1,534 @@
"""
This module provides decorators which append
documentation to OperationStore() and components created in the blueprints.
"""
from collections.abc import Sequence
from functools import wraps
from inspect import isawaitable, isclass
from typing import (
Any,
Callable,
Literal,
Optional,
TypeVar,
Union,
overload,
)
from sanic import Blueprint
from sanic.exceptions import InvalidUsage, SanicException
from sanic_ext.extensions.openapi import definitions
from sanic_ext.extensions.openapi.builders import (
OperationStore,
SpecificationBuilder,
)
from sanic_ext.extensions.openapi.definitions import Component
from sanic_ext.extensions.openapi.types import (
Array,
Binary,
Boolean,
Byte,
Date,
DateTime,
Double,
Email,
Float,
Integer,
Long,
Object,
Password,
Schema,
String,
Time,
)
from sanic_ext.extras.validation.setup import do_validation, generate_schema
from sanic_ext.utils.extraction import extract_request
__all__ = (
"definitions",
"body",
"component",
"definition",
"deprecated",
"description",
"document",
"exclude",
"no_autodoc",
"operation",
"parameter",
"response",
"secured",
"summary",
"tag",
"Array",
"Binary",
"Boolean",
"Byte",
"Component",
"Date",
"DateTime",
"Double",
"Email",
"Float",
"Integer",
"Long",
"Object",
"Password",
"String",
"Time",
)
def _content_or_component(content):
if isclass(content):
spec = SpecificationBuilder()
if spec._components["schemas"].get(content.__name__):
content = definitions.Component(content)
return content
@overload
def exclude(flag: bool = True, *, bp: Blueprint) -> None: ...
@overload
def exclude(flag: bool = True) -> Callable: ...
def exclude(flag: bool = True, *, bp: Optional[Blueprint] = None):
if bp:
for route in bp.routes:
exclude(flag)(route.handler)
return
def inner(func):
OperationStore()[func].exclude(flag)
return func
return inner
T = TypeVar("T")
def operation(name: str) -> Callable[[T], T]:
def inner(func):
OperationStore()[func].name(name)
return func
return inner
def summary(text: str) -> Callable[[T], T]:
def inner(func):
OperationStore()[func].describe(summary=text)
return func
return inner
def description(text: str) -> Callable[[T], T]:
def inner(func):
OperationStore()[func].describe(description=text)
return func
return inner
def document(
url: Union[str, definitions.ExternalDocumentation],
description: Optional[str] = None,
) -> Callable[[T], T]:
if isinstance(url, definitions.ExternalDocumentation):
description = url.fields["description"]
url = url.fields["url"]
def inner(func):
OperationStore()[func].document(url, description)
return func
return inner
def tag(*args: Union[str, definitions.Tag]) -> Callable[[T], T]:
def inner(func):
OperationStore()[func].tag(*args)
return func
return inner
def deprecated(maybe_func=None) -> Callable[[T], T]:
def inner(func):
OperationStore()[func].deprecate()
return func
return inner(maybe_func) if maybe_func else inner
def no_autodoc(maybe_func=None) -> Callable[[T], T]:
def inner(func):
OperationStore()[func].disable_autodoc()
return func
return inner(maybe_func) if maybe_func else inner
def body(
content: Any,
*,
validate: bool = False,
body_argument: str = "body",
**kwargs,
) -> Callable[[T], T]:
body_content = _content_or_component(content)
params = {**kwargs}
validation_schema = None
if isinstance(body_content, definitions.RequestBody):
params = {**body_content.fields, **params}
body_content = params.pop("content")
if validate:
if callable(validate):
model = validate
else:
model = body_content
validation_schema = generate_schema(body_content)
def inner(func):
@wraps(func)
async def handler(*handler_args, **handler_kwargs):
request = extract_request(*handler_args)
if validate:
try:
data = request.json
allow_multiple = False
allow_coerce = False
except InvalidUsage:
data = request.form
allow_multiple = True
allow_coerce = True
await do_validation(
model=model,
data=data,
schema=validation_schema,
request=request,
kwargs=handler_kwargs,
body_argument=body_argument,
allow_multiple=allow_multiple,
allow_coerce=allow_coerce,
)
retval = func(*handler_args, **handler_kwargs)
if isawaitable(retval):
retval = await retval
return retval
if func in OperationStore():
OperationStore()[handler] = OperationStore().pop(func)
OperationStore()[handler].body(body_content, **params)
return handler
return inner
@overload
def parameter(
*,
parameter: definitions.Parameter,
**kwargs,
) -> Callable[[T], T]: ...
@overload
def parameter(
name: None,
schema: None,
location: None,
parameter: definitions.Parameter,
**kwargs,
) -> Callable[[T], T]: ...
@overload
def parameter(
name: str,
schema: Optional[Union[type, Schema]] = None,
location: Optional[str] = None,
parameter: None = None,
**kwargs,
) -> Callable[[T], T]: ...
def parameter(
name: Optional[str] = None,
schema: Optional[Union[type, Schema]] = None,
location: Optional[str] = None,
parameter: Optional[definitions.Parameter] = None,
**kwargs,
) -> Callable[[T], T]:
if parameter:
if name or schema or location:
raise SanicException(
"When using a parameter object, you cannot pass "
"other arguments."
)
if not schema:
schema = str
if not location:
location = "query"
def inner(func: Callable):
if parameter:
# Temporary solution convert in to location,
# need to be changed later.
fields = dict(parameter.fields)
if "in" in fields:
fields["location"] = fields.pop("in")
OperationStore()[func].parameter(**fields)
else:
OperationStore()[func].parameter(name, schema, location, **kwargs)
return func
return inner
def response(
status: Union[Literal["default"], int] = "default",
content: Any = str,
description: Optional[str] = None,
*,
response: Optional[definitions.Response] = None,
**kwargs,
) -> Callable[[T], T]:
if response:
if (
status != "default"
or content is not str
or description is not None
):
raise SanicException(
"When using a response object, you cannot pass "
"other arguments."
)
status = response.fields["status"]
content = response.fields["content"]
description = response.fields["description"]
def inner(func):
OperationStore()[func].response(status, content, description, **kwargs)
return func
return inner
def secured(*args, **kwargs) -> Callable[[T], T]:
def inner(func):
OperationStore()[func].secured(*args, **kwargs)
return func
return inner
Model = TypeVar("Model")
def component(
model: Optional[Model] = None,
*,
name: Optional[str] = None,
field: str = "schemas",
) -> Callable[[T], T]:
def wrap(m):
return component(m, name=name, field=field)
if not model:
return wrap
params = {}
if name:
params["name"] = name
if field:
params["field"] = field
definitions.Component(model, **params)
return model
def definition(
*,
exclude: Optional[bool] = None,
operation: Optional[str] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
document: Optional[Union[str, definitions.ExternalDocumentation]] = None,
tag: Optional[
Union[
Union[str, definitions.Tag], Sequence[Union[str, definitions.Tag]]
]
] = None,
deprecated: bool = False,
body: Optional[Union[dict[str, Any], definitions.RequestBody, Any]] = None,
parameter: Optional[
Union[
Union[dict[str, Any], definitions.Parameter, str],
list[Union[dict[str, Any], definitions.Parameter, str]],
]
] = None,
response: Optional[
Union[
Union[dict[str, Any], definitions.Response, Any],
list[Union[dict[str, Any], definitions.Response]],
]
] = None,
secured: Optional[dict[str, Any]] = None,
validate: bool = False,
body_argument: str = "body",
) -> Callable[[T], T]:
validation_schema = None
body_content = None
def inner(func):
nonlocal validation_schema
nonlocal body_content
glbl = globals()
if body:
kwargs = {}
content = body
if isinstance(content, definitions.RequestBody):
kwargs = content.fields
elif isinstance(content, dict):
if "content" in content:
kwargs = content
else:
kwargs["content"] = content
else:
content = _content_or_component(content)
kwargs["content"] = content
if validate:
kwargs["validate"] = validate
kwargs["body_argument"] = body_argument
func = glbl["body"](**kwargs)(func)
if exclude is not None:
func = glbl["exclude"](exclude)(func)
if operation:
func = glbl["operation"](operation)(func)
if summary:
func = glbl["summary"](summary)(func)
if description:
func = glbl["description"](description)(func)
if document:
kwargs = {}
if isinstance(document, str):
kwargs["url"] = document
else:
kwargs["url"] = document.fields["url"]
kwargs["description"] = document.fields["description"]
func = glbl["document"](**kwargs)(func)
if tag:
taglist = []
op = (
"extend"
if isinstance(tag, (list, tuple, set, frozenset))
else "append"
)
getattr(taglist, op)(tag)
func = glbl["tag"](*taglist)(func)
if deprecated:
func = glbl["deprecated"]()(func)
if parameter:
paramlist = []
op = (
"extend"
if isinstance(parameter, (list, tuple, set, frozenset))
else "append"
)
getattr(paramlist, op)(parameter)
for param in paramlist:
kwargs = {}
if isinstance(param, definitions.Parameter):
kwargs = param.fields
if "in" in kwargs:
kwargs["location"] = kwargs.pop("in")
elif isinstance(param, dict) and "name" in param:
kwargs = param
elif isinstance(param, str):
kwargs["name"] = param
else:
raise SanicException(
"parameter must be a Parameter instance, a string, or "
"a dictionary containing at least 'name'."
)
if "schema" not in kwargs:
kwargs["schema"] = str
func = glbl["parameter"](**kwargs)(func)
if response:
resplist = []
op = (
"extend"
if isinstance(response, (list, tuple, set, frozenset))
else "append"
)
getattr(resplist, op)(response)
if len(resplist) > 1 and any(
not isinstance(item, definitions.Response)
and not isinstance(item, dict)
for item in resplist
):
raise SanicException(
"Cannot use multiple bare custom models to define "
"multiple responses like openapi.definition(response=["
"MyModel1, MyModel2]). Instead, you should wrap them in a "
"dict or a Response object. See "
"https://sanic.dev/en/plugins/sanic-ext/openapi/decorators"
".html#response for more details."
)
for resp in resplist:
kwargs = {}
if isinstance(resp, definitions.Response):
kwargs = resp.fields
elif isinstance(resp, dict):
if "content" in resp:
kwargs = resp
else:
kwargs["content"] = resp
else:
kwargs["content"] = resp
if "status" not in kwargs:
kwargs["status"] = "default"
func = glbl["response"](**kwargs)(func)
if secured:
func = glbl["secured"](secured)(func)
return func
return inner
@@ -0,0 +1,452 @@
import json
import uuid
from dataclasses import MISSING, is_dataclass
from datetime import date, datetime, time
from enum import Enum
from inspect import getmembers, isclass, isfunction, ismethod
from typing import (
Any,
Optional,
Union,
get_args,
get_origin,
get_type_hints,
)
from sanic_routing.patterns import alpha, ext, nonemptystr, parse_date, slug
from sanic_ext.utils.typing import (
UnionType,
is_attrs,
is_generic,
is_msgspec,
is_pydantic,
)
try:
import attrs
NOTHING: Any = attrs.NOTHING
except ImportError:
NOTHING = object()
try:
import msgspec
from msgspec.inspect import Metadata as MsgspecMetadata
from msgspec.inspect import type_info as msgspec_type_info
MsgspecMetadata: Any = MsgspecMetadata
NODEFAULT: Any = msgspec.NODEFAULT
UNSET: Any = msgspec.UNSET
class MsgspecAdapter(msgspec.Struct):
name: str
default: Any
metadata: dict
except ImportError:
def msgspec_type_info(struct):
pass
class MsgspecAdapter:
pass
MsgspecMetadata = object()
NODEFAULT = object()
UNSET = object()
class Definition:
__nullable__: Optional[list[str]] = []
__ignore__: Optional[list[str]] = []
def __init__(self, **kwargs):
self._fields: dict[str, Any] = self.guard(kwargs)
@property
def fields(self):
return self._fields
def guard(self, fields):
return {
k: v
for k, v in fields.items()
if k in _properties(self).keys() or k.startswith("x-")
}
def serialize(self):
return {
k: self._value(v)
for k, v in _serialize(self.fields).items()
if (
k not in self.__ignore__
and (
v is not None
or (
isinstance(self.__nullable__, list)
and (not self.__nullable__ or k in self.__nullable__)
)
)
)
}
def __str__(self):
return json.dumps(self.serialize())
@staticmethod
def _value(value):
if isinstance(value, Enum):
return value.value
return value
class Schema(Definition):
title: str
description: str
type: str
format: str
nullable: bool
required: bool
default: None
example: None
oneOf: list[Definition]
anyOf: list[Definition]
allOf: list[Definition]
additionalProperties: dict[str, str]
multipleOf: int
maximum: int
exclusiveMaximum: bool
minimum: int
exclusiveMinimum: bool
maxLength: int
minLength: int
pattern: str
enum: Union[list[Any], Enum]
@staticmethod
def make(value, **kwargs):
_type = type(value)
origin = get_origin(value)
args = get_args(value)
if origin in (Union, UnionType):
if type(None) in args:
kwargs["nullable"] = True
filtered = [arg for arg in args if arg is not type(None)] # noqa
if len(filtered) == 1:
return Schema.make(filtered[0], **kwargs)
return Schema(
oneOf=[Schema.make(arg) for arg in filtered], **kwargs
)
for field in ("type", "format"):
kwargs.pop(field, None)
if isinstance(value, Schema):
return value
if value is bool:
return Boolean(**kwargs)
elif value is int:
return Integer(**kwargs)
elif value is float:
return Float(**kwargs)
elif value is str or value in (nonemptystr, ext, slug, alpha):
return String(**kwargs)
elif value is bytes:
return Byte(**kwargs)
elif value is bytearray:
return Binary(**kwargs)
elif value is date:
return Date(**kwargs)
elif value is time:
return Time(**kwargs)
elif value is datetime or value is parse_date:
return DateTime(**kwargs)
elif value is uuid.UUID:
return UUID(**kwargs)
elif value is Any:
return AnyValue(**kwargs)
if _type is bool:
return Boolean(default=value, **kwargs)
elif _type is int:
return Integer(default=value, **kwargs)
elif _type is float:
return Float(default=value, **kwargs)
elif _type is str:
return String(default=value, **kwargs)
elif _type is bytes:
return Byte(default=value, **kwargs)
elif _type is bytearray:
return Binary(default=value, **kwargs)
elif _type is date:
return Date(**kwargs)
elif _type is time:
return Time(**kwargs)
elif _type is datetime:
return DateTime(**kwargs)
elif _type is uuid.UUID:
return UUID(**kwargs)
elif _type is list:
if len(value) == 0:
schema = Schema(nullable=True)
elif len(value) == 1:
schema = Schema.make(value[0])
else:
schema = Schema(oneOf=[Schema.make(x) for x in value])
return Array(schema, **kwargs)
elif _type is dict:
return Object.make(value, **kwargs)
elif (
(is_generic(value) or is_generic(_type))
and origin is dict
and len(args) == 2
):
kwargs["additionalProperties"] = Schema.make(args[1])
return Object(**kwargs)
elif (is_generic(value) or is_generic(_type)) and origin is list:
kwargs.pop("items", None)
return Array(Schema.make(args[0]), **kwargs)
elif _type is type(Enum):
available = [item.value for item in value.__members__.values()]
available_types = list({type(item) for item in available})
schema_type = (
available_types[0] if len(available_types) == 1 else "string"
)
return Schema.make(
schema_type,
enum=[item.value for item in value.__members__.values()],
)
else:
return Object.make(value, **kwargs)
class Boolean(Schema):
def __init__(self, **kwargs):
super().__init__(type="boolean", **kwargs)
class Integer(Schema):
def __init__(self, **kwargs):
super().__init__(type="integer", format="int32", **kwargs)
class Long(Schema):
def __init__(self, **kwargs):
super().__init__(type="integer", format="int64", **kwargs)
class Float(Schema):
def __init__(self, **kwargs):
super().__init__(type="number", format="float", **kwargs)
class Double(Schema):
def __init__(self, **kwargs):
super().__init__(type="number", format="double", **kwargs)
class String(Schema):
def __init__(self, **kwargs):
super().__init__(type="string", **kwargs)
class Byte(Schema):
def __init__(self, **kwargs):
super().__init__(type="string", format="byte", **kwargs)
class Binary(Schema):
def __init__(self, **kwargs):
super().__init__(type="string", format="binary", **kwargs)
class Date(Schema):
def __init__(self, **kwargs):
super().__init__(type="string", format="date", **kwargs)
class Time(Schema):
def __init__(self, **kwargs):
super().__init__(type="string", format="time", **kwargs)
class DateTime(Schema):
def __init__(self, **kwargs):
super().__init__(type="string", format="date-time", **kwargs)
class Password(Schema):
def __init__(self, **kwargs):
super().__init__(type="string", format="password", **kwargs)
class Email(Schema):
def __init__(self, **kwargs):
super().__init__(type="string", format="email", **kwargs)
class UUID(Schema):
def __init__(self, **kwargs):
super().__init__(type="string", format="uuid", **kwargs)
class AnyValue(Schema):
@classmethod
def make(cls, value: Any, **kwargs):
return cls(
AnyValue={},
**kwargs,
)
class Object(Schema):
properties: dict[str, Schema]
maxProperties: int
minProperties: int
def __init__(
self, properties: Optional[dict[str, Schema]] = None, **kwargs
):
if properties:
kwargs["properties"] = properties
super().__init__(type="object", **kwargs)
@classmethod
def make(cls, value: Any, **kwargs):
extra: dict[str, Any] = {}
# Extract from field metadata if msgspec, pydantic, attrs, or dataclass
if isclass(value):
fields = ()
if is_pydantic(value):
try:
value = value.__pydantic_model__
except AttributeError:
...
extra = value.schema()["properties"]
elif is_attrs(value):
fields = value.__attrs_attrs__
elif is_dataclass(value):
fields = value.__dataclass_fields__.values()
elif is_msgspec(value):
# adapt to msgspec metadata layout -- annotated type --
# to match dataclass "metadata" attribute
fields = [
MsgspecAdapter(
name=f.name,
default=(
MISSING
if f.default in (UNSET, NODEFAULT)
else f.default
),
metadata=getattr(f.type, "extra", {}),
)
for f in msgspec_type_info(value).fields
]
if fields:
extra = {
field.name: {
"title": field.name.title(),
**(
{"default": field.default}
if field.default not in (MISSING, NOTHING)
else {}
),
**dict(field.metadata).get("openapi", {}),
}
for field in fields
}
return cls(
{
k: Schema.make(v, **extra.get(k, {}))
for k, v in _properties(value).items()
},
**kwargs,
)
class Array(Schema):
items: Any
maxItems: int
minItems: int
uniqueItems: bool
def __init__(self, items: Any, **kwargs):
super().__init__(type="array", items=Schema.make(items), **kwargs)
def _serialize(value) -> Any:
if isinstance(value, Definition):
return value.serialize()
if isinstance(value, type) and issubclass(value, Enum):
return [item.value for item in value.__members__.values()]
if isinstance(value, dict):
return {k: _serialize(v) for k, v in value.items()}
if isinstance(value, list):
return [_serialize(v) for v in value]
return value
def _properties(value: object) -> dict:
try:
fields = {
x: val
for x, v in getmembers(value, _is_property)
if (val := _extract(v)) and x in value.__dict__
}
except AttributeError:
fields = {}
cls = value if callable(value) else value.__class__
extra = value if isinstance(value, dict) else {}
try:
annotations = get_type_hints(cls)
except (NameError, TypeError):
if hasattr(value, "__annotations__"):
annotations = value.__annotations__
else:
annotations = {}
annotations.pop("return", None)
try:
output = {
k: v
for k, v in {**fields, **annotations, **extra}.items()
if not k.startswith("_")
and not (
isclass(v)
and isclass(cls)
and v.__qualname__.endswith(
f"{getattr(cls, '__name__', '<unknown>')}."
f"{getattr(v, '__name__', '<unknown>')}"
)
)
}
except TypeError:
return {}
return output
def _extract(item):
if isinstance(item, property):
hints = get_type_hints(item.fget)
return hints.get("return")
return item
def _is_property(item):
return not isfunction(item) and not ismethod(item)
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<title>__HTML_TITLE__</title>
<style>
body {
margin: 0;
padding: 0;
}
__HTML_CUSTOM_CSS__
</style>
</head>
<body>
<redoc spec-url="__URL_PREFIX__/openapi.json"></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"></script>
</body>
</html>
@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/__VERSION__/swagger-ui.min.css">
<title>__HTML_TITLE__</title>
<style>
body {
margin: 0;
padding: 0;
}
__HTML_CUSTOM_CSS__
</style>
</head>
<body>
<div id="openapi"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/__VERSION__/swagger-ui-bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/__VERSION__/swagger-ui-standalone-preset.min.js"></script>
<script>
window.onload = function () {
const ui = SwaggerUIBundle({
url: "__URL_PREFIX__/openapi.json",
dom_id: "#openapi",
configUrl: "__URL_PREFIX__/swagger-config",
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
})
}
</script>
</body>
</html>