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

Support subscriptions extensions #3554

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
102 commits
Select commit Hold shift + click to select a range
2c534a1
Support subscriptions extensions
nrbnlulu Jun 4, 2023
6525ee9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 4, 2023
b92c51b
Add RELEASE.md
nrbnlulu Jun 4, 2023
01a76cd
Merge remote-tracking branch 'origin/support_extensions_on_subscripti…
nrbnlulu Jun 4, 2023
6ff3315
mypy fixes.
nrbnlulu Jun 5, 2023
20bcfed
remove positional only / (py3.7)
nrbnlulu Jun 5, 2023
9ba707a
merge main
nrbnlulu Jul 2, 2024
f69d746
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 2, 2024
060c719
reverts
nrbnlulu Jul 2, 2024
62a130b
intial subscription test pass
nrbnlulu Jul 2, 2024
7b45bfa
wip. pass manager to graphql core
nrbnlulu Jul 2, 2024
e1db94b
wip: migrate to new graphql core
nrbnlulu Jul 2, 2024
70d2f4c
don't use supports resolve
nrbnlulu Jul 2, 2024
7f44c77
revert
nrbnlulu Jul 2, 2024
dc32183
revert unneeded changes
nrbnlulu Jul 3, 2024
5701d21
restore tests; inject execution_context on operation
nrbnlulu Jul 3, 2024
9dcb3fc
don't use class; depecate
nrbnlulu Jul 3, 2024
7e1c770
wip: ensure execution context is distinct.
nrbnlulu Jul 4, 2024
6a84415
wip: first inital successful run after refactor
nrbnlulu Jul 4, 2024
222a4e5
wip: more tests pass
nrbnlulu Jul 4, 2024
4a0f1be
better release.md
nrbnlulu Jul 5, 2024
8fa0cec
all previous extensions tests pass
nrbnlulu Jul 5, 2024
fc7977b
update release.md
nrbnlulu Jul 5, 2024
81914a2
improve tests readability
nrbnlulu Jul 5, 2024
89e501b
test_subscription_success_many_fields pass
nrbnlulu Jul 5, 2024
29b5db8
test_subscription_first_yields_error
nrbnlulu Jul 5, 2024
1438390
test_extensino_results_are_cleared_between_yields
nrbnlulu Jul 5, 2024
cb917ab
test_extensino_results_are_cleared_between_yields
nrbnlulu Jul 5, 2024
ece37b4
fix extensions tests
nrbnlulu Jul 5, 2024
50063c2
ai lints
nrbnlulu Jul 5, 2024
1c65fce
refactor; fix more tests
nrbnlulu Jul 7, 2024
3ab7bb4
ensure `on_execute` and `get_result` hooks are deterministically orde…
nrbnlulu Jul 7, 2024
0533539
handle `on_execute` exceptions.
nrbnlulu Jul 7, 2024
a6680c1
move missing query error before parsing phase.
nrbnlulu Jul 7, 2024
e6ca184
wip: remove unneeded exception handler for `sync_execute`
nrbnlulu Jul 7, 2024
923699a
fix more tests
nrbnlulu Jul 7, 2024
446dc5e
refactor: separate subscription tests from normal tests.
nrbnlulu Jul 7, 2024
39a1beb
fix websocket tests
nrbnlulu Jul 7, 2024
88f2bd5
fix mypy
nrbnlulu Jul 7, 2024
2e9db61
move to graphql-core origin@main
nrbnlulu Jul 8, 2024
9b166a3
nit
nrbnlulu Jul 9, 2024
9c57abd
fix: handle not awaitable result of `original_subscribe`
nrbnlulu Jul 9, 2024
8a8fd2a
add th `assert_next`
nrbnlulu Jul 14, 2024
bdb568b
nits
nrbnlulu Jul 14, 2024
062d9c0
nit
nrbnlulu Jul 14, 2024
9a1c071
nit
nrbnlulu Jul 14, 2024
dd909b3
nits
nrbnlulu Jul 14, 2024
b8f40a1
reorder execute.py
nrbnlulu Jul 14, 2024
61154dc
docs.
nrbnlulu Jul 14, 2024
e455da0
use `.aclose`
nrbnlulu Jul 14, 2024
9a12488
fix mypy issues.
nrbnlulu Jul 14, 2024
f6a924c
fix unused `else`
nrbnlulu Jul 14, 2024
f070858
move execution context injection upwards.
nrbnlulu Jul 14, 2024
fe86760
nit
nrbnlulu Jul 14, 2024
2bdfa9e
add test for when extensios not return anything.
nrbnlulu Jul 15, 2024
f8bf56d
merge main
nrbnlulu Jul 18, 2024
7d702c7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 18, 2024
945465e
wip: remove implements_get_result check
nrbnlulu Jul 23, 2024
5d10ad9
Merge branch 'refs/heads/main' into support_extensions_on_subscriptions
nrbnlulu Jul 24, 2024
da0b46e
Merge branch 'refs/heads/main' into support_extensions_on_subscriptions
nrbnlulu Jul 24, 2024
785d227
working on tests
nrbnlulu Jul 24, 2024
db3180f
schema tests pass
nrbnlulu Jul 24, 2024
4d7fa9e
lints
nrbnlulu Jul 24, 2024
3e55bc2
fix more tests
nrbnlulu Jul 25, 2024
e0f4158
wip: working on tests.
nrbnlulu Jul 25, 2024
a0972a8
fix graphql-transport-ws
nrbnlulu Jul 25, 2024
5567971
graphql-ws tests pass
nrbnlulu Jul 25, 2024
aa7a4bf
tests were always running on 3.3 XD
nrbnlulu Jul 25, 2024
0dc97dd
pass middleware manager only in 3.3
nrbnlulu Jul 25, 2024
e57d115
fix some mypy issues
nrbnlulu Jul 25, 2024
28f6b07
fix more tests.
nrbnlulu Jul 25, 2024
c728171
fix graphql-ws-transport protocol behaviour on (pre)execution errors
nrbnlulu Jul 25, 2024
7f810c5
resolve optimization todos.
nrbnlulu Jul 25, 2024
5b5b7ce
add `long_runnning` subscription benchmark; update release.md; improv…
nrbnlulu Jul 25, 2024
4df5523
fix contextvar issue + few redundant changes.
nrbnlulu Jul 28, 2024
ea1041e
feat: add lazy loading for images in test_subscriptions benchmark
nrbnlulu Aug 1, 2024
a5e5068
typos in readme
nrbnlulu Aug 1, 2024
6d5a754
fix tests.
nrbnlulu Aug 1, 2024
d33dbd7
recify reviews; nits & move `_implements_resolve` to `SchemaExtension`
nrbnlulu Aug 8, 2024
925d3e7
refactor: update GraphQL version check logic in utils/__init__.py
nrbnlulu Aug 8, 2024
4c25c81
Merge branch 'main' into support_extensions_on_subscriptions
nrbnlulu Aug 8, 2024
1fc8c54
Merge branch 'main' into support_extensions_on_subscriptions
nrbnlulu Aug 8, 2024
d98bef2
rectify review comments
nrbnlulu Aug 9, 2024
66b0266
rectify @DoctorJohn review
nrbnlulu Aug 15, 2024
ee1ee1c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 15, 2024
fd6bdaf
rectify @bellini666 comments
nrbnlulu Aug 18, 2024
26f8437
Merge branch 'main' into support_extensions_on_subscriptions
nrbnlulu Aug 18, 2024
835d1f4
Improve protocol handling for subscriptions
patrick91 Sep 2, 2024
9ce6ff0
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 2, 2024
da3b12a
Lint
patrick91 Sep 2, 2024
7af8bc9
Restore tests
patrick91 Sep 2, 2024
3ba6a83
Update wrong return type
patrick91 Sep 2, 2024
83061ed
Merge branch 'feature/multiple-protocols' into support_extensions_on_…
patrick91 Sep 2, 2024
b65322e
Update doc
patrick91 Sep 2, 2024
87fd45d
Temporarily skip on_execute, see #3613
patrick91 Sep 2, 2024
5d37f83
Update multipart tests
patrick91 Sep 2, 2024
cbd7c90
Fix bad merge
patrick91 Sep 3, 2024
ea1e4fd
Fix type
patrick91 Sep 3, 2024
fdc2e4e
remove unneeded async gen wrapper since we removed support for on_exe…
nrbnlulu Sep 7, 2024
9e0d0aa
Merge branch 'main' into support_extensions_on_subscriptions
patrick91 Sep 10, 2024
27b626b
Update release notes
patrick91 Sep 10, 2024
56e0a6f
Add tweet.md
patrick91 Sep 10, 2024
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
36 changes: 36 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
Release type: minor

