Repository: strawberry-graphql/strawberry

Issue:
add support for aws chalice
AWS Chalice is a python micro-services framework, which hosts endpoints using aws api gateway and aws lambdas.

https://github.com/aws/chalice

A class can be added to route requests and return appropriate responses in a similar manner to the strawberry flask framework.


Discussion:

Resolving patch:
diff --git a/strawberry/chalice/__init__.py b/strawberry/chalice/__init__.py
new file mode 100644
diff --git a/strawberry/chalice/graphiql.py b/strawberry/chalice/graphiql.py
new file mode 100644
--- /dev/null
+++ b/strawberry/chalice/graphiql.py
@@ -0,0 +1,18 @@
+import functools
+from pathlib import Path
+
+
+@functools.lru_cache()
+def render_graphiql_page() -> str:
+    """
+    Loads the graphiql html file into a string and returns it. Replacing subscription
+    enabled as false, this is because this chalice integration does not currently support
+    subscriptions. This function returns a static result, so cache it in ram, saving us
+    from loading the file from disk each time.
+    Returns:
+        A cached string containing a static graphiql page.
+    """
+    graphiql_path = Path(__file__).parent.parent / "static/graphiql.html"
+    html_string = graphiql_path.read_text()
+
+    return html_string.replace("{{ SUBSCRIPTION_ENABLED }}", "false")
diff --git a/strawberry/chalice/views.py b/strawberry/chalice/views.py
new file mode 100644
--- /dev/null
+++ b/strawberry/chalice/views.py
@@ -0,0 +1,124 @@
+from http import HTTPStatus
+
+from chalice.app import BadRequestError, CaseInsensitiveMapping, Request, Response
+from strawberry.chalice.graphiql import render_graphiql_page
+from strawberry.http import GraphQLHTTPResponse, process_result
+from strawberry.schema import BaseSchema
+from strawberry.types import ExecutionResult
+
+
+class GraphQLView:
+    def __init__(self, schema: BaseSchema, render_graphiql: bool = True):
+        self._schema = schema
+        self.graphiql = render_graphiql
+
+    @staticmethod
+    def render_graphiql() -> str:
+        """
+        Returns a string containing the html for the graphiql webpage. It also caches the
+        result using lru cache. This saves loading from disk each time it is invoked.
+        Returns:
+            The GraphiQL html page as a string
+        """
+        result = render_graphiql_page()
+        return result
+
+    @staticmethod
+    def has_html_been_asked_for(headers: CaseInsensitiveMapping) -> bool:
+        """
+        Do the headers indicate that the invoker has requested html?
+        Args:
+            headers: A dictionary containing the headers in the request
+
+        Returns:
+            Whether html has been requested True for yes, False for no
+        """
+        accept_headers = headers.get("accept", None)
+
+        if accept_headers is None:
+            return False
+
+        if "text/html" in accept_headers:
+            return True
+
+        if "*/*" in accept_headers:
+            return True
+
+        return False
+
+    @staticmethod
+    def invalid_query_response() -> Response:
+        """
+        A response for malformed queries
+        Returns:
+        An errors response
+        """
+        return Response(
+            body={
+                "errors": ["Provide a valid graphql query in the body of your request"]
+            },
+            status_code=HTTPStatus.OK,
+        )
+
+    @staticmethod
+    def invalid_rest_verb_response() -> Response:
+        """
+        A response for calling the graphql endpoint with a non POST request
+        Returns:
+
+        """
+        return Response(
+            body={"errors": ["GraphQL queries must be of request type POST"]},
+            status_code=HTTPStatus.OK,
+        )
+
+    def execute_request(self, request: Request) -> Response:
+        """
+        Parse the request process it with strawberry and return a response
+        Args:
+            request: The chalice request this contains the headers and body
+
+        Returns:
+            A chalice response
+        """
+        if self.graphiql:
+            if (
+                self.has_html_been_asked_for(request.headers)
+                and request.method == "GET"
+            ):
+                graphiql_page: str = self.render_graphiql()
+                return Response(
+                    body=graphiql_page,
+                    headers={"content-type": "text/html"},
+                    status_code=200,
+                )
+
+        if not request.method == "POST":
+            return self.invalid_rest_verb_response()
+
+        try:
+            request_data = request.json_body
+        except BadRequestError:
+            return self.invalid_query_response()
+
+        if request_data is None:
+            return self.invalid_query_response()
+        try:
+            query = request_data["query"]
+            variables = request_data.get("variables")
+            operation_name = request_data.get("operationName")
+
+        except (KeyError, TypeError):
+            return self.invalid_query_response()
+
+        result: ExecutionResult = self._schema.execute_sync(
+            query,
+            variable_values=variables,
+            context_value=request,
+            operation_name=operation_name,
+            root_value=None,
+        )
+
+        http_result: GraphQLHTTPResponse = process_result(result)
+
+        return Response(body=http_result)


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -110,18 +110,3 @@ jobs:
           token: ${{ secrets.CODECOV_TOKEN }}
           fail_ci_if_error: true
           verbose: true
