Skip to content

Commit b570a5a

Browse files
committed
refactor: add webob-graphql as optional feature
1 parent 35ed87d commit b570a5a

File tree

10 files changed

+1060
-1
lines changed

10 files changed

+1060
-1
lines changed

graphql_server/webob/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .graphqlview import GraphQLView
2+
3+
__all__ = ["GraphQLView"]

graphql_server/webob/graphqlview.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import copy
2+
from collections import MutableMapping
3+
from functools import partial
4+
5+
from graphql.error import GraphQLError
6+
from graphql.type.schema import GraphQLSchema
7+
from webob import Response
8+
9+
from graphql_server import (
10+
HttpQueryError,
11+
encode_execution_results,
12+
format_error_default,
13+
json_encode,
14+
load_json_body,
15+
run_http_query,
16+
)
17+
18+
from .render_graphiql import render_graphiql
19+
20+
21+
class GraphQLView:
22+
schema = None
23+
request = None
24+
root_value = None
25+
context = None
26+
pretty = False
27+
graphiql = False
28+
graphiql_version = None
29+
graphiql_template = None
30+
middleware = None
31+
batch = False
32+
charset = "UTF-8"
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+
def get_root_value(self):
45+
return self.root_value
46+
47+
def get_context_value(self):
48+
context = (
49+
copy.copy(self.context)
50+
if self.context and isinstance(self.context, MutableMapping)
51+
else {}
52+
)
53+
if isinstance(context, MutableMapping) and "request" not in context:
54+
context.update({"request": self.request})
55+
return context
56+
57+
def get_middleware(self):
58+
return self.middleware
59+
60+
format_error = staticmethod(format_error_default)
61+
encode = staticmethod(json_encode)
62+
63+
def dispatch_request(self):
64+
try:
65+
request_method = self.request.method.lower()
66+
data = self.parse_body()
67+
68+
show_graphiql = request_method == "get" and self.should_display_graphiql()
69+
catch = show_graphiql
70+
71+
pretty = self.pretty or show_graphiql or self.request.params.get("pretty")
72+
73+
execution_results, all_params = run_http_query(
74+
self.schema,
75+
request_method,
76+
data,
77+
query_data=self.request.params,
78+
batch_enabled=self.batch,
79+
catch=catch,
80+
# Execute options
81+
root_value=self.get_root_value(),
82+
context_value=self.get_context_value(),
83+
middleware=self.get_middleware(),
84+
)
85+
result, status_code = encode_execution_results(
86+
execution_results,
87+
is_batch=isinstance(data, list),
88+
format_error=self.format_error,
89+
encode=partial(self.encode, pretty=pretty), # noqa
90+
)
91+
92+
if show_graphiql:
93+
return Response(
94+
render_graphiql(params=all_params[0], result=result),
95+
charset=self.charset,
96+
content_type="text/html",
97+
)
98+
99+
return Response(
100+
result,
101+
status=status_code,
102+
charset=self.charset,
103+
content_type="application/json",
104+
)
105+
106+
except HttpQueryError as e:
107+
parsed_error = GraphQLError(e.message)
108+
return Response(
109+
self.encode(dict(errors=[self.format_error(parsed_error)])),
110+
status=e.status_code,
111+
charset=self.charset,
112+
headers=e.headers or {},
113+
content_type="application/json",
114+
)
115+
116+
# WebOb
117+
def parse_body(self):
118+
# We use mimetype here since we don't need the other
119+
# information provided by content_type
120+
content_type = self.request.content_type
121+
if content_type == "application/graphql":
122+
return {"query": self.request.body.decode("utf8")}
123+
124+
elif content_type == "application/json":
125+
return load_json_body(self.request.body.decode("utf8"))
126+
127+
elif content_type in (
128+
"application/x-www-form-urlencoded",
129+
"multipart/form-data",
130+
):
131+
return self.request.params
132+
133+
return {}
134+
135+
def should_display_graphiql(self):
136+
if not self.graphiql or "raw" in self.request.params:
137+
return False
138+
139+
return self.request_wants_html()
140+
141+
def request_wants_html(self):
142+
best = self.request.accept.best_match(["application/json", "text/html"])
143+
return best == "text/html"
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
from string import Template
2+
3+
from graphql_server.webob.utils import tojson
4+
5+
GRAPHIQL_VERSION = "0.7.1"
6+
7+
TEMPLATE = Template(
8+
"""<!--
9+
The request to this GraphQL server provided the header "Accept: text/html"
10+
and as a result has been presented GraphiQL - an in-browser IDE for
11+
exploring GraphQL.
12+
If you wish to receive JSON, provide the header "Accept: application/json" or
13+
add "&raw" to the end of the URL within a browser.
14+
-->
15+
<!DOCTYPE html>
16+
<html>
17+
<head>
18+
<style>
19+
html, body {
20+
height: 100%;
21+
margin: 0;
22+
overflow: hidden;
23+
width: 100%;
24+
}
25+
</style>
26+
<meta name="referrer" content="no-referrer">
27+
<link
28+
href="//cdn.jsdelivr.net/graphiql/{{graphiql_version}}/graphiql.css"
29+
rel="stylesheet" />
30+
<script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>
31+
<script src="//cdn.jsdelivr.net/react/15.0.0/react.min.js"></script>
32+
<script src="//cdn.jsdelivr.net/react/15.0.0/react-dom.min.js"></script>
33+
<script
34+
src="//cdn.jsdelivr.net/graphiql/{{graphiql_version}}/graphiql.min.js">
35+
</script>
36+
</head>
37+
<body>
38+
<script>
39+
// Collect the URL parameters
40+
var parameters = {};
41+
window.location.search.substr(1).split('&').forEach(function (entry) {
42+
var eq = entry.indexOf('=');
43+
if (eq >= 0) {
44+
parameters[decodeURIComponent(entry.slice(0, eq))] =
45+
decodeURIComponent(entry.slice(eq + 1));
46+
}
47+
});
48+
// Produce a Location query string from a parameter object.
49+
function locationQuery(params) {
50+
return '?' + Object.keys(params).map(function (key) {
51+
return encodeURIComponent(key) + '=' +
52+
encodeURIComponent(params[key]);
53+
}).join('&');
54+
}
55+
// Derive a fetch URL from the current URL, sans the GraphQL parameters.
56+
var graphqlParamNames = {
57+
query: true,
58+
variables: true,
59+
operationName: true
60+
};
61+
var otherParams = {};
62+
for (var k in parameters) {
63+
if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) {
64+
otherParams[k] = parameters[k];
65+
}
66+
}
67+
var fetchURL = locationQuery(otherParams);
68+
// Defines a GraphQL fetcher using the fetch API.
69+
function graphQLFetcher(graphQLParams) {
70+
return fetch(fetchURL, {
71+
method: 'post',
72+
headers: {
73+
'Accept': 'application/json',
74+
'Content-Type': 'application/json'
75+
},
76+
body: JSON.stringify(graphQLParams),
77+
credentials: 'include',
78+
}).then(function (response) {
79+
return response.text();
80+
}).then(function (responseBody) {
81+
try {
82+
return JSON.parse(responseBody);
83+
} catch (error) {
84+
return responseBody;
85+
}
86+
});
87+
}
88+
// When the query and variables string is edited, update the URL bar so
89+
// that it can be easily shared.
90+
function onEditQuery(newQuery) {
91+
parameters.query = newQuery;
92+
updateURL();
93+
}
94+
function onEditVariables(newVariables) {
95+
parameters.variables = newVariables;
96+
updateURL();
97+
}
98+
function onEditOperationName(newOperationName) {
99+
parameters.operationName = newOperationName;
100+
updateURL();
101+
}
102+
function updateURL() {
103+
history.replaceState(null, null, locationQuery(parameters));
104+
}
105+
// Render <GraphiQL /> into the body.
106+
ReactDOM.render(
107+
React.createElement(GraphiQL, {
108+
fetcher: graphQLFetcher,
109+
onEditQuery: onEditQuery,
110+
onEditVariables: onEditVariables,
111+
onEditOperationName: onEditOperationName,
112+
query: {{query|tojson}},
113+
response: {{result|tojson}},
114+
variables: {{variables|tojson}},
115+
operationName: {{operation_name|tojson}},
116+
}),
117+
document.body
118+
);
119+
</script>
120+
</body>
121+
</html>"""
122+
)
123+
124+
125+
def render_graphiql(
126+
params, result, graphiql_version=None, graphiql_template=None, graphql_url=None
127+
):
128+
graphiql_version = graphiql_version or GRAPHIQL_VERSION
129+
template = graphiql_template or TEMPLATE
130+
131+
if result != "null":
132+
result = tojson(result)
133+
134+
return template.substitute(
135+
graphiql_version=graphiql_version,
136+
graphql_url=tojson(graphql_url or ""),
137+
result=result,
138+
query=tojson(params and params.query or None),
139+
variables=tojson(params and params.variables or None),
140+
operation_name=tojson(params and params.operation_name or None),
141+
)

