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,38 @@
|
||||
from enum import Enum, IntEnum, auto
|
||||
|
||||
|
||||
class StrEnum(str, Enum): # no cov
|
||||
def _generate_next_value_(name: str, *args) -> str: # type: ignore
|
||||
return name.lower()
|
||||
|
||||
def __eq__(self, value: object) -> bool:
|
||||
value = str(value).upper()
|
||||
return super().__eq__(value)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.value)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
|
||||
class Server(StrEnum):
|
||||
"""Server types."""
|
||||
|
||||
SANIC = auto()
|
||||
ASGI = auto()
|
||||
|
||||
|
||||
class Mode(StrEnum):
|
||||
"""Server modes."""
|
||||
|
||||
PRODUCTION = auto()
|
||||
DEBUG = auto()
|
||||
|
||||
|
||||
class ServerStage(IntEnum):
|
||||
"""Server stages."""
|
||||
|
||||
STOPPED = auto()
|
||||
PARTIAL = auto()
|
||||
SERVING = auto()
|
||||
@@ -0,0 +1,48 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import suppress
|
||||
from importlib import import_module
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
def setup_ext(app: Sanic, *, fail: bool = False, **kwargs):
|
||||
"""Setup Sanic Extensions.
|
||||
|
||||
Requires Sanic Extensions to be installed.
|
||||
|
||||
Args:
|
||||
app (Sanic): Sanic application.
|
||||
fail (bool, optional): Raise an error if Sanic Extensions is not
|
||||
installed. Defaults to `False`.
|
||||
**kwargs: Keyword arguments to pass to `sanic_ext.Extend`.
|
||||
|
||||
Returns:
|
||||
sanic_ext.Extend: Sanic Extensions instance.
|
||||
"""
|
||||
|
||||
if not app.config.AUTO_EXTEND:
|
||||
return
|
||||
|
||||
sanic_ext = None
|
||||
with suppress(ModuleNotFoundError):
|
||||
sanic_ext = import_module("sanic_ext")
|
||||
|
||||
if not sanic_ext: # no cov
|
||||
if fail:
|
||||
raise RuntimeError(
|
||||
"Sanic Extensions is not installed. You can add it to your "
|
||||
"environment using:\n$ pip install sanic[ext]\nor\n$ pip "
|
||||
"install sanic-ext"
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
if not getattr(app, "_ext", None):
|
||||
Ext = getattr(sanic_ext, "Extend")
|
||||
app._ext = Ext(app, **kwargs)
|
||||
|
||||
return app.ext
|
||||
@@ -0,0 +1,72 @@
|
||||
import re
|
||||
import sys
|
||||
|
||||
from os import environ
|
||||
|
||||
from sanic.helpers import is_atty
|
||||
|
||||
|
||||
BASE_LOGO = """
|
||||
|
||||
Sanic
|
||||
Build Fast. Run Fast.
|
||||
|
||||
"""
|
||||
COFFEE_LOGO = """\033[48;2;255;13;104m \033[0m
|
||||
\033[38;2;255;255;255;48;2;255;13;104m ▄████████▄ \033[0m
|
||||
\033[38;2;255;255;255;48;2;255;13;104m ██ ██▀▀▄ \033[0m
|
||||
\033[38;2;255;255;255;48;2;255;13;104m ███████████ █ \033[0m
|
||||
\033[38;2;255;255;255;48;2;255;13;104m ███████████▄▄▀ \033[0m
|
||||
\033[38;2;255;255;255;48;2;255;13;104m ▀███████▀ \033[0m
|
||||
\033[48;2;255;13;104m \033[0m
|
||||
Dark roast. No sugar."""
|
||||
|
||||
COLOR_LOGO = """\033[48;2;255;13;104m \033[0m
|
||||
\033[38;2;255;255;255;48;2;255;13;104m ▄███ █████ ██ \033[0m
|
||||
\033[38;2;255;255;255;48;2;255;13;104m ██ \033[0m
|
||||
\033[38;2;255;255;255;48;2;255;13;104m ▀███████ ███▄ \033[0m
|
||||
\033[38;2;255;255;255;48;2;255;13;104m ██ \033[0m
|
||||
\033[38;2;255;255;255;48;2;255;13;104m ████ ████████▀ \033[0m
|
||||
\033[48;2;255;13;104m \033[0m
|
||||
Build Fast. Run Fast."""
|
||||
|
||||
FULL_COLOR_LOGO = """
|
||||
|
||||
\033[38;2;255;13;104m ▄███ █████ ██ \033[0m ▄█▄ ██ █ █ ▄██████████
|
||||
\033[38;2;255;13;104m ██ \033[0m █ █ █ ██ █ █ ██
|
||||
\033[38;2;255;13;104m ▀███████ ███▄ \033[0m ▀ █ █ ██ ▄ █ ██
|
||||
\033[38;2;255;13;104m ██\033[0m █████████ █ ██ █ █ ▄▄
|
||||
\033[38;2;255;13;104m ████ ████████▀ \033[0m █ █ █ ██ █ ▀██ ███████
|
||||
|
||||
""" # noqa
|
||||
|
||||
SVG_LOGO_SIMPLE = """<svg id=logo-simple viewBox="0 0 964 279"><desc>Sanic</desc><path d="M107 222c9-2 10-20 1-22s-20-2-30-2-17 7-16 14 6 10 15 10h30zm115-1c16-2 30-11 35-23s6-24 2-33-6-14-15-20-24-11-38-10c-7 3-10 13-5 19s17-1 24 4 15 14 13 24-5 15-14 18-50 0-74 0h-17c-6 4-10 15-4 20s16 2 23 3zM251 83q9-1 9-7 0-15-10-16h-13c-10 6-10 20 0 22zM147 60c-4 0-10 3-11 11s5 13 10 12 42 0 67 0c5-3 7-10 6-15s-4-8-9-8zm-33 1c-8 0-16 0-24 3s-20 10-25 20-6 24-4 36 15 22 26 27 78 8 94 3c4-4 4-12 0-18s-69 8-93-10c-8-7-9-23 0-30s12-10 20-10 12 2 16-3 1-15-5-18z" fill="#ff0d68"/><path d="M676 74c0-14-18-9-20 0s0 30 0 39 20 9 20 2zm-297-10c-12 2-15 12-23 23l-41 58H340l22-30c8-12 23-13 30-4s20 24 24 38-10 10-17 10l-68 2q-17 1-48 30c-7 6-10 20 0 24s15-8 20-13 20 -20 58-21h50 c20 2 33 9 52 30 8 10 24-4 16-13L384 65q-3-2-5-1zm131 0c-10 1-12 12-11 20v96c1 10-3 23 5 32s20-5 17-15c0-23-3-46 2-67 5-12 22-14 32-5l103 87c7 5 19 1 18-9v-64c-3-10-20-9-21 2s-20 22-30 13l-97-80c-5-4-10-10-18-10zM701 76v128c2 10 15 12 20 4s0-102 0-124s-20-18-20-7z M850 63c-35 0-69-2-86 15s-20 60-13 66 13 8 16 0 1-10 1-27 12-26 20-32 66-5 85-5 31 4 31-10-18-7-54-7M764 159c-6-2-15-2-16 12s19 37 33 43 23 8 25-4-4-11-11-14q-9-3-22-18c-4-7-3-16-10-19zM828 196c-4 0-8 1-10 5s-4 12 0 15 8 2 12 2h60c5 0 10-2 12-6 3-7-1-16-8-16z" fill="#1f1f1f"/></svg>""" # noqa
|
||||
|
||||
ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
||||
|
||||
|
||||
def get_logo(full: bool = False, coffee: bool = False) -> str:
|
||||
"""Get the Sanic logo.
|
||||
|
||||
Will return the full color logo if the terminal supports it.
|
||||
|
||||
Args:
|
||||
full (bool, optional): Use the full color logo. Defaults to `False`.
|
||||
coffee (bool, optional): Use the coffee logo. Defaults to `False`.
|
||||
|
||||
Returns:
|
||||
str: Sanic logo.
|
||||
"""
|
||||
logo = (
|
||||
(FULL_COLOR_LOGO if full else (COFFEE_LOGO if coffee else COLOR_LOGO))
|
||||
if is_atty()
|
||||
else BASE_LOGO
|
||||
)
|
||||
|
||||
if (
|
||||
sys.platform == "darwin"
|
||||
and environ.get("TERM_PROGRAM") == "Apple_Terminal"
|
||||
):
|
||||
logo = ansi_pattern.sub("", logo)
|
||||
|
||||
return logo
|
||||
@@ -0,0 +1,179 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from shutil import get_terminal_size
|
||||
from textwrap import indent, wrap
|
||||
|
||||
from sanic import __version__
|
||||
from sanic.helpers import is_atty
|
||||
from sanic.log import logger
|
||||
|
||||
|
||||
class MOTD(ABC):
|
||||
"""Base class for the Message of the Day (MOTD) display."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
logo: str | None,
|
||||
serve_location: str,
|
||||
data: dict[str, str],
|
||||
extra: dict[str, str],
|
||||
) -> None:
|
||||
self.logo = logo
|
||||
self.serve_location = serve_location
|
||||
self.data = data
|
||||
self.extra = extra
|
||||
self.key_width = 0
|
||||
self.value_width = 0
|
||||
|
||||
@abstractmethod
|
||||
def display(self):
|
||||
"""Display the MOTD."""
|
||||
|
||||
@classmethod
|
||||
def output(
|
||||
cls,
|
||||
logo: str | None,
|
||||
serve_location: str,
|
||||
data: dict[str, str],
|
||||
extra: dict[str, str],
|
||||
) -> None:
|
||||
"""Output the MOTD.
|
||||
|
||||
Args:
|
||||
logo (Optional[str]): Logo to display.
|
||||
serve_location (str): Location to serve.
|
||||
data (Dict[str, str]): Data to display.
|
||||
extra (Dict[str, str]): Extra data to display.
|
||||
"""
|
||||
motd_class = MOTDTTY if is_atty() else MOTDBasic
|
||||
motd_class(logo, serve_location, data, extra).display()
|
||||
|
||||
|
||||
class MOTDBasic(MOTD):
|
||||
"""A basic MOTD display.
|
||||
|
||||
This is used when the terminal does not support ANSI escape codes.
|
||||
"""
|
||||
|
||||
def display(self):
|
||||
if self.logo:
|
||||
logger.debug(self.logo)
|
||||
lines = [f"Sanic v{__version__}"]
|
||||
if self.serve_location:
|
||||
lines.append(f"Goin' Fast @ {self.serve_location}")
|
||||
lines += [
|
||||
*(f"{key}: {value}" for key, value in self.data.items()),
|
||||
*(f"{key}: {value}" for key, value in self.extra.items()),
|
||||
]
|
||||
for line in lines:
|
||||
logger.info(line)
|
||||
|
||||
|
||||
class MOTDTTY(MOTD):
|
||||
"""A MOTD display for terminals that support ANSI escape codes."""
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_variables()
|
||||
|
||||
def set_variables(self): # no cov
|
||||
"""Set the variables used for display."""
|
||||
fallback = (108, 24)
|
||||
terminal_width = max(
|
||||
get_terminal_size(fallback=fallback).columns, fallback[0]
|
||||
)
|
||||
self.max_value_width = terminal_width - fallback[0] + 36
|
||||
|
||||
self.key_width = 4
|
||||
self.value_width = self.max_value_width
|
||||
if self.data:
|
||||
self.key_width = max(map(len, self.data.keys()))
|
||||
self.value_width = min(
|
||||
max(map(len, self.data.values())), self.max_value_width
|
||||
)
|
||||
if self.extra:
|
||||
self.key_width = max(
|
||||
self.key_width, max(map(len, self.extra.keys()))
|
||||
)
|
||||
self.value_width = min(
|
||||
max((*map(len, self.extra.values()), self.value_width)),
|
||||
self.max_value_width,
|
||||
)
|
||||
self.logo_lines = self.logo.split("\n") if self.logo else []
|
||||
self.logo_line_length = 24
|
||||
self.centering_length = (
|
||||
self.key_width + self.value_width + 2 + self.logo_line_length
|
||||
)
|
||||
self.display_length = self.key_width + self.value_width + 2
|
||||
|
||||
def display(self, version=True, action="Goin' Fast", out=None):
|
||||
"""Display the MOTD.
|
||||
|
||||
Args:
|
||||
version (bool, optional): Display the version. Defaults to `True`.
|
||||
action (str, optional): Action to display. Defaults to
|
||||
`"Goin' Fast"`.
|
||||
out (Optional[Callable], optional): Output function. Defaults to
|
||||
`None`.
|
||||
"""
|
||||
if not out:
|
||||
out = logger.info
|
||||
header = "Sanic"
|
||||
if version:
|
||||
header += f" v{__version__}"
|
||||
header = header.center(self.centering_length)
|
||||
running = (
|
||||
f"{action} @ {self.serve_location}" if self.serve_location else ""
|
||||
).center(self.centering_length)
|
||||
length = len(header) + 2 - self.logo_line_length
|
||||
first_filler = "─" * (self.logo_line_length - 1)
|
||||
second_filler = "─" * length
|
||||
display_filler = "─" * (self.display_length + 2)
|
||||
lines = [
|
||||
f"\n┌{first_filler}─{second_filler}┐",
|
||||
f"│ {header} │",
|
||||
f"│ {running} │",
|
||||
f"├{first_filler}┬{second_filler}┤",
|
||||
]
|
||||
|
||||
self._render_data(lines, self.data, 0)
|
||||
if self.extra:
|
||||
logo_part = self._get_logo_part(len(lines) - 4)
|
||||
lines.append(f"│ {logo_part} ├{display_filler}┤")
|
||||
self._render_data(lines, self.extra, len(lines) - 4)
|
||||
|
||||
self._render_fill(lines)
|
||||
|
||||
lines.append(f"└{first_filler}┴{second_filler}┘\n")
|
||||
out(indent("\n".join(lines), " "))
|
||||
|
||||
def _render_data(self, lines, data, start):
|
||||
offset = 0
|
||||
for idx, (key, value) in enumerate(data.items(), start=start):
|
||||
key = key.rjust(self.key_width)
|
||||
|
||||
wrapped = wrap(value, self.max_value_width, break_on_hyphens=False)
|
||||
for wrap_index, part in enumerate(wrapped):
|
||||
part = part.ljust(self.value_width)
|
||||
logo_part = self._get_logo_part(idx + offset + wrap_index)
|
||||
display = (
|
||||
f"{key}: {part}"
|
||||
if wrap_index == 0
|
||||
else (" " * len(key) + f" {part}")
|
||||
)
|
||||
lines.append(f"│ {logo_part} │ {display} │")
|
||||
if wrap_index:
|
||||
offset += 1
|
||||
|
||||
def _render_fill(self, lines):
|
||||
filler = " " * self.display_length
|
||||
idx = len(lines) - 5
|
||||
for i in range(1, len(self.logo_lines) - idx):
|
||||
logo_part = self.logo_lines[idx + i]
|
||||
lines.append(f"│ {logo_part} │ {filler} │")
|
||||
|
||||
def _get_logo_part(self, idx):
|
||||
try:
|
||||
logo_part = self.logo_lines[idx]
|
||||
except IndexError:
|
||||
logo_part = " " * (self.logo_line_length - 3)
|
||||
return logo_part
|
||||
@@ -0,0 +1,90 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from contextlib import contextmanager
|
||||
from queue import Queue
|
||||
from threading import Thread
|
||||
|
||||
|
||||
if os.name == "nt": # noqa
|
||||
import ctypes # noqa
|
||||
|
||||
class _CursorInfo(ctypes.Structure):
|
||||
_fields_ = [("size", ctypes.c_int), ("visible", ctypes.c_byte)]
|
||||
|
||||
|
||||
class Spinner: # noqa
|
||||
"""Spinner class to show a loading spinner in the terminal.
|
||||
|
||||
Used internally by the `loading` context manager.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str) -> None:
|
||||
self.message = message
|
||||
self.queue: Queue[int] = Queue()
|
||||
self.spinner = self.cursor()
|
||||
self.thread = Thread(target=self.run)
|
||||
|
||||
def start(self):
|
||||
self.queue.put(1)
|
||||
self.thread.start()
|
||||
self.hide()
|
||||
|
||||
def run(self):
|
||||
while self.queue.get():
|
||||
output = f"\r{self.message} [{next(self.spinner)}]"
|
||||
sys.stdout.write(output)
|
||||
sys.stdout.flush()
|
||||
time.sleep(0.1)
|
||||
self.queue.put(1)
|
||||
|
||||
def stop(self):
|
||||
self.queue.put(0)
|
||||
self.thread.join()
|
||||
self.show()
|
||||
|
||||
@staticmethod
|
||||
def cursor():
|
||||
while True:
|
||||
yield from "|/-\\"
|
||||
|
||||
@staticmethod
|
||||
def hide():
|
||||
if os.name == "nt":
|
||||
ci = _CursorInfo()
|
||||
handle = ctypes.windll.kernel32.GetStdHandle(-11)
|
||||
ctypes.windll.kernel32.GetConsoleCursorInfo(
|
||||
handle, ctypes.byref(ci)
|
||||
)
|
||||
ci.visible = False
|
||||
ctypes.windll.kernel32.SetConsoleCursorInfo(
|
||||
handle, ctypes.byref(ci)
|
||||
)
|
||||
elif os.name == "posix":
|
||||
sys.stdout.write("\033[?25l")
|
||||
sys.stdout.flush()
|
||||
|
||||
@staticmethod
|
||||
def show():
|
||||
if os.name == "nt":
|
||||
ci = _CursorInfo()
|
||||
handle = ctypes.windll.kernel32.GetStdHandle(-11)
|
||||
ctypes.windll.kernel32.GetConsoleCursorInfo(
|
||||
handle, ctypes.byref(ci)
|
||||
)
|
||||
ci.visible = True
|
||||
ctypes.windll.kernel32.SetConsoleCursorInfo(
|
||||
handle, ctypes.byref(ci)
|
||||
)
|
||||
elif os.name == "posix":
|
||||
sys.stdout.write("\033[?25h")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def loading(message: str = "Loading"): # noqa
|
||||
spinner = Spinner(message)
|
||||
spinner.start()
|
||||
yield
|
||||
spinner.stop()
|
||||
@@ -0,0 +1,115 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from socket import socket
|
||||
from ssl import SSLContext
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from sanic.application.constants import Mode, Server, ServerStage
|
||||
from sanic.log import VerbosityFilter, logger
|
||||
from sanic.server.async_server import AsyncioServer
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
@dataclass
|
||||
class ApplicationServerInfo:
|
||||
"""Information about a server instance."""
|
||||
|
||||
settings: dict[str, Any]
|
||||
stage: ServerStage = field(default=ServerStage.STOPPED)
|
||||
server: AsyncioServer | None = field(default=None)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ApplicationState:
|
||||
"""Application state.
|
||||
|
||||
This class is used to store the state of the application. It is
|
||||
instantiated by the application and is available as `app.state`.
|
||||
"""
|
||||
|
||||
app: Sanic
|
||||
asgi: bool = field(default=False)
|
||||
coffee: bool = field(default=False)
|
||||
fast: bool = field(default=False)
|
||||
host: str = field(default="")
|
||||
port: int = field(default=0)
|
||||
ssl: SSLContext | None = field(default=None)
|
||||
sock: socket | None = field(default=None)
|
||||
unix: str | None = field(default=None)
|
||||
mode: Mode = field(default=Mode.PRODUCTION)
|
||||
reload_dirs: set[Path] = field(default_factory=set)
|
||||
auto_reload: bool = field(default=False)
|
||||
server: Server = field(default=Server.SANIC)
|
||||
is_running: bool = field(default=False)
|
||||
is_started: bool = field(default=False)
|
||||
is_stopping: bool = field(default=False)
|
||||
verbosity: int = field(default=0)
|
||||
workers: int = field(default=0)
|
||||
primary: bool = field(default=True)
|
||||
server_info: list[ApplicationServerInfo] = field(default_factory=list)
|
||||
|
||||
# This property relates to the ApplicationState instance and should
|
||||
# not be changed except in the __post_init__ method
|
||||
_init: bool = field(default=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self._init = True
|
||||
|
||||
def __setattr__(self, name: str, value: Any) -> None:
|
||||
if self._init and name == "_init":
|
||||
raise RuntimeError(
|
||||
"Cannot change the value of _init after instantiation"
|
||||
)
|
||||
super().__setattr__(name, value)
|
||||
if self._init and hasattr(self, f"set_{name}"):
|
||||
getattr(self, f"set_{name}")(value)
|
||||
|
||||
def set_mode(self, value: str | Mode):
|
||||
if hasattr(self.app, "error_handler"):
|
||||
self.app.error_handler.debug = self.app.debug
|
||||
if getattr(self.app, "configure_logging", False) and self.app.debug:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
def set_verbosity(self, value: int) -> None:
|
||||
"""Set the verbosity level.
|
||||
|
||||
Args:
|
||||
value (int): Verbosity level.
|
||||
"""
|
||||
VerbosityFilter.verbosity = value
|
||||
|
||||
@property
|
||||
def is_debug(self) -> bool:
|
||||
"""Check if the application is in debug mode.
|
||||
|
||||
Returns:
|
||||
bool: `True` if the application is in debug mode, `False`
|
||||
otherwise.
|
||||
"""
|
||||
return self.mode is Mode.DEBUG
|
||||
|
||||
@property
|
||||
def stage(self) -> ServerStage:
|
||||
"""Get the server stage.
|
||||
|
||||
Returns:
|
||||
ServerStage: Server stage.
|
||||
"""
|
||||
if not self.server_info:
|
||||
return ServerStage.STOPPED
|
||||
|
||||
if all(info.stage is ServerStage.SERVING for info in self.server_info):
|
||||
return ServerStage.SERVING
|
||||
elif any(
|
||||
info.stage is ServerStage.SERVING for info in self.server_info
|
||||
):
|
||||
return ServerStage.PARTIAL
|
||||
|
||||
return ServerStage.STOPPED
|
||||
Reference in New Issue
Block a user