Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial Quart Subscription Support #3818

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft

Conversation

treo
Copy link

@treo treo commented Mar 24, 2025

Description

Strawberry currently doesn't support subscriptions on Quart. This PR aims to rectify that.

Types of Changes

  • Core
  • Bugfix
  • New feature
  • Enhancement/optimization
  • Documentation

Checklist

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • I have tested the changes and verified that they work and don't break anything (as well as I can manage).

Summary by Sourcery

Adds initial support for GraphQL subscriptions using Quart, including WebSocket handling and integration with the strawberry library. It also includes necessary updates to the test suite to ensure the new functionality works as expected.

New Features:

  • Adds support for GraphQL subscriptions on Quart using WebSockets.
  • Introduces a QuartWebSocketAdapter for handling WebSocket connections.
  • Adds a register_route class method to GraphQLView to register both HTTP and WebSocket handlers for a given path.

Tests:

  • Adds tests for the new Quart WebSocket functionality.

Copy link
Contributor

sourcery-ai bot commented Mar 24, 2025

Reviewer's Guide by Sourcery

This pull request introduces initial support for GraphQL subscriptions in Strawberry running on Quart. It includes a WebSocket adapter, updates the GraphQL view to handle WebSocket requests, and adds necessary testing infrastructure.

Updated class diagram for GraphQLView

classDiagram
    class GraphQLView {
        -schema: Schema
        -allow_queries_via_get: bool
        -keep_alive: bool
        -keep_alive_interval: float
        -debug: bool
        -subscription_protocols: list[str]
        -connection_init_wait_timeout: timedelta
        -multipart_uploads_enabled: bool
        +__init__(
            schema: Schema,
            graphiql: Optional[bool] = None,
            graphql_ide: Optional[GraphQL_IDE] = "graphiql",
            allow_queries_via_get: bool = True,
            keep_alive: bool = True,
            keep_alive_interval: float = 1,
            debug: bool = False,
            subscription_protocols: list[str] = [
                GRAPHQL_TRANSPORT_WS_PROTOCOL,
                GRAPHQL_WS_PROTOCOL,
            ],
            connection_init_wait_timeout: timedelta = timedelta(minutes=1),
            multipart_uploads_enabled: bool = False,
        ) -> None
        +is_websocket_request(request: Request) TypeGuard[Request]
        +pick_websocket_subprotocol(request: Request) Optional[str]
        +create_websocket_response(request: Request, subprotocol: Optional[str]) Response
        +register_route(cls: Quart, rule_name: str, path: str, **kwargs)
    }
    class AsyncBaseHTTPView
    GraphQLView --|> AsyncBaseHTTPView : inherits

    note for GraphQLView "Adds websocket support"
Loading

File-Level Changes

Change Details Files
Adds a WebSocket adapter for Quart to handle WebSocket-based GraphQL subscriptions.
  • Introduces QuartWebSocketAdapter to manage WebSocket communication.
  • Implements iter_json to asynchronously iterate over JSON messages from the WebSocket.
  • Implements send_json to send JSON messages over the WebSocket.
  • Implements close to close the WebSocket connection.
  • Adds websocket_adapter_class to GraphQLView.
  • Adds is_websocket_request to check if the request is a WebSocket request.
  • Adds pick_websocket_subprotocol to negotiate the WebSocket subprotocol.
  • Adds create_websocket_response to create a WebSocket response.
  • Adds register_route to register both HTTP and WebSocket handlers for a given path.
strawberry/quart/views.py
Adds WebSocket support to the Quart HTTP client for testing subscriptions.
  • Adds ws_connect to establish a WebSocket connection.
  • Introduces QuartWebSocketClient to interact with the WebSocket connection.
  • Implements send_text, send_json, send_bytes, receive, receive_json, and close methods for the WebSocket client.
  • Adds properties for accessing the accepted subprotocol, closed state, close code, and close reason.
tests/http/clients/quart.py
Adds a fixture to conftest.py to include Quart.
  • Adds QuartHttpClient to the list of HTTP clients to test.
