Skip to main content

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.

Test Release Package version Supported Python versions License

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

repositron-0.4.0.tar.gz (13.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

repositron-0.4.0-py3-none-any.whl (16.0 kB view details)

Uploaded Python 3

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

Hashes for repositron-0.4.0.tar.gz
Algorithm Hash digest
SHA256 4111c1158ac8d39de5149d76e40be56484d72d58fee683a1c1a7116f70761c59
MD5 bcd046f8d2484aa54dfdecc06dce8362
BLAKE2b-256 770b0bdefc7eea4a2dfc60596f46b5ed8f2d2c5c0e359f8f6a5289756ced6846

See more details on using hashes here.

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

Hashes for repositron-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c2716cf77c58208f9de8a2eb470b2218001c76b282dd3904b45d12ec2eec479e
MD5 b2734d1f3df772b7278739670efcf3ae
BLAKE2b-256 88476901af0811cc6dc85ebf071a25560ce35e4bd811bab3fad28cca92681527

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page