strawberry-graphql / strawberry-sqlalchemy

A SQLAlchemy Integration for strawberry-graphql
MIT License
91 stars 26 forks source link

Flask, SQLAlchemy, Mapper: Event Loop error when querrying relationships #61

Closed Philaeux closed 12 months ago

Philaeux commented 12 months ago

I have a backend in Flask, with SQLAlchemy models and strawberry for the GraphQL. I have been using Manual GraphQL Types and converting manually the SQLalchemy models to graphQL Types. Now, I am switching to this library to ditch these GQL Types and generate them from SQLAlchemy. It works fine for almost everything. I got a strange gevent error when fetcing relationships data.

Example of my models

class Software(Base):
    __tablename__ = "software"

    id: Mapped[str] = mapped_column(primary_key=True, index=True)
    full_name: Mapped[str] = mapped_column()
    editor: Mapped[str] = mapped_column()
    description: Mapped[str] = mapped_column()
    language: Mapped[str] = mapped_column()

    tags: Mapped[List["Tag"]] = relationship(back_populates="software", cascade="all, delete-orphan")

class Tag(Base):
    __tablename__ = "tag"

    id: Mapped[uuid.UUID] = mapped_column(UUID(), primary_key=True, default=uuid.uuid4)
    name: Mapped[str] = mapped_column()
    software_id: Mapped[str] = mapped_column(ForeignKey("software.id"), index=True)
    font_color: Mapped[str] = mapped_column()
    background_color: Mapped[str] = mapped_column()

    software: Mapped["Software"] = relationship("Software", foreign_keys=software_id, back_populates="tags")

Gennerate types this way

# ORM MAPPING
strawberry_sqlalchemy_mapper = StrawberrySQLAlchemyMapper()

@strawberry_sqlalchemy_mapper.type(SoftwareEntity)
class Software:
    pass

@strawberry_sqlalchemy_mapper.type(TagEntity)
class Tag:
    pass

strawberry_sqlalchemy_mapper.finalize()
additional_types = list(strawberry_sqlalchemy_mapper.mapped_types.values())
schema = strawberry.Schema(query=Query, mutation=Mutation, types=additional_types)

And finally I link it that way to flask:

class MyGraphQLView(GraphQLView):
    init_every_request = False
    config = None
    session_factory = None

    def get_context(self, request: Request, response: Response) -> Any:
        return {
            "config": MyGraphQLView.config,
            "session_factory": MyGraphQLView.session_factory,
            "sqlalchemy_loader": StrawberrySQLAlchemyLoader(bind=MyGraphQLView.session_factory),
        }

# ...
self.engine = create_engine(self.uri, echo=False)
self.session_factory = sessionmaker(self.engine)
MyGraphQLView.config = self.config
MyGraphQLView.session_factory = self.session_factory
self.app.add_url_rule(
    "/graphql",
    view_func=MyGraphQLView.as_view("graphql_view", schema=schema)
)

The error I'm facing is the following:

