Skip to content

Commit 66b8a2b

Browse files
authored
Merge flask-graphql (#37)
* refactor: add flask-graphql as optional feature * refactor(server): default_format_error to __all__ * chore: rename dir flask-graphql to flask * chore: add extras require all key * chore: update gitignore * fix(sc): move params query check to try-except * refactor(flask): remove unused backend param * tests(flask): graphiqlview and graphqlview * styles: apply black, isort, flake8 formatting * chore: add all requires to test env * chore(flask): remove blueprint module * refactor(flask): remove py27 imports and unused test * styles: apply black, isort and flake8 formatting
1 parent 865ee9d commit 66b8a2b

11 files changed

+1213
-18
lines changed

.gitignore

+196-12
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,203 @@
1-
*.pyc
2-
*.pyo
31

2+
# Created by https://www.gitignore.io/api/python,intellij+all,visualstudiocode
3+
# Edit at https://www.gitignore.io/?templates=python,intellij+all,visualstudiocode
4+
5+
### Intellij+all ###
6+
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
7+
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
8+
9+
# User-specific stuff
10+
.idea/**/workspace.xml
11+
.idea/**/tasks.xml
12+
.idea/**/usage.statistics.xml
13+
.idea/**/dictionaries
14+
.idea/**/shelf
15+
16+
# Generated files
17+
.idea/**/contentModel.xml
18+
19+
# Sensitive or high-churn files
20+
.idea/**/dataSources/
21+
.idea/**/dataSources.ids
22+
.idea/**/dataSources.local.xml
23+
.idea/**/sqlDataSources.xml
24+
.idea/**/dynamic.xml
25+
.idea/**/uiDesigner.xml
26+
.idea/**/dbnavigator.xml
27+
28+
# Gradle
29+
.idea/**/gradle.xml
30+
.idea/**/libraries
31+
32+
# Gradle and Maven with auto-import
33+
# When using Gradle or Maven with auto-import, you should exclude module files,
34+
# since they will be recreated, and may cause churn. Uncomment if using
35+
# auto-import.
36+
# .idea/modules.xml
37+
# .idea/*.iml
38+
# .idea/modules
39+
# *.iml
40+
# *.ipr
41+
42+
# CMake
43+
cmake-build-*/
44+
45+
# Mongo Explorer plugin
46+
.idea/**/mongoSettings.xml
47+
48+
# File-based project format
49+
*.iws
50+
51+
# IntelliJ
52+
out/
53+
54+
# mpeltonen/sbt-idea plugin
55+
.idea_modules/
56+
57+
# JIRA plugin
58+
atlassian-ide-plugin.xml
59+
60+
# Cursive Clojure plugin
61+
.idea/replstate.xml
62+
63+
# Crashlytics plugin (for Android Studio and IntelliJ)
64+
com_crashlytics_export_strings.xml
65+
crashlytics.properties
66+
crashlytics-build.properties
67+
fabric.properties
68+
69+
# Editor-based Rest Client
70+
.idea/httpRequests
71+
72+
# Android studio 3.1+ serialized cache file
73+
.idea/caches/build_file_checksums.ser
74+
75+
### Intellij+all Patch ###
76+
# Ignores the whole .idea folder and all .iml files
77+
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
78+
79+
.idea/
80+
81+
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
82+
83+
*.iml
84+
modules.xml
85+
.idea/misc.xml
86+
*.ipr
87+
88+
# Sonarlint plugin
89+
.idea/sonarlint
90+
91+
### Python ###
92+
# Byte-compiled / optimized / DLL files
93+
__pycache__/
94+
*.py[cod]
95+
*$py.class
96+
97+
# C extensions
98+
*.so
99+
100+
# Distribution / packaging
101+
.Python
102+
build/
103+
develop-eggs/
104+
dist/
105+
downloads/
106+
eggs/
107+
.eggs/
108+
lib/
109+
lib64/
110+
parts/
111+
sdist/
112+
var/
113+
wheels/
114+
pip-wheel-metadata/
115+
share/python-wheels/
116+
*.egg-info/
117+
.installed.cfg
4118
*.egg
5-
*.egg-info
119+
MANIFEST
6120

7-
.cache
121+
# PyInstaller
122+
# Usually these files are written by a python script from a template
123+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
124+
*.manifest
125+
*.spec
126+
127+
# Installer logs
128+
pip-log.txt
129+
pip-delete-this-directory.txt
130+
131+
# Unit test / coverage reports
132+
htmlcov/
133+
.tox/
134+
.nox/
8135
.coverage
9-
.idea
10-
.mypy_cache
11-
.pytest_cache
12-
.tox
13-
.venv
136+
.coverage.*
137+
.cache
138+
nosetests.xml
139+
coverage.xml
140+
*.cover
141+
.hypothesis/
142+
.pytest_cache/
143+
144+
# Translations
145+
*.mo
146+
*.pot
147+
148+
# Scrapy stuff:
149+
.scrapy
150+
151+
# Sphinx documentation
152+
docs/_build/
153+
154+
# PyBuilder
155+
target/
156+
157+
# pyenv
158+
.python-version
159+
160+
# pipenv
161+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
162+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
163+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
164+
# install all needed dependencies.
165+
#Pipfile.lock
166+
167+
# celery beat schedule file
168+
celerybeat-schedule
169+
170+
# SageMath parsed files
171+
*.sage.py
172+
173+
# Spyder project settings
174+
.spyderproject
175+
.spyproject
176+
177+
# Rope project settings
178+
.ropeproject
179+
180+
# Mr Developer
181+
.mr.developer.cfg
182+
.project
183+
.pydevproject
184+
185+
# mkdocs documentation
186+
/site
187+
188+
# mypy
189+
.mypy_cache/
190+
.dmypy.json
191+
dmypy.json
192+
193+
# Pyre type checker
194+
.pyre/
195+
196+
### VisualStudioCode ###
14197
.vscode
15198

16-
/build/
17-
/dist/
199+
### VisualStudioCode Patch ###
200+
# Ignore all local history of files
201+
.history
18202

19-
docs
203+
# End of https://www.gitignore.io/api/python,intellij+all,visualstudiocode

graphql_server/__init__.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"GraphQLResponse",
3131
"ServerResponse",
3232
"format_execution_result",
33+
"format_error_default",
3334
]
3435

