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

Custom Server example with Flask #125

Closed
jrast opened this issue Mar 31, 2019 · 14 comments
Closed

Custom Server example with Flask #125

jrast opened this issue Mar 31, 2019 · 14 comments
Assignees
Milestone

Comments

@jrast
Copy link
Contributor

jrast commented Mar 31, 2019

Hey there! I ported the custom server example from django to flask.

It's quite short and it would be nice to add it to the docs.

from ariadne import gql, ResolverMap, make_executable_schema
from ariadne.constants import PLAYGROUND_HTML
from graphql import format_error, graphql_sync
from flask import request, jsonify
from flask.views import MethodView

type_defs = gql("""
    type Query {
        hello: String!
    }
""")


query = ResolverMap("Query")


@query.field("hello")
def resolve_hello(_, info):
    request = info.context
    print(request.headers)
    user_agent = request.headers.get("User-Agent", "Guest")
    return "Hello, %s!" % user_agent


schema = make_executable_schema(type_defs, query)


class GraphQlView(MethodView):

    def get(self):
        return PLAYGROUND_HTML, 200

    def post(self):
        data = request.get_json()
        if data is None or not isinstance(data, dict):
            return 'Bad Request', 400

        variables = data.get('variables')
        if variables and not isinstance(variables, dict):
            return 'Bad Request', 400

        # Note: Passing the request to the context is option. In Flask, the current
        #   request is allways accessible as flask.request.
        result = graphql_sync(
            schema,
            data.get('query'),
            context_value=request,
            variable_values=variables,
            operation_name=data.get('operationName')
        )

        response = {"data": result.data}
        if result.errors:
            response["errors"] = [format_error(e) for e in result.errors]

        return jsonify(response)
@command-tab
Copy link

Does this example respond to GraphQL queries on the / route, or /graphql (or something else)? If something other than /, I'm curious where that route path is defined. Thanks for posting this!

@NyanKiyoshi
Copy link
Contributor

NyanKiyoshi commented Apr 1, 2019

Flask users would have to add a rule to their Flask application. They would do like this:

app.add_url_rule('/graphql', view_func=GraphQlView.as_view('graphql'))`

That would be good to add as note in the documentation, as most Flask users don't use method views, i.e. blueprints or app.route!

@jrast
Copy link
Contributor Author

jrast commented Apr 1, 2019

Maybe it would be better to rewrite the example using normal view functions.

@rafalp
Copy link
Contributor

rafalp commented Apr 1, 2019

It's been a while since I've did anything with flask, but wouldn't something like this be possible and more idiomatic?

@app.route('/graphql/', methods=['GET'])
def graphql_playground():
    return PLAYGROUND_HTML, 200


@app.route('/graphql/', methods=['POST'])
def graphql_server():
    ...

@jrast
Copy link
Contributor Author

jrast commented Apr 1, 2019

I rewrote the example as a complete flask application, which is ready to run:

from ariadne import gql, ResolverMap, make_executable_schema
from ariadne.constants import PLAYGROUND_HTML
from graphql import format_error, graphql_sync
from flask import Flask, request, jsonify


type_defs = gql("""
    type Query {
        hello: String!
    }
""")


query = ResolverMap("Query")


@query.field("hello")
def resolve_hello(_, info):
    request = info.context
    user_agent = request.headers.get("User-Agent", "Guest")
    return "Hello, %s!" % user_agent


app = Flask(__name__)


@app.route('/graphql/', methods=['GET'])
def graphql_playgroud():
    """Serving the GraphQL Playground

    Note: This endpoint is not required if you do not want to provide the playground.
      But keep in mind that clients can still explore your API, for example by
      using the GraphQL desktop app.
    """
    return PLAYGROUND_HTML, 200


@app.route('/graphql/', methods=['POST'])
def graphql_server():
    """Serve GraphQL queries"""
    data = request.get_json()
    if data is None or not isinstance(data, dict):
        return 'Bad Request', 400

    variables = data.get('variables')
    if variables and not isinstance(variables, dict):
        return 'Bad Request', 400

    # Note: Passing the request to the context is option. In Flask, the current
    #   request is allways accessible as flask.request.
    result = graphql_sync(
        make_executable_schema(type_defs, query),
        data.get('query'),
        context_value=request,
        variable_values=variables,
        operation_name=data.get('operationName')
    )

    response = {"data": result.data}
    if result.errors:
        response["errors"] = [format_error(e) for e in result.errors]

    return jsonify(response)


if __name__ == '__main__':
    app.run(debug=True)

This structure is well known, even for Flask beginners. More expirienced users know how to adapt this to using Blueprints or a Method view.

jrast added a commit to jrast/ariadne that referenced this issue Apr 1, 2019
@jrast
Copy link
Contributor Author

jrast commented Apr 5, 2019

If #133 gets merged, the view function for POST can be simplified, so if the merge is done, #126 should be updated.

@drice
Copy link
Contributor

drice commented Apr 8, 2019

Should this use ariadne's graphql_sync?

@NyanKiyoshi
Copy link
Contributor

NyanKiyoshi commented Apr 8, 2019

@tbsf Flask is not handling requests in async. If you want to use async methods, you should look for sanic or vibora. If that's the question.

@drice
Copy link
Contributor

drice commented Apr 8, 2019

@NyanKiyoshi graphql_sync is not async https://github.com/mirumee/ariadne/blob/master/ariadne/graphql.py#L57

I think this should look something like:

from ariadne import graphql_sync, make_executable_schema

...

@app.route("/graphql", methods=["POST"])
def graphql_server():
    """Serve GraphQL queries"""
    data = request.get_json()

    result, response = graphql_sync(
        make_executable_schema(schema, query),
        data=data,
        context_value=request,
        debug=True,
    )
    return jsonify(response)

@rafalp
Copy link
Contributor

rafalp commented Apr 8, 2019

You should keep make_executable_schema outside of views. There's no need to parse type definitions and bind resolvers on every API request.

@patrys
Copy link
Contributor

patrys commented Apr 9, 2019

This is correct. Especially given that make_executable_schema modifies the schema in place, so if you're using asynchronous execution, you could be modifying the object that existing async tasks depend on.

jrast added a commit to jrast/ariadne that referenced this issue Apr 10, 2019
@cpouldev
Copy link

cpouldev commented Apr 11, 2019

I had success using middleware instead of manually declare the routes. Is there any drawback on that?

Code sample:

from ariadne import make_executable_schema
from ariadne.wsgi import GraphQLMiddleware, GraphQL
from myapp.graphql import type_defs, resolvers

app = Flask(__name__)
# ...
# ...
schema = make_executable_schema(type_defs, resolvers)
gql_app = GraphQL(schema)
app.wsgi_app = GraphQLMiddleware(app.wsgi_app, gql_app, '/api/v1/gql')

@jrast
Copy link
Contributor Author

jrast commented Apr 12, 2019

@cpuldev this is possible, but I think this has some drawbacks:

  • You are not able to inject custom context variables
  • It's not possible to decorate the route, which is often used for authorization

Maybe this can be done somehow, I don't know GraphQLMiddleware exactly, but it's certainly not as easy as if you use a normal route.

@rafalp rafalp added this to the 0.4 milestone May 6, 2019
@rafalp rafalp self-assigned this May 6, 2019
@rafalp
Copy link
Contributor

rafalp commented May 7, 2019

Our docs have been updated to include Flask integration example. Thanks!

@rafalp rafalp closed this as completed May 7, 2019
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

No branches or pull requests

7 participants