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

Docs improvement Subscriptions #376

Merged
merged 38 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
9925103
write down how to run an asgi test-server
Sep 26, 2023
3b8550c
adding the runserver_asgi command, referred to in the docs
Sep 26, 2023
d303235
Relying on django integrated daphne instead of re-inventing the wheel
Sep 27, 2023
a58fd45
show end result
Sep 27, 2023
01314b2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 27, 2023
d8fe53d
Update docs/guide/subscriptions.md
sdobbelaere Sep 29, 2023
d92e90f
Update docs/guide/subscriptions.md
sdobbelaere Sep 29, 2023
d6b7361
Update docs/guide/subscriptions.md
sdobbelaere Sep 29, 2023
ccd0dc8
Update docs/guide/subscriptions.md
sdobbelaere Sep 29, 2023
fd873be
Centralise get_current_user DRY, add new router that is Auth enabled …
Sep 29, 2023
3e5091f
Merge conlifcts
Sep 29, 2023
4a983de
Refactoring to explain AGSI support better, and include the new Auth …
Sep 29, 2023
66869df
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 29, 2023
bc4d11a
Merge branch 'strawberry-graphql:main' into subscriptions
sdobbelaere Sep 29, 2023
5de8f89
Update docs/guide/subscriptions.md
sdobbelaere Sep 30, 2023
2f34eed
Update strawberry_django/auth/queries.py
sdobbelaere Sep 30, 2023
cbd154e
Update tests/projects/schema.py
sdobbelaere Sep 30, 2023
346c8dc
Applying suggestions
Sep 30, 2023
ab9bb7b
remove baseuser imports
Sep 30, 2023
17281ea
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 30, 2023
6600d5c
force user object loading for async
Sep 30, 2023
3d7dc69
Merge branch 'subscriptions' of github.com:sdobbelaere/strawberry-gra…
Sep 30, 2023
d406c72
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 30, 2023
7e361bd
Update strawberry_django/auth/utils.py
sdobbelaere Sep 30, 2023
79caaef
styling
Oct 1, 2023
1cccdd4
merge conflicts
Oct 1, 2023
1d8399d
Adding return types
Oct 2, 2023
d28b9df
adjust docs to reflect new import path
Oct 2, 2023
597e7af
remove typing to ensure correct import
Oct 2, 2023
05ab688
Merge branch 'main' into subscriptions
Oct 3, 2023
6d51db2
Merge remote-tracking branch 'origin/main' into subscriptions
bellini666 Oct 9, 2023
f1e1b62
refactor: improve get_current_user typing
bellini666 Oct 9, 2023
e5d775f
fix(pyright): fix remaining typing issues
bellini666 Oct 9, 2023
b0a464a
allow for accing a fake context in order to run tests
Oct 9, 2023
e08e0bb
Merge branch 'subscriptions' of github.com:sdobbelaere/strawberry-gra…
Oct 9, 2023
d3d70b3
Improve fording to satisfy pre-commit alex
Oct 9, 2023
1bcbb67
revert context override, and fix docs
Oct 9, 2023
9deaffb
Fix indentation and historical typo
Oct 10, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ def resolver(root, info: Info):

## How to access the current user object in resolvers?

The current user object is accessible via the `info.context.request.user` object.
The current user object is accessible via the `get_current_user` method.

```python
from strawberry_django.auth.queries import get_current_user

def resolver(root, info: Info):
current_user = info.context.request.user
current_user = get_current_user(info)
```

## Autocompletion with editors
Expand Down
138 changes: 136 additions & 2 deletions docs/guide/subscriptions.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,140 @@
### Subscriptions
# Subscriptions

