A typed, generic repository base for SQLAlchemy 2.0. Full CRUD with no per-table boilerplate.
Project description
repositron
A typed, generic repository base for SQLAlchemy 2.0. Full CRUD, zero per-table boilerplate.
Documentation · Filtering · Projection · Hooks · API
Every SQLAlchemy project rewrites the same repository layer: a class per table
wrapping session.query(...), the same get / list / count, the same
pagination math, the same "turn the ORM row into something light to return". It
is mechanical, easy to get subtly wrong, and you write it again for the next
table.
repositron writes that layer once, generically. Declare a model (and optionally a DTO and write payloads), inherit one class, and get a fully typed repository, every method checked against the types you declared.
from dataclasses import dataclass
from repositron import Repository, UNSET, UnsetType
@dataclass(frozen=True, slots=True)
class UserDTO: # light, detached, serializes straight to JSON
id: int
name: str # renamed from the model column `full_name`
email: str
@dataclass
class UserUpdate:
full_name: str | UnsetType = UNSET # absent = leave alone; None = SET NULL
email: str | UnsetType = UNSET
class UserRepository(Repository[User, UserDTO, UserCreate, UserUpdate]):
field_mapping = {"full_name": "name"} # the whole repository
That is the whole repository. get / first / list / list_paginated /
count / exists / create / update / delete all come for free, typed
against UserDTO:
repo = UserRepository(session)
repo.get(1) # -> UserDTO | None
repo.list(is_active=True, order_by=User.created_at.desc()) # -> list[UserDTO]
repo.list_paginated(0, 20, order_by=User.created_at.desc()) # -> PaginatedResult[UserDTO]
repo.create(UserCreate(full_name="Ada", email="ada@x.com")) # -> int (new id)
repo.update(1, UserUpdate(full_name="Ada L.")) # only that field; others untouched
Why repositron
It cuts the layer you keep rewriting. One generic base replaces the
per-table CRUD class, and every method is typed off the generic parameters, so
your editor knows repo.list() is list[UserDTO] and repo.get(id) is checked
against the key type you declared (int, str, uuid.UUID).
Two ways to filter, in one call. Equality is keyed by attribute name,
anything else is a plain SQLAlchemy expression, and they combine. A None value
means IS NULL; UNSET skips the filter, so optional query params pass straight
through without branching.
repo.list(is_active=True, extra_filters=[User.age > 18], order_by=User.id)
# WHERE is_active = true AND age > 18 ORDER BY id
Updates that can actually write NULL. UNSET means "leave this column
alone", None means "set it to NULL", the distinction the hand-written
if x is not None pattern silently loses.
Projection that is real column selection. Index the repo with a narrow shape
and it narrows the SELECT itself, it does not fetch the row and drop fields. The
injected repository is untouched, the projection lasts only for the call.
@dataclass(frozen=True, slots=True)
class UserCard:
id: int
name: str
repo[UserCard].list(is_active=True)
# SELECT users.id, users.full_name FROM users WHERE is_active = true
# -> list[UserCard] (only those two columns ever leave the database)
Extend without overriding. Hooks
layer a derived column, an enriched DTO, or an audit row onto the base, and
@writes gives a
custom write the same flush/commit/rollback the built-ins get, no self.session
plumbing.
Your choice of DTO. A dataclass that serializes straight to JSON (so the same
object is your repository return value and your FastAPI response_model), the
model itself, or a Pydantic schema you already have.
Install
uv add repositron # or: pip install repositron
Requires Python 3.13+ and sqlalchemy>=2.0, the only dependency.
Documentation
Full guides and API reference at repositron.fa.dev.br.
License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file repositron-0.4.0.tar.gz.
File metadata
- Download URL: repositron-0.4.0.tar.gz
- Upload date:
- Size: 13.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4111c1158ac8d39de5149d76e40be56484d72d58fee683a1c1a7116f70761c59
|
|
| MD5 |
bcd046f8d2484aa54dfdecc06dce8362
|
|
| BLAKE2b-256 |
770b0bdefc7eea4a2dfc60596f46b5ed8f2d2c5c0e359f8f6a5289756ced6846
|
File details
Details for the file repositron-0.4.0-py3-none-any.whl.
File metadata
- Download URL: repositron-0.4.0-py3-none-any.whl
- Upload date:
- Size: 16.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c2716cf77c58208f9de8a2eb470b2218001c76b282dd3904b45d12ec2eec479e
|
|
| MD5 |
b2734d1f3df772b7278739670efcf3ae
|
|
| BLAKE2b-256 |
88476901af0811cc6dc85ebf071a25560ce35e4bd811bab3fad28cca92681527
|