0

I am using context manager with fastapi session, my setup is below:

from sqlmodel import create_engine, Session

from app.core.config import settings

engine = create_engine(settings.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True)
SessionLocal = Session(autocommit=False, autoflush=False, bind=engine)

then is then consumed by

from contextlib import contextmanager
from sqlmodel import Session
from app.db.session import SessionLocal

@contextmanager
def get_session():
    with SessionLocal as session:
        yield session

which this is used by an endpoint

@router.post("/")
def create_book(
    *,
    db: Session = Depends(get_session),
    book_in: models.BookCreate,
    current_user: models.User = Depends(get_current_active_user),
) -> models.BookCreate:
    """
    Create new book.
    """
    # print(current_user.id)
    book = crud.book.create_with_owner(db=db, obj_in=book_in, owner_id=current_user.id)
    return book

With the setup above, I am expecting everything to work, but that is not the case; instead, I am getting the error below:

backend-1  |   File "/app/app/api/api_v1/endpoints/book.py", line 42, in create_book
backend-1  |     book = crud.book.create_with_owner(db=db, obj_in=book_in, owner_id=current_user.id)
backend-1  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
backend-1  |   File "/app/app/crud/crud_book.py", line 23, in create_with_owner
backend-1  |     db.add(db_obj)
backend-1  |     ^^^^^^
backend-1  | AttributeError: '_GeneratorContextManager' object has no attribute 'add'

below are the details from crud_book.py

class CRUDBook(CRUDBase[Book, BookCreate, BookUpdate]):
    def create_with_owner(
        self, db: Session, *, obj_in: BookCreate, owner_id: UUID
    ) -> Book:
        # with get_session() as db:
        obj_in_data = jsonable_encoder(obj_in)
        obj_in_data = dict(obj_in)

        print(db)
        # TODO: Check if owner_id is none

        db_obj = self.model(**obj_in_data, owner_id=owner_id)
        db.add(db_obj)
        db.commit()
        db.refresh(db_obj)
        return db_obj

When introduced another with context with the crud_book.py, the session does work but that comes with a cost since the session only exists while creating the book object in db but gets closed there after thus ending up with the below error

