From 2d935542e166fe174474c036a42f0bc43d0766bb Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 16 Oct 2018 05:24:20 -0700 Subject: [PATCH 01/11] v0.0.7, immutable response object --- CHANGELOG.md | 3 +++ responder/__version__.py | 2 +- responder/models.py | 49 +++++++++++++++++++++++----------------- 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 658d54c..a0aa8f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# v0.0.7 + - Immutable Request object. + # v0.0.6: - Ability to mount WSGI apps. - Supply content-type when serving up the schema. diff --git a/responder/__version__.py b/responder/__version__.py index 034f46c..6526deb 100644 --- a/responder/__version__.py +++ b/responder/__version__.py @@ -1 +1 @@ -__version__ = "0.0.6" +__version__ = "0.0.7" diff --git a/responder/models.py b/responder/models.py index ed928f7..0fed8ab 100644 --- a/responder/models.py +++ b/responder/models.py @@ -91,12 +91,7 @@ class Request: __slots__ = [ "_starlette", "formats", - "headers", - "mimetype", - "method", - "full_url", - "url", - "params", + "_headers", "_encoding", ] @@ -109,27 +104,39 @@ class Request: for header, value in self._starlette.headers.items(): headers[header] = value - self.headers = ( - headers - ) #: A case-insensitive dictionary, containing all headers sent in the Request. + self._headers = headers - self.mimetype = self.headers.get("Content-Type", "") + @property + def headers(self): + """A case-insensitive dictionary, containing all headers sent in the Request.""" + return self._headers - self.method = ( - self._starlette.method.lower() - ) #: The incoming HTTP method used for the request, lower-cased. + @property + def mimetype(self): + return self.headers.get("Content-Type", "") - self.full_url = str( - self._starlette.url - ) #: The full URL of the Request, query parameters and all. + @property + def method(self): + """The incoming HTTP method used for the request, lower-cased.""" + return self._starlette.method.lower() - self.url = rfc3986.urlparse(self.full_url) #: The parsed URL of the Request + @property + def full_url(self): + """The full URL of the Request, query parameters and all.""" + return str(self._starlette.url) + + @property + def url(self): + """The parsed URL of the Request.""" + return rfc3986.urlparse(self.full_url) + + @property + def params(self): + """A dictionary of the parsed query parameters used for the Request.""" try: - self.params = QueryDict( - self.url.query - ) #: A dictionary of the parsed query parameters used for the Request. + return QueryDict(self.url.query) except AttributeError: - self.params = {} + return QueryDict({}) @property async def encoding(self): From 93172ea1d0307fd4c574beffb0aa56831fde5509 Mon Sep 17 00:00:00 2001 From: Joachim Jablon Date: Tue, 16 Oct 2018 14:41:30 +0200 Subject: [PATCH 02/11] Fix typo --- docs/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 9bbcbb1..35336fe 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -51,7 +51,7 @@ Features - Class-based views without inheritence. - ASGI framework, the future of Python web services. - The ability to mount any ASGI / WSGI app at a subroute. -- *f-string syntax* route declration. +- *f-string syntax* route declaration. - Mutable response object, passed into each view. No need to return anything. - Background tasks, spawned off in a ``ThreadPoolExecutor``. - GraphQL support! From 4db2289b7e319d2b34ecee4d4eb7b858c2235cf5 Mon Sep 17 00:00:00 2001 From: ybv Date: Tue, 16 Oct 2018 22:39:09 +0530 Subject: [PATCH 03/11] Add status code rest for class based view --- tests/test_responder.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_responder.py b/tests/test_responder.py index 01d92f2..f336cba 100644 --- a/tests/test_responder.py +++ b/tests/test_responder.py @@ -149,6 +149,15 @@ def test_request_and_get(api, session): assert "LIFE" in r.headers +def test_class_based_view_status_code(api): + @api.route("/") + class ThingsResource: + def on_request(self, req, resp): + resp.status_code = responder.status_codes.HTTP_416 + + assert api.session().get("http://;/").status_code == responder.status_codes.HTTP_416 + + def test_query_params(api, url, session): @api.route("/") def route(req, resp): From c55c9056215a6ed1ace3745cd6714cc8ce9a06a5 Mon Sep 17 00:00:00 2001 From: pesap Date: Tue, 16 Oct 2018 17:23:47 -0700 Subject: [PATCH 04/11] Fix typo --- docs/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 9bbcbb1..35336fe 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -51,7 +51,7 @@ Features - Class-based views without inheritence. - ASGI framework, the future of Python web services. - The ability to mount any ASGI / WSGI app at a subroute. -- *f-string syntax* route declration. +- *f-string syntax* route declaration. - Mutable response object, passed into each view. No need to return anything. - Background tasks, spawned off in a ``ThreadPoolExecutor``. - GraphQL support! From a9a4ceaa78c8af9213e485bf0c228da35c555511 Mon Sep 17 00:00:00 2001 From: taoufik07 Date: Wed, 17 Oct 2018 04:37:31 +0100 Subject: [PATCH 05/11] Add weight to Route --- responder/routes.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/responder/routes.py b/responder/routes.py index 51a0150..f902e36 100644 --- a/responder/routes.py +++ b/responder/routes.py @@ -1,5 +1,5 @@ from parse import parse, search - +import re def memoize(f): def helper(self, s): @@ -55,3 +55,8 @@ class Route: url = f"http://;{url}" return url + + def _weight(self): + l = -len(set(re.findall(r'{([a-zA-Z]\w*)}', self.route))) + return l != 0, l + From f0479019c34e80caf5344566b0df7f5cee61691a Mon Sep 17 00:00:00 2001 From: taoufik07 Date: Wed, 17 Oct 2018 04:38:08 +0100 Subject: [PATCH 06/11] Order the routes based on the weight --- responder/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/responder/api.py b/responder/api.py index 4ec94d3..0eeb95f 100644 --- a/responder/api.py +++ b/responder/api.py @@ -222,9 +222,10 @@ class API: """ if check_existing: assert route not in self.routes - # TODO: Support grpahiql. self.routes[route] = Route(route, endpoint) + # TODO: A better datastructer or sort it once the app is loaded + self.routes = dict(sorted(self.routes.items(), key=lambda item: item[1]._weight())) def default_response(self, req, resp): resp.status_code = status_codes.HTTP_404 From f7657679acd7bd512707a6ac8f8e52e12d403b17 Mon Sep 17 00:00:00 2001 From: Taoufik Date: Wed, 17 Oct 2018 05:07:29 +0100 Subject: [PATCH 07/11] A verbose name --- responder/routes.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/responder/routes.py b/responder/routes.py index f902e36..fe45151 100644 --- a/responder/routes.py +++ b/responder/routes.py @@ -1,5 +1,6 @@ -from parse import parse, search import re +from parse import parse, search + def memoize(f): def helper(self, s): @@ -57,6 +58,6 @@ class Route: return url def _weight(self): - l = -len(set(re.findall(r'{([a-zA-Z]\w*)}', self.route))) - return l != 0, l + params_count = -len(set(re.findall(r'{([a-zA-Z]\w*)}', self.route))) + return params_count != 0, params_count From 148a430da45aab30d9dae87fd0154a80fbe7c863 Mon Sep 17 00:00:00 2001 From: Manish P Mathai Date: Tue, 16 Oct 2018 22:36:54 -0700 Subject: [PATCH 08/11] Fix typo in quickstart example --- docs/source/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index 904afaf..fc12ab7 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -115,7 +115,7 @@ Here, we'll process our data in the background, while responding immediately to # Parse the incoming data as form-encoded. # Note: 'json' and 'yaml' formats are also automatically supported. - data = await resp.media() + data = await req.media() # Process the data (in the background). process_data(data) From 85f9c33b2b74d1188c828a69bdd944d00d394411 Mon Sep 17 00:00:00 2001 From: ArtemGordinsky Date: Wed, 17 Oct 2018 08:00:03 +0200 Subject: [PATCH 09/11] Integrate GraphiQL --- responder/api.py | 37 ++++---- responder/routes.py | 5 ++ responder/templates/graphiql.html | 143 ++++++++++++++++++++++++++++++ tests/test_responder.py | 7 ++ 4 files changed, 172 insertions(+), 20 deletions(-) create mode 100644 responder/templates/graphiql.html diff --git a/responder/api.py b/responder/api.py index 4ec94d3..7ceb8d9 100644 --- a/responder/api.py +++ b/responder/api.py @@ -51,6 +51,7 @@ class API: self.static_dir = Path(os.path.abspath(static_dir)) self.static_route = f"/{static_dir}" self.templates_dir = Path(os.path.abspath(templates_dir)) + self.built_in_templates_dir = Path(os.path.abspath(os.path.dirname(__file__) + '/templates')) self.routes = {} self.schemas = {} @@ -180,11 +181,10 @@ class API: view = self.routes[route].endpoint(**params) except TypeError: view = self.routes[route].endpoint - try: - # GraphQL Schema. - assert hasattr(view, "execute") + + if self.routes[route].is_graphql: await self.graphql_response(req, resp, schema=view) - except AssertionError: + else: # WSGI App. # try: # return view( @@ -213,7 +213,6 @@ class API: return resp def add_route(self, route, endpoint, *, check_existing=True): - # TODO: add graphiql """Add a route to the API. :param route: A string representation of the route. @@ -223,7 +222,6 @@ class API: if check_existing: assert route not in self.routes - # TODO: Support grpahiql. self.routes[route] = Route(route, endpoint) def default_response(self, req, resp): @@ -276,6 +274,12 @@ class API: return req.text async def graphql_response(self, req, resp, schema): + show_graphiql = req.method.lower() == 'get' and req.accepts('text/html') + + if show_graphiql: + resp.content = self.template('graphiql.html', endpoint=req.url.path) + return + query = await self._resolve_graphql_query(req) result = schema.execute(query) result, status_code = encode_execution_results( @@ -350,20 +354,13 @@ class API: # Give reference to self. values.update(api=self) - if auto_escape: - env = jinja2.Environment( - loader=jinja2.FileSystemLoader( - str(self.templates_dir), followlinks=True - ), - autoescape=jinja2.select_autoescape(["html", "xml"]), - ) - else: - env = jinja2.Environment( - loader=jinja2.FileSystemLoader( - str(self.templates_dir), followlinks=True - ), - autoescape=jinja2.select_autoescape([]), - ) + env = jinja2.Environment( + loader=jinja2.FileSystemLoader( + [str(self.templates_dir), str(self.built_in_templates_dir)], + followlinks=True + ), + autoescape=jinja2.select_autoescape(["html", "xml"] if auto_escape else []), + ) template = env.get_template(name) return template.render(**values) diff --git a/responder/routes.py b/responder/routes.py index 51a0150..4c37e10 100644 --- a/responder/routes.py +++ b/responder/routes.py @@ -1,3 +1,4 @@ +from graphql import GraphQLSchema from parse import parse, search @@ -55,3 +56,7 @@ class Route: url = f"http://;{url}" return url + + @property + def is_graphql(self): + return isinstance(self.endpoint, GraphQLSchema) diff --git a/responder/templates/graphiql.html b/responder/templates/graphiql.html new file mode 100644 index 0000000..df10de8 --- /dev/null +++ b/responder/templates/graphiql.html @@ -0,0 +1,143 @@ +{% set GRAPHIQL_VERSION = '0.12.0' %} + + + + + + + + + + + + + + + +
Loading...
+ + + diff --git a/tests/test_responder.py b/tests/test_responder.py index 01d92f2..84feb5e 100644 --- a/tests/test_responder.py +++ b/tests/test_responder.py @@ -235,6 +235,13 @@ def test_graphql_schema_json_query(api, schema): r = api.session().post("http://;/", json={"query": "{ hello }"}) assert r.ok +def test_graphiql(api, schema): + api.add_route("/", schema) + + r = api.session().get("http://;/", headers={"Accept": "text/html"}) + assert r.ok + assert 'GraphiQL' in r.text + def test_json_uploads(api, session): @api.route("/") From e5cef0d9c0c783790cd5ce061a1858f7227b7cb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radek=20Posto=C5=82owicz?= Date: Wed, 17 Oct 2018 10:08:59 +0200 Subject: [PATCH 10/11] Fix typo --- docs/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 9bbcbb1..35336fe 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -51,7 +51,7 @@ Features - Class-based views without inheritence. - ASGI framework, the future of Python web services. - The ability to mount any ASGI / WSGI app at a subroute. -- *f-string syntax* route declration. +- *f-string syntax* route declaration. - Mutable response object, passed into each view. No need to return anything. - Background tasks, spawned off in a ``ThreadPoolExecutor``. - GraphQL support! From 7d1f991ce4c77ebe2fc9e4d4f90e7470de8fe0b4 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Wed, 17 Oct 2018 02:52:22 -0700 Subject: [PATCH 11/11] changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0aa8f7..2e95d2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# v0.0.8 +- GraphiQL Support + # v0.0.7 - Immutable Request object.