Skip to main content

csrd-repository

Base repository and database adapter abstractions for FastAPI microservices.

Package: csrd.repository · Import: from csrd.repository import BaseRepository, SQLiteAdapter

What's included

  • BaseRepository — abstract repository with model parsing (requires an adapter)
  • ABCDatabaseAdapter / DBProtocol — async database adapter abstraction with lifecycle and explicit transactions
  • SQLiteAdapter — async SQLite implementation via aiosqlite (persistent connection, atomic upsert)
  • PGAdapter — PostgreSQL via psycopg with a psycopg_pool connection pool, sslmode / libpq options
  • MariaAdapter — MariaDB/MySQL via pymysql with a bounded connection pool, ssl / options
  • ExecuteResult — immutable snapshot of query metadata (replaces raw cursor return)
  • Optional sqlalchemy integration for query building (pip install csrd-repository[sql])

Installation

uv pip install "csrd-repository @ git+ssh://git@github.com/csrd-api/fastapi-common.git#subdirectory=packages/repository"

Dependencies

  • csrd-models (Tier 2)

Network drivers are optional extras:

uv pip install "csrd-repository[pg]"     # psycopg[binary] + psycopg-pool
uv pip install "csrd-repository[maria]"  # pymysql

Adapter lifecycle and connection pooling

Create one adapter per process, connect() it at startup and close() it at shutdown. PGAdapter owns a psycopg_pool.ConnectionPool; MariaAdapter owns a small built-in bounded pool; SQLiteAdapter holds one connection. Queries issued before connect() (or after close()) raise RuntimeError — the adapters never open a hidden, unmanaged pool.

adapter = PGAdapter(
    host="db", port=5432, user="svc", password=secret, database="svc",
    pool_min_size=1,      # opened eagerly by connect()
    pool_max_size=10,     # hard cap on concurrent server connections
    pool_max_idle=600.0,  # seconds before an idle connection is closed
    pool_timeout=30.0,    # wait for a free connection / for connect()
    pool_check=True,      # validate a connection on checkout, replace dead ones
)

connect() waits until pool_min_size connections are established, so an unreachable database fails at startup. Plain queries borrow a pooled connection in autocommit mode for one statement.

FastAPI

The scaffolded lifespan creates the adapter once and stores it on app state; request handlers (HTTP and WebSocket) read it from there:

@asynccontextmanager
async def _db_lifespan(app: FastAPI) -> AsyncIterator[dict[str, object]]:
    adapter = create_adapter(settings)
    await adapter.connect()
    try:
        yield {"db_adapter": adapter}
    finally:
        await adapter.close()


def _adapter_from_state(connection: HTTPConnection) -> PGAdapter:
    return connection.app.state.db_adapter


Adapter = Annotated[PGAdapter, Depends(_adapter_from_state)]

Never use Depends(create_adapter): that would build (and, with pooling, need to open) a new adapter per request.

Workers and scripts (outside FastAPI)

Use the adapter as an async context manager, or call connect()/close() around the worker's lifetime — not per task:

async def main() -> None:
    async with create_adapter(settings) as adapter:   # connect() ... close()
        await run_worker(adapter)

If tasks run on short-lived event loops (for example one asyncio.run() per task), wrap each run in async with create_adapter(settings) as adapter: so the pool is always closed; prefer one long-lived loop and adapter per worker process where possible, since every connect() opens fresh connections.

PostgreSQL TLS and connection options

PGAdapter(
    host="10.10.10.80", port=5432, user="svc", password=secret, database="svc",
    sslmode="verify-full",
    options={
        "sslrootcert": "/etc/ssl/certs/db-ca.pem",
        "application_name": "catalog-service",
        "connect_timeout": "5",
    },
)
  • sslmode: str | None = None — when None the keyword is omitted, so libpq still honours PGSSLMODE (and other PG* variables) from the environment.
  • options: Mapping[str, str] | None — any other libpq keyword. Keys that duplicate explicit arguments (host, port, user, password, dbname, sslmode) raise ValueError.
  • The conninfo is built with psycopg.conninfo.make_conninfo, so passwords containing spaces, quotes or backslashes are quoted correctly. The pool uses this same conninfo.

MariaAdapter accepts ssl: Mapping[str, Any] | None (passed to pymysql.connect(ssl=...), e.g. {"ca": "/etc/ssl/ca.pem"}) and options: Mapping[str, Any] | None for other pymysql.connect keywords.

