Scaffold project, data model and artwork
Establish the LLeMbas foundation: FastAPI/Jinja/SQLite layout, the ORM schema, and the original SVG identity. Notable decisions, all recorded in comments at the point they matter: - No Alembic. SQLite only, schema created at startup, so models carry a few columns nothing reads yet (Message.parent_id for branching, content_parts_json for multimodal turns). Adding them later to a live database without migrations is the painful path. - Sessions are server-side rows keyed by a SHA-256 of the cookie value, not JWTs, so logout and bans revoke access immediately. - Upstream API keys are Fernet-encrypted with a key derived from LEMBAS_SECRET_KEY. decrypt() fails soft to "" so rotating the secret degrades to re-entering keys rather than crashing the admin UI. - Artwork is generated by scripts/build_artwork.py rather than hand-drawn per file: the mallorn leaf appears in the icon, favicon, lockup and banner, and one source is the only way those stay in sync. The wordmark is Source Serif 4 (OFL) converted to outlines, because a README banner cannot load a webfont and <text> would render in whatever serif the viewer happens to have. - Icons live in a template partial, not assets/, because same-document <use href="#id"> is universally supported and the cross-document form is not. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
"""Application configuration, loaded from the environment and/or a .env file."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import Field, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Runtime configuration. Every variable is prefixed ``LEMBAS_``."""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="LEMBAS_",
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
secret_key: str = Field(default="")
|
||||
data_dir: Path = Path("./data")
|
||||
|
||||
host: str = "127.0.0.1"
|
||||
port: int = 8080
|
||||
reload: bool = False
|
||||
log_level: Literal["debug", "info", "warning", "error"] = "info"
|
||||
|
||||
allow_signup: bool = True
|
||||
default_theme: Literal["moria", "shire"] = "moria"
|
||||
session_ttl: int = 60 * 60 * 24 * 30
|
||||
request_timeout: float = 300.0
|
||||
|
||||
@field_validator("secret_key")
|
||||
@classmethod
|
||||
def _generate_secret_if_absent(cls, v: str) -> str:
|
||||
# A generated key lets `lembas serve` work out of the box, but it changes
|
||||
# on every restart: sessions drop and stored API keys become unreadable.
|
||||
# main.py warns loudly about this. Never rely on it in production.
|
||||
return v or secrets.token_urlsafe(48)
|
||||
|
||||
@property
|
||||
def db_path(self) -> Path:
|
||||
return self.data_dir / "lembas.db"
|
||||
|
||||
@property
|
||||
def uploads_dir(self) -> Path:
|
||||
return self.data_dir / "uploads"
|
||||
|
||||
def ensure_dirs(self) -> None:
|
||||
self.data_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.uploads_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> Settings:
|
||||
"""Cached singleton so config is parsed once per process."""
|
||||
return Settings()
|
||||
|
||||
|
||||
settings = get_settings()
|
||||
Reference in New Issue
Block a user