как поправить 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)