Initial implementation of Sahsa Clock
Pygame framebuffer clock for Le Potato (ARM Debian) with aiohttp webhook server. Renders 12-hour clock directly to /dev/fb0 (no X11/Wayland). Supports full-screen message overlays pushed via a browser dashboard or Bearer-token API. Includes first-run setup wizard, session-based dashboard auth, bcrypt password storage, per-IP rate limiting, and systemd service unit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
90
config.py
Normal file
90
config.py
Normal file
@@ -0,0 +1,90 @@
|
||||
import re
|
||||
import secrets
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
except ImportError:
|
||||
try:
|
||||
import tomli as tomllib # type: ignore[no-redef]
|
||||
except ImportError:
|
||||
print("Error: Python 3.11+ is required (for tomllib), or install tomli: pip install tomli")
|
||||
sys.exit(1)
|
||||
|
||||
CONFIG_PATH = Path("config.toml")
|
||||
|
||||
|
||||
@dataclass
|
||||
class AppConfig:
|
||||
width: int | None
|
||||
height: int | None
|
||||
fps: int
|
||||
clock_font_path: str
|
||||
message_font_path: str
|
||||
port: int
|
||||
default_duration: float
|
||||
api_token: str
|
||||
rate_limit: int
|
||||
password_hash: str
|
||||
session_timeout_hours: int
|
||||
config_path: Path
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: str | Path = CONFIG_PATH) -> "AppConfig":
|
||||
p = Path(path)
|
||||
if not p.exists():
|
||||
print(f"Error: Config file not found: {p}")
|
||||
print("Copy config.toml to your working directory and edit it.")
|
||||
sys.exit(1)
|
||||
|
||||
with open(p, "rb") as f:
|
||||
raw = tomllib.load(f)
|
||||
|
||||
display = raw.get("display", {})
|
||||
server = raw.get("server", {})
|
||||
api = raw.get("api", {})
|
||||
rate = raw.get("rate_limit", {})
|
||||
dashboard = raw.get("dashboard", {})
|
||||
|
||||
token = api.get("token", "").strip()
|
||||
if not token:
|
||||
token = secrets.token_hex(32)
|
||||
_update_config_field(p, "token", token)
|
||||
print(f"\n[sahsa-clock] Generated API bearer token (saved to config.toml):")
|
||||
print(f" {token}\n")
|
||||
|
||||
return cls(
|
||||
width=display.get("width"),
|
||||
height=display.get("height"),
|
||||
fps=display.get("fps", 10),
|
||||
clock_font_path=display.get("clock_font_path", "").strip(),
|
||||
message_font_path=display.get("message_font_path", "").strip(),
|
||||
port=server.get("port", 8080),
|
||||
default_duration=float(server.get("default_duration_seconds", 20)),
|
||||
api_token=token,
|
||||
rate_limit=rate.get("requests_per_minute", 20),
|
||||
password_hash=dashboard.get("password_hash", "").strip(),
|
||||
session_timeout_hours=dashboard.get("session_timeout_hours", 8),
|
||||
config_path=p,
|
||||
)
|
||||
|
||||
def save_password_hash(self, hashed: str) -> None:
|
||||
_update_config_field(self.config_path, "password_hash", hashed)
|
||||
self.password_hash = hashed
|
||||
|
||||
|
||||
def _update_config_field(config_path: Path, key: str, value: str) -> None:
|
||||
"""Update a quoted string field in config.toml using regex replacement."""
|
||||
content = config_path.read_text()
|
||||
pattern = rf'^({re.escape(key)}\s*=\s*)"[^"]*"'
|
||||
|
||||
def replacer(m: re.Match) -> str:
|
||||
return f'{m.group(1)}"{value}"'
|
||||
|
||||
new_content = re.sub(pattern, replacer, content, flags=re.MULTILINE)
|
||||
if new_content == content:
|
||||
print(f"Warning: Could not update '{key}' in {config_path}. Field not found.")
|
||||
return
|
||||
config_path.write_text(new_content)
|
||||
Reference in New Issue
Block a user