Transactions

All adapters expose an explicit unit-of-work context:

async with adapter.transaction() as tx:
    users = user_repository.with_adapter(tx)
    credentials = credential_repository.with_adapter(tx)
    user = await users.insert("users", user_values)
    await credentials.insert("credentials", {"user_id": user["id"], **credential_values})

The yielded adapter uses one physical connection (borrowed from the pool for the whole block with autocommit off, and returned rolled back/reset). Normal exit commits; exceptions and cancellation roll back. A transaction-bound adapter raises RuntimeError if reused after context exit.

Joining an open transaction

adapter.in_transaction is True while an adapter is bound to an open transaction. Nesting transaction() on a bound adapter raises RuntimeError unless you opt in with join=True:

async def insert_many(self, rows):
    # All-or-nothing when called alone; part of the caller's unit of work
    # when called with a transaction-bound adapter.
    async with self._adapter.transaction(join=True) as tx:
        for row in rows:
            await tx.insert("cards", row)
  • On a bound adapter, join=True yields that same adapter; the joined block neither commits nor rolls back — the outermost transaction() does. An exception escaping the joined block rolls back the whole outer transaction unless the outer code catches it.
  • On an unbound adapter, join=True opens a new transaction as usual.
  • Joining is never implicit: code whose write must survive an outer rollback (for example a security revocation) should keep calling plain transaction() on an unbound adapter, which never joins.

Locking remains database-specific: PostgreSQL and MariaDB support their native row-lock syntax (for example SELECT ... FOR UPDATE); SQLite serializes operations on its single connection and does not support FOR UPDATE.

Metadata

Release files for csrd-repository 0.8.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for csrd-repository 0.8.0
File Size Uploaded
csrd_repository-0.8.0.tar.gz 25.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for csrd-repository 0.8.0
File Interpreter ABI Platform
csrd_repository-0.8.0-py3-none-any.whl Python 3 none any Details

Total release size: 50.2 kB

Release files / csrd_repository-0.8.0.tar.gz

Download URL csrd_repository-0.8.0.tar.gz
Size 25.8 kB
Tags Source
SHA-256 checksum
How to use checksums
25ac517b5fcd7b7dd9c9e19b6b5abdc41e4db0bc60d242b4e28f0625d8c9664c
BLAKE2b-256 checksum
How to use checksums
f65009b8dc78af5efa06c5e8ac9e596ed8eeba8eb6be204be7fe4b161790b81e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

Release files / csrd_repository-0.8.0-py3-none-any.whl

Download URL csrd_repository-0.8.0-py3-none-any.whl
Size 24.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ede1b5c323473153bedc4d594d910f0aa788e58091cbc335be4467f238c3825d
BLAKE2b-256 checksum
How to use checksums
784db72d90af933bde23998bc331d150114d1524b9cf1d5cde7b4c1b372e097a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.8.0 This release

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.30

2 release files

0.5.29

2 release files

0.5.28

2 release files

0.5.27

2 release files

0.5.26

2 release files

0.5.25

2 release files

0.5.24

2 release files

0.5.23

2 release files

0.5.22

2 release files

0.5.21

2 release files

0.5.9

2 release files

0.5.8

2 release files

0.5.7

2 release files

0.5.6

2 release files

0.5.5

2 release files

0.5.4

2 release files

0.5.3

2 release files

0.5.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.9

2 release files

0.3.8

2 release files

0.3.7

2 release files

0.3.6

2 release files

0.3.5

2 release files

0.3.4

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.45

2 release files

0.1.44

2 release files

0.1.43

2 release files

0.1.42

2 release files

0.1.41

2 release files

0.1.40

2 release files

0.1.39

2 release files

0.1.38

2 release files

0.1.37

2 release files

0.1.36

2 release files

0.1.35

2 release files

0.1.34

2 release files

0.1.33

2 release files

0.1.32

2 release files

0.1.31

2 release files

0.1.30

2 release files

0.1.29

2 release files

0.1.28

2 release files

0.1.26

2 release files

0.1.25

2 release files

0.1.24

2 release files

0.1.23

2 release files

0.1.22

2 release files

0.1.21

2 release files

0.1.20

2 release files

0.1.19

2 release files

0.1.18

2 release files

0.1.17

2 release files

0.1.15

2 release files

0.1.14

2 release files

0.1.13

2 release files

0.1.11

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page