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,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)