Skip to content

Release Tracker Codebase Walkthrough

The final version of the Release Tracker app at github.com/nnja/release-tracker

It's mostly the same app, with a few additions:

  • Timestamp columns on Task
  • A lifespan startup hook
  • A static-bundled frontend.

Created and Updated Timestamps on Task

Project has had a created_at column from the start.

For the final project, we added a created_at and an updated_at to Task.

src/release_tracker/models.py
class Task(TaskBase, table=True):
    __tablename__ = "tasks"

    id: int | None = Field(default=None, primary_key=True)
    project_id: int = Field(
        foreign_key="projects.id", ondelete="CASCADE", index=True
    )

    project: Project = Relationship(back_populates="tasks")

    created_at: datetime = Field(
        default_factory=utc_now,
        sa_column=Column(DateTime(timezone=True), nullable=False),
    )
    updated_at: datetime = Field(
        default_factory=utc_now,
        sa_column=Column(
            DateTime(timezone=True),
            nullable=False,
            onupdate=utc_now,
        ),
    )

What's new:

  • updated_at with onupdate=utc_now.
    • SQLAlchemy automatically calls utc_now() on every UPDATE that touches the row. The application doesn't have to remember to bump the timestamp manually.
  • ondelete="CASCADE" on the foreign key.
    • When a project is deleted, the database deletes its tasks in the same statement. Without cascade, deleting a project that still has tasks raises a foreign-key violation and the request fails with a 409.

TaskRead exposes both timestamps so clients can sort task lists by recency:

Python
class TaskRead(TaskBase):
    id: int
    project_id: int
    project_name: str
    project_slug: str
    created_at: datetime
    updated_at: datetime

The migration that introduces these columns also recreates the foreign key with ON DELETE CASCADE. Because both changes ride together, an existing database picks up the cascade behavior the same time it picks up the timestamps.

The lifespan Hook

The final main.py calls configure_logging from a lifespan async context manager, not at module import:

src/release_tracker/main.py
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager

from fastapi import FastAPI

from . import __version__
from .config import configure_logging, get_settings


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
    configure_logging(debug=get_settings().debug)
    yield


app = FastAPI(
    title=APP_NAME,
    version=__version__,
    description="...",
    lifespan=lifespan,
)

lifespan is FastAPI's startup/shutdown hook. Code before yield runs once when the app boots; code after yield (none here) runs once on shutdown.


The final app uses lifespan because real applications usually several startup tasks that will live in lifespan.

App Metadata: __version__ and /health

src/release_tracker/__init__.py
__version__ = "0.1.0"
src/release_tracker/main.py
@app.get("/", tags=["meta"])
def read_root() -> dict[str, str]:
    return {
        "app": APP_NAME,
        "version": __version__,
        "docs": "/docs",
        "health": "/health",
    }

__version__ is exported by the package so clients hitting / can tell which build is running. The tags=["meta"] on both routes groups them together in the docs UI under a "meta" section, separating operational endpoints from the business endpoints.

The /health endpoint is the same database-checking pattern from the exercise: SessionDep provides a session, session.exec(text("SELECT 1")) verifies the database is reachable, and the response is either {"status": "healthy"} (200) or {"status": "unhealthy"} (503). Container orchestrators (Kubernetes, AWS ECS, fly.io) call it to decide whether a container is ready to receive traffic.

Dependency Injection Stays the Same

The dependency tree is the same as the previous chapter: SessionDep, ProjectDep, TaskDep, CurrentUserDep. None of the new additions required a new dependency.

The Test Suite

The final app includes a slightly fuller test suite (tests/test_health.py, tests/test_config.py, plus the auth/projects/tasks coverage we already wrote).

The concepts are the same: in-memory SQLite, an auth_client fixture, sample-data fixtures via the API. Reading those tests to see how tests scale.

Running the Demo

To see the dashboard end-to-end:

shell
git clone https://github.com/nnja/release-tracker
cd release-tracker
uv sync
cp .env.example .env
# Edit .env to set a JWT_SECRET_KEY. The example file points at:
#   uv run python -c "import secrets; print(secrets.token_urlsafe(32))"

docker compose up -d  # bring Postgres up
uv run alembic upgrade head
uv run python -m scripts.seed
uv run fastapi dev

Open http://localhost:8000/app in a browser. The dashboard reads the projects and tasks the seed script created and presents them with filters across status, priority, and project. To make changes, head to http://localhost:8000/docs and use the docs UI's Authorize flow with the credentials the seed script printed.