This release adds support for schema-extensions in subscriptions.

Here's a small example of how to use them (they work the same way as query and
mutation extensions):

nrbnlulu marked this conversation as resolved.
Show resolved Hide resolved
```python
nrbnlulu marked this conversation as resolved.
Show resolved Hide resolved
import asyncio
from typing import AsyncIterator

import strawberry
from strawberry.extensions.base_extension import SchemaExtension


@strawberry.type
class Subscription:
@strawberry.subscription
async def notifications(self, info: strawberry.Info) -> AsyncIterator[str]:
for _ in range(3):
yield "Hello"


class MyExtension(SchemaExtension):
async def on_operation(self):
# This would run when the subscription starts
print("Subscription started")
yield
# The subscription has ended
print("Subscription ended")


schema = strawberry.Schema(
query=Query, subscription=Subscription, extensions=[MyExtension]
)
```
5 changes: 5 additions & 0 deletions TWEET.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
🆕 Release $version is out! Thanks to $contributor for the PR 👏

This release adds supports for schema extensions to subscriptions!

Get it here 👉 $release_url
1 change: 1 addition & 0 deletions docs/breaking-changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ title: List of breaking changes and deprecations

# List of breaking changes and deprecations

- [Version 0.240.0 - 10 September 2024](./breaking-changes/0.240.0.md)
- [Version 0.236.0 - 17 July 2024](./breaking-changes/0.236.0.md)
- [Version 0.233.0 - 29 May 2024](./breaking-changes/0.233.0.md)
- [Version 0.217.0 - 18 December 2023](./breaking-changes/0.217.0.md)
Expand Down
36 changes: 36 additions & 0 deletions docs/breaking-changes/0.240.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
title: 0.240.0 Breaking Changes
slug: breaking-changes/0.240.0
---

