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.

Before (`v4`)Now (`v5`)

    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.

Before (`v4`)Now (`v5`)

    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:

Before (`v4`)Now (`v5`)

    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`

Before (`v4`)Now (`v5`)

    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.

Before (`v4`)Now (`v5`)

    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 ...`.