Transactional session management for SQLAlchemy use cases (sync + async).
Project description
fastapi-transactional
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
- The decorator opens a
session_manager()(sync) orasync_session_manager()(async) context. - The manager creates a session from the factory you registered with
configure(...)and stores it in aContextVar. - The decorator scans
selffor attributes that areDatabaseRepository/AsyncDatabaseRepositoryinstances and sets their.dbto the active session. - Your method runs. On clean exit the session commits; on any exception it rolls back.
- 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
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 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f77075140c59380e1b98e2e0e254bc8ebb4ac63374304c847d6715327dba1a91
|
|
| MD5 |
5c54f0a5984065c9dde0aa80e3edddd5
|
|
| BLAKE2b-256 |
ff6605902ded2e0f3f7240ff9f7e77a3ab969f296700ae8cf70fe7bd4b6628d1
|
Provenance
The following attestation bundles were made for fastapi_transactional-0.1.0.tar.gz:
Publisher:
release.yml on oviladrosa/fastapi-transactional
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fastapi_transactional-0.1.0.tar.gz -
Subject digest:
f77075140c59380e1b98e2e0e254bc8ebb4ac63374304c847d6715327dba1a91 - Sigstore transparency entry: 1520642341
- Sigstore integration time:
-
Permalink:
oviladrosa/fastapi-transactional@aa6fbf68fa487722cf0a2944b34a95354b11cc70 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/oviladrosa
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@aa6fbf68fa487722cf0a2944b34a95354b11cc70 -
Trigger Event:
push
-
Statement type:
File details
Details for the file fastapi_transactional-0.1.0-py3-none-any.whl.
File metadata
- Download URL: fastapi_transactional-0.1.0-py3-none-any.whl
- Upload date:
- Size: 11.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
29d0b5dbd37ffdd6e9756dcfaa63d7777f4653d3715779376cd27800abb7b40c
|
|
| MD5 |
120e7589dd22e04821fd71ab92c50acf
|
|
| BLAKE2b-256 |
be997d5d5576ea011651ab6f044a1fb546e2a34c80107517c02ae8195331cc4b
|
Provenance
The following attestation bundles were made for fastapi_transactional-0.1.0-py3-none-any.whl:
Publisher:
release.yml on oviladrosa/fastapi-transactional
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fastapi_transactional-0.1.0-py3-none-any.whl -
Subject digest:
29d0b5dbd37ffdd6e9756dcfaa63d7777f4653d3715779376cd27800abb7b40c - Sigstore transparency entry: 1520642361
- Sigstore integration time:
-
Permalink:
oviladrosa/fastapi-transactional@aa6fbf68fa487722cf0a2944b34a95354b11cc70 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/oviladrosa
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@aa6fbf68fa487722cf0a2944b34a95354b11cc70 -
Trigger Event:
push
-
Statement type: