repositron - repositron
Skip to content
Introduction
Guides
API reference
Design notes
repositron¶
A typed, generic repository base for SQLAlchemy 2.0.
Full CRUD, with zero per-table boilerplate .
Get started<br>Guides
Every SQLAlchemy project ends up with the same folder: one repository class per<br>table, each wrapping select(...) / session.scalars(...) in the same get,<br>the same list, the same pagination math. repositron writes that layer once,<br>generically, and types it against your model and your return shape.
Hand-written, per table Declared once
from sqlalchemy import select
class TaskRepository:<br>def __init__(self, session: Session) -> None:<br>self.session = session
def get(self, id: int) -> TaskDTO | None:<br>row = self.session.scalars(select(Task).where(Task.id == id)).first()<br>if row is None:<br>return None<br>return TaskDTO(id=row.id, title=row.title, status=row.status, assignee_id=row.assignee_id)
def list(self, *, status: str | None = None) -> list[TaskDTO]:<br>stmt = select(Task)<br>if status is not None:<br>stmt = stmt.where(Task.status == status)<br>rows = self.session.scalars(stmt).all()<br>return [TaskDTO(id=r.id, title=r.title, status=r.status, assignee_id=r.assignee_id) for r in rows]
def update(self, id: int, *, assignee_id: int | None = None) -> bool:<br>task = self.session.scalars(select(Task).where(Task.id == id)).first()<br>if task is None:<br>return False<br>if assignee_id is not None: # and how do you unassign on purpose?<br>task.assignee_id = assignee_id<br>self.session.flush()<br>return True
# ...count, delete, first, pagination, then again for the next ten tables.
from dataclasses import dataclass<br>from repositron import Repository, UNSET, UnsetType
@dataclass(frozen=True, slots=True)<br>class TaskDTO: # light, detached, serializes straight to JSON<br>id: int<br>title: str<br>status: str<br>assignee_id: int | None
@dataclass<br>class TaskCreate:<br>workspace_id: int<br>title: str
@dataclass<br>class TaskUpdate:<br>title: str | UnsetType = UNSET # absent leaves it; None sets NULL<br>status: str | UnsetType = UNSET<br>assignee_id: int | None | UnsetType = UNSET
class TaskRepository(Repository[Task, TaskDTO, TaskCreate, TaskUpdate]):<br>...
Every method from the other tab now exists, typed against TaskDTO, with no<br>further code.
What you get¶
Typed end to end
repo.list() is list[TaskDTO], and your editor knows it. No casts, no<br>Any. The return value is the same object your API serializes.
Two ways to filter, one call
Equality by keyword and arbitrary SQLAlchemy expressions, combined. You never<br>pick between readable and powerful.
Filtering
Updates that write NULL on purpose
UNSET leaves a column alone; None sets it to NULL. The is not None<br>pattern cannot tell those apart. repositron can.
Updating rows
Load only what you need
repo[Card].list() selects just that shape's columns, for one call, without<br>touching the injected repository.
Projection
Pagination that refuses to lie
list_paginated requires order_by and raises if you forget, turning a<br>production heisenbug into an error at the call site.
Pagination
One dependency
Just sqlalchemy>=2.0. Dataclass DTOs add nothing else; Pydantic is detected<br>only if your DTO is one.
Install¶
uvpip
uv add repositron
pip install repositron
Python 3.13+ and sqlalchemy>=2.0.
Get started
Back to top
Copyright © 2026 Felipe Adeildo