vtt/src/ttfrog/app.py
2025-10-04 10:48:18 -07:00

145 lines
5.0 KiB
Python

import io
import sys
from pathlib import Path
from types import SimpleNamespace
from dotenv import dotenv_values
from flask import Flask, session
from flask_session import Session
from grung.db import GrungDB
from tinydb.storages import MemoryStorage
from ttfrog import schema
from ttfrog.exceptions import ApplicationNotInitializedError
class ApplicationContext:
"""
The global context for the application, this class provides access to the Flask app instance, the GrungDB instance,
and the loaded configuration.
To prevent multiple contexts from being created, the class is instantiated at import time and replaces the module in
the symbol table. The first time it is imported, callers should call both .load_config() and .initialize(); this is
typically done at program start.
After being intialized, callers can import ttfrog.app and interact with the ApplicationContext instance directly:
>>> from ttfrog import app
>>> print(app.config.NAME)
ttfrog
"""
CONFIG_DEFAULTS = """
# ttfrog Defaults
NAME=ttfrog
LOG_LEVEL=INFO
SECRET_KEY=fnord
IN_MEMORY_DB=
DATA_ROOT=~/.dnd/ttfrog/
ADMIN_USERNAME=admin
ADMIN_EMAIL=admin@telisar
THEME=default
VIEW_URI=/
"""
def __init__(self):
self.config: SimpleNamespace = None
self.web: Flask = None
self.db: GrungDB = None
self._initialized = False
def load_config(self, defaults: Path | None = Path("~/.dnd/ttfrog/defaults"), **overrides) -> None:
"""
Load the user configuration from the following in sources, in order:
1. ApplicationContext.CONFIG_DEFAULTS
2. The user's configuration defaults file, if any
3. Overrides specified by the caller, if any
Once the configuration is loaded, the path attribute is also configured.
"""
config_file = defaults.expanduser() if defaults else None
self.config = SimpleNamespace(
**{
**dotenv_values(stream=io.StringIO(ApplicationContext.CONFIG_DEFAULTS)),
**(dotenv_values(config_file) if config_file else {}),
**overrides,
}
)
data_root = Path(self.config.DATA_ROOT).expanduser()
self.path = SimpleNamespace(
config=config_file,
data_root=data_root,
database=data_root / f"{self.config.NAME}.json",
sessions=data_root / "session_cache"
)
def initialize(self, db: GrungDB = None, force: bool = False) -> None:
"""
Instantiate both the database and the flask application.
"""
if force or not self._initialized:
if self.config.IN_MEMORY_DB:
self.db = GrungDB.with_schema(schema, storage=MemoryStorage)
else:
self.db = GrungDB.with_schema(
schema, self.path.database, sort_keys=True, indent=4, separators=(",", ": ")
)
self.theme = Path(__file__).parent / "themes" / "default"
self.web = Flask(self.config.NAME, template_folder=self.theme)
self.web.config["SECRET_KEY"] = self.config.SECRET_KEY
self.web.config["SEND_FILE_MAX_AGE_DEFAULT"] = 0
self.web.config["DEBUG"] = True
self.web.config["SESSION_TYPE"] = "filesystem"
self.web.config["SESSION_REFRESH_EACH_REQUEST"] = True
self.web.config["SESSION_FILE_DIR"] = self.path.sessions
Session(self.web)
self._initialized = True
def check_state(self) -> None:
if not self._initialized:
raise ApplicationNotInitializedError("This action requires the application to be initialized.")
def add_member(self, parent: schema.Page, child: schema.Page):
parent.members.append(self.db.save(child))
parent = self.db.save(parent)
return parent.get_child(child)
def bootstrap(self):
"""
Bootstrap the database entries by populating the first Page, the Admin user and the Admins group.
"""
self.check_state()
# create the top-level pages
root = self.db.save(schema.Page(name=self.config.VIEW_URI, title="Home", body="This is the home page"))
users = self.add_member(root, schema.Page(name="User", title="Users", body="users go here."))
groups = self.add_member(root, schema.Page(name="Group", title="Groups", body="groups go here."))
npcs = self.add_member(root, schema.Page(name="NPC", title="NPCS!", body="NPCS!"))
# create the NPCs
sabetha = self.db.save(schema.NPC(name="Sabetha", body=""))
johns = self.db.save(schema.NPC(name="John", body=""))
self.add_member(npcs, sabetha)
self.add_member(npcs, johns)
# create the admin user and admins group
admin = self.add_member(users, schema.User(name=self.config.ADMIN_USERNAME, email=self.config.ADMIN_EMAIL))
admins = self.db.save(schema.Group(name="administrators"))
admins = self.add_member(groups, admins)
self.add_member(admins, admin)
sys.modules[__name__] = ApplicationContext()