2025-09-25 22:31:37 -07:00
|
|
|
import io
|
2025-09-24 01:28:23 -07:00
|
|
|
import sys
|
2025-09-25 22:31:37 -07:00
|
|
|
from pathlib import Path
|
|
|
|
|
from types import SimpleNamespace
|
2025-09-21 22:11:56 -07:00
|
|
|
|
2025-09-25 22:31:37 -07:00
|
|
|
from dotenv import dotenv_values
|
2025-09-21 22:11:56 -07:00
|
|
|
from flask import Flask
|
2025-09-24 22:03:30 -07:00
|
|
|
from grung.db import GrungDB
|
2025-09-27 16:20:08 -07:00
|
|
|
from grung.exceptions import UniqueConstraintError
|
2025-09-24 01:28:23 -07:00
|
|
|
from tinydb.storages import MemoryStorage
|
2025-09-21 22:11:56 -07:00
|
|
|
|
2025-09-24 22:03:30 -07:00
|
|
|
from ttfrog import schema
|
2025-09-25 22:31:37 -07:00
|
|
|
from ttfrog.exceptions import ApplicationNotInitializedError
|
2025-09-21 22:11:56 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ApplicationContext:
|
2025-09-25 22:31:37 -07:00
|
|
|
"""
|
|
|
|
|
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=
|
|
|
|
|
|
2025-09-27 16:20:08 -07:00
|
|
|
THEME=default
|
|
|
|
|
|
2025-09-25 22:31:37 -07:00
|
|
|
"""
|
|
|
|
|
|
2025-09-24 01:28:23 -07:00
|
|
|
def __init__(self):
|
2025-09-25 22:31:37 -07:00
|
|
|
self.config: SimpleNamespace = None
|
|
|
|
|
self.web: Flask = None
|
2025-09-24 22:03:30 -07:00
|
|
|
self.db: GrungDB = None
|
2025-09-21 22:11:56 -07:00
|
|
|
self._initialized = False
|
|
|
|
|
|
2025-09-25 22:31:37 -07:00
|
|
|
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",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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:
|
2025-09-24 22:03:30 -07:00
|
|
|
self.db = GrungDB.with_schema(schema, storage=MemoryStorage)
|
|
|
|
|
else:
|
2025-09-25 22:31:37 -07:00
|
|
|
self.db = GrungDB.with_schema(schema, self.path.database)
|
|
|
|
|
|
2025-09-27 16:20:08 -07:00
|
|
|
self.theme = Path(__file__).parent / 'themes' / "default"
|
|
|
|
|
|
|
|
|
|
self.web = Flask(self.config.NAME, template_folder=self.theme)
|
2025-09-25 22:31:37 -07:00
|
|
|
self.web.config["SECRET_KEY"] = self.config.SECRET_KEY
|
|
|
|
|
|
2025-09-24 01:28:23 -07:00
|
|
|
self._initialized = True
|
2025-09-21 22:11:56 -07:00
|
|
|
|
2025-09-25 22:31:37 -07:00
|
|
|
def check_state(self) -> None:
|
|
|
|
|
if not self._initialized:
|
|
|
|
|
raise ApplicationNotInitializedError("This action requires the application to be initialized.")
|
|
|
|
|
|
|
|
|
|
def bootstrap(self):
|
|
|
|
|
"""
|
|
|
|
|
Bootstrap the database entries by populating the first Page, the Admin user and the Admins group.
|
|
|
|
|
"""
|
|
|
|
|
self.check_state()
|
2025-09-27 16:20:08 -07:00
|
|
|
try:
|
|
|
|
|
self.db.save(schema.Page(parent_id=None, stub="", title="_", body=""))
|
|
|
|
|
except UniqueConstraintError:
|
|
|
|
|
pass
|
|
|
|
|
try:
|
|
|
|
|
admin = self.db.save(schema.User(name=self.config.ADMIN_USERNAME, email=self.config.ADMIN_EMAIL))
|
|
|
|
|
except UniqueConstraintError:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
self.db.save(schema.Group(name="admins", users=[admin]))
|
|
|
|
|
except UniqueConstraintError:
|
|
|
|
|
pass
|
2025-09-25 22:31:37 -07:00
|
|
|
|
2025-09-21 22:11:56 -07:00
|
|
|
|
2025-09-24 01:28:23 -07:00
|
|
|
sys.modules[__name__] = ApplicationContext()
|