# v0.240.0 updates `Schema.subscribe`'s signature

In order to support schema extensions in subscriptions and errors that can be
raised before the execution of the subscription, we had to update the signature
of `Schema.subscribe`.

Previously it was:

```python
async def subscribe(
self,
query: str,
variable_values: Optional[Dict[str, Any]] = None,
context_value: Optional[Any] = None,
root_value: Optional[Any] = None,
operation_name: Optional[str] = None,
) -> Union[AsyncIterator[GraphQLExecutionResult], GraphQLExecutionResult]:
```

Now it is:

```python
async def subscribe(
self,
query: Optional[str],
variable_values: Optional[Dict[str, Any]] = None,
context_value: Optional[Any] = None,
root_value: Optional[Any] = None,
operation_name: Optional[str] = None,
) -> Union[AsyncGenerator[ExecutionResult, None], PreExecutionError]:
```
9 changes: 2 additions & 7 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
PYTHON_VERSIONS = ["3.12", "3.11", "3.10", "3.9", "3.8"]
GQL_CORE_VERSIONS = [
"3.2.3",
"3.3.0",
"3.3.0a6",
]

COMMON_PYTEST_OPTIONS = [
Expand Down Expand Up @@ -44,12 +44,7 @@


def _install_gql_core(session: Session, version: str) -> None:
# hack for better workflow names # noqa: FIX004
if version == "3.2.3":
session._session.install(f"graphql-core=={version}") # type: ignore
session._session.install(
"https://github.com/graphql-python/graphql-core/archive/876aef67b6f1e1f21b3b5db94c7ff03726cb6bdf.zip"
) # type: ignore
session._session.install(f"graphql-core=={version}")


gql_core_parametrize = nox.parametrize(
Expand Down
4 changes: 2 additions & 2 deletions strawberry/channels/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,9 @@ async def subscribe(
message_type = response["type"]
if message_type == NextMessage.type:
payload = NextMessage(**response).payload
ret = ExecutionResult(payload["data"], None)
ret = ExecutionResult(payload.get("data"), None)
if "errors" in payload:
ret.errors = self.process_errors(payload["errors"])
ret.errors = self.process_errors(payload.get("errors") or [])
nrbnlulu marked this conversation as resolved.
Show resolved Hide resolved
ret.extensions = payload.get("extensions", None)
yield ret
elif message_type == ErrorMessage.type:
Expand Down
13 changes: 10 additions & 3 deletions strawberry/extensions/base_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ class LifecycleStep(Enum):
class SchemaExtension:
execution_context: ExecutionContext

def __init__(self, *, execution_context: ExecutionContext) -> None:
self.execution_context = execution_context

# to support extensions that still use the old signature
# we have an optional argument here for ease of initialization.
nrbnlulu marked this conversation as resolved.
Show resolved Hide resolved
def __init__(
self, *, execution_context: ExecutionContext | None = None
) -> None: ...
def on_operation( # type: ignore
self,
) -> AsyncIteratorOrIterator[None]: # pragma: no cover
Expand Down Expand Up @@ -61,6 +63,11 @@ def resolve(
def get_results(self) -> AwaitableOrValue[Dict[str, Any]]:
return {}

@classmethod
def _implements_resolve(cls) -> bool:
"""Whether the extension implements the resolve method."""
return cls.resolve is not SchemaExtension.resolve


Hook = Callable[[SchemaExtension], AsyncIteratorOrIterator[None]]

Expand Down
45 changes: 8 additions & 37 deletions strawberry/extensions/runner.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
from __future__ import annotations

import inspect
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union

from graphql import MiddlewareManager
from typing import TYPE_CHECKING, Any, Dict, List, Optional

from strawberry.extensions.context import (
ExecutingContextManager,
Expand All @@ -13,39 +11,22 @@
)
from strawberry.utils.await_maybe import await_maybe

from . import SchemaExtension

if TYPE_CHECKING:
from strawberry.types import ExecutionContext

from . import SchemaExtension


class SchemaExtensionsRunner:
extensions: List[SchemaExtension]

def __init__(
self,
execution_context: ExecutionContext,
extensions: Optional[
List[Union[Type[SchemaExtension], SchemaExtension]]
] = None,
extensions: Optional[List[SchemaExtension]] = None,
) -> None:
self.execution_context = execution_context

if not extensions:
extensions = []

init_extensions: List[SchemaExtension] = []

for extension in extensions:
# If the extension has already been instantiated then set the
# `execution_context` attribute
if isinstance(extension, SchemaExtension):
extension.execution_context = execution_context
init_extensions.append(extension)
else:
init_extensions.append(extension(execution_context=execution_context))
nrbnlulu marked this conversation as resolved.
Show resolved Hide resolved

self.extensions = init_extensions
self.extensions = extensions or []
patrick91 marked this conversation as resolved.
Show resolved Hide resolved

def operation(self) -> OperationContextManager:
return OperationContextManager(self.extensions)
Expand All @@ -61,29 +42,19 @@ def executing(self) -> ExecutingContextManager:

def get_extensions_results_sync(self) -> Dict[str, Any]:
data: Dict[str, Any] = {}

for extension in self.extensions:
if inspect.iscoroutinefunction(extension.get_results):
msg = "Cannot use async extension hook during sync execution"
raise RuntimeError(msg)

data.update(extension.get_results()) # type: ignore

return data

async def get_extensions_results(self) -> Dict[str, Any]:
async def get_extensions_results(self, ctx: ExecutionContext) -> Dict[str, Any]:
data: Dict[str, Any] = {}

for extension in self.extensions:
results = await await_maybe(extension.get_results())
data.update(results)
data.update(await await_maybe(extension.get_results()))

data.update(ctx.extensions_results)
return data

def as_middleware_manager(self, *additional_middlewares: Any) -> MiddlewareManager:
middlewares = tuple(self.extensions) + additional_middlewares

return MiddlewareManager(*middlewares)


__all__ = ["SchemaExtensionsRunner"]
3 changes: 2 additions & 1 deletion strawberry/http/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import json
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional
from typing_extensions import TypedDict
from typing_extensions import Literal, TypedDict

if TYPE_CHECKING:
from strawberry.types import ExecutionResult
Expand Down Expand Up @@ -33,6 +33,7 @@ class GraphQLRequestData:
query: Optional[str]
variables: Optional[Dict[str, Any]]
operation_name: Optional[str]
protocol: Literal["http", "multipart-subscription"] = "http"


def parse_query_params(params: Dict[str, str]) -> Dict[str, Any]:
Expand Down
16 changes: 16 additions & 0 deletions strawberry/http/async_base_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
Tuple,
Union,
)
from typing_extensions import Literal

from graphql import GraphQLError

Expand Down Expand Up @@ -121,6 +122,15 @@ async def execute_operation(

assert self.schema

if request_data.protocol == "multipart-subscription":
return await self.schema.subscribe(
request_data.query, # type: ignore
variable_values=request_data.variables,
context_value=context,
root_value=root_value,
operation_name=request_data.operation_name,
)

return await self.schema.execute(
request_data.query,
root_value=root_value,
Expand Down Expand Up @@ -312,21 +322,27 @@ async def parse_http_body(
) -> GraphQLRequestData:
content_type, params = parse_content_type(request.content_type or "")

protocol: Literal["http", "multipart-subscription"] = "http"

if request.method == "GET":
data = self.parse_query_params(request.query_params)
if self._is_multipart_subscriptions(content_type, params):
protocol = "multipart-subscription"
elif "application/json" in content_type:
data = self.parse_json(await request.get_body())
elif content_type == "multipart/form-data":
data = await self.parse_multipart(request)
elif self._is_multipart_subscriptions(content_type, params):
data = await self.parse_multipart_subscriptions(request)
protocol = "multipart-subscription"
else:
raise HTTPException(400, "Unsupported content type")

return GraphQLRequestData(
query=data.get("query"),
variables=data.get("variables"),
operation_name=data.get("operationName"),
protocol=protocol,
)

async def process_result(
Expand Down
6 changes: 3 additions & 3 deletions strawberry/schema/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from strawberry.types import (
ExecutionContext,
ExecutionResult,
SubscriptionExecutionResult,
)
from strawberry.types.base import StrawberryObjectDefinition
from strawberry.types.enum import EnumDefinition
Expand All @@ -24,6 +23,7 @@
from strawberry.types.union import StrawberryUnion

from .config import StrawberryConfig
from .subscribe import SubscriptionResult


class BaseSchema(Protocol):
Expand All @@ -43,7 +43,7 @@ async def execute(
root_value: Optional[Any] = None,
operation_name: Optional[str] = None,
allowed_operation_types: Optional[Iterable[OperationType]] = None,
) -> Union[ExecutionResult, SubscriptionExecutionResult]:
) -> ExecutionResult:
raise NotImplementedError

@abstractmethod
Expand All @@ -66,7 +66,7 @@ async def subscribe(
context_value: Optional[Any] = None,
root_value: Optional[Any] = None,
operation_name: Optional[str] = None,
) -> Any:
) -> SubscriptionResult:
nrbnlulu marked this conversation as resolved.
Show resolved Hide resolved
raise NotImplementedError

@abstractmethod
Expand Down
Loading
Loading