tests/websockets/conftest.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!
  • Generate a plan of action for an issue: Comment @sourcery-ai plan on
    an issue to generate a plan of action for it.

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@treo
Copy link
Author

treo commented Mar 24, 2025

Some manual testing indicates that things work as I'd expect them.

But as of right now, I'm still struggling to figure out the issues with the automatic testing.

It appears that the Quart Test Client tries to be a bit too helpful by throwing exceptions for many of the cases where we'd want to get the original message:
https://github.com/pallets/quart/blob/main/src/quart/testing/connections.py#L179-L200

After struggling with it for hours, I've decided to try the Starlette Test Client and simply treat the Quart application as any old ASGI application.

With that I managed to make some progress, but again Quart seems to want some specific ASGI spec version to pass on the full connection close message: https://github.com/pallets/quart/blob/main/src/quart/asgi.py#L347-L348

Any ideas what else I could try here?

@treo
Copy link
Author

treo commented Mar 25, 2025

I've managed to make the Quart test client work, and now we're at just 20 Test failures.

However, I'm not quite sure how to progress on those. Some are simply a limitation of the test client, like not being able to tell what sub protocol was selected, others seem like I'd need to understand the quart test app implementation even more to really figure them out.

Copy link

codecov bot commented Mar 25, 2025

Codecov Report

Attention: Patch coverage is 89.44099% with 17 lines in your changes missing coverage. Please review.

Project coverage is 95.22%. Comparing base (1edd9bb) to head (d3384b8).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3818      +/-   ##
==========================================
- Coverage   97.23%   95.22%   -2.02%     
==========================================
  Files         503      499       -4     
  Lines       33580    32571    -1009     
  Branches     1717     1703      -14     
==========================================
- Hits        32653    31016    -1637     
- Misses        708     1283     +575     
- Partials      219      272      +53     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

codspeed-hq bot commented Mar 25, 2025

CodSpeed Performance Report

Merging #3818 will not alter performance

Comparing treo:main (d3384b8) with main (1edd9bb)

Summary

✅ 21 untouched benchmarks

@DoctorJohn DoctorJohn self-requested a review March 25, 2025 15:13
Copy link
Member

@DoctorJohn DoctorJohn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @treo, thanks for the PR!

I left some comments just so that I won't forget about them later. Let's first focus on getting the test to pass and then polish everything at the end.

As for the tests, I liked your idea on Discord to use the starlette test client to work around the issues the quart test client is causing. Since quart is an ASGI framework, I don't see a reason against using the startlette ASGI test client, especially since this is our internal test client only used for testing.

Comment on lines +216 to +232
@classmethod
def register_route(cls, app: Quart, rule_name: str, path: str, **kwargs):
"""Helper method to register both HTTP and WebSocket handlers for a given path.

Args:
app: The Quart application
rule_name: The name of the rule
path: The path to register the handlers for
**kwargs: Parameters to pass to the GraphQLView constructor
"""
# Register both HTTP and WebSocket handler at the same path
view_func = cls.as_view(rule_name, **kwargs)
app.add_url_rule(path, view_func=view_func, methods=["GET", "POST"])

# Register the WebSocket handler using the same view function
# Quart will handle routing based on the WebSocket upgrade header
app.add_url_rule(path, view_func=view_func, methods=["GET"], websocket=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd prefer if we left it up to the user to register the two URL rules. The examples in our docs for adding HTTP/WS/HTTP+WS URL rules would then end up looking more consistent.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more of a convenience method, but if you think that just documenting it is good enough, then I'm fine to remove it. Code that doesn't exist doesn't have any bugs :D

async def close(self, code: int, reason: str) -> None:
await self.ws.close(code, reason=reason)


class GraphQLView(
AsyncBaseHTTPView[
Request, Response, Response, Request, Response, Context, RootValue
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, we would adjust these types to include quart.Websocket and make create_websocket_response return websocket etc. so that we end up with sound types.

That being said, I recognize quart loves their global context vars, so we could go with None here too. In that case get_context might need an update, since it's currently just using the return value from create_websocket_response which is None.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I wasn't quite sure how to approach this exactly because of the context vars, But once the tests are all passing, we can take a look at cleaning this up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants