From b570a5a5aa0938c66d47a96b3c365bd4f125a0f7 Mon Sep 17 00:00:00 2001 From: KingDarBoja Date: Sun, 7 Jun 2020 14:06:06 -0500 Subject: [PATCH 1/5] refactor: add webob-graphql as optional feature --- graphql_server/webob/__init__.py | 3 + graphql_server/webob/graphqlview.py | 143 ++++++ graphql_server/webob/render_graphiql.py | 141 ++++++ graphql_server/webob/utils.py | 56 +++ setup.py | 8 +- tests/webob/__init__.py | 0 tests/webob/app.py | 55 +++ tests/webob/schema.py | 43 ++ tests/webob/test_graphiqlview.py | 43 ++ tests/webob/test_graphqlview.py | 569 ++++++++++++++++++++++++ 10 files changed, 1060 insertions(+), 1 deletion(-) create mode 100644 graphql_server/webob/__init__.py create mode 100644 graphql_server/webob/graphqlview.py create mode 100644 graphql_server/webob/render_graphiql.py create mode 100644 graphql_server/webob/utils.py create mode 100644 tests/webob/__init__.py create mode 100644 tests/webob/app.py create mode 100644 tests/webob/schema.py create mode 100644 tests/webob/test_graphiqlview.py create mode 100644 tests/webob/test_graphqlview.py diff --git a/graphql_server/webob/__init__.py b/graphql_server/webob/__init__.py new file mode 100644 index 0000000..8f5beaf --- /dev/null +++ b/graphql_server/webob/__init__.py @@ -0,0 +1,3 @@ +from .graphqlview import GraphQLView + +__all__ = ["GraphQLView"] diff --git a/graphql_server/webob/graphqlview.py b/graphql_server/webob/graphqlview.py new file mode 100644 index 0000000..7880e62 --- /dev/null +++ b/graphql_server/webob/graphqlview.py @@ -0,0 +1,143 @@ +import copy +from collections import MutableMapping +from functools import partial + +from graphql.error import GraphQLError +from graphql.type.schema import GraphQLSchema +from webob import Response + +from graphql_server import ( + HttpQueryError, + encode_execution_results, + format_error_default, + json_encode, + load_json_body, + run_http_query, +) + +from .render_graphiql import render_graphiql + + +class GraphQLView: + schema = None + request = None + root_value = None + context = None + pretty = False + graphiql = False + graphiql_version = None + graphiql_template = None + middleware = None + batch = False + charset = "UTF-8" + + def __init__(self, **kwargs): + super(GraphQLView, self).__init__() + for key, value in kwargs.items(): + if hasattr(self, key): + setattr(self, key, value) + + assert isinstance( + self.schema, GraphQLSchema + ), "A Schema is required to be provided to GraphQLView." + + def get_root_value(self): + return self.root_value + + def get_context_value(self): + context = ( + copy.copy(self.context) + if self.context and isinstance(self.context, MutableMapping) + else {} + ) + if isinstance(context, MutableMapping) and "request" not in context: + context.update({"request": self.request}) + return context + + def get_middleware(self): + return self.middleware + + format_error = staticmethod(format_error_default) + encode = staticmethod(json_encode) + + def dispatch_request(self): + try: + request_method = self.request.method.lower() + data = self.parse_body() + + show_graphiql = request_method == "get" and self.should_display_graphiql() + catch = show_graphiql + + pretty = self.pretty or show_graphiql or self.request.params.get("pretty") + + execution_results, all_params = run_http_query( + self.schema, + request_method, + data, + query_data=self.request.params, + batch_enabled=self.batch, + catch=catch, + # Execute options + root_value=self.get_root_value(), + context_value=self.get_context_value(), + middleware=self.get_middleware(), + ) + result, status_code = encode_execution_results( + execution_results, + is_batch=isinstance(data, list), + format_error=self.format_error, + encode=partial(self.encode, pretty=pretty), # noqa + ) + + if show_graphiql: + return Response( + render_graphiql(params=all_params[0], result=result), + charset=self.charset, + content_type="text/html", + ) + + return Response( + result, + status=status_code, + charset=self.charset, + content_type="application/json", + ) + + except HttpQueryError as e: + parsed_error = GraphQLError(e.message) + return Response( + self.encode(dict(errors=[self.format_error(parsed_error)])), + status=e.status_code, + charset=self.charset, + headers=e.headers or {}, + content_type="application/json", + ) + + # WebOb + def parse_body(self): + # We use mimetype here since we don't need the other + # information provided by content_type + content_type = self.request.content_type + if content_type == "application/graphql": + return {"query": self.request.body.decode("utf8")} + + elif content_type == "application/json": + return load_json_body(self.request.body.decode("utf8")) + + elif content_type in ( + "application/x-www-form-urlencoded", + "multipart/form-data", + ): + return self.request.params + + return {} + + def should_display_graphiql(self): + if not self.graphiql or "raw" in self.request.params: + return False + + return self.request_wants_html() + + def request_wants_html(self): + best = self.request.accept.best_match(["application/json", "text/html"]) + return best == "text/html" diff --git a/graphql_server/webob/render_graphiql.py b/graphql_server/webob/render_graphiql.py new file mode 100644 index 0000000..7e4f012 --- /dev/null +++ b/graphql_server/webob/render_graphiql.py @@ -0,0 +1,141 @@ +from string import Template + +from graphql_server.webob.utils import tojson + +GRAPHIQL_VERSION = "0.7.1" + +TEMPLATE = Template( + """ + + + + + + + + + + + + + + +""" +) + + +def render_graphiql( + params, result, graphiql_version=None, graphiql_template=None, graphql_url=None +): + graphiql_version = graphiql_version or GRAPHIQL_VERSION + template = graphiql_template or TEMPLATE + + if result != "null": + result = tojson(result) + + return template.substitute( + graphiql_version=graphiql_version, + graphql_url=tojson(graphql_url or ""), + result=result, + query=tojson(params and params.query or None), + variables=tojson(params and params.variables or None), + operation_name=tojson(params and params.operation_name or None), + ) diff --git a/graphql_server/webob/utils.py b/graphql_server/webob/utils.py new file mode 100644 index 0000000..d77eace --- /dev/null +++ b/graphql_server/webob/utils.py @@ -0,0 +1,56 @@ +import json + +_slash_escape = "\\/" not in json.dumps("/") + + +def dumps(obj, **kwargs): + """Serialize ``obj`` to a JSON formatted ``str`` by using the application's + configured encoder (:attr:`~webob.WebOb.json_encoder`) if there is an + application on the stack. + This function can return ``unicode`` strings or ascii-only bytestrings by + default which coerce into unicode strings automatically. That behavior by + default is controlled by the ``JSON_AS_ASCII`` configuration variable + and can be overridden by the simplejson ``ensure_ascii`` parameter. + """ + encoding = kwargs.pop("encoding", None) + rv = json.dumps(obj, **kwargs) + if encoding is not None and isinstance(rv, str): + rv = rv.encode(encoding) + return rv + + +def htmlsafe_dumps(obj, **kwargs): + """Works exactly like :func:`dumps` but is safe for use in `` """ -) + + +def escape_js_value(value): + quotation = False + if value.startswith('"') and value.endswith('"'): + quotation = True + value = value[1: len(value) - 1] + + value = value.replace("\\\\n", "\\\\\\n").replace("\\n", "\\\\n") + if quotation: + value = '"' + value.replace('\\\\"', '"').replace('"', '\\"') + '"' + + return value + + +def process_var(template, name, value, jsonify=False): + pattern = r"{{\s*" + name + r"(\s*|[^}]+)*\s*}}" + if jsonify and value not in ["null", "undefined"]: + value = json.dumps(value) + value = escape_js_value(value) + + return re.sub(pattern, value, template) + + +def simple_renderer(template, **values): + replace = ["graphiql_version"] + replace_jsonify = ["query", "result", "variables", "operation_name"] + + for r in replace: + template = process_var(template, r, values.get(r, "")) + + for r in replace_jsonify: + template = process_var(template, r, values.get(r, ""), True) + + return template def render_graphiql( - params, result, graphiql_version=None, graphiql_template=None, graphql_url=None + graphiql_version=None, + graphiql_template=None, + params=None, + result=None, ): graphiql_version = graphiql_version or GRAPHIQL_VERSION template = graphiql_template or TEMPLATE - if result != "null": - result = tojson(result) - - return template.substitute( - graphiql_version=graphiql_version, - graphql_url=tojson(graphql_url or ""), - result=result, - query=tojson(params and params.query or None), - variables=tojson(params and params.variables or None), - operation_name=tojson(params and params.operation_name or None), - ) + template_vars = { + "graphiql_version": graphiql_version, + "query": params and params.query, + "variables": params and params.variables, + "operation_name": params and params.operation_name, + "result": result, + } + + source = simple_renderer(template, **template_vars) + return source + diff --git a/graphql_server/webob/utils.py b/graphql_server/webob/utils.py deleted file mode 100644 index d77eace..0000000 --- a/graphql_server/webob/utils.py +++ /dev/null @@ -1,56 +0,0 @@ -import json - -_slash_escape = "\\/" not in json.dumps("/") - - -def dumps(obj, **kwargs): - """Serialize ``obj`` to a JSON formatted ``str`` by using the application's - configured encoder (:attr:`~webob.WebOb.json_encoder`) if there is an - application on the stack. - This function can return ``unicode`` strings or ascii-only bytestrings by - default which coerce into unicode strings automatically. That behavior by - default is controlled by the ``JSON_AS_ASCII`` configuration variable - and can be overridden by the simplejson ``ensure_ascii`` parameter. - """ - encoding = kwargs.pop("encoding", None) - rv = json.dumps(obj, **kwargs) - if encoding is not None and isinstance(rv, str): - rv = rv.encode(encoding) - return rv - - -def htmlsafe_dumps(obj, **kwargs): - """Works exactly like :func:`dumps` but is safe for use in ``