как поправить MissingGreenlet SQLAlchemy python FastAPI

У меня есть patch метод на обновление

выглядит примерно так

@router.patch("/{id}", response_model=ResponseBlockSchema, status_code=status.HTTP_200_OK)
async def update_block_router(
        update_data: UpdateBlockSchema,
        block_id: str = Query(..., alias="id"),
        block_service: BlockService = Depends(get_block_service),
):
    block = await block_service.update_block(
        block_id=uuid.UUID(block_id),
        update_data=update_data.dict(),
    )

    if not block:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Block not found",
        )

    return block

вот есть pydantic схемы для реквестов и респонсов

class BaseBlockSchema(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    type: Optional[str]
    props: dict
    order: int


class ChildBlockSchema(BaseBlockSchema):
    pass


class ParentBlockSchema(BaseBlockSchema):
    pageId: Optional[uuid.UUID]
    parentId: Optional[uuid.UUID]


class CreateBlockSchema(ParentBlockSchema):
    children: list[ChildBlockSchema]


class UpdateBlockSchema(ParentBlockSchema):
    pass


class ResponseBlockSchema(ParentBlockSchema):
    id: uuid.UUID


class ResponseBlockSchemaAfterCreate(ResponseBlockSchema):
    children: list[ChildBlockSchema]

не обращайте внимаение на схему для обновления, так как она не вписывается в концепцию патч метода, это дело поправимое ))

еще можно заметить, что я подтягиваю зависимость block_service

async def get_block_service(session: AsyncSession = Depends(get_async_session)) -> BlockService:
    return BlockService(
        BlockRepository(session=session),
    )

вот еще часть конфиг файла

from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession

DATABASE_URL = f"postgresql+asyncpg://{config.POSTGRES_USER}:{config.POSTGRES_PASSWORD}@{config.POSTGRES_HOST}:{config.POSTGRES_PORT}/{config.POSTGRES_DB}"


engine = create_async_engine(DATABASE_URL)
async_session_maker = async_sessionmaker(engine, expire_on_commit=False)


# генерирует сессии для БД
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
    async with async_session_maker() as session:
        yield session

вроде как правильно использую получение асинхронных сессий

вот сам метод для обновления

class BlockService:
    def __init__(self, block_repo: BlockRepository):
        self.block_repo = block_repo
    async def update_block(self, block_id: uuid.UUID, update_data: dict) -> Optional[Block]:
        block = await self.block_repo.get(block_id)
        if not block:
            return None

        for key, value in update_data.items():
            if hasattr(block, key):
                setattr(block, key, value)

        if block.parentId == block.id:
            raise ValueError("Block cannot be its own parent.")

        return await self.block_repo.save(block)

можно заметить какой-то репозиторий

class BlockRepository(BaseDBRepository[models.Block]):
    def __init__(self,
                 session: AsyncSession,
                 *args, **kwargs):
        super().__init__(models.Block,
                         session,
                         *args, **kwargs)

    async def get_blocks_by_page(self, page_id: UUID, many: bool = True) -> Optional[Sequence[models.Block] | models.Block]:
        return await self.get_where(many=many, whereclause=models.Block.page_id == page_id)

и тут тоже есть базовый репозиторий. По большей части он нужен только для тривиальных взаимодействий с БД

AbstractModel = TypeVar('AbstractModel')


def rollback_wrapper(func):
    @wraps(func)
    async def inner(self, *args, **kwargs):
        try:
            return await func(self, *args, **kwargs)
        except SQLAlchemyError as e:
            await self.session.rollback()
            logger.error(f"Error during DB operation: {e}")
            raise e
    return inner


class BaseDBRepository(Generic[AbstractModel]):
    type_model: type[AbstractModel]

    def __init__(self,
                 type_model: type[AbstractModel],
                 session: AsyncSession,
                 *args, **kwargs):
        self.type_model = type_model
        self.session = session

    @rollback_wrapper
    async def get(self,
                  ident: Any,
                  options: list = None
                  ) -> AbstractModel | None:
        """Get an ONE model from the database with PK.

        :param options:
        :param ident: Key which need to find entry in database
        :return:
        """
        async with self.session.begin_nested():
            options = options or []
            statement = select(self.type_model).options(*options).where(self.type_model.id == ident)
            result = await self.session.execute(statement)
            return result.unique().scalar_one_or_none()

    @rollback_wrapper
    async def get_where(self,
                        many: bool = False,
                        whereclause=None,
                        limit: int | None = None,
                        offset: int | None = None,
                        order_by=None) -> Sequence[AbstractModel] | AbstractModel | None:
        """Get an ONE model from the database with whereclause.
        :param offset:
        :param many:
        :param whereclause: Clause by which entry will be found
        :param limit: Number of elements per query
        :param order_by: Name of field for ordering
        :return: Model if only one model was found, else None.
        """
        async with self.session.begin_nested():
            statement = select(self.type_model)
            if whereclause is not None:
                statement = statement.where(whereclause)
            if limit is not None:
                statement = statement.limit(limit)
            if offset is not None:
                statement = statement.offset(offset)
            if order_by is not None:
                statement = statement.order_by(order_by)

            result = await self.session.execute(statement)
            return result.unique().scalars().all() if many else result.scalar_one_or_none()

    @rollback_wrapper
    async def delete(self,
                     obj: AbstractModel):
        logger.debug(f"[DB] Saving: {obj}")
        async with self.session.begin_nested():
            await self.session.delete(obj)
            await self.session.commit()

    @rollback_wrapper
    async def save(
            self,
            obj: AbstractModel | Sequence[AbstractModel],
            many: bool = False
    ) -> AbstractModel | Sequence[AbstractModel]:
        """

        :param obj: object or objects to save
        :param many: flag for many saves
        :return:
        """
        async with self.session.begin_nested():
            logger.debug(f"[DB] Saving: {obj}")
            if many and isinstance(obj, Sequence):
                self.session.add_all(obj)
            else:
                self.session.add(obj)
            await self.session.commit()
            if many and isinstance(obj, Sequence):
                for obj_item in obj:
                    await self.session.refresh(obj_item)
            return obj

проверяя через дебаггер почему-то я получаю ошибку в месте цикла в методе на обновление.

то есть тут

        for key, value in update_data.items():
            if hasattr(block, key):
                setattr(block, key, value)

потому что я поставил точку дебаггера на условие сразу после цикла, там уже падала ошибка

вот модель блока

class Block(Base):
    __tablename__ = "blocks"

    id = Column(UUID, primary_key=True, default=uuid.uuid4)
    type = Column(String, nullable=False)
    props = Column(JSON, nullable=False)
    parentId = Column(UUID, ForeignKey("blocks.id", ondelete="SET NULL"), nullable=True)
    pageId = Column(UUID, ForeignKey("pages.id", ondelete="SET NULL"), nullable=True)
    order = Column(Integer, nullable=False)
    created_at = Column(DateTime, default=func.now(), nullable=False)
    updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False)

    parent = relationship("Block", back_populates="children", remote_side="Block.id", lazy="subquery")

    children = relationship("Block", back_populates="parent", cascade="all, delete-orphan", lazy="subquery")

    page = relationship("Page", back_populates="blocks", lazy="subquery")

в целом поля pydantic схемы и этой модели совпадают по именам, так что в процессах сериализации и десериализации проблем быть не должно (и да, я знаю, что называть переменные/поля класса в camelCase стиле в питоне не принято, позже пофикшу с помощью alias)

падает такая ошибка

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)


Ответы (0 шт):