3536

@@ -230,11 +231,11 @@ def get_response(
230231
as a parameter.
231232
"""
232233

233-
if not params.query:
234-
raise HttpQueryError(400, "Must provide query string.")
235-
236234
# noinspection PyBroadException
237235
try:
236+
if not params.query:
237+
raise HttpQueryError(400, "Must provide query string.")
238+
238239
# Parse document to trigger a new HttpQueryError if allow_only_query is True
239240
try:
240241
document = parse(params.query)

graphql_server/flask/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .graphqlview import GraphQLView
2+
3+
__all__ = ["GraphQLView"]

graphql_server/flask/graphqlview.py

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
from functools import partial
2+
3+
from flask import Response, request
4+
from flask.views import View
5+
from graphql.error import GraphQLError
6+
from graphql.type.schema import GraphQLSchema
7+
8+
from graphql_server import (
9+
HttpQueryError,
10+
encode_execution_results,
11+
format_error_default,
12+
json_encode,
13+
load_json_body,
14+
run_http_query,
15+
)
16+
17+
from .render_graphiql import render_graphiql
18+
19+
20+
class GraphQLView(View):
21+
schema = None
22+
executor = None
23+
root_value = None
24+
pretty = False
25+
graphiql = False
26+
graphiql_version = None
27+
graphiql_template = None
28+
graphiql_html_title = None
29+
middleware = None
30+
batch = False
31+
32+
methods = ["GET", "POST", "PUT", "DELETE"]
33+
34+
def __init__(self, **kwargs):
35+
super(GraphQLView, self).__init__()
36+
for key, value in kwargs.items():
37+
if hasattr(self, key):
38+
setattr(self, key, value)
39+
40+
assert isinstance(
41+
self.schema, GraphQLSchema
42+
), "A Schema is required to be provided to GraphQLView."
43+
44+
# noinspection PyUnusedLocal
45+
def get_root_value(self):
46+
return self.root_value
47+
48+
def get_context_value(self):
49+
return request
50+
51+
def get_middleware(self):
52+
return self.middleware
53+
54+
def get_executor(self):
55+
return self.executor
56+
57+
def render_graphiql(self, params, result):
58+
return render_graphiql(
59+
params=params,
60+
result=result,
61+
graphiql_version=self.graphiql_version,
62+
graphiql_template=self.graphiql_template,
63+
graphiql_html_title=self.graphiql_html_title,
64+
)
65+
66+
format_error = staticmethod(format_error_default)
67+
encode = staticmethod(json_encode)
68+
69+
def dispatch_request(self):
70+
try:
71+
request_method = request.method.lower()
72+
data = self.parse_body()
73+
74+
show_graphiql = request_method == "get" and self.should_display_graphiql()
75+
catch = show_graphiql
76+
77+
pretty = self.pretty or show_graphiql or request.args.get("pretty")
78+
79+
extra_options = {}
80+
executor = self.get_executor()
81+
if executor:
82+
# We only include it optionally since
83+
# executor is not a valid argument in all backends
84+
extra_options["executor"] = executor
85+
86+
execution_results, all_params = run_http_query(
87+
self.schema,
88+
request_method,
89+
data,
90+
query_data=request.args,
91+
batch_enabled=self.batch,
92+
catch=catch,
93+
# Execute options
94+
root_value=self.get_root_value(),
95+
context_value=self.get_context_value(),
96+
middleware=self.get_middleware(),
97+
**extra_options
98+
)
99+
result, status_code = encode_execution_results(
100+
execution_results,
101+
is_batch=isinstance(data, list),
102+
format_error=self.format_error,
103+
encode=partial(self.encode, pretty=pretty),
104+
)
105+
106+
if show_graphiql:
107+
return self.render_graphiql(params=all_params[0], result=result)
108+
109+
return Response(result, status=status_code, content_type="application/json")
110+
111+
except HttpQueryError as e:
112+
parsed_error = GraphQLError(e.message)
113+
return Response(
114+
self.encode(dict(errors=[self.format_error(parsed_error)])),
115+
status=e.status_code,
116+
headers=e.headers,
117+
content_type="application/json",
118+
)
119+
120+
# Flask
121+
def parse_body(self):
122+
# We use mimetype here since we don't need the other
123+
# information provided by content_type
124+
content_type = request.mimetype
125+
if content_type == "application/graphql":
126+
return {"query": request.data.decode("utf8")}
127+
128+
elif content_type == "application/json":
129+
return load_json_body(request.data.decode("utf8"))
130+
131+
elif content_type in (
132+
"application/x-www-form-urlencoded",
133+
"multipart/form-data",
134+
):
135+
return request.form
136+
137+
return {}
138+
139+
def should_display_graphiql(self):
140+
if not self.graphiql or "raw" in request.args:
141+
return False
142+
143+
return self.request_wants_html()
144+
145+
def request_wants_html(self):
146+
best = request.accept_mimetypes.best_match(["application/json", "text/html"])
147+
return (
148+
best == "text/html"
149+
and request.accept_mimetypes[best]
150+
> request.accept_mimetypes["application/json"]
151+
)

0 commit comments

Comments
 (0)