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,416 @@
from __future__ import annotations
import asyncio
from dataclasses import dataclass
from itertools import count
from typing import Optional
from uuid import UUID
import pytest
from sanic import Request, json, text
from sanic.exceptions import SanicException
from sanic.views import HTTPMethodView
from sanic_ext import Extend
@dataclass
class Name:
name: str
@dataclass
class PersonID:
person_id: int
@dataclass
class NamedPerson:
name: str
@dataclass
class Person:
person_id: PersonID
name: str
age: int
@classmethod
async def create(cls, person_id: int) -> Person:
return cls(person_id=PersonID(person_id), name="noname", age=111)
@dataclass
class AsyncName:
name: str
def __await__(self):
return self
counter = count()
class A:
@classmethod
def create(cls, request: Request):
next(counter)
return cls()
class B:
def __init__(self, a: A):
self.a = a
@classmethod
def create(cls, request: Request, a: A):
next(counter)
return cls(a)
class C:
def __init__(self, b: B):
self.b = b
@classmethod
def create(cls, request: Request, b: B):
next(counter)
return cls(b)
class D:
def __init__(self, a: A):
self.a = a
@classmethod
def create(cls, request: Request, a: A):
next(counter)
return cls(a)
class E:
def __init__(self, c: C, d: D):
self.c = c
self.d = d
@classmethod
def create(cls, request: Request, c: C, d: D):
next(counter)
return cls(c, d)
class Alpha: ...
class Beta:
def __init__(self, alpha: Alpha) -> None:
self.alpha = alpha
class Gamma:
def __init__(self, beta: Beta, request: Optional[Request] = None) -> None:
self.beta = beta
self.request = request
class Delta:
def __init__(self, gamma: Gamma, request: Request) -> None:
self.gamma = gamma
self.request = request
@dataclass
class Foo:
ident: UUID
def make_gamma_with_request(beta: Beta, request: Request):
return Gamma(beta, request)
def make_gamma_without_request(beta: Beta):
return Gamma(beta)
def test_injection_not_allowed_when_ext_disabled(bare_app):
ext = Extend(bare_app, built_in_extensions=False)
with pytest.raises(
SanicException, match="Injection extension not enabled"
):
ext.add_dependency(1, 2)
def test_injection_of_matched_object(app):
@app.get("/person/<name:str>")
def handler(request, name: Name):
request.ctx.name = name
return text(name.name)
app.ext.add_dependency(Name)
request, response = app.test_client.get("/person/george")
assert response.body == b"george"
assert isinstance(request.ctx.name, Name)
assert request.ctx.name.name == "george"
def test_injection_of_matched_object_as_deprecated_injection(app):
@app.get("/person/<name:str>")
def handler(request, name: Name):
request.ctx.name = name
return text(name.name)
message = (
"The 'ext.injection' method has been deprecated and will be removed "
"in v22.6. Please use 'ext.add_dependency' instead."
)
with pytest.warns(DeprecationWarning, match=message):
app.ext.injection(Name)
request, response = app.test_client.get("/person/george")
assert response.body == b"george"
assert isinstance(request.ctx.name, Name)
assert request.ctx.name.name == "george"
def test_injection_of_simple_object(app):
@app.get("/person/<name>")
def handler(request, name: str, person: NamedPerson):
request.ctx.person = person
return text(person.name)
app.ext.add_dependency(NamedPerson)
request, response = app.test_client.get("/person/george")
assert response.body == b"george"
assert isinstance(request.ctx.person, NamedPerson)
assert request.ctx.person.name == "george"
def test_injection_of_object_with_constructor(app):
@app.get("/person/<person_id:int>")
async def person_details(request, person_id: PersonID, person: Person):
request.ctx.person_id = person_id
request.ctx.person = person
return text(
f"{person.person_id.person_id}\n{person.name}\n{person.age}"
)
app.ext.add_dependency(Person, Person.create)
app.ext.add_dependency(PersonID)
request, response = app.test_client.get("/person/999")
assert response.body == b"999\nnoname\n111"
assert isinstance(request.ctx.person_id, PersonID)
assert isinstance(request.ctx.person, Person)
assert request.ctx.person.person_id == request.ctx.person_id
assert request.ctx.person.person_id.person_id == 999
assert request.ctx.person.name == "noname"
assert request.ctx.person.age == 111
def test_injection_on_cbv(app):
class View(HTTPMethodView, attach=app, uri="/person/<name:str>"):
async def get(self, request, name: Name):
request.ctx.name = name
return text(name.name)
@staticmethod
async def post(request, name: Name):
request.ctx.name = name
return text(name.name)
app.ext.add_dependency(Name)
for client in (app.test_client.get, app.test_client.post):
request, response = client("/person/george")
assert response.body == b"george"
assert isinstance(request.ctx.name, Name)
assert request.ctx.name.name == "george"
def test_nested_dependencies(app):
app.ext.add_dependency(A, A.create)
app.ext.add_dependency(B, B.create)
app.ext.add_dependency(C, C.create)
app.ext.add_dependency(D, D.create)
app.ext.add_dependency(E, E.create)
@app.get("/")
async def nested(request: Request, c: C, e: E):
return json(
[
isinstance(c, C),
isinstance(c.b, B),
isinstance(c.b.a, A),
isinstance(e, E),
isinstance(e.c, C),
isinstance(e.d, D),
isinstance(e.d.a, A),
]
)
_, response = app.test_client.get("/")
assert all(response.json)
# TODO:
# - After implementing https://github.com/sanic-org/sanic-ext/issues/77
# this should be == 5
assert next(counter) == 9
def test_injection_on_websocket(app):
ev = asyncio.Event()
app.ext.dependency(A())
@app.websocket("/foo")
async def handler(request, ws, foo: A):
assert isinstance(foo, A)
if isinstance(foo, A):
ev.set()
request, response = app.test_client.websocket("/foo")
assert ev.is_set()
def test_injection_of_awaitable_variable_in_do_cast(app):
"""Test for do_cast() iscoroutine() check"""
@app.get("/person/<name:str>")
def handler(request, name: AsyncName):
request.ctx.name = name
return text(name.name)
app.ext.add_dependency(AsyncName)
request, response = app.test_client.get("/person/george")
assert response.body == b"george"
assert isinstance(request.ctx.name, AsyncName)
assert request.ctx.name.name == "george"
def test_injection_of_awaitable_variable_in_call(app):
"""Test for __call__() iscoroutine() check"""
@app.get("/person/<name:str>")
def handler(request, name: AsyncName):
request.ctx.name = name
return text(name.name)
def test():
return AsyncName("george")
app.ext.dependency(test())
request, response = app.test_client.get("/person/george")
assert response.body == b"george"
assert isinstance(request.ctx.name, AsyncName)
assert request.ctx.name.name == "george"
def test_injection_class_constructors(app):
app.ext.add_dependency(Alpha)
app.ext.add_dependency(Beta)
@app.get("/")
def handler(request: Request, beta: Beta):
return json({"is_beta": isinstance(beta, Beta)})
_, response = app.test_client.get("/")
assert response.json == {"is_beta": True}
def test_injection_class_constructors_with_optional_request(app):
app.ext.add_dependency(Alpha)
app.ext.add_dependency(Beta)
app.ext.add_dependency(Gamma)
@app.get("/")
def handler(request: Request, gamma: Gamma):
return json(
{
"is_gamma": isinstance(gamma, Gamma),
"has_request": bool(gamma.request),
}
)
_, response = app.test_client.get("/")
assert response.json == {"is_gamma": True, "has_request": True}
def test_injection_class_constructors_with_request(app):
app.ext.add_dependency(Alpha)
app.ext.add_dependency(Beta)
app.ext.add_dependency(Gamma)
app.ext.add_dependency(Delta)
@app.get("/")
def handler(request: Request, delta: Delta):
return json(
{
"is_delta": isinstance(delta, Delta),
"has_request": bool(delta.request),
}
)
_, response = app.test_client.get("/")
assert response.json == {"is_delta": True, "has_request": True}
def test_injection_class_constructors_with_func_and_request(app):
app.ext.add_dependency(Alpha)
app.ext.add_dependency(Beta)
app.ext.add_dependency(Gamma, make_gamma_with_request)
@app.get("/")
def handler(request: Request, gamma: Gamma):
return json(
{
"is_gamma": isinstance(gamma, Gamma),
"has_request": bool(gamma.request),
}
)
_, response = app.test_client.get("/")
assert response.json == {"is_gamma": True, "has_request": True}
def test_injection_class_constructors_with_func_and_no_request(app):
app.ext.add_dependency(Alpha)
app.ext.add_dependency(Beta)
app.ext.add_dependency(Gamma, make_gamma_without_request)
@app.get("/")
def handler(request: Request, gamma: Gamma):
return json(
{
"is_gamma": isinstance(gamma, Gamma),
"has_request": bool(gamma.request),
}
)
_, response = app.test_client.get("/")
assert response.json == {"is_gamma": True, "has_request": False}
def test_injection_of_lambda_properties(app):
@app.get("/foo")
def handler(request, foo: Foo):
return json(request.id == foo.ident and isinstance(foo.ident, UUID))
app.ext.add_dependency(Foo, lambda request: Foo(request.id), "request")
_, response = app.test_client.get("/foo")
assert response.body == b"true"
@@ -0,0 +1,69 @@
from dataclasses import dataclass
import pytest
from sanic import Sanic, json
@dataclass
class Foo:
bar: int
def test_constant_in_registry(app: Sanic):
assert len(app.ext._constant_registry._registry) == 0
app.ext.add_constant("bar", 999)
assert len(app.ext._constant_registry._registry) == 1
assert "bar" in app.ext._constant_registry
def test_constant_is_injected(app: Sanic):
app.ext.add_constant("bar", 999)
@app.get("/")
async def handler(_, bar: int):
return json({"bar": bar})
_, response = app.test_client.get("/")
assert response.json["bar"] == 999
assert app.config.BAR == 999
def test_constant_is_injected_into_constructor(app: Sanic):
app.ext.add_constant("bar", 999)
app.ext.add_dependency(Foo)
@app.get("/")
async def handler(_, foo: Foo):
return json({"foo": foo.bar})
_, response = app.test_client.get("/")
assert response.json["foo"] == 999
assert app.config.BAR == 999
def test_load_config(app: Sanic):
app.ext.load_constants()
assert len(app.ext._constant_registry._registry) == 0
app.ext.load_constants({**app.config}, overwrite=True)
assert len(app.ext._constant_registry._registry) == len(
[k for k in app.config.keys() if k.isupper()]
)
assert "request_buffer_size" in app.ext._constant_registry
def test_load_custom(app: Sanic):
app.ext.load_constants({"foo": "bar"})
assert len(app.ext._constant_registry._registry) == 1
assert app.ext._constant_registry.get("foo") == "bar"
def test_load_overwrite(app: Sanic):
app.ext.load_constants({"foo": "bar"})
with pytest.raises(
ValueError, match="A value for FOO has already been assigned"
):
app.ext.load_constants({"foo": "bar"})
app.ext.load_constants({"foo": "bar"}, True)
@@ -0,0 +1,45 @@
from sanic import Request, text
class Foo:
def bar(self):
return "foobar"
def test_dependency_added(app):
foo = Foo()
foobar = Foo()
app.ext.dependency(foo)
app.ext.dependency(foobar, name="something")
assert app.ctx._dependencies.foo is foo
assert app.ctx._dependencies.something is foobar
def test_dependency_injection(app):
foo = Foo()
app.ext.dependency(foo)
@app.get("/getfoo")
async def getfoo(request: Request, foo: Foo):
return text(foo.bar())
_, response = app.test_client.get("/getfoo")
assert response.text == "foobar"
def test_dependency_injection_head(app):
foo = Foo()
app.ext.dependency(foo)
@app.get("/getfoo")
async def getfoo(request: Request, foo: Foo):
return text(foo.bar())
_, response = app.test_client.head("/getfoo")
assert int(response.headers.get("content-length", 0)) == 6
@@ -0,0 +1,42 @@
from unittest.mock import Mock
import pytest
from sanic.exceptions import SanicException
from sanic.signals import Event
from sanic_ext.config import Config
from sanic_ext.extensions.injection.injector import add_injection
def test_default_config_signal():
config = Config()
assert config.INJECTION_SIGNAL == Event.HTTP_ROUTING_AFTER
def test_not_allowed_signals_error():
with pytest.raises(SanicException):
Config(injection_signal=Event.HTTP_LIFECYCLE_REQUEST)
def test_http_routing_after_succeeds():
Config(injection_signal=Event.HTTP_ROUTING_AFTER)
@pytest.mark.skipif(
not hasattr(Event, "HTTP_HANDLER_BEFORE"),
reason="Sanic version does not support HTTP_HANDLER_BEFORE",
)
def test_http_handler_before_succeeds():
Config(injection_signal=Event.HTTP_HANDLER_BEFORE)
def test_add_injection_uses_signal_config():
app = Mock()
app.signal = Mock(return_value=Mock())
app.ext.config.INJECTION_SIGNAL = "random_string"
app.ext.config.INJECTION_PRIORITY = 99999
add_injection(app, Mock(), Mock())
app.signal.assert_called_once_with("random_string", priority=99999)
@@ -0,0 +1,185 @@
from __future__ import annotations
from unittest.mock import AsyncMock, Mock, call
import pytest
from sanic import Request
from sanic.exceptions import ServerError
from sanic_ext.exceptions import InitError
from sanic_ext.extensions.injection.constructor import Constructor, gather_args
from sanic_ext.extensions.injection.registry import (
ConstantRegistry,
InjectionRegistry,
)
class Foo:
def __init__(self, num: int):
self.num = num
self.mock = AsyncMock()
@classmethod
async def create(cls, request: Request, bar: int):
instance = cls(bar)
await instance.mock(request, bar)
return instance
class Bar:
def __init__(self, foo: Foo):
self.foo = foo
@classmethod
async def create(cls, request: Request, foo: Foo):
return cls(foo)
class Chicken:
@classmethod
def create(cls, egg: Egg):
return cls()
class Egg:
@classmethod
def create(cls, chicken: Chicken):
return cls()
class InheritedRequest(Request): ...
class Baz:
@classmethod
async def create(cls, request: InheritedRequest):
return cls()
@pytest.mark.asyncio
async def test_gather_args():
func = AsyncMock()
func.return_value = 999
injections = {"some_foo": (Foo, func)}
request = object()
kwargs = {"zero": 0, "one": True, "two": "2", "three": None}
args = await gather_args(injections, request, **kwargs)
assert args == {"some_foo": 999}
assert func.call_args == call(request, **kwargs)
@pytest.mark.asyncio
async def test_circular_refs():
injections = InjectionRegistry()
injections.register(Chicken, Chicken.create)
injections.register(Egg, Egg.create)
assert isinstance(injections[Chicken], Constructor)
assert isinstance(injections[Egg], Constructor)
message = (
"Circular dependency injection detected on 'create'. Check "
"dependencies of 'create' which may contain circular dependency "
f"chain with {Chicken}."
)
with pytest.raises(InitError, match=message):
injections.finalize(Mock(), ConstantRegistry({}), [])
@pytest.mark.asyncio
@pytest.mark.parametrize("raises,allowed", ((False, (int,)), (True, [])))
async def test_finalize_allowed_types(raises, allowed):
injections = InjectionRegistry()
injections.register(Foo, Foo.create)
if raises:
with pytest.raises(
InitError, match=".Could not find the following dependencies."
):
injections.finalize(Mock(), ConstantRegistry({}), allowed)
else:
injections.finalize(Mock(), ConstantRegistry({}), allowed)
@pytest.mark.asyncio
async def test_constructor():
injections = InjectionRegistry()
injections.register(Foo, Foo.create)
injections.finalize(Mock(), ConstantRegistry({}), {int})
request = object()
constructor = injections[Foo]
foo = await constructor(request, bar=999)
assert isinstance(foo, Foo)
assert foo.num == 999
assert constructor.pass_kwargs
foo.mock.assert_awaited_once_with(request, 999)
@pytest.mark.asyncio
async def test_constructor_nested():
injections = InjectionRegistry()
injections.register(Foo, Foo.create)
injections.register(Bar, Bar.create)
injections.finalize(Mock(), ConstantRegistry({}), {int})
request = object()
constructor_bar = injections[Bar]
bar = await constructor_bar(request, bar=999)
assert isinstance(bar, Bar)
assert isinstance(bar.foo, Foo)
assert bar.foo.num == 999
assert not constructor_bar.pass_kwargs
bar.foo.mock.assert_awaited_once_with(request, 999)
@pytest.mark.asyncio
async def test_constructor_failure_kwargs():
injections = InjectionRegistry()
injections.register(Foo, Foo.create)
injections.finalize(Mock(), ConstantRegistry({}), {int})
request = object()
constructor = injections[Foo]
message = (
"Failure to inject dependencies. Make sure that all dependencies "
"for 'create' have been registered."
)
with pytest.raises(ServerError, match=message):
await constructor(request)
@pytest.mark.asyncio
async def test_constructor_failure_nested():
injections = InjectionRegistry()
injections.register(Foo, Foo.create)
injections.register(Bar, Bar.create)
injections.finalize(Mock(), ConstantRegistry({}), {int})
request = object()
constructor: Bar = injections[Bar]
message = (
"Failure to inject dependencies. Make sure that all dependencies "
"for 'create' have been registered."
)
with pytest.raises(ServerError, match=message):
await constructor(request)
@pytest.mark.asyncio
async def test_constructor_with_inherited_request():
injections = InjectionRegistry()
injections.register(Baz, Baz.create)
injections.finalize(Mock(), ConstantRegistry({}), [])
request = object()
constructor_baz = injections[Baz]
baz = await constructor_baz(request)
assert isinstance(baz, Baz)
assert not constructor_baz.pass_kwargs