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>
456 lines
12 KiB
Python
456 lines
12 KiB
Python
"""
|
|
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)
|