strawberry-graphql / strawberry

A GraphQL library for Python that leverages type annotations 🍓
https://strawberry.rocks
MIT License
4k stars 530 forks source link

Channels Integration for Strawberry #1408

Closed LucidDan closed 7 months ago

LucidDan commented 3 years ago

It would be great to have built-in support for Channels. Even though closely related to Django, Channels can stand largely on its own, and certainly has a number of features that can make Subscriptions implementation much simpler, especially in a large project by leveraging the Channel Layers and Groups communication features.

What I would like to see in a complete Channels implementation (spoiler alert, I already have a partially completed PR working on this):

I've started building this, and have the Channels Consumers and an example and some unit tests so far. The unit tests are mostly just adapted from FastAPI, so there is definitely more to be added.

While the pull request (#1407) is not done yet, what is already pushed is in a good state and I would love to get feedback on it, or further ideas or expansions for this. I've been a dev for many years but this is my first attempt at a significant OSS contribution, so I'm sure I'm getting some things wrong along the way here!

Upvote & Fund

Fund with Polar

LucidDan commented 3 years ago

Thoughts on the high-level API for subscriptions:

In a Django application there's every chance you have sync views, and you want to be able to hand off events to the subscription asyncgenerator for delivery to the client.

In Channels, the way to do this is probably to use Layers and groups. Each websocket consumer has its own channel name in Layers. The most logical approach to publishing an event to a subscription is for the resolver to join a group for that event type, and then any part of the django project (even worker processes, or other servers in a large deployment) can do a group_send() to the Layers group, and the event will be consumed by the subscription.

It's not too crazy to build this yourself, but I think it would be nice to wrap it up and provide a pre-packaged event protocol and so on. Just thinking on the spot at the moment, but the API for a Django app might look something like for example (contrived example using some stripe webhook code I had handy):

The view:

from django.http import (
    HttpRequest,
    HttpResponse,
)
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from strawberry.channels.sync_events import send_subscription_event
@require_POST
@csrf_exempt
def stripe_webhook(request: HttpRequest) -> HttpResponse:
    payload: bytes = request.body
    sig_header = request.META["HTTP_STRIPE_SIGNATURE"]
    process_stripe_webhook(sig_header, str(payload, "utf-8"))
    send_subscription_event(group="stripe", event="webhook_received", data=payload)
    return HttpResponse(status=200)

The Subscription field:

@strawberry.type
class Subscription:
    @strawberry.subscription
    async def listen_to_stripe(
        self,
        info: Info[StrawberryChannelsContext, None],
        stripe: StripePayload,
    ) -> str:
        """
        Subscribe to stripe webhooks. This is a ridiculous example that no one should ever be doing.
        """
        async for item in info.context.request.receive_subscription_events("stripe"):
            if item.event == "webhook_received":
                yield StripePayload(event)
            # ...

Only just started thinking this part through, thoughts and ideas welcome...there might be a better way or it might not be necessary/worth it at all...

Obviously it might also be better to do a specific interface via a separate object in the context, eg "info.context.subscriptions.receive_events()" or something... 🤔

jkimbo commented 2 years ago

@LucidDan I like it! Do you think we could use ContextVariables to access the receive_subscription method inside a subscription resolver?

LucidDan commented 2 years ago

@LucidDan I like it! Do you think we could use ContextVariables to access the receive_subscription method inside a subscription resolver?

Hmm, maybe? To be honest, I've never looked at contextvars much. I've done some research and reading just now, and it intrigues me, particularly in that it guarantees to be local to the eventloop task, but I'm not sure what concrete value we'd get from it in this scenario. I'll keep it in mind as I build this bit out though.

Also, thinking about this, a more logical interface might be more like:

from strawberry.channels import receive_subscription_events

# ...

    async for event in receive_subscription_events(info.context.request, "webhook"):
        # ...

Not having the method defined on the context or request is probably better. I'll likely have a prototype of this built today to experiment with, probably in an example repo along with a demo of using the Channels integration that has already been completed in the PR.

DoctorJohn commented 7 months ago

Another night going through old issues. Correct me if I'm wrong, but AFAIK most if not all of the points in this issue have been resolved. Mostly by the addition and continues improvement of our channels integration. If something is missing feel free to reopen.

bellini666 commented 7 months ago

Another night going through old issues. Correct me if I'm wrong, but AFAIK most if not all of the points in this issue have been resolved. Mostly be the addition and continues improvement of our channels integration. If something is missing feel free to reopen.

I think so.

Of course we can always have more docs and more examples, but in general all points in this issue have been fixed! :)