Subscriptions are supported using the
[Strawberry Django Channels](https://strawberry.rocks/docs/integrations/channels) integration.

Check its docs to know how to use it.
This guide will give you a minimal working example to get you going.
There are 3 parts to this guide:

1. Making Django compatible
2. Setup local testing
3. Creating your first subscription

## Making Django compatible

It's important to realise that Django doesn't support websockets out of the box.
To resolve this, we can help the platform along a little.

This implementation is based on Django Channels - this means that should you wish - there is a lot more websockets fun to be had. If you're interested, head over to [Django Channels](https://channels.readthedocs.io).

To add the base compatibility, go to your `MyProject.asgi.py` file and replace it with the following content.
Ensure that you replace the relevant code with your setup.

```python
# MyProject.asgi.py
import os

from django.core.asgi import get_asgi_application
from strawberry_django.routers import AuthGraphQLProtocolTypeRouter

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "MyProject.settings") # CHANGE the project name
django_asgi_app = get_asgi_application()

# Import your Strawberry schema after creating the django ASGI application
# This ensures django.setup() has been called before any ORM models are imported
# for the schema.

from .schema import schema # CHANGE path to where you housed your schema file.
application = AuthGraphQLProtocolTypeRouter(
schema,
django_application=django_asgi_app,
)
```

Also, ensure that you enable subscriptions on your AsgiGraphQLView in `MyProject.urls.py`:

```python
...

urlpatterns = [
...
path(
'graphql/',
AsyncGraphQLView.as_view(
schema=schema,
graphiql=settings.DEBUG,
subscriptions_enabled=True,
),
),
...
]

```

Note, django-channels allows for a lot more complexity. Here we just cover the basic framework to get
subscriptions to run on Django with minimal effort.
sdobbelaere marked this conversation as resolved.
Show resolved Hide resolved

## Setup local testing

The classic `./manage.py runserver` will not support subscriptions as it runs on WSGI mode. However, Django has ASGI server support out of the box through Daphne, which will override the runserver command to support our desired ASGI support.

There are other asgi servers available, such as Uvicorn and Hypercorn. For the sake of simplicity we'll use Daphne as it comes with the runserver override. [Django Docs](https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/daphne/) This shouldn't stop you from using any of the other ASGI flavours in production or local testing like Uvicorn or Hypercorn

To get started: Firstly, we need install Daphne to handle the workload, so let's install it:

```bash
pip install daphne
```

Secondly, we need to add `daphne` to your settings.py file before 'django.contrib.staticfiles'

```python
INSTALLED_APPS = [
...
'daphne',
'django.contrib.staticfiles',
...
]
```

and add your `ASGI_APPLICATION` setting in your settings.py

```python
# settings.py
...
ASGI_APPLICATION = 'MyProject.asgi.application'
...
```

Now you can run your test-server like as usual, but with ASGI support:

```bash
./manage.py runserver
```

## Creating your first subscription

Once you've taken care of those 2 setup steps, your first subscription is a breeze.
Go and edit your schema-file and add:

```python
import asyncio
import strawberry

@strawberry.type
class Subscription:
@strawberry.subscription
async def count(self, target: int = 100) -> int:
for i in range(target):
yield i
await asyncio.sleep(0.5)
```

That's pretty much it for this basic start.
See for yourself by running your test server `./manange.py runserver` and opening `http://127.0.0.1:8000/graphql/` in your browser. Now run:

```graphql
subscription {
count(target: 10)
}
```

You should see something like (where the count changes every .5s to a max of 9)

```json
{
"data": {
"count": 9
}
}
```
5 changes: 4 additions & 1 deletion docs/guide/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,15 @@ You can use that `info` parameter to, for example,
limit access to results based on the current user in the request:

```{.python title=types.py}
from stawberry_django.auth.utils import get_current_user

@strawberry.django.type(models.Fruit)
class Berry:

@classmethod
def get_queryset(cls, queryset, info, **kwargs):
if not info.context.request.user.is_staff:
user = get_current_user(info)
if not user.is_staff:
# Restrict access to top secret berries if the user is not a staff member
queryset = queryset.filter(is_top_secret=False)
return queryset.filter(name__contains="berry")
Expand Down
21 changes: 20 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Markdown = "^3.3.7"
Pygments = "^2.15.1"
factory-boy = "^3.2.1"
django-guardian = "^2.4.0"
channels = { version = ">=3.0.5" }

[tool.poetry.extras]
debug-toolbar = ["django-debug-toolbar"]
Expand Down
13 changes: 10 additions & 3 deletions strawberry_django/auth/queries.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
from strawberry.types import Info

import strawberry_django

from .utils import get_current_user


def resolve_current_user(info):
if not info.context.request.user.is_authenticated:
def resolve_current_user(info: Info):
user = get_current_user(info)

if not getattr(user, "is_authenticated", False):
return None
return info.context.request.user

return user


def current_user():
Expand Down
56 changes: 56 additions & 0 deletions strawberry_django/auth/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from typing import Literal, Optional, overload

from asgiref.sync import sync_to_async
from strawberry.types import Info

from strawberry_django.utils.typing import UserType


@overload
def get_current_user(info: Info, *, strict: Literal[True]) -> UserType: ...


@overload
def get_current_user(info: Info, *, strict: bool = False) -> Optional[UserType]: ...


def get_current_user(info: Info, *, strict: bool = False) -> Optional[UserType]:
"""Get and return the current user based on various scenarios."""
try:
user = info.context.request.user
except AttributeError:
try:
# queries/mutations in ASGI move the user into consumer scope
user = info.context.get("request").consumer.scope["user"]
except AttributeError:
# websockets / subscriptions move scope inside of the request
user = info.context.get("request").scope.get("user")

if user is None:
raise ValueError("No user found in the current request")

# Access an attribute inside the user object to force loading it in async contexts.
if user is not None:
_ = user.is_authenticated

return user


@overload
async def aget_current_user(
info: Info,
*,
strict: Literal[True],
) -> UserType: ...


@overload
async def aget_current_user(
info: Info,
*,
strict: bool = False,
) -> Optional[UserType]: ...


async def aget_current_user(info: Info, *, strict: bool = False) -> Optional[UserType]:
return await sync_to_async(get_current_user)(info, strict=strict)
Loading
Loading