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,97 @@
|
||||
import asyncio
|
||||
|
||||
from collections.abc import Awaitable, MutableMapping
|
||||
from typing import Any, Callable
|
||||
|
||||
from sanic.exceptions import BadRequest
|
||||
from sanic.models.protocol_types import TransportProtocol
|
||||
from sanic.server.websockets.connection import WebSocketConnection
|
||||
|
||||
|
||||
ASGIScope = MutableMapping[str, Any]
|
||||
ASGIMessage = MutableMapping[str, Any]
|
||||
ASGISend = Callable[[ASGIMessage], Awaitable[None]]
|
||||
ASGIReceive = Callable[[], Awaitable[ASGIMessage]]
|
||||
|
||||
|
||||
class MockProtocol: # no cov
|
||||
def __init__(self, transport: "MockTransport", loop):
|
||||
self.transport = transport
|
||||
self._not_paused = asyncio.Event()
|
||||
self._not_paused.set()
|
||||
self._complete = asyncio.Event()
|
||||
|
||||
def pause_writing(self) -> None:
|
||||
self._not_paused.clear()
|
||||
|
||||
def resume_writing(self) -> None:
|
||||
self._not_paused.set()
|
||||
|
||||
async def complete(self) -> None:
|
||||
self._not_paused.set()
|
||||
await self.transport.send(
|
||||
{"type": "http.response.body", "body": b"", "more_body": False}
|
||||
)
|
||||
|
||||
@property
|
||||
def is_complete(self) -> bool:
|
||||
return self._complete.is_set()
|
||||
|
||||
async def push_data(self, data: bytes) -> None:
|
||||
if not self.is_complete:
|
||||
await self.transport.send(
|
||||
{"type": "http.response.body", "body": data, "more_body": True}
|
||||
)
|
||||
|
||||
async def drain(self) -> None:
|
||||
await self._not_paused.wait()
|
||||
|
||||
|
||||
class MockTransport(TransportProtocol): # no cov
|
||||
_protocol: MockProtocol | None
|
||||
|
||||
def __init__(
|
||||
self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend
|
||||
) -> None:
|
||||
self.scope = scope
|
||||
self._receive = receive
|
||||
self._send = send
|
||||
self._protocol = None
|
||||
self.loop: asyncio.AbstractEventLoop | None = None
|
||||
|
||||
def get_protocol(self) -> MockProtocol: # type: ignore
|
||||
if not self._protocol:
|
||||
self._protocol = MockProtocol(self, self.loop)
|
||||
return self._protocol
|
||||
|
||||
def get_extra_info(self, info: str, default=None) -> str | bool | None:
|
||||
if info == "peername":
|
||||
return self.scope.get("client")
|
||||
elif info == "sslcontext":
|
||||
return self.scope.get("scheme") in ["https", "wss"]
|
||||
return default
|
||||
|
||||
def get_websocket_connection(self) -> WebSocketConnection:
|
||||
try:
|
||||
return self._websocket_connection
|
||||
except AttributeError:
|
||||
raise BadRequest("Improper websocket connection.")
|
||||
|
||||
def create_websocket_connection(
|
||||
self, send: ASGISend, receive: ASGIReceive
|
||||
) -> WebSocketConnection:
|
||||
self._websocket_connection = WebSocketConnection(
|
||||
send, receive, self.scope.get("subprotocols", [])
|
||||
)
|
||||
return self._websocket_connection
|
||||
|
||||
def add_task(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
async def send(self, data) -> None:
|
||||
# TODO:
|
||||
# - Validation on data and that it is formatted properly and is valid
|
||||
await self._send(data)
|
||||
|
||||
async def receive(self) -> ASGIMessage:
|
||||
return await self._receive()
|
||||
@@ -0,0 +1,69 @@
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
|
||||
class REPLLocal(NamedTuple):
|
||||
var: Any
|
||||
name: str
|
||||
desc: str
|
||||
|
||||
|
||||
class REPLContext:
|
||||
BUILTINS = {
|
||||
"app": "The Sanic application instance",
|
||||
"sanic": "The Sanic module",
|
||||
"do": "An async function to fake a request to the application",
|
||||
"client": "A client to access the Sanic app instance using httpx",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._locals: set[REPLLocal] = set()
|
||||
|
||||
def add(
|
||||
self,
|
||||
var: Any,
|
||||
name: str | None = None,
|
||||
desc: str | None = None,
|
||||
):
|
||||
"""Add a local variable to be available in REPL context.
|
||||
|
||||
Args:
|
||||
var (Any): A module, class, object or a class.
|
||||
name (Optional[str], optional): An alias for the local. Defaults to None.
|
||||
desc (Optional[str], optional): A brief description for the local. Defaults to None.
|
||||
""" # noqa: E501
|
||||
if name is None:
|
||||
try:
|
||||
name = var.__name__
|
||||
except AttributeError:
|
||||
name = var.__class__.__name__
|
||||
|
||||
if desc is None:
|
||||
try:
|
||||
desc = var.__doc__ or ""
|
||||
except AttributeError:
|
||||
desc = str(type(var))
|
||||
|
||||
assert isinstance(desc, str) and isinstance(
|
||||
name, str
|
||||
) # Just to make mypy happy
|
||||
|
||||
if name in self.BUILTINS:
|
||||
raise ValueError(f"Cannot override built-in variable: {name}")
|
||||
|
||||
desc = self._truncate(desc)
|
||||
|
||||
self._locals.add(REPLLocal(var, name, desc))
|
||||
|
||||
def __setattr__(self, name: str, value: Any):
|
||||
if name.startswith("_"):
|
||||
super().__setattr__(name, value)
|
||||
else:
|
||||
self.add(value, name)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._locals)
|
||||
|
||||
@staticmethod
|
||||
def _truncate(s: str, limit: int = 40) -> str:
|
||||
s = s.replace("\n", " ")
|
||||
return s[:limit] + "..." if len(s) > limit else s
|
||||
@@ -0,0 +1,80 @@
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
from typing import Callable, NamedTuple
|
||||
|
||||
from sanic.handlers.directory import DirectoryHandler
|
||||
from sanic.models.handler_types import (
|
||||
ErrorMiddlewareType,
|
||||
ListenerType,
|
||||
MiddlewareType,
|
||||
SignalHandler,
|
||||
)
|
||||
from sanic.types import HashableDict
|
||||
|
||||
|
||||
class FutureRoute(NamedTuple):
|
||||
handler: str
|
||||
uri: str
|
||||
methods: Iterable[str] | None
|
||||
host: str | list[str]
|
||||
strict_slashes: bool
|
||||
stream: bool
|
||||
version: int | None
|
||||
name: str
|
||||
ignore_body: bool
|
||||
websocket: bool
|
||||
subprotocols: list[str] | None
|
||||
unquote: bool
|
||||
static: bool
|
||||
version_prefix: str
|
||||
error_format: str | None
|
||||
route_context: HashableDict
|
||||
|
||||
|
||||
class FutureListener(NamedTuple):
|
||||
listener: ListenerType
|
||||
event: str
|
||||
priority: int
|
||||
|
||||
|
||||
class FutureMiddleware(NamedTuple):
|
||||
middleware: MiddlewareType
|
||||
attach_to: str
|
||||
|
||||
|
||||
class FutureException(NamedTuple):
|
||||
handler: ErrorMiddlewareType
|
||||
exceptions: list[BaseException]
|
||||
|
||||
|
||||
class FutureStatic(NamedTuple):
|
||||
uri: str
|
||||
file_or_directory: Path
|
||||
pattern: str
|
||||
use_modified_since: bool
|
||||
use_content_range: bool
|
||||
stream_large_files: bool | int
|
||||
name: str
|
||||
host: str | None
|
||||
strict_slashes: bool | None
|
||||
content_type: str | None
|
||||
resource_type: str | None
|
||||
directory_handler: DirectoryHandler
|
||||
follow_external_symlink_files: bool
|
||||
follow_external_symlink_dirs: bool
|
||||
|
||||
|
||||
class FutureSignal(NamedTuple):
|
||||
handler: SignalHandler
|
||||
event: str
|
||||
condition: dict[str, str] | None
|
||||
exclusive: bool
|
||||
priority: int
|
||||
|
||||
|
||||
class FutureRegistry(set): ...
|
||||
|
||||
|
||||
class FutureCommand(NamedTuple):
|
||||
name: str
|
||||
func: Callable
|
||||
@@ -0,0 +1,30 @@
|
||||
from asyncio.events import AbstractEventLoop
|
||||
from collections.abc import Coroutine
|
||||
from typing import Any, Callable, TypeVar
|
||||
|
||||
import sanic
|
||||
|
||||
from sanic import request
|
||||
from sanic.response import BaseHTTPResponse, HTTPResponse
|
||||
|
||||
|
||||
Sanic = TypeVar("Sanic", bound="sanic.Sanic")
|
||||
Request = TypeVar("Request", bound="request.Request")
|
||||
|
||||
MiddlewareResponse = (
|
||||
HTTPResponse | None | Coroutine[Any, Any, HTTPResponse | None]
|
||||
)
|
||||
RequestMiddlewareType = Callable[[Request], MiddlewareResponse]
|
||||
ResponseMiddlewareType = Callable[
|
||||
[Request, BaseHTTPResponse], MiddlewareResponse
|
||||
]
|
||||
ErrorMiddlewareType = Callable[
|
||||
[Request, BaseException], Coroutine[Any, Any, None] | None
|
||||
]
|
||||
MiddlewareType = RequestMiddlewareType | ResponseMiddlewareType
|
||||
ListenerType = (
|
||||
Callable[[Sanic], Coroutine[Any, Any, None] | None]
|
||||
| Callable[[Sanic, AbstractEventLoop], Coroutine[Any, Any, None] | None]
|
||||
)
|
||||
RouteHandler = Callable[..., Coroutine[Any, Any, HTTPResponse | None]]
|
||||
SignalHandler = Callable[..., Coroutine[Any, Any, None]]
|
||||
@@ -0,0 +1,34 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from base64 import b64decode
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass()
|
||||
class Credentials:
|
||||
auth_type: str | None
|
||||
token: str | None
|
||||
_username: str | None = field(default=None)
|
||||
_password: str | None = field(default=None)
|
||||
|
||||
def __post_init__(self):
|
||||
if self._auth_is_basic:
|
||||
self._username, self._password = (
|
||||
b64decode(self.token.encode("utf-8")).decode().split(":")
|
||||
)
|
||||
|
||||
@property
|
||||
def username(self):
|
||||
if not self._auth_is_basic:
|
||||
raise AttributeError("Username is available for Basic Auth only")
|
||||
return self._username
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
if not self._auth_is_basic:
|
||||
raise AttributeError("Password is available for Basic Auth only")
|
||||
return self._password
|
||||
|
||||
@property
|
||||
def _auth_is_basic(self) -> bool:
|
||||
return self.auth_type == "Basic"
|
||||
@@ -0,0 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import BaseTransport
|
||||
from typing import TYPE_CHECKING, Protocol
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic.http.constants import HTTP
|
||||
from sanic.models.asgi import ASGIScope
|
||||
|
||||
|
||||
class HTMLProtocol(Protocol):
|
||||
def __html__(self) -> str | bytes: ...
|
||||
|
||||
def _repr_html_(self) -> str | bytes: ...
|
||||
|
||||
|
||||
class Range(Protocol):
|
||||
start: int | None
|
||||
end: int | None
|
||||
size: int | None
|
||||
total: int | None
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
class TransportProtocol(BaseTransport):
|
||||
scope: ASGIScope
|
||||
version: HTTP
|
||||
__slots__ = ()
|
||||
@@ -0,0 +1,71 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ssl import SSLContext, SSLObject
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
from sanic.models.protocol_types import TransportProtocol
|
||||
|
||||
|
||||
class Signal:
|
||||
stopped = False
|
||||
|
||||
|
||||
class ConnInfo:
|
||||
"""
|
||||
Local and remote addresses and SSL status info.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"client_port",
|
||||
"client",
|
||||
"client_ip",
|
||||
"ctx",
|
||||
"lost",
|
||||
"peername",
|
||||
"server_port",
|
||||
"server",
|
||||
"server_name",
|
||||
"sockname",
|
||||
"ssl",
|
||||
"cert",
|
||||
"network_paths",
|
||||
)
|
||||
|
||||
def __init__(self, transport: TransportProtocol, unix=None):
|
||||
self.ctx = SimpleNamespace()
|
||||
self.lost = False
|
||||
self.peername: tuple[str, int] | None = None
|
||||
self.server = self.client = ""
|
||||
self.server_port = self.client_port = 0
|
||||
self.client_ip = ""
|
||||
self.sockname = addr = transport.get_extra_info("sockname")
|
||||
self.ssl = False
|
||||
self.server_name = ""
|
||||
self.cert: dict[str, Any] = {}
|
||||
self.network_paths: list[Any] = []
|
||||
sslobj: SSLObject | None = transport.get_extra_info("ssl_object") # type: ignore
|
||||
sslctx: SSLContext | None = transport.get_extra_info("ssl_context") # type: ignore
|
||||
if sslobj:
|
||||
self.ssl = True
|
||||
self.server_name = getattr(sslobj, "sanic_server_name", None) or ""
|
||||
self.cert = dict(getattr(sslobj.context, "sanic", {}))
|
||||
if sslctx and not self.cert:
|
||||
self.cert = dict(getattr(sslctx, "sanic", {}))
|
||||
if isinstance(addr, str): # UNIX socket
|
||||
self.server = unix or addr
|
||||
return
|
||||
# IPv4 (ip, port) or IPv6 (ip, port, flowinfo, scopeid)
|
||||
if isinstance(addr, tuple):
|
||||
self.server = addr[0] if len(addr) == 2 else f"[{addr[0]}]"
|
||||
self.server_port = addr[1]
|
||||
# self.server gets non-standard port appended
|
||||
if addr[1] != (443 if self.ssl else 80):
|
||||
self.server = f"{self.server}:{addr[1]}"
|
||||
self.peername = addr = transport.get_extra_info("peername")
|
||||
self.network_paths = transport.get_extra_info("network_paths") # type: ignore
|
||||
|
||||
if isinstance(addr, tuple):
|
||||
self.client = addr[0] if len(addr) == 2 else f"[{addr[0]}]"
|
||||
self.client_ip = addr[0]
|
||||
self.client_port = addr[1]
|
||||
Reference in New Issue
Block a user