Skip to content

API reference

Everything the package exposes, in one place. For task-oriented walkthroughs, start with the guides.

Imports

from repositron import (
    Repository,            # full CRUD generic base
    ReadOnlyRepository,    # read-only generic base
    PaginatedResult,       # the {items, total} container
    OrderBy,               # the order_by argument type
    UNSET, UnsetType,      # the partial-update sentinel and its type
    on,                    # decorator: tag a method as a lifecycle hook
    writes,                # decorator: give a custom write flush/commit/rollback
)

Type parameters

Repository[Model, DTO = Model, Create = object, Update = object, PK = int]
ReadOnlyRepository[Model, DTO = Model, PK = int]

Model is required. DTO defaults to the model itself (reads return the model, unhydrated). Create and Update are the payload dataclasses your writes accept. PK is the primary-key type, defaulting to int; declare it (last, after the others) when your key is a str or uuid. So Repository[Workspace] is a valid read/write repository returning Workspace with an int key, and you add the other parameters only as you need them. See primary keys.

Class attributes

Set these on your repository subclass.

Attribute Type Purpose Default
field_mapping dict[str, str] {model_column: dto_field} for renamed fields {}
pk_column str \| InstrumentedAttribute primary-key column, by name or column reference "id"
class TaskRepository(Repository[Task, TaskDTO, TaskCreate, TaskUpdate]):
    field_mapping = {"created_at": "opened_at"}   # model column : DTO field
    pk_column = Task.id   # or "id"

Read methods

Available on both ReadOnlyRepository and Repository.

get(id) -> DTO | None
Fetch one row by primary key, hydrated to the DTO. None if absent.
first(*, extra_filters=None, order_by=None, **filters) -> DTO | None
The first row matching the filters, or None. See filtering.
list(*, extra_filters=None, order_by=None, **filters) -> list[DTO]
All rows matching the filters, each hydrated to the DTO.
list_paginated(offset, limit=20, *, extra_filters=None, order_by, **filters) -> PaginatedResult[DTO]
A page plus the unpaginated total. order_by is required; omitting it raises ValueError. See pagination.
count(*, extra_filters=None, **filters) -> int
Count of rows matching the filters.
exists(id) -> bool
Whether a row with this primary key exists.
repo[Shape]
A clone bound to Shape for the next call, triggering column projection when Shape is a narrow dataclass. See projection.

Constructor

Repository(session, *, autocommit=False, rollback_on_error=True)
ReadOnlyRepository(session, *, autocommit=False, rollback_on_error=True)
session is the caller-owned SQLAlchemy Session. With autocommit=True, every write commits after its flush; the default flushes only and leaves the transaction to you. rollback_on_error (True by default) rolls the session back before re-raising when a flush or commit fails; set it to False to leave that rollback to you. Both bases take these: a ReadOnlyRepository has no automatic writes, but they govern any @writes method it hosts. See transactions.

Write methods

Available on Repository only. Each takes commit: bool | None = None: None follows the instance's autocommit, True/False overrides it for that one call. On a commit failure the session is rolled back and the error re-raised.

create(payload, *, commit=None) -> PK
Insert from a dataclass payload and flush. UNSET fields are omitted so the column default applies. Returns the new primary key, typed as the PK parameter (int by default).
update(id, payload, *, commit=None) -> bool
Partial-update from a dataclass payload and flush. UNSET fields are skipped; None is written as NULL. False if no row has that key. See updates.
delete(id, *, commit=None) -> bool
Delete by primary key and flush. False if no row has that key.

Lifecycle hooks

Tag a method with @on(event, mode=...) to run it inside the base's create / update / delete / hydration. Collected once per class; an unknown event raises TypeError at import. See hooks.

@on(event, mode) receives returns
"create", "before" model, payload nothing
"create", "after" model nothing
"update", "before" model, payload nothing
"update", "after" model nothing
"delete", "before" model nothing
"delete", "after" model nothing
"hydrate", "build" model the DTO
"hydrate", "after" model, dto the DTO

before write hooks run before the flush; after run once the model has its primary key. hydrate hooks fire on every read that hydrates, but not on repo[Shape] projection. build is single-winner (most-derived wins); the others all run, base classes before subclasses.

Custom hydration

_hydrate(self, model) -> DTO
Build the DTO from a model. Override when the automatic conversion cannot build your DTO at all; to merely add a derived field, prefer a hydrate hook. The override is the same thing as a build hook, spelled as a method. See custom hydration.

@writes

@writes on a custom write method runs it through the repository's flush, commit, and rollback, so the body only does session work. It works on both Repository and ReadOnlyRepository, so a read-mostly repository can still own an occasional hand-written write. The method may declare *, commit: bool | None = None to expose the per-call override; without it the write flushes (and commits if autocommit=True). See transactions on custom writes.

Filter values

The values a **filters keyword understands beyond an ordinary match:

Value Effect
None filter by IS NULL
UNSET skip this filter entirely

PaginatedResult

@dataclass(frozen=True, slots=True)
class PaginatedResult[DTO]:
    items: list[DTO]   # this page
    total: int         # all matching rows, ignoring offset/limit

Design principles

The ideas that explain every choice in the API.

  • The session is the caller's. repositron flushes and never closes the session, so transaction boundaries stay in your application code by default. Committing is opt-in, per instance (autocommit=True) or per write (commit=True); see transactions. A custom write gets the same deal through @writes, so you never hand-roll flush and rollback.
  • Extending is adding, not replacing. A hook layers behavior onto what the base already does, so you write only the part that is yours and inherit the rest. The base holds itself to the same rule: its own DTO construction is a build hook you can replace, not a method you must reach around. An override is the rare fallback, not the extension point.
  • One source of truth per field name. A rename declared once in field_mapping applies to both hydration and projection.
  • Ordering is never implicit. list and first are unordered unless asked; list_paginated refuses to run without one. See pagination.
  • UNSET is one canonical singleton, compared by identity, shared across the whole library. There is no per-project variant to get subtly wrong.