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:
@@ -0,0 +1,180 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import is_dataclass
|
||||
from inspect import isclass, iscoroutine
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Optional,
|
||||
get_args,
|
||||
get_type_hints,
|
||||
)
|
||||
|
||||
from sanic import Request
|
||||
from sanic.app import Sanic
|
||||
from sanic.exceptions import ServerError
|
||||
|
||||
from sanic_ext.exceptions import InitError
|
||||
from sanic_ext.utils.typing import (
|
||||
is_attrs,
|
||||
is_msgspec,
|
||||
is_optional,
|
||||
is_pydantic,
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .registry import ConstantRegistry, InjectionRegistry
|
||||
|
||||
|
||||
class Constructor:
|
||||
EXEMPT_ANNOTATIONS = (Request,)
|
||||
|
||||
def __init__(
|
||||
self, func: Callable[..., Any], request_arg: Optional[str] = None
|
||||
):
|
||||
self.func = func
|
||||
self.injections: dict[str, tuple[type, Constructor]] = {}
|
||||
self.constants: dict[str, Any] = {}
|
||||
self.pass_kwargs: bool = False
|
||||
self.request_arg = request_arg
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"<{self.__class__.__name__}:{self.func.__name__}>"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__}(func={self.func.__name__})>"
|
||||
|
||||
async def __call__(self, request, **kwargs):
|
||||
try:
|
||||
args = await gather_args(self.injections, request, **kwargs)
|
||||
args.update(self.constants)
|
||||
if self.pass_kwargs:
|
||||
args.update(kwargs)
|
||||
|
||||
if self.request_arg:
|
||||
args[self.request_arg] = request
|
||||
|
||||
retval = self.func(**args)
|
||||
|
||||
if iscoroutine(retval):
|
||||
retval = await retval
|
||||
return retval
|
||||
except TypeError as e:
|
||||
raise ServerError(
|
||||
"Failure to inject dependencies. Make sure that all "
|
||||
f"dependencies for '{self.func.__name__}' have been "
|
||||
"registered."
|
||||
) from e
|
||||
|
||||
def prepare(
|
||||
self,
|
||||
app: Sanic,
|
||||
injection_registry: InjectionRegistry,
|
||||
constant_registry: ConstantRegistry,
|
||||
allowed_types: set[type[object]],
|
||||
) -> None:
|
||||
hints = self._get_hints()
|
||||
hints.pop("return", None)
|
||||
missing = []
|
||||
for param, annotation in hints.items():
|
||||
if param in constant_registry:
|
||||
self.constants[param] = getattr(app.config, param.upper())
|
||||
if annotation in allowed_types:
|
||||
self.pass_kwargs = True
|
||||
if is_optional(annotation):
|
||||
annotation = get_args(annotation)[0]
|
||||
if not isclass(annotation):
|
||||
missing.append((param, annotation))
|
||||
continue
|
||||
if issubclass(annotation, Request):
|
||||
self.request_arg = param
|
||||
if (
|
||||
annotation not in self.EXEMPT_ANNOTATIONS
|
||||
and not issubclass(annotation, self.EXEMPT_ANNOTATIONS)
|
||||
and annotation not in allowed_types
|
||||
):
|
||||
dependency = injection_registry.get(annotation)
|
||||
if not dependency:
|
||||
missing.append((param, annotation))
|
||||
continue
|
||||
self.injections[param] = (annotation, dependency)
|
||||
|
||||
if missing:
|
||||
dependencies = "\n".join(
|
||||
[f" - {param}: {annotation}" for param, annotation in missing]
|
||||
)
|
||||
raise InitError(
|
||||
"Unable to resolve dependencies for "
|
||||
f"'{self.func.__name__}'. Could not find the following "
|
||||
f"dependencies:\n{dependencies}.\nMake sure the dependencies "
|
||||
"are declared using ext.injection. See "
|
||||
"https://sanicframework.org/en/plugins/sanic-ext/injection."
|
||||
"html#injecting-services for more details."
|
||||
)
|
||||
|
||||
checked: set[type[object]] = set()
|
||||
current: set[type[object]] = set()
|
||||
self.check_circular(checked, current)
|
||||
|
||||
def check_circular(
|
||||
self,
|
||||
checked: set[type[object]],
|
||||
current: set[type[object]],
|
||||
) -> None:
|
||||
dependencies = set(self.injections.values())
|
||||
for dependency, constructor in dependencies:
|
||||
self._visit(dependency, constructor, checked, current)
|
||||
|
||||
def _visit(
|
||||
self,
|
||||
dependency: type[object],
|
||||
constructor: Constructor,
|
||||
checked: set[type[object]],
|
||||
current: set[type[object]],
|
||||
):
|
||||
if dependency in checked:
|
||||
return
|
||||
elif dependency in current:
|
||||
raise InitError(
|
||||
"Circular dependency injection detected on "
|
||||
f"'{self.func.__name__}'. Check dependencies of "
|
||||
f"'{constructor.func.__name__}' which may contain "
|
||||
f"circular dependency chain with {dependency}."
|
||||
)
|
||||
|
||||
current.add(dependency)
|
||||
constructor.check_circular(checked, current)
|
||||
current.remove(dependency)
|
||||
checked.add(dependency)
|
||||
|
||||
def _get_hints(self):
|
||||
if (
|
||||
not isclass(self.func)
|
||||
or is_dataclass(self.func)
|
||||
or is_attrs(self.func)
|
||||
or is_pydantic(self.func)
|
||||
or is_msgspec(self.func)
|
||||
):
|
||||
return get_type_hints(self.func)
|
||||
elif isclass(self.func):
|
||||
return get_type_hints(self.func.__init__)
|
||||
raise InitError(f"Cannot get type hints for {self.func}")
|
||||
|
||||
|
||||
async def gather_args(injections, request, **kwargs) -> dict[str, Any]:
|
||||
return {
|
||||
name: await do_cast(_type, constructor, request, **kwargs)
|
||||
for name, (_type, constructor) in injections.items()
|
||||
}
|
||||
|
||||
|
||||
async def do_cast(_type, constructor, request, **kwargs):
|
||||
cast = constructor if constructor else _type
|
||||
args = [request] if constructor else []
|
||||
|
||||
retval = cast(*args, **kwargs)
|
||||
if iscoroutine(retval):
|
||||
retval = await retval
|
||||
return retval
|
||||
@@ -0,0 +1,22 @@
|
||||
from ..base import Extension
|
||||
from .injector import add_injection
|
||||
from .registry import ConstantRegistry, InjectionRegistry
|
||||
|
||||
|
||||
class InjectionExtension(Extension):
|
||||
name = "injection"
|
||||
|
||||
def startup(self, bootstrap) -> None:
|
||||
self.constant_registry = ConstantRegistry(self.app.config)
|
||||
self.registry = InjectionRegistry()
|
||||
add_injection(self.app, self.registry, self.constant_registry)
|
||||
bootstrap._injection_registry = self.registry
|
||||
bootstrap._constant_registry = self.constant_registry
|
||||
if self.config.INJECTION_LOAD_CUSTOM_CONSTANTS:
|
||||
self.app.ext.load_constants()
|
||||
|
||||
def label(self):
|
||||
return (
|
||||
f"{self.registry.length} dependencies; "
|
||||
f"{self.constant_registry.length} constants"
|
||||
)
|
||||
@@ -0,0 +1,123 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
from inspect import getmembers, isclass, isfunction
|
||||
from typing import Any, Callable, Optional, get_type_hints
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.constants import HTTP_METHODS
|
||||
|
||||
from sanic_ext.config import PRIORITY
|
||||
from sanic_ext.extensions.injection.constructor import gather_args
|
||||
|
||||
from .registry import ConstantRegistry, InjectionRegistry, SignatureRegistry
|
||||
|
||||
|
||||
def add_injection(
|
||||
app: Sanic,
|
||||
injection_registry: InjectionRegistry,
|
||||
constant_registry: ConstantRegistry,
|
||||
) -> None:
|
||||
signature_registry = _setup_signature_registry(
|
||||
app, injection_registry, constant_registry
|
||||
)
|
||||
|
||||
@app.listener("before_server_start", priority=PRIORITY)
|
||||
async def finalize_injections(app: Sanic, _):
|
||||
router_converters = {
|
||||
allowed[0] for allowed in app.router.regex_types.values()
|
||||
}
|
||||
router_types = set()
|
||||
for converter in router_converters:
|
||||
if isclass(converter):
|
||||
router_types.add(converter)
|
||||
elif isfunction(converter):
|
||||
hints = get_type_hints(converter)
|
||||
if return_type := hints.get("return"):
|
||||
router_types.add(return_type)
|
||||
injection_registry.finalize(app, constant_registry, router_types)
|
||||
|
||||
injection_signal = app.ext.config.INJECTION_SIGNAL
|
||||
injection_priority = app.ext.config.INJECTION_PRIORITY
|
||||
|
||||
@app.signal(injection_signal, priority=injection_priority)
|
||||
async def inject_kwargs(request, **_):
|
||||
nonlocal signature_registry
|
||||
|
||||
for name in (
|
||||
request.route.name,
|
||||
f"{request.route.name}_{request.method.lower()}",
|
||||
):
|
||||
dependencies, constants = signature_registry.get(
|
||||
name, (None, None)
|
||||
)
|
||||
if dependencies or constants:
|
||||
break
|
||||
|
||||
if dependencies:
|
||||
injected_args = await gather_args(
|
||||
dependencies, request, **request.match_info
|
||||
)
|
||||
request.match_info.update(injected_args)
|
||||
if constants:
|
||||
request.match_info.update(constants)
|
||||
|
||||
|
||||
def _http_method_predicate(member):
|
||||
return isfunction(member) and member.__name__ in HTTP_METHODS
|
||||
|
||||
|
||||
def _setup_signature_registry(
|
||||
app: Sanic,
|
||||
injection_registry: InjectionRegistry,
|
||||
constant_registry: ConstantRegistry,
|
||||
) -> SignatureRegistry:
|
||||
registry = SignatureRegistry()
|
||||
|
||||
@app.listener("before_server_start", priority=PRIORITY - 1)
|
||||
async def setup_signatures(app, _):
|
||||
nonlocal registry
|
||||
|
||||
for route in app.router.routes:
|
||||
if ".openapi." in route.name:
|
||||
continue
|
||||
handlers = [(route.name, route.handler)]
|
||||
viewclass = getattr(route.handler, "view_class", None)
|
||||
if viewclass:
|
||||
handlers = [
|
||||
(f"{route.name}_{name}", member)
|
||||
for name, member in getmembers(
|
||||
viewclass, _http_method_predicate
|
||||
)
|
||||
]
|
||||
for name, handler in handlers:
|
||||
if route_handler := getattr(
|
||||
handler, "__route_handler__", None
|
||||
):
|
||||
handler = route_handler
|
||||
if isinstance(handler, partial):
|
||||
if handler.func == app._websocket_handler:
|
||||
handler = handler.args[0]
|
||||
else:
|
||||
handler = handler.func
|
||||
try:
|
||||
hints = get_type_hints(handler)
|
||||
except TypeError:
|
||||
continue
|
||||
|
||||
dependencies: dict[
|
||||
str, tuple[type, Optional[Callable[..., Any]]]
|
||||
] = {}
|
||||
constants: dict[str, Any] = {}
|
||||
for param, annotation in hints.items():
|
||||
if annotation in injection_registry:
|
||||
dependencies[param] = (
|
||||
annotation,
|
||||
injection_registry[annotation],
|
||||
)
|
||||
if param in constant_registry:
|
||||
constants[param] = app.config[param.upper()]
|
||||
|
||||
registry.register(name, dependencies, constants)
|
||||
|
||||
return registry
|
||||
@@ -0,0 +1,115 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from sanic.app import Sanic
|
||||
from sanic.config import Config
|
||||
|
||||
from .constructor import Constructor
|
||||
|
||||
|
||||
class InjectionRegistry:
|
||||
def __init__(self):
|
||||
self._registry: dict[type, Optional[Callable[..., Any]]] = {}
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._registry[key]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self._registry)
|
||||
|
||||
def __contains__(self, other: Any):
|
||||
return other in self._registry
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self._registry.get(key, default)
|
||||
|
||||
def register(
|
||||
self,
|
||||
_type: type,
|
||||
constructor: Optional[Callable[..., Any]],
|
||||
request_arg: Optional[str] = None,
|
||||
) -> None:
|
||||
constructor = constructor or _type
|
||||
constructor = Constructor(constructor, request_arg=request_arg)
|
||||
self._registry[_type] = constructor
|
||||
|
||||
def finalize(
|
||||
self, app: Sanic, constant_registry: ConstantRegistry, allowed_types
|
||||
):
|
||||
for constructor in self._registry.values():
|
||||
if isinstance(constructor, Constructor):
|
||||
constructor.prepare(
|
||||
app, self, constant_registry, allowed_types
|
||||
)
|
||||
|
||||
@property
|
||||
def length(self):
|
||||
return len(self._registry)
|
||||
|
||||
|
||||
class SignatureRegistry:
|
||||
def __init__(self):
|
||||
self._registry: dict[
|
||||
str,
|
||||
tuple[
|
||||
dict[str, tuple[type, Optional[Callable[..., Any]]]],
|
||||
dict[str, Any],
|
||||
],
|
||||
] = {}
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._registry[key]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self._registry)
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self._registry.get(key, default)
|
||||
|
||||
def register(
|
||||
self,
|
||||
route_name: str,
|
||||
dependencies: dict[str, tuple[type, Optional[Callable[..., Any]]]],
|
||||
constants: Optional[dict[str, Any]] = None,
|
||||
) -> None:
|
||||
self._registry[route_name] = (dependencies, constants or {})
|
||||
|
||||
|
||||
class ConstantRegistry:
|
||||
def __init__(self, config: Config):
|
||||
self._config = config
|
||||
self._registry: set[str] = set()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self._registry)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._registry)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._registry[key]
|
||||
|
||||
def __contains__(self, other: Any):
|
||||
return other in self._registry
|
||||
|
||||
def register(self, key: str, value: Any, overwrite: bool):
|
||||
attribute = key.upper()
|
||||
if attribute in self._config and not overwrite:
|
||||
raise ValueError(
|
||||
f"A value for {attribute} has already been assigned"
|
||||
)
|
||||
key = key.lower()
|
||||
setattr(self._config, attribute, value)
|
||||
return self._registry.add(key)
|
||||
|
||||
def get(self, key: str):
|
||||
key = key.lower()
|
||||
if key not in self._registry:
|
||||
raise ValueError
|
||||
attribute = key.upper()
|
||||
return getattr(self._config, attribute)
|
||||
|
||||
@property
|
||||
def length(self):
|
||||
return len(self._registry)
|
||||
Reference in New Issue
Block a user