diff --git a/CHANGELOG.md b/CHANGELOG.md index 658d54c..2e95d2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# v0.0.8 +- GraphiQL Support + +# 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/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! 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) 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/api.py b/responder/api.py index 4ec94d3..eb3f245 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. @@ -222,9 +221,9 @@ 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 @@ -276,6 +275,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 +355,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/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): diff --git a/responder/routes.py b/responder/routes.py index 51a0150..796296c 100644 --- a/responder/routes.py +++ b/responder/routes.py @@ -1,3 +1,5 @@ +import re +from graphql import GraphQLSchema from parse import parse, search @@ -55,3 +57,11 @@ class Route: url = f"http://;{url}" return url + + def _weight(self): + params_count = -len(set(re.findall(r'{([a-zA-Z]\w*)}', self.route))) + return params_count != 0, params_count + + @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' %} + + + + +
+ + + + + + + + + + +