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,24 @@
from sanic.exceptions import SanicException
from ..base import Extension
from .logger import Logger
class LoggingExtension(Extension):
name = "logging"
MIN_VERSION = (22, 9)
def startup(self, bootstrap) -> None:
if self.included():
if self.MIN_VERSION > bootstrap.sanic_version:
min_version = ".".join(map(str, self.MIN_VERSION))
sanic_version = ".".join(map(str, bootstrap.sanic_version))
raise SanicException(
f"The logging extension only works with Sanic "
f"v{min_version} and above. It looks like you are "
f"running {sanic_version}."
)
Logger.setup(self.app)
def included(self):
return self.config.LOGGING
@@ -0,0 +1,116 @@
import logging
from typing import Any, Optional, TypedDict
class LoggerConfig(TypedDict):
level: str
propagate: bool
handlers: list[str]
class HandlerConfig(TypedDict):
class_: str
level: str
stream: Optional[str]
formatter: Optional[str]
class FormatterConfig(TypedDict):
class_: str
format: Optional[str]
datefmt: Optional[str]
class LoggingConfig(TypedDict):
version: int
disable_existing_loggers: bool
formatters: dict[str, FormatterConfig]
handlers: dict[str, HandlerConfig]
loggers: dict[str, LoggerConfig]
class LoggingConfigExtractor:
def __init__(self):
self.version = 1
self.disable_existing_loggers = False
self.formatters: dict[str, FormatterConfig] = {}
self.handlers: dict[str, HandlerConfig] = {}
self.loggers: dict[str, LoggerConfig] = {}
def add_logger(self, logger: logging.Logger):
self._extract_logger_config(logger)
self._extract_handlers(logger)
def compile(self) -> LoggingConfig:
output = {
"version": self.version,
"disable_existing_loggers": self.disable_existing_loggers,
"formatters": self.formatters,
"handlers": self.handlers,
"loggers": self.loggers,
}
return self._clean(output)
def _extract_logger_config(self, logger: logging.Logger):
config: LoggerConfig = {
"level": logging.getLevelName(logger.level),
"propagate": logger.propagate,
"handlers": [handler.get_name() for handler in logger.handlers],
}
self.loggers[logger.name] = config
def _extract_handlers(self, logger: logging.Logger):
for handler in logger.handlers:
self._extract_handler_config(handler)
def _extract_handler_config(self, handler: logging.Handler):
handler_name = handler.get_name()
if handler_name in self.handlers:
return
config: HandlerConfig = {
"class_": self._full_name(handler),
"level": logging.getLevelName(handler.level),
"formatter": (
self._formatter_name(handler.formatter)
if handler.formatter
else None
),
"stream": None,
}
# if (stream := getattr(handler, "stream", None)) and (
# stream_name := getattr(stream, "name", None)
# ):
# config["stream"] = stream_name
self.handlers[handler_name] = config
if handler.formatter:
self._extract_formatter_config(handler.formatter)
def _extract_formatter_config(self, formatter: logging.Formatter):
formatter_name = self._formatter_name(formatter)
if formatter_name in self.formatters:
return
config: FormatterConfig = {
"class_": self._full_name(formatter),
"format": formatter._fmt,
"datefmt": formatter.datefmt,
}
self.formatters[formatter_name] = config
def _clean(self, d: dict[str, Any]) -> dict[str, Any]:
return {
k.replace("class_", "class"): self._clean(v)
if isinstance(v, dict)
else v
for k, v in d.items()
}
@staticmethod
def _formatter_name(
formatter: logging.Formatter, prefix: str = "formatter"
):
return f"{prefix}_{formatter.__class__.__name__}".lower()
@staticmethod
def _full_name(obj):
return f"{obj.__module__}.{obj.__class__.__name__}"
@@ -0,0 +1,126 @@
import logging
from collections import defaultdict
from logging import LogRecord
from logging.handlers import QueueHandler
from multiprocessing import Manager
from queue import Empty, Full
from signal import SIGINT, SIGTERM
from signal import signal as signal_func
from sanic import Sanic
from sanic.log import logger as root_logger
from sanic.log import logger as server_logger
from sanic.logging.setup import setup_logging
from sanic_ext.extensions.logging.extractor import LoggingConfigExtractor
async def prepare_logger(app: Sanic, *_):
Logger.prepare(app)
async def setup_logger(app: Sanic, *_):
logger = Logger()
extractor = LoggingConfigExtractor()
for logger_name in app.config.LOGGERS:
extractor.add_logger(logging.getLogger(logger_name))
app.manager.manage(
"Logger",
logger,
{
"queue": app.shared_ctx.logger_queue,
"config": extractor.compile(),
},
transient=True,
)
class SanicQueueHandler(QueueHandler):
def emit(self, record: LogRecord) -> None:
try:
return super().enqueue(record)
except Full:
server_logger.warning(
"Background logger is full. Emitting log in process."
)
server_logger.handle(record)
async def setup_server_logging(app: Sanic):
qhandler = SanicQueueHandler(app.shared_ctx.logger_queue)
app.ctx._logger_handlers = defaultdict(list)
app.ctx._qhandler = qhandler
for logger_name in app.config.LOGGERS:
logger_instance = logging.getLogger(logger_name)
for handler in logger_instance.handlers:
logger_instance.removeHandler(handler)
logger_instance.addHandler(qhandler)
async def remove_server_logging(app: Sanic):
for logger, handlers in app.ctx._logger_handlers.items():
logger.removeHandler(app.ctx._qhandler)
for handler in handlers:
logger.addHandler(handler)
class Logger:
LOGGERS: list[str] = []
def __init__(self):
self.run = True
self.loggers = {
logger: logging.getLogger(logger) for logger in self.LOGGERS
}
def __call__(self, queue, config) -> None:
signal_func(SIGINT, self.stop)
signal_func(SIGTERM, self.stop)
logging.config.dictConfig(config)
setup_loggers = set(config["loggers"].keys())
enabled_loggers = set(self.loggers.keys())
missing = enabled_loggers - setup_loggers
root_logger.info(
f"Setup background logging for: {', '.join(setup_loggers)}"
)
if missing:
root_logger.warning(
f"Logger config not found for: {', '.join(missing)}"
)
setup_logging(True, no_color=False, log_extra=True)
while self.run:
try:
record: LogRecord = queue.get(timeout=0.05)
except Empty:
continue
logger = self.loggers.get(record.name)
logger.handle(record)
def stop(self, *_):
if self.run:
self.run = False
@classmethod
def update_cls_loggers(cls, logger_names: list[str]):
cls.LOGGERS = logger_names
@classmethod
def prepare(cls, app: Sanic):
sync_manager = Manager()
logger_queue = sync_manager.Queue(
maxsize=app.config.LOGGING_QUEUE_MAX_SIZE
)
app.shared_ctx.logger_queue = logger_queue
cls.update_cls_loggers(app.config.LOGGERS)
@classmethod
def setup(cls, app: Sanic):
app.main_process_start(prepare_logger)
app.main_process_ready(setup_logger)
app.before_server_start(setup_server_logging)
app.before_server_stop(remove_server_logging)