Skip to content

Concepts

repositron leans on a handful of words, in its API and across these docs. None of them are invented here, they are the everyday vocabulary of data access in Python, but it is worth pinning down what each means so the rest reads cleanly.

The repository pattern

A repository is the one object that knows how to read and write a given kind of record. Instead of scattering select(Task)... across your routes, services, and scripts, you put that logic behind a TaskRepository and call repo.get(1) or repo.list(status="open"). The rest of your code asks for what it wants and never touches the database directly.

The point is a seam. On one side, your application speaks in domain terms (repo.create(...), repo.list(...)); on the other, the repository speaks SQL. You can read the calling code without reading SQL, swap the storage details without touching callers, and test against a fake repository. That is the repository pattern, an old idea, and a good one.

The catch is that hand-writing one repository class per table is tedious and repetitive: the same get / list / count, the same pagination math, the same row-to-object mapping, table after table. repositron is a generic, typed base that gives you all of that from a single declaration, so you write a repository without writing the boilerplate. The rest of this page is the vocabulary that base uses.

Model

The model is your SQLAlchemy mapped class, the Task, Workspace, or Member with Mapped[...] columns that maps to a table. It is the source of truth for the schema, and repositron reads everything it needs (columns, the primary key) off it. You already have models; repositron does not replace them.

class Task(Base):
    __tablename__ = "tasks"
    id: Mapped[int] = mapped_column(primary_key=True)
    workspace_id: Mapped[int]
    title: Mapped[str]
    description: Mapped[str | None]
    status: Mapped[str]
    assignee_id: Mapped[int | None]
    created_at: Mapped[datetime]
    archived_at: Mapped[datetime | None]

DTO

A DTO (data transfer object) is the shape a repository returns. A model instance is tied to the session, lazy-loads, and carries the whole row; often you want something lighter to hand to an HTTP layer or another part of the app: a plain object with exactly the fields you need, detached from the database.

In repositron the DTO is a type parameter. Point it at a dataclass and reads come back as that dataclass; leave it off and reads return the model itself. Either way, your editor knows the return type.

@dataclass(frozen=True, slots=True)
class TaskDTO:
    id: int
    title: str
    status: str
    assignee_id: int | None

The DTO is an optimization, not a requirement, choosing a return type covers when each kind pays off.

Hydration

Hydration is turning a model row into a DTO, copying the columns across, honoring any renames, building the DTO instance. repositron does this for you on every read: a dataclass DTO is built field by field, a Pydantic DTO goes through model_validate, and a model-as-DTO is returned untouched.

When a DTO needs a value no column holds (a count, a join, a derived field), you add it with a hydrate hook, repositron hydrates the columns, your hook fills the rest. When the DTO is something the automatic build can't produce at all, like a plain str, you replace the build itself with a build hook.

Payload

A payload is the shape a repository accepts on a write. Reads return a DTO; writes take a payload, a TaskCreate for create, a TaskUpdate for update. Keeping them separate from the DTO means a create can require different fields than a read returns, and an update can express "leave this field alone" as distinct from "set it to NULL" (see updating rows).

@dataclass
class TaskCreate:
    workspace_id: int
    title: str
    description: str | None = None

Primary key

The primary key (PK) is the column that identifies one row, what get, update, and delete take, and what create returns. It is id and an int by default. When yours differs, you declare two separate things: its type (the last type parameter, e.g. str or uuid.UUID) and, if the column is not named id, its name (the pk_column attribute). Primary keys covers why those are two knobs and not one.

Projection

Projection is reading only some columns instead of the whole row. A list endpoint that shows a title and a status does not need the description, the assignee, and the timestamps. With repo[Shape], repositron selects only the columns that narrow Shape declares and returns that shape, for that one call, the database ships less and you get back exactly what you asked for. See projecting columns.

repo[TaskCard].list()   # SELECT id, title, status  ->  list[TaskCard]

With the vocabulary in hand, Get started puts it together into a working repository in a few lines.