-
-  mypy:
-    runs-on: ubuntu-latest
-    container: python:3.9
-
-    steps:
-      - uses: actions/checkout@v2
-
-      - run: pip install poetry
-      - run: poetry config experimental.new-installer false
-
-      - name: Install dependencies
-        run: poetry install
-
-      - run: poetry run mypy --ignore-missing-imports --config-file mypy.ini --install-types --non-interactive --show-traceback .
diff --git a/tests/chalice/__init__.py b/tests/chalice/__init__.py
new file mode 100644
diff --git a/tests/chalice/app.py b/tests/chalice/app.py
new file mode 100644
--- /dev/null
+++ b/tests/chalice/app.py
@@ -0,0 +1,37 @@
+import strawberry
+from chalice import Chalice  # type: ignore
+from chalice.app import Request
+from strawberry.chalice.views import GraphQLView
+
+
+app = Chalice(app_name="TheStackBadger")
+
+
+@strawberry.type
+class Query:
+    @strawberry.field
+    def greetings(self) -> str:
+        return "hello"
+
+
+@strawberry.type
+class Mutation:
+    @strawberry.mutation
+    def echo(self, string_to_echo: str) -> str:
+        return string_to_echo
+
+
+schema = strawberry.Schema(query=Query, mutation=Mutation)
+view = GraphQLView(schema=schema, render_graphiql=True)
+
+
+@app.route("/")
+def index():
+    return {"strawberry": "cake"}
+
+
+@app.route("/graphql", methods=["GET", "POST"], content_types=["application/json"])
+def handle_graphql():
+    request: Request = app.current_request
+    result = view.execute_request(request)
+    return result
diff --git a/tests/chalice/test_views.py b/tests/chalice/test_views.py
new file mode 100644
--- /dev/null
+++ b/tests/chalice/test_views.py
@@ -0,0 +1,96 @@
+import json
+from http import HTTPStatus
+
+import pytest
+
+from chalice.test import Client, HTTPResponse
+
+from .app import app
+
+
+def test_chalice_server_index_route_returns():
+    with Client(app) as client:
+        response = client.http.get("/")
+        assert response.status_code == HTTPStatus.OK
+        assert response.json_body == {"strawberry": "cake"}
+
+
+@pytest.mark.parametrize("header_value", ["text/html", "*/*"])
+def test_graphiql_view_is_returned_if_accept_is_html_or_accept_all(header_value):
+    with Client(app) as client:
+        headers = {"Accept": header_value}
+        response = client.http.get("/graphql", headers=headers)
+        assert response.status_code == HTTPStatus.OK
+        assert "GraphiQL" in str(response.body)
+
+
+def test_graphiql_view_is_not_returned_if_accept_headers_is_none():
+    with Client(app) as client:
+        response = client.http.get("/graphql", headers=None)
+        assert response.status_code == HTTPStatus.OK
+        assert response_is_of_error_type(response)
+
+
+def response_is_of_error_type(response: HTTPResponse) -> bool:
+    if "errors" in response.json_body.keys():
+        if response.status_code == HTTPStatus.OK:
+            return True
+    return False
+
+
+def test_get_graphql_view_with_json_accept_type_is_rejected():
+    with Client(app) as client:
+        headers = {"Accept": "application/json"}
+        response = client.http.get("/graphql", headers=headers)
+        assert response.status_code == HTTPStatus.OK
+        assert response_is_of_error_type(response)
+
+
+def test_malformed_unparsable_json_query_returns_error():
+    with Client(app) as client:
+        headers = {"Accept": "application/json", "Content-Type": "application/json"}
+
+        # The query key in the json dict is missing
+        query = "I am a malformed query"
+        response = client.http.post("/graphql", headers=headers, body=json.dumps(query))
+        assert response_is_of_error_type(response)
+
+
+# These tests are checking that graphql is getting routed through the endpoint
+# correctly to the strawberry execute_sync command, so just test one happy path case of a
+# query and a mutation
+def test_graphiql_query():
+    with Client(app) as client:
+        headers = {"Accept": "application/json", "Content-Type": "application/json"}
+
+        query = {"query": "query GreetMe {greetings}"}
+        response = client.http.post("/graphql", headers=headers, body=json.dumps(query))
+        assert response.status_code == HTTPStatus.OK
+        assert not response_is_of_error_type(response)
+        assert "hello" == response.json_body["data"]["greetings"]
+
+
+def test_graphiql_mutation():
+    with Client(app) as client:
+        headers = {"Accept": "application/json", "Content-Type": "application/json"}
+        query = {"query": """mutation EchoMutation { echo(stringToEcho: "mark")} """}
+        response = client.http.post("/graphql", headers=headers, body=json.dumps(query))
+        assert response.status_code == HTTPStatus.OK
+        assert "mark" == response.json_body["data"]["echo"]
+
+
+def test_graphiql_query_with_malformed_json_gives_http_ok_and_bad_request_message():
+    with Client(app) as client:
+        headers = {"Accept": "application/json", "Content-Type": "application/json"}
+
+        response = client.http.post("/graphql", headers=headers, body="{invalidjson")
+        assert response.status_code == HTTPStatus.OK
+        assert response_is_of_error_type(response)
+
+
+def test_graphiql_query_with_no_request_body():
+    with Client(app) as client:
+        headers = {"Accept": "application/json"}
+        response = client.http.post("/graphql", headers=headers, body="")
+        assert response.status_code == HTTPStatus.OK
+        assert response_is_of_error_type(response)
diff --git a/tests/types/test_object_types.py b/tests/types/test_object_types.py
--- a/tests/types/test_object_types.py
+++ b/tests/types/test_object_types.py
@@ -1,3 +1,4 @@
+# type: ignore
 from enum import Enum
 from typing import List, Optional, TypeVar