Skip to main content

Transactional session management for SQLAlchemy use cases (sync + async).

Project description

fastapi-transactional

PyPI version Python versions License: MIT CI

Transactional session management for SQLAlchemy use cases — sync and async. Built for FastAPI and other apps that follow hexagonal / clean architecture, where a single use case orchestrates multiple repositories and they all need to share one transaction.

@with_transaction
def execute(self, policy_id: str) -> None:
    policy = self.policy_repo.find_by_id(policy_id)
    policy.status = "ACTIVE"
    self.policy_repo.save(policy)
    self.notification_repo.save(Notification(policy_id=policy_id))
    # Commit on success. Rollback on any exception. Both repos share the same session.

Why

In a hexagonal architecture, a use case typically depends on multiple repositories:

class ActivatePolicyUseCase:
    def __init__(self):
        self.policy_repo = PolicyRepository()
        self.notification_repo = NotificationRepository()

If each repository opens its own session, you lose transactional guarantees — a failure in notification_repo.save() won't undo the change in policy_repo.save(). The usual fix is to pass a session around explicitly, which leaks infrastructure into the domain layer.

fastapi-transactional solves this with a ContextVar-based session and a one-line decorator. Every repository inheriting from DatabaseRepository automatically receives the active session before the use case runs.

Install

pip install fastapi-transactional

Requires Python 3.10+ and SQLAlchemy 2.0+.

Quickstart — sync

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from fastapi_transactional import (
    DatabaseRepository, configure, with_transaction,
)

# 1. Configure once at startup.
engine = create_engine("postgresql+psycopg://user:pass@host/db")
SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False)
configure(session_factory=SessionLocal)


# 2. Inherit from DatabaseRepository — `self.db` is injected automatically.
class PolicyRepository(DatabaseRepository):
    def find_by_id(self, policy_id):
        return self.db.get(Policy, policy_id)

    def save(self, policy):
        self.db.add(policy)


# 3. Decorate the use case method.
class ActivatePolicyUseCase:
    def __init__(self):
        self.policy_repo = PolicyRepository()

    @with_transaction
    def execute(self, policy_id: str) -> None:
        policy = self.policy_repo.find_by_id(policy_id)
        policy.status = "ACTIVE"
        self.policy_repo.save(policy)

Quickstart — async

from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from fastapi_transactional import (
    AsyncDatabaseRepository, configure, with_async_transaction,
)

engine = create_async_engine("postgresql+asyncpg://user:pass@host/db")
AsyncSessionLocal = async_sessionmaker(bind=engine, expire_on_commit=False)
configure(async_session_factory=AsyncSessionLocal)


class PolicyRepository(AsyncDatabaseRepository):
    async def find_by_id(self, policy_id):
        return await self.db.get(Policy, policy_id)

    async def save(self, policy):
        self.db.add(policy)


class ActivatePolicyUseCase:
    def __init__(self):
        self.policy_repo = PolicyRepository()

    @with_async_transaction
    async def execute(self, policy_id: str) -> None:
        policy = await self.policy_repo.find_by_id(policy_id)
        policy.status = "ACTIVE"
        await self.policy_repo.save(policy)

You can configure both sync and async factories at the same time — they live in separate ContextVars and don't interfere.

How it works

  1. The decorator opens a session_manager() (sync) or async_session_manager() (async) context.
  2. The manager creates a session from the factory you registered with configure(...) and stores it in a ContextVar.
  3. The decorator scans self for attributes that are DatabaseRepository / AsyncDatabaseRepository instances and sets their .db to the active session.
  4. Your method runs. On clean exit the session commits; on any exception it rolls back.
  5. The session is always closed and removed from the ContextVar.

ContextVar is thread-safe and asyncio-safe — each request handler gets its own slot, so concurrent requests never see each other's sessions.

Nested use cases

A use case can call another use case without worrying about double transactions:

class Outer:
    def __init__(self):
        self.inner = Inner()

    @with_transaction
    def execute(self):
        self.inner.execute()      # Reuses the outer session.
        self.repo.save(thing)     # Same transaction.
        # Single commit when execute() returns.

When session_manager() sees an existing session bound to the context, it yields that session instead of opening a new one — so the outermost call owns the transaction lifecycle.

Advanced — accessing the session directly

For code that isn't a repository (e.g. a service that needs to run a raw query inside the active transaction):

from fastapi_transactional import get_current_session

def some_service():
    session = get_current_session()
    if session is None:
        raise RuntimeError("Must be called inside a transactional scope")
    session.execute(...)

The async equivalents are get_current_async_session, set_current_async_session, clear_current_async_session.

You can also use the context manager directly without the decorator:

with session_manager() as session:
    session.execute(...)
# Commit happens here. Rollback on exception.

API reference

Symbol Purpose
configure(session_factory=..., async_session_factory=...) Register session factories (call once at startup).
reset() Clear the registered factories (useful in tests).
session_manager() / async_session_manager() Context manager that opens a transactional scope.
with_transaction / with_async_transaction Decorator wrapping a method in a transactional scope and injecting sessions into repositories.
DatabaseRepository / AsyncDatabaseRepository Base class — gives the repo a self.db slot.
get_current_session / set_current_session / clear_current_session Direct access to the sync ContextVar.
get_current_async_session / set_current_async_session / clear_current_async_session Direct access to the async ContextVar.

Tests

pip install -e ".[test]"
pytest --cov=fastapi_transactional

Contributing

Issues and pull requests welcome. See CONTRIBUTING.md.

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

fastapi_transactional-0.1.0.tar.gz (12.7 kB view details)

Uploaded Source

Built Distribution

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

fastapi_transactional-0.1.0-py3-none-any.whl (11.3 kB view details)

Uploaded Python 3

File details

Details for the file fastapi_transactional-0.1.0.tar.gz.

File metadata

  • Download URL: fastapi_transactional-0.1.0.tar.gz
  • Upload date:
  • Size: 12.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for fastapi_transactional-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f77075140c59380e1b98e2e0e254bc8ebb4ac63374304c847d6715327dba1a91
MD5 5c54f0a5984065c9dde0aa80e3edddd5
BLAKE2b-256 ff6605902ded2e0f3f7240ff9f7e77a3ab969f296700ae8cf70fe7bd4b6628d1

See more details on using hashes here.

Provenance

The following attestation bundles were made for fastapi_transactional-0.1.0.tar.gz:

Publisher: release.yml on oviladrosa/fastapi-transactional

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file fastapi_transactional-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for fastapi_transactional-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 29d0b5dbd37ffdd6e9756dcfaa63d7777f4653d3715779376cd27800abb7b40c
MD5 120e7589dd22e04821fd71ab92c50acf
BLAKE2b-256 be997d5d5576ea011651ab6f044a1fb546e2a34c80107517c02ae8195331cc4b

See more details on using hashes here.

Provenance

The following attestation bundles were made for fastapi_transactional-0.1.0-py3-none-any.whl:

Publisher: release.yml on oviladrosa/fastapi-transactional

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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