feat: phase 2 content model + cache — SQLite schema, markdown, TTL
Stand up the full SQLite content layer: all 7 tables from the authoritative schema with WAL + foreign-keys enforced per-connection, entity dataclasses plus row mappers, hand-rolled versioned migrations tracked in schema_migrations, and an idempotent Python seed (system user + welcome post + About page). Add a Markdown->HTML service using markdown-it-py with a strict bleach allowlist (tables intentionally omitted on both sides). Add a typed in-process TTLCache[K,V] and wire it into real DB-backed PostService and PageService, both exposing invalidate_all() for Phase 4 admin writes. Rewire / and /about to read from the DB; homepage renders the seeded welcome post, About renders page.title + sanitized body_html_cached. Update the Phase 1 route tests accordingly. Mark Phase 2 complete in docs/ROADMAP.md.
This commit is contained in:
50
app/main.py
50
app/main.py
@@ -4,6 +4,15 @@ The factory pattern (``create_app``) keeps test setup straightforward and
|
||||
lets us swap in alternate configurations without module-level side
|
||||
effects. ``app = create_app()`` at import time is what Uvicorn references
|
||||
via ``app.main:app``.
|
||||
|
||||
Phase 2 additions:
|
||||
- Build a shared SQLAlchemy :class:`~sqlalchemy.Engine` from
|
||||
``settings.database_url`` and attach the per-connection
|
||||
PRAGMA listener (WAL + foreign keys).
|
||||
- Apply SQL migrations from :mod:`app.models.migrations`.
|
||||
- Run the idempotent seed (welcome post, About page, system user).
|
||||
- Instantiate :class:`PostService` and :class:`PageService` and
|
||||
expose them on ``app.state`` for route-level DI.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -17,9 +26,13 @@ from fastapi.templating import Jinja2Templates
|
||||
|
||||
from app import __version__
|
||||
from app.config import get_settings
|
||||
from app.db import build_engine, run_migrations
|
||||
from app.logging_config import configure_logging
|
||||
from app.models.seed import run_seed
|
||||
from app.routes.health import router as health_router
|
||||
from app.routes.public import router as public_router
|
||||
from app.services.pages import PageService
|
||||
from app.services.posts import PostService
|
||||
|
||||
|
||||
# Resolve the package root once so template / static paths stay correct
|
||||
@@ -33,16 +46,19 @@ _STATIC_DIR: Path = _PACKAGE_ROOT / "static"
|
||||
def create_app() -> FastAPI:
|
||||
"""Build and return the FastAPI application.
|
||||
|
||||
Responsibilities:
|
||||
- Load validated configuration via :func:`get_settings`.
|
||||
- Initialize structured logging *before* any logger is used.
|
||||
- Instantiate FastAPI with canonical title + version.
|
||||
- Mount the ``/static`` directory for CSS, JS, and image assets.
|
||||
- Attach the shared :class:`Jinja2Templates` to ``app.state`` so route
|
||||
dependencies can retrieve it without a circular import on this
|
||||
module.
|
||||
- Register routers (Phase 1: health + public).
|
||||
- Emit a single ``app_started`` structured log event.
|
||||
Responsibilities (in strict order):
|
||||
|
||||
1. Load validated configuration via :func:`get_settings`.
|
||||
2. Initialize structured logging *before* any logger is used.
|
||||
3. Build the SQLAlchemy engine and install the PRAGMA listener.
|
||||
4. Apply SQL migrations (idempotent — no-op after first boot).
|
||||
5. Run the seed (idempotent — marked via ``schema_migrations``).
|
||||
6. Instantiate :class:`PostService` / :class:`PageService` and
|
||||
attach them to ``app.state`` so route dependencies can resolve
|
||||
them via ``request.app.state``.
|
||||
7. Mount static files, attach the shared :class:`Jinja2Templates`,
|
||||
and register routers.
|
||||
8. Emit a single ``app_started`` structured log event.
|
||||
"""
|
||||
# Parse + validate configuration first so a bad environment fails fast
|
||||
# with a clear pydantic error before we touch logging / FastAPI.
|
||||
@@ -52,6 +68,13 @@ def create_app() -> FastAPI:
|
||||
# very first log line already flows through our processor chain.
|
||||
configure_logging(settings.app_env)
|
||||
|
||||
# --- Database plumbing --------------------------------------------------
|
||||
# Engine is a process-wide resource. Built here so that migrations
|
||||
# and seed both run on the same pool/config as the running app.
|
||||
engine = build_engine(settings.database_url)
|
||||
run_migrations(engine)
|
||||
run_seed(engine)
|
||||
|
||||
application = FastAPI(
|
||||
title="Chicken Babies R Us",
|
||||
version=__version__,
|
||||
@@ -78,6 +101,13 @@ def create_app() -> FastAPI:
|
||||
# function defined next to the routes.
|
||||
application.state.templates = Jinja2Templates(directory=_TEMPLATES_DIR)
|
||||
|
||||
# Store the engine + services on ``app.state`` so the
|
||||
# dependency-injection helpers in :mod:`app.services.*` can find
|
||||
# them without importing this module (circular-import-safe).
|
||||
application.state.engine = engine
|
||||
application.state.post_service = PostService(engine)
|
||||
application.state.page_service = PageService(engine)
|
||||
|
||||
# Register routers. Kept explicit (no dynamic discovery) so the set of
|
||||
# mounted endpoints is trivially auditable.
|
||||
application.include_router(health_router)
|
||||
|
||||
Reference in New Issue
Block a user