Skip to content

Commit 31a0a1d

Browse files
committed
First version graphql-flask package
0 parents  commit 31a0a1d

22 files changed

+2047
-0
lines changed

.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
*.pyc
2+
.idea
3+
.cache
4+
.tox
5+
*.egg
6+
*.egg-info
7+
.coverage

.travis.yml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
language: python
2+
sudo: false
3+
matrix:
4+
include:
5+
- python: "pypy"
6+
env: TOX_ENV=pypy
7+
- python: "2.7"
8+
env: TOX_ENV=py27
9+
- python: "3.3"
10+
env: TOX_ENV=py33
11+
- python: "3.4"
12+
env: TOX_ENV=py34
13+
- python: "3.5"
14+
env: TOX_ENV=py35,import-order,flake8
15+
16+
cache:
17+
directories:
18+
- $HOME/.cache/pip
19+
- $TRAVIS_BUILD_DIR/.tox
20+
21+
install:
22+
- pip install tox coveralls
23+
24+
script:
25+
- tox -e $TOX_ENV -- --cov=graphql_flask
26+
after_success:
27+
- coveralls

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2015 Syrus Akbary
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# `graphql-flask`
2+
3+
[![Build Status](https://travis-ci.org/graphql-python/graphql-graphql-flask.svg?branch=master)](https://travis-ci.org/graphql-python/graphql-flask) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphql-flask/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphql-flask?branch=master) [![PyPI version](https://badge.fury.io/py/graphql-flask.svg)](https://badge.fury.io/py/graphql-flask)
4+
5+
A `Flask` package that provides two main views for operate with `GraphQL`:
6+
* `GraphQLView`: endpoint for expose the GraphQL schema
7+
* `GraphiQLView`: Graphical Interface for operate with GraphQL easily
8+
9+
## Usage
10+
Use it like you would any other Flask View.
11+
12+
```python
13+
from flask_graphql import GraphQLView, GraphiQLView
14+
15+
app.add_url_rule('/graphql', view_func=GraphQLView.as_view('graphql', schema=schema))
16+
```
17+
18+
### Supported options
19+
* `schema`: The `GraphQLSchema` object that you want the view to execute when it gets a valid request.
20+
* `pretty`: Whether or not you want the response to be pretty printed JSON.
21+
* `executor`: The `Executor` that you want to use to execute queries.
22+
* `root_value`: The `root_value` you want to provide to `executor.execute`.
23+
24+
You can also subclass `GraphQLView` and overwrite `get_root_value(self, request)` to have a dynamic root value
25+
per request.
26+
27+
```python
28+
class UserRootValue(GraphQLView):
29+
def get_root_value(self, request):
30+
return request.user
31+
32+
```

graphql_flask/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .blueprint import GraphQL
2+
from .graphqlview import GraphQLView
3+
from .graphiqlview import GraphiQLView
4+
5+
__all__ = ['GraphQL', 'GraphQLView', 'GraphiQLView']

graphql_flask/blueprint.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from flask import Blueprint
2+
from .graphqlview import GraphQLView
3+
from .graphiqlview import GraphiQLView
4+
5+
6+
class GraphQL(object):
7+
def __init__(self, app, schema, **options):
8+
self.app = app
9+
self.blueprint = Blueprint('graphql', __name__,
10+
template_folder='templates',
11+
static_folder='./static/')
12+
13+
app.add_url_rule('/graphql', view_func=GraphQLView.as_view('graphql', schema=schema, **options))
14+
app.add_url_rule('/graphiql', view_func=GraphiQLView.as_view('graphiql'))
15+
16+
self.app.register_blueprint(self.blueprint)

graphql_flask/graphiqlview.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from flask import render_template
2+
from flask.views import View
3+
4+
5+
class GraphiQLView(View):
6+
template_name = 'graphiql/index.html'
7+
methods = ['GET']
8+
9+
def __init__(self, **kwargs):
10+
super(GraphiQLView, self).__init__()
11+
for key, value in kwargs.items():
12+
if hasattr(self, key):
13+
setattr(self, key, value)
14+
15+
def dispatch_request(self):
16+
return render_template(self.template_name)

graphql_flask/graphqlview.py

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import json
2+
from werkzeug.exceptions import BadRequest, MethodNotAllowed
3+
from flask import Response, request
4+
from flask.views import View
5+
from graphql.core import Source, parse
6+
from graphql.core.error import GraphQLError, format_error as format_graphql_error
7+
from graphql.core.execution import ExecutionResult, get_default_executor
8+
from graphql.core.type.schema import GraphQLSchema
9+
from graphql.core.utils.get_operation_ast import get_operation_ast
10+
import six
11+
12+
13+
class HttpError(Exception):
14+
def __init__(self, response, message=None, *args, **kwargs):
15+
self.response = response
16+
self.message = message = message or response.description.decode()
17+
super(HttpError, self).__init__(message, *args, **kwargs)
18+
19+
20+
class GraphQLView(View):
21+
schema = None
22+
executor = None
23+
root_value = None
24+
pretty = False
25+
26+
methods = ['GET', 'POST', 'PUT', 'DELETE']
27+
28+
def __init__(self, **kwargs):
29+
super(GraphQLView, self).__init__()
30+
for key, value in kwargs.items():
31+
if hasattr(self, key):
32+
setattr(self, key, value)
33+
34+
if not self.executor:
35+
self.executor = get_default_executor()
36+
37+
assert isinstance(self.schema, GraphQLSchema), 'A Schema is required to be provided to GraphQLView.'
38+
39+
# noinspection PyUnusedLocal
40+
def get_root_value(self, request):
41+
return self.root_value
42+
43+
def get_request_context(self, request):
44+
return request
45+
46+
def dispatch_request(self):
47+
try:
48+
if request.method.lower() not in ('get', 'post'):
49+
raise HttpError(MethodNotAllowed(['GET', 'POST'], 'GraphQL only supports GET and POST requests.'))
50+
51+
execution_result = self.execute_graphql_request(request)
52+
response = {}
53+
54+
if execution_result.errors:
55+
response['errors'] = [self.format_error(e) for e in execution_result.errors]
56+
57+
if execution_result.invalid:
58+
status_code = 400
59+
else:
60+
status_code = 200
61+
response['data'] = execution_result.data
62+
63+
return Response(
64+
status=status_code,
65+
response=self.json_encode(request, response),
66+
content_type='application/json'
67+
)
68+
69+
except HttpError as e:
70+
return Response(
71+
self.json_encode(request, {
72+
'errors': [self.format_error(e)]
73+
}),
74+
status=e.response.code,
75+
headers={'Allow': ['GET, POST']},
76+
content_type='application/json'
77+
)
78+
79+
def json_encode(self, request, d):
80+
if not self.pretty and not request.args.get('pretty'):
81+
return json.dumps(d, separators=(',', ':'))
82+
83+
return json.dumps(d, sort_keys=True,
84+
indent=2, separators=(',', ': '))
85+
86+
# noinspection PyBroadException
87+
def parse_body(self, request):
88+
content_type = self.get_content_type(request)
89+
90+
if content_type == 'application/graphql':
91+
return {'query': request.data.decode()}
92+
93+
elif content_type == 'application/json':
94+
try:
95+
request_json = json.loads(request.data.decode())
96+
assert isinstance(request_json, dict)
97+
return request_json
98+
except:
99+
raise HttpError(BadRequest('POST body sent invalid JSON.'))
100+
101+
elif content_type == 'application/x-www-form-urlencoded':
102+
return request.form
103+
104+
return {}
105+
106+
def execute(self, *args, **kwargs):
107+
return self.executor.execute(self.schema, *args, **kwargs)
108+
109+
def execute_graphql_request(self, request):
110+
query, variables, operation_name = self.get_graphql_params(request, self.parse_body(request))
111+
112+
if not query:
113+
raise HttpError(BadRequest('Must provide query string.'))
114+
115+
source = Source(query, name='GraphQL request')
116+
117+
try:
118+
document_ast = parse(source)
119+
except Exception as e:
120+
return ExecutionResult(errors=[e], invalid=True)
121+
122+
if request.method.lower() == 'get':
123+
operation_ast = get_operation_ast(document_ast, operation_name)
124+
if operation_ast and operation_ast.operation != 'query':
125+
raise HttpError(MethodNotAllowed(
126+
['POST'], 'Can only perform a {} operation from a POST request.'.format(operation_ast.operation)
127+
))
128+
129+
try:
130+
return self.execute(
131+
document_ast,
132+
self.get_root_value(request),
133+
variables,
134+
operation_name=operation_name,
135+
request_context=self.get_request_context(request)
136+
)
137+
except Exception as e:
138+
return ExecutionResult(errors=[e], invalid=True)
139+
140+
@staticmethod
141+
def get_graphql_params(request, data):
142+
query = request.args.get('query') or data.get('query')
143+
variables = request.args.get('variables') or data.get('variables')
144+
145+
if variables and isinstance(variables, six.text_type):
146+
try:
147+
variables = json.loads(variables)
148+
except:
149+
raise HttpError(BadRequest('Variables are invalid JSON.'))
150+
151+
operation_name = request.args.get('operationName') or data.get('operationName')
152+
153+
return query, variables, operation_name
154+
155+
@staticmethod
156+
def format_error(error):
157+
if isinstance(error, GraphQLError):
158+
return format_graphql_error(error)
159+
160+
return {'message': six.text_type(error)}
161+
162+
@staticmethod
163+
def get_content_type(request):
164+
return request.content_type

graphql_flask/static/graphiql/fetch.min.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)