pythonsqlalchemyfastapi

How to use AsyncSession.sync_session or how to have both sync and async session with the same scope of objects, single transaction?


I'd like to use AsyncSession.sync_session but the code below fails with error. The code is a simplified version of real case.

import sqlalchemy as sa
import uvicorn
from fastapi import FastAPI, Depends
from sqlalchemy import MetaData, select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import declarative_base
    
app = FastAPI()
metadata = MetaData()
Base = declarative_base(metadata=metadata)
engine = create_async_engine(
    "postgresql+asyncpg://postgres:postgres@localhost:5433/portal-podrjadchika-local", echo=True
)
SessionLocal = async_sessionmaker(bind=engine, expire_on_commit=False)


class User(Base):
    __tablename__ = "users"

    id = sa.Column(sa.Integer, autoincrement=True, primary_key=True, index=True)
    first_name = sa.Column(sa.String)
    last_name = sa.Column(sa.String)


async def get_session() -> AsyncSession:
    session = SessionLocal()
    try:
        yield session
        await session.commit()
    except Exception as e:
        await session.rollback()
        raise e
    finally:
        await session.close()


@app.on_event("startup")
async def on_startup() -> None:
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)
        await conn.run_sync(Base.metadata.create_all)


@app.post('/users')
async def get_users(session: AsyncSession = Depends(get_session)):
    stmt = select(User)
    print(session.sync_session.scalars(stmt))  # fails with error sqlalchemy.exc.MissingGreenlet: greenlet_spawn has not been called; can't call await_only() here. Was IO attempted in an unexpected place? (Background on this error at: https://sqlalche.me/e/20/xd2s)
    return "user"


if __name__ == "__main__":
    uvicorn.run("so:app", reload=True)

Traceback:

ERROR:    Exception in ASGI application Traceback (most recent call
last):   File
"/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/uvicorn/protocols/http/h11_impl.py", line 408, in run_asgi
    result = await app(  # type: ignore[func-returns-value]   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py",
line 84, in __call__
    return await self.app(scope, receive, send)   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/fastapi/applications.py",
line 1054, in __call__
    await super().__call__(scope, receive, send)   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/applications.py",
line 123, in __call__
    await self.middleware_stack(scope, receive, send)   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/middleware/errors.py",
line 186, in __call__
    raise exc   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/middleware/errors.py",
line 164, in __call__
    await self.app(scope, receive, _send)   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 65, in __call__
    await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)   File
"/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/_exception_handler.py",
line 64, in wrapped_app
    raise exc   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/_exception_handler.py",
line 53, in wrapped_app
    await app(scope, receive, sender)   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/routing.py",
line 756, in __call__
    await self.middleware_stack(scope, receive, send)   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/routing.py",
line 776, in app
    await route.handle(scope, receive, send)   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/routing.py",
line 297, in handle
    await self.app(scope, receive, send)   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/routing.py",
line 77, in app
    await wrap_app_handling_exceptions(app, request)(scope, receive, send)   File
"/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/_exception_handler.py",
line 64, in wrapped_app
    raise exc   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/_exception_handler.py",
line 53, in wrapped_app
    await app(scope, receive, sender)   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/routing.py",
line 72, in app
    response = await func(request)   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/fastapi/routing.py",
line 278, in app
    raw_response = await run_endpoint_function(   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/fastapi/routing.py",
line 191, in run_endpoint_function
    return await dependant.call(**values)   File "/Users/alber.aleksandrov/PycharmProjects/Playground/fstp/so.py", line
50, in get_users
    print(session.sync_session.scalars(stmt))   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/sqlalchemy/orm/session.py",
line 2337, in scalars
    return self._execute_internal(   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/sqlalchemy/orm/session.py",
line 2120, in _execute_internal
    result: Result[Any] = compile_state_cls.orm_execute_statement(   File
"/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/sqlalchemy/orm/context.py",
line 293, in orm_execute_statement
    result = conn.execute(   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/sqlalchemy/engine/base.py",
line 1412, in execute
    return meth(   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/sqlalchemy/sql/elements.py",
line 483, in _execute_on_connection
    return connection._execute_clauseelement(   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/sqlalchemy/engine/base.py",
line 1635, in _execute_clauseelement
    ret = self._execute_context(   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/sqlalchemy/engine/base.py",
line 1844, in _execute_context
    return self._exec_single_context(   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/sqlalchemy/engine/base.py",
line 1984, in _exec_single_context
    self._handle_dbapi_exception(   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/sqlalchemy/engine/base.py",
line 2342, in _handle_dbapi_exception
    raise exc_info[1].with_traceback(exc_info[2])   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/sqlalchemy/engine/base.py",
line 1965, in _exec_single_context
    self.dialect.do_execute(   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/sqlalchemy/engine/default.py",
line 921, in do_execute
    cursor.execute(statement, parameters)   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py",
line 561, in execute
    self._adapt_connection.await_(   File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/sqlalchemy/util/_concurrency_py3k.py",
line 116, in await_only
    raise exc.MissingGreenlet( sqlalchemy.exc.MissingGreenlet: greenlet_spawn has not been called; can't call await_only() here. Was
IO attempted in an unexpected place? (Background on this error at:
https://sqlalche.me/e/20/xd2s)

How can I fix the error? Or how to have both sync and async session with the same scope of objects, single transaction? I need it to make Factory Boy https://factoryboy.readthedocs.io/en/stable/orms.html#sqlalchemy use sync session to instanciate objects.


Solution

  • Because you are using async session you need to await the method.

    @app.post('/users')
    async def get_users(session: AsyncSession = Depends(get_session)):
        stmt = select(User)
        result = await session.scalars(stmt)
        users = result.all()  # Fetch all the users
        return users
    

    For the factory-boy part it doesn't need to be the same transaction or session to use it. You just need to use the same database. To do that just update _create a method in factory:

        @classmethod
        def _create(cls, model_class, *args, **kwargs):
            with some_sync_session() as session:
               session.expire_on_commit = False
               obj = model_class(*args, **kwargs)
               session.add(obj)
               session.commit()
               session.expunge_all()
            return obj
    

    some_sync_session is just sessionmaker which points to the correct database.

    I did the similar stuff when I was writing tests for my package. You can check how I set up model factory here at github.