backend-1  |   File "/usr/local/lib/python3.11/site-packages/sqlmodel/main.py", line 597, in validate
backend-1  |     return cls.from_orm(value)
backend-1  |            ^^^^^^^^^^^^^^^^^^^
backend-1  |   File "/usr/local/lib/python3.11/site-packages/sqlmodel/main.py", line 552, in from_orm
backend-1  |     values, fields_set, validation_error = validate_model(cls, obj)
backend-1  |                                            ^^^^^^^^^^^^^^^^^^^^^^^^
backend-1  |   File "pydantic/main.py", line 1056, in pydantic.main.validate_model
backend-1  |   File "pydantic/utils.py", line 441, in pydantic.utils.GetterDict.get
backend-1  |   File "/usr/local/lib/python3.11/site-packages/sqlalchemy/orm/attributes.py", line 482, in __get__
backend-1  |     return self.impl.get(state, dict_)
backend-1  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^
backend-1  |   File "/usr/local/lib/python3.11/site-packages/sqlalchemy/orm/attributes.py", line 942, in get
backend-1  |     value = self._fire_loader_callables(state, key, passive)
backend-1  |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
backend-1  |   File "/usr/local/lib/python3.11/site-packages/sqlalchemy/orm/attributes.py", line 978, in _fire_loader_callables
backend-1  |     return self.callable_(state, passive)
backend-1  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
backend-1  |   File "/usr/local/lib/python3.11/site-packages/sqlalchemy/orm/strategies.py", line 863, in _load_for_state
backend-1  |     raise orm_exc.DetachedInstanceError(
backend-1  | sqlalchemy.orm.exc.DetachedInstanceError: Parent instance <Book at 0x7fc8a3087b90> is not bound to a Session; lazy load operation of attribute 'authors' cannot proceed (Background on this error at: https://sqlalche.me/e/14/bhk3)

just in case, below are the details that are expected during bookcreate

import datetime
from uuid import uuid4, UUID
from typing import TYPE_CHECKING

from sqlmodel import SQLModel, Field, Relationship

# if TYPE_CHECKING:
from .genre import Genre  # noqa: F401
from .author import Author  # noqa: F401
from .narrator import Narrator  # noqa: F401
from .publisher import Publisher  # noqa: F401
from .user import User, UserRead  # noqa: F401
from .book_genre_link import BookGenreLink  # noqa: F401
from .book_author_link import BookAuthorLink  # noqa: F401
from .book_narrator_link import BookNarratorLink  # noqa: F401
from .book_publisher_link import BookPublisherLink  # noqa: F401


class BookBase(SQLModel):
    title: str = Field(index=True)
    subtitle: str | None = Field(default=None, index=True)
    description: str = Field(index=True)
    runtime: datetime.time | None = Field(default=None)
    rating: float | None = Field(default=None)
    published_date: datetime.date | None = Field(default=None)
    cover_image: str | None = Field(default=None)

class Book(BookBase, table=True):
    id: UUID = Field(
        default_factory=uuid4,
        primary_key=True,
        index=True,
        nullable=False,
    )
    owner: list["User"] = Relationship(back_populates="books")
    owner_id: UUID | None = Field(default=None, foreign_key="user.id")
    authors: list["Author"] = Relationship(
        back_populates="books", link_model=BookAuthorLink
    )
    publishers: list["Publisher"] = Relationship(
        back_populates="books", link_model=BookPublisherLink
    )
    genres: list["Genre"] = Relationship(
        back_populates="books", link_model=BookGenreLink
    )
    narrators: list["Narrator"] = Relationship(
        back_populates="books", link_model=BookNarratorLink
    )
    # creation_date: datetime = Field(default=datetime.utcnow())
    # update_date: datetime = Field(default=datetime.utcnow())


class BookCreate(BookBase):
    authors: list["Author"] = []
    publishers: list["Publisher"] = []
    genres: list["Genre"] = []
    narrators: list["Narrator"] = []

Why do I need two multiple context managers to handle the session, or where have i gone wrong?

David Buck
  • 3,752
  • 35
  • 31
  • 35
Olaw2jr
  • 5
  • 3
  • you don't need `@contextmanager` above `get_session` – python_user Jan 14 '23 at 13:35
  • With getting rid of `@contextmanager` do i still need the second context manager? – Olaw2jr Jan 14 '23 at 13:40
  • you must be able to use it without that – python_user Jan 14 '23 at 13:42
  • getting a separate error right now of generator type error `audible-backend-1 | File "/usr/local/lib/python3.11/contextlib.py", line 222, in __aexit__ backend-1 | await self.gen.athrow(typ, value, traceback) backend-1 | File "/usr/local/lib/python3.11/site-packages/fastapi/concurrency.py", line 36, in contextmanager_in_threadpool backend-1 | raise e backend-1 | TypeError: 'generator' object does not support the context manager protocol ` – Olaw2jr Jan 14 '23 at 13:43
  • just so I was clear, you have to remove `contextmanager` above `get_session` then just use `db` as it it were a session, no other context manager is necessary – python_user Jan 14 '23 at 13:44
  • Yeah precisely that is what I have now, gotten rid of the @contextmanger and with the statement of the crud endpoint – Olaw2jr Jan 14 '23 at 13:53
  • The other imports are on imports are just the models and schema that are related with the book model. – Olaw2jr Jan 14 '23 at 14:49

1 Answers1

1

For some reason, FastAPI dependencies should be generators, not context managers (it's mentioned in the documentation). What I usually do is

from contextlib import contextmanager

def get_db():
    # gaenerator using yield

db_context = contextmanager(get_db)

Then I can use Depends(get_db) in my FastAPI endpoints, and with db_context() as db: in other places (e.g. Celery tasks, CRON jobs, etc.).

M.O.
  • 1,712
  • 1
  • 6
  • 18
  • Hi @M.0. with the above where do I need to handle the context manager is it within the get_db method or within the create_with_owner in the BookCrud class? – Olaw2jr Jan 14 '23 at 14:52
  • If you only have FastAPI endpoints, then you don't really need it, just use the dependency and then pass that to functions that need it (like you have in `create_with_owner`). It's only if you need a database connection outside of FastAPI that It's needed. – M.O. Jan 14 '23 at 15:05