Traceback (most recent call last):
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask\app.py", line 1478, in __call__
    return self.wsgi_app(environ, start_response)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask\app.py", line 1458, in wsgi_app
    response = self.handle_exception(e)
               ^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask_cors\extension.py", line 176, in wrapped_function
    return cors_after_request(app.make_response(f(*args, **kwargs)))
                                                ^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask\app.py", line 1455, in wsgi_app
    response = self.full_dispatch_request()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask\app.py", line 869, in full_dispatch_request
    rv = self.handle_user_exception(e)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask_cors\extension.py", line 176, in wrapped_function
    return cors_after_request(app.make_response(f(*args, **kwargs)))
                                                ^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask\app.py", line 867, in full_dispatch_request
    rv = self.dispatch_request()
         ^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask\app.py", line 852, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask\views.py", line 115, in view
    return current_app.ensure_sync(self.dispatch_request)(**kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\flask\views.py", line 100, in dispatch_request
    return self.run(request=request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\http\sync_base_view.py", line 199, in run
    result = self.execute_operation(
             ^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\http\sync_base_view.py", line 128, in execute_operation
    return self.schema.execute_sync(
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\schema\schema.py", line 288, in execute_sync
    result = execute_sync(
             ^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\schema\execute.py", line 236, in execute_sync
    ensure_future(result).cancel()
  File "C:\Program Files\Python311\Lib\asyncio\tasks.py", line 649, in ensure_future
    return _ensure_future(coro_or_future, loop=loop)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Program Files\Python311\Lib\asyncio\tasks.py", line 668, in _ensure_future
    loop = events._get_event_loop(stacklevel=4)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Program Files\Python311\Lib\asyncio\events.py", line 692, in get_event_loop
    raise RuntimeError('There is no current event loop in thread %r.'
RuntimeError: There is no current event loop in thread 'Thread-5 (process_request_thread)'.

when running the following request in graphql

{
  softwares{
    id
    fullName
    editor
    description
    language
    tags {
      __typename
      edges{
        node{
          id
        }
      }
    }
  }
}

If I remove the tag section in the querry, I get no error, so I guess I have an issue with relationships. From the error, I figuered it's because fetching relationship is done as an async task. However, because Flask is usin strawberry as a View, I guess It is a thread spawned for every request, but it has no event loop running on it. I figured that I would need to run a asyncio.get_event_loop() to spawn one in this new thread, but I'm not sure where to do it.
If you guys have a better idea how to do it, or if I should just dump Flask for something not view based ?

Upvote & Fund

Fund with Polar

Philaeux commented 12 months ago

This is my software query btw:


@strawberry.type
class QuerySoftware:
    @strawberry.field
    def softwares(self, info) -> list[Annotated["Software", strawberry.lazy("..types")]]:
        with info.context['session_factory']() as session:
            sql = select(Software).order_by(Software.full_name)
            return session.execute(sql).scalars().all()
TimDumol commented 12 months ago

Hi @Philaeux -- try inheriting from AsyncGraphQLView instead. The loader assumes an async context is present.

Philaeux commented 12 months ago

Now I get this error which seems to be that something else should be in the bind. I'm passing a sessionmaker atm.

'sessionmaker' object has no attribute 'scalars'

GraphQL request:8:5
7 |     language
8 |     tags {
  |     ^
9 |                     __typename
Traceback (most recent call last):
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\graphql\execution\execute.py", line 528, in await_result
    return_type, field_nodes, info, path, await result
                                          ^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\schema\schema_converter.py", line 704, in _async_resolver
    return await await_maybe(
           ^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\utils\await_maybe.py", line 12, in await_maybe
    return await value
           ^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry_sqlalchemy_mapper\mapper.py", line 389, in wrapper
    for related_object in await resolver(self, info)
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry_sqlalchemy_mapper\mapper.py", line 426, in resolve
    related_objects = await loader.loader_for(relationship).load(
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\dataloader.py", line 251, in dispatch_batch
    values = await loader.load_fn(keys)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry_sqlalchemy_mapper\loader.py", line 73, in load_fn
    rows = await self._scalars_all(query)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry_sqlalchemy_mapper\loader.py", line 54, in _scalars_all
    return self._bind.scalars(*args, **kwargs).all()
           ^^^^^^^^^^^^^^^^^^
AttributeError: 'sessionmaker' object has no attribute 'scalars'
Stack (most recent call last):
  File "C:\Program Files\Python311\Lib\threading.py", line 995, in _bootstrap
    self._bootstrap_inner()
  File "C:\Program Files\Python311\Lib\threading.py", line 1038, in _bootstrap_inner
    self.run()
  File "C:\Program Files\Python311\Lib\threading.py", line 975, in run
    self._target(*self._args, **self._kwargs)
  File "C:\Program Files\Python311\Lib\concurrent\futures\thread.py", line 83, in _worker
    work_item.run()
  File "C:\Program Files\Python311\Lib\concurrent\futures\thread.py", line 58, in run
    result = self.fn(*self.args, **self.kwargs)
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\asgiref\sync.py", line 285, in _run_event_loop
    loop.run_until_complete(coro)
  File "C:\Program Files\Python311\Lib\asyncio\base_events.py", line 640, in run_until_complete
    self.run_forever()
  File "C:\Program Files\Python311\Lib\asyncio\windows_events.py", line 321, in run_forever
    super().run_forever()
  File "C:\Program Files\Python311\Lib\asyncio\base_events.py", line 607, in run_forever
    self._run_once()
  File "C:\Program Files\Python311\Lib\asyncio\base_events.py", line 1919, in _run_once
    handle._run()
  File "C:\Program Files\Python311\Lib\asyncio\events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\asgiref\sync.py", line 353, in main_wrap
    result = await self.awaitable(*args, **kwargs)
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\flask\views.py", line 158, in dispatch_request
    return await self.run(request=request)
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\http\async_base_view.py", line 186, in run
    result = await self.execute_operation(
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\http\async_base_view.py", line 118, in execute_operation
    return await self.schema.execute(
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\schema\schema.py", line 256, in execute
    result = await execute(
  File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\schema\execute.py", line 156, in execute
    process_errors(result.errors, execution_context)
Philaeux commented 12 months ago

Yes it works now, I though the binding should return the sessionmaker factory when in fact, it has to return a session object.

So I have this view now that fixes the problem, maybe it will help someone else in the future.

class MyGraphQLView(AsyncGraphQLView):
    init_every_request = False
    config = None
    session_factory = None

    async def get_context(self, request: Request, response: Response) -> Any:
        return {
            "config": MyGraphQLView.config,
            "session_factory": MyGraphQLView.session_factory,
            "sqlalchemy_loader": StrawberrySQLAlchemyLoader(bind=MyGraphQLView.session_factory()),
        }