graphql_server/webob/utils.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import json
2+
3+
_slash_escape = "\\/" not in json.dumps("/")
4+
5+
6+
def dumps(obj, **kwargs):
7+
"""Serialize ``obj`` to a JSON formatted ``str`` by using the application's
8+
configured encoder (:attr:`~webob.WebOb.json_encoder`) if there is an
9+
application on the stack.
10+
This function can return ``unicode`` strings or ascii-only bytestrings by
11+
default which coerce into unicode strings automatically. That behavior by
12+
default is controlled by the ``JSON_AS_ASCII`` configuration variable
13+
and can be overridden by the simplejson ``ensure_ascii`` parameter.
14+
"""
15+
encoding = kwargs.pop("encoding", None)
16+
rv = json.dumps(obj, **kwargs)
17+
if encoding is not None and isinstance(rv, str):
18+
rv = rv.encode(encoding)
19+
return rv
20+
21+
22+
def htmlsafe_dumps(obj, **kwargs):
23+
"""Works exactly like :func:`dumps` but is safe for use in ``<script>``
24+
tags. It accepts the same arguments and returns a JSON string. Note that
25+
this is available in templates through the ``|tojson`` filter which will
26+
also mark the result as safe. Due to how this function escapes certain
27+
characters this is safe even if used outside of ``<script>`` tags.
28+
The following characters are escaped in strings:
29+
- ``<``
30+
- ``>``
31+
- ``&``
32+
- ``'``
33+
This makes it safe to embed such strings in any place in HTML with the
34+
notable exception of double quoted attributes. In that case single
35+
quote your attributes or HTML escape it in addition.
36+
.. versionchanged:: 0.10
37+
This function's return value is now always safe for HTML usage, even
38+
if outside of script tags or if used in XHTML. This rule does not
39+
hold true when using this function in HTML attributes that are double
40+
quoted. Always single quote attributes if you use the ``|tojson``
41+
filter. Alternatively use ``|tojson|forceescape``.
42+
"""
43+
rv = (
44+
dumps(obj, **kwargs)
45+
.replace(u"<", u"\\u003c")
46+
.replace(u">", u"\\u003e")
47+
.replace(u"&", u"\\u0026")
48+
.replace(u"'", u"\\u0027")
49+
)
50+
if not _slash_escape:
51+
rv = rv.replace("\\/", "/")
52+
return rv
53+
54+
55+
def tojson(obj, **kwargs):
56+
return htmlsafe_dumps(obj, **kwargs)

setup.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,15 @@
2727
"sanic>=19.9.0,<20",
2828
]
2929

30+
install_webob_requires = [
31+
"webob>=1.8.6,<2",
32+
]
33+
3034
install_all_requires = \
3135
install_requires + \
3236
install_flask_requires + \
33-
install_sanic_requires
37+
install_sanic_requires + \
38+
install_webob_requires
3439

3540
setup(
3641
name="graphql-server-core",
@@ -62,6 +67,7 @@
6267
"dev": install_all_requires + dev_requires,
6368
"flask": install_flask_requires,
6469
"sanic": install_sanic_requires,
70+
"webob": install_webob_requires,
6571
},
6672
include_package_data=True,
6773
zip_safe=False,

tests/webob/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)