chore: rework DB module (#324)

This commit is contained in:
d3vyce
2026-06-25 21:11:50 +02:00
committed by GitHub
parent 22f307d0fc
commit 9698a0743b
23 changed files with 1662 additions and 931 deletions
+628 -186
View File
File diff suppressed because it is too large Load Diff
+12 -6
View File
@@ -91,13 +91,19 @@ async def seed(session: AsyncSession):
class TestAppSessionDep:
@pytest.mark.anyio
async def test_get_db_yields_async_session(self):
"""get_db yields a real AsyncSession when called directly."""
from docs_src.examples.pagination_search.db import get_db
"""The Database dependency yields a real AsyncSession when called directly."""
from starlette.requests import Request
gen = get_db()
session = await gen.__anext__()
assert isinstance(session, AsyncSession)
await gen.aclose()
from fastapi_toolsets.db import Database
db = Database(DATABASE_URL)
try:
gen = db(Request({"type": "http", "headers": []}))
session = await gen.__anext__()
assert isinstance(session, AsyncSession)
await gen.aclose()
finally:
await db.engine.dispose()
class TestOffsetPagination:
+28 -42
View File
@@ -1506,8 +1506,8 @@ class TestListensFor:
assert all(e["event"] == "change" for e in _listener_events)
class TestEventSessionWithGetTransaction:
"""Verify callbacks fire correctly when using get_transaction / lock_tables."""
class TestEventSessionWithTransaction:
"""Verify callbacks fire correctly when using transaction / lock_tables."""
@pytest.fixture(autouse=True)
def clear_events(self):
@@ -1517,10 +1517,10 @@ class TestEventSessionWithGetTransaction:
@pytest.mark.anyio
async def test_callbacks_fire_after_outer_commit_not_savepoint(self, mixin_session):
"""get_transaction creates a savepoint; callbacks fire only on outer commit."""
from fastapi_toolsets.db import get_transaction
"""transaction creates a savepoint; callbacks fire only on outer commit."""
from fastapi_toolsets.db import transaction
async with get_transaction(mixin_session):
async with transaction(mixin_session):
obj = WatchedModel(status="active", other="x")
mixin_session.add(obj)
@@ -1535,14 +1535,14 @@ class TestEventSessionWithGetTransaction:
@pytest.mark.anyio
async def test_nested_transactions_accumulate_events(self, mixin_session):
"""Multiple get_transaction blocks accumulate events for a single commit."""
from fastapi_toolsets.db import get_transaction
"""Multiple transaction blocks accumulate events for a single commit."""
from fastapi_toolsets.db import transaction
async with get_transaction(mixin_session):
async with transaction(mixin_session):
obj1 = WatchedModel(status="first", other="x")
mixin_session.add(obj1)
async with get_transaction(mixin_session):
async with transaction(mixin_session):
obj2 = WatchedModel(status="second", other="y")
mixin_session.add(obj2)
@@ -1556,14 +1556,14 @@ class TestEventSessionWithGetTransaction:
@pytest.mark.anyio
async def test_savepoint_rollback_suppresses_events(self, mixin_session):
"""Objects from a rolled-back savepoint don't fire callbacks."""
from fastapi_toolsets.db import get_transaction
from fastapi_toolsets.db import transaction
survivor = WatchedModel(status="kept", other="x")
mixin_session.add(survivor)
await mixin_session.flush()
try:
async with get_transaction(mixin_session):
async with transaction(mixin_session):
doomed = WatchedModel(status="doomed", other="y")
mixin_session.add(doomed)
await mixin_session.flush()
@@ -1590,9 +1590,9 @@ class TestEventSessionWithGetTransaction:
assert len(creates) == 1
@pytest.mark.anyio
async def test_update_inside_get_transaction(self, mixin_session):
"""UPDATE events fire with correct changes after get_transaction commit."""
from fastapi_toolsets.db import get_transaction
async def test_update_inside_transaction(self, mixin_session):
"""UPDATE events fire with correct changes after transaction commit."""
from fastapi_toolsets.db import transaction
obj = WatchedModel(status="initial", other="x")
mixin_session.add(obj)
@@ -1600,7 +1600,7 @@ class TestEventSessionWithGetTransaction:
_test_events.clear()
async with get_transaction(mixin_session):
async with transaction(mixin_session):
obj.status = "updated"
await mixin_session.commit()
@@ -1696,7 +1696,7 @@ class TestEventSessionWithNullableFields:
class TestEventSessionWithFastAPIDependency:
"""Verify EventSession works when session comes from create_db_dependency."""
"""Verify EventSession works when session comes from the Database dependency."""
@pytest.fixture(autouse=True)
def clear_events(self):
@@ -1706,31 +1706,24 @@ class TestEventSessionWithFastAPIDependency:
@pytest.mark.anyio
async def test_create_event_fires_via_dependency(self):
"""CREATE callback fires when session is provided by create_db_dependency."""
"""CREATE callback fires when session is provided by the Database dependency."""
from fastapi import Depends, FastAPI
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import (
AsyncSession,
async_sessionmaker,
create_async_engine,
)
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from fastapi_toolsets.db import create_db_dependency
from fastapi_toolsets.db import Database
from fastapi_toolsets.models import EventSession
engine = create_async_engine(DATABASE_URL, echo=False)
session_factory = async_sessionmaker(
engine, expire_on_commit=False, class_=EventSession
)
async with engine.begin() as conn:
await conn.run_sync(MixinBase.metadata.create_all)
get_db = create_db_dependency(session_factory)
db = Database(engine=engine, session_class=EventSession)
app = FastAPI()
@app.post("/watched")
async def create_watched(session: AsyncSession = Depends(get_db)):
async def create_watched(session: AsyncSession = Depends(db)):
obj = WatchedModel(status="from-api", other="x")
session.add(obj)
return {"id": str(obj.id)}
@@ -1753,40 +1746,33 @@ class TestEventSessionWithFastAPIDependency:
@pytest.mark.anyio
async def test_update_event_fires_via_dependency(self):
"""UPDATE callback fires when session is provided by create_db_dependency."""
"""UPDATE callback fires when session is provided by the Database dependency."""
from fastapi import Depends, FastAPI
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import (
AsyncSession,
async_sessionmaker,
create_async_engine,
)
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from fastapi_toolsets.db import create_db_dependency
from fastapi_toolsets.db import Database
from fastapi_toolsets.models import EventSession
engine = create_async_engine(DATABASE_URL, echo=False)
session_factory = async_sessionmaker(
engine, expire_on_commit=False, class_=EventSession
)
async with engine.begin() as conn:
await conn.run_sync(MixinBase.metadata.create_all)
get_db = create_db_dependency(session_factory)
db = Database(engine=engine, session_class=EventSession)
app = FastAPI()
# Pre-seed an object.
async with session_factory() as seed_session:
async with db.session() as seed_session:
obj = WatchedModel(status="initial", other="x")
seed_session.add(obj)
await seed_session.commit()
await seed_session.flush()
obj_id = obj.id
_test_events.clear()
@app.put("/watched/{item_id}")
async def update_watched(item_id: str, session: AsyncSession = Depends(get_db)):
async def update_watched(item_id: str, session: AsyncSession = Depends(db)):
from sqlalchemy import select
stmt = select(WatchedModel).where(WatchedModel.id == item_id)
+7 -7
View File
@@ -11,7 +11,7 @@ from sqlalchemy.engine import make_url
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from fastapi_toolsets.db import get_transaction
from fastapi_toolsets.db import transaction
from fastapi_toolsets.fixtures import Context, FixtureRegistry, LoadStrategy
from fastapi_toolsets.pytest import (
create_async_client,
@@ -387,14 +387,14 @@ class TestCreateDbSession:
assert session.autoflush is False
@pytest.mark.anyio
async def test_get_transaction_commits_visible_to_separate_session(self):
"""Data written via get_transaction() is committed and visible to other sessions."""
async def test_transaction_commits_visible_to_separate_session(self):
"""Data written via transaction() is committed and visible to other sessions."""
role_id = uuid.uuid4()
async with create_db_session(DATABASE_URL, Base, drop_tables=False) as session:
# Simulate what _create_fixture_function does: insert via get_transaction
# Simulate what _create_fixture_function does: insert via transaction()
# with no explicit commit afterward.
async with get_transaction(session):
async with transaction(session):
role = Role(id=role_id, name="visible_to_other_session")
session.add(role)
@@ -409,9 +409,9 @@ class TestCreateDbSession:
result = await other.execute(select(Role).where(Role.id == role_id))
fetched = result.scalar_one_or_none()
assert fetched is not None, (
"Fixture data inserted via get_transaction() must be committed "
"Fixture data inserted via transaction() must be committed "
"and visible to a separate session. If create_db_session uses "
"create_db_context, auto-begin forces get_transaction() into "
"db.session(), auto-begin forces transaction() into "
"savepoints instead of real commits."
)
assert fetched.name == "visible_to_other_session"