Skip to content

Migrating to v5.0

This page covers every breaking change introduced in v5.0 and the steps required to update your code.


Database

db.py is now the db/ package, built around one object, Database, that owns the engine and sessionmaker. The free functions that took a session_maker you built and passed around yourself are gone from request-handling code; Database builds the sessionmaker for you.

create_db_dependency / create_db_context removed in favor of Database

Build one Database with your URL (or an existing engine=), then use the instance directly as the FastAPI dependency, and db.session() for sessions outside request handlers.

from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from fastapi_toolsets.db import create_db_dependency, create_db_context

engine = create_async_engine("postgresql+asyncpg://...")
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)

get_db = create_db_dependency(session_maker=SessionLocal)
get_db_context = create_db_context(session_maker=SessionLocal)

@app.get("/users")
async def list_users(session: AsyncSession = Depends(get_db)):
    ...

async def seed():
    async with get_db_context() as session:
        ...
from fastapi_toolsets.db import Database

db = Database(url="postgresql+asyncpg://...")

@app.get("/users")
async def list_users(session: AsyncSession = Depends(db)):
    ...

async def seed():
    async with db.session() as session:
        ...

Call db.install(app) to also commit before the response is sent (instead of in dependency teardown) and to dispose the engine on shutdown. See the db module docs.

get_transaction renamed to transaction

Same behavior (savepoint when already in a transaction, new transaction otherwise), new name, same import path.

from fastapi_toolsets.db import get_transaction

async with get_transaction(session=session):
    session.add(model)
from fastapi_toolsets.db import transaction

async with transaction(session=session):
    session.add(model)

If you have a Database instance, db.begin() opens a session already inside a transaction:

async with db.begin() as session:
    session.add(User(name="ada"))

lock_tables is now also a Database method

The free lock_tables(session_maker, tables, ...) function still exists for callers who manage their own session factory, but prefer db.lock_tables(tables, ...), which drops the session_maker argument:

from fastapi_toolsets.db import lock_tables, LockMode

async with lock_tables(session_maker=session_maker, tables=[Order], mode=LockMode.EXCLUSIVE) as session:
    await process_order(session, order_id)
from fastapi_toolsets.db import LockMode

async with db.lock_tables(tables=[Order], mode=LockMode.EXCLUSIVE) as session:
    await process_order(session, order_id)

create_database and cleanup_tables moved to fastapi_toolsets.db.testing

from fastapi_toolsets.db import create_database, cleanup_tables
from fastapi_toolsets.db.testing import create_database, cleanup_tables

Fixtures

get_obj_by_attr / get_field_by_attr are now FixtureRegistry methods

Both also change their first argument: instead of the fixture function, pass the fixture's registered name and let the registry look it up.

from fastapi_toolsets.fixtures import get_obj_by_attr, get_field_by_attr

@fixtures.register(depends_on=["roles"])
def users():
    admin_role = get_obj_by_attr(fixtures=roles, attr_name="name", value="admin")
    admin_role_id = get_field_by_attr(fixtures=roles, attr_name="name", value="admin")
    return [User(id=1, username="alice", role_id=admin_role.id)]
@fixtures.register(depends_on=["roles"])
def users():
    admin_role = fixtures.obj(name="roles", attr_name="name", value="admin")
    admin_role_id = fixtures.field(name="roles", attr_name="name", value="admin")
    return [User(id=1, username="alice", role_id=admin_role.id)]

Loading/listing by context now always includes Context.BASE

FixtureRegistry.get_variants/get_by_context, load_fixtures_by_context, and the fixtures list/fixtures load CLI commands now implicitly union every queried context with Context.BASE. In v4, requesting a single context (e.g. "testing") returned only fixtures tagged with that context; in v5 it also returns every Context.BASE-tagged fixture.

If you relied on strict exclusion of base fixtures, move them out of Context.BASE into their own context.

load_fixtures / load_fixtures_by_context now return reloaded instances

Loaded objects are re-queried from the database after insert, with all relationships eager-loaded, instead of returning the exact objects your fixture function constructed. Code that compares returned objects by identity to the fixture function's output, or that expected relationships to remain unloaded, should be updated — and expect one extra query per model type per load.

Security

The security module has been removed and moved to a dedicated python package: fastapi-multiauth.

Run uv add fastapi-multiauth and replace from fastapi_toolsets.security import ... with from fastapi_multiauth import ....