responder

This commit is contained in:
2018-10-12 06:22:33 -04:00
parent fabe7b9427
commit 2f52ccbe4e
5 changed files with 135 additions and 11 deletions
+37 -3
View File
@@ -124,12 +124,46 @@ Future Ideas
- Potentially support ASGI instead of WSGI. Will the tradeoffs be worth it? This is a question to ask. Procedural code works well for 90% use cases.
- If frontend websites are supported, provide an official way to run webpack.
When can I use it?
------------------
When it's ready. It's not. I started work on this a few days ago. It works surprisingly well, considering! :)
Installation
============
.. code-block:: shell
$ pipenv install responder
✨🍰✨
Only **Python 3.6+** is supported.
API Documentation
=================
Requests & Responses
--------------------
.. module:: responder
.. autoclass:: Request
:inherited-members:
.. autoclass:: Response
:inherited-members:
API Class
---------
.. autoclass:: API
:inherited-members:
Utility Functions
-----------------
.. autofunction::
Indices and tables
==================
+72 -6
View File
@@ -21,6 +21,13 @@ from .formats import get_formats
# TODO: consider moving status codes here
class API:
"""The primary web-service class.
:param static_dir: The directory to use for static files. Will be created for you if it doesn't already exist.
:param templates_dir: The directory to use for templates. Will be created for you if it doesn't already exist.
:param enable_hsts: If ``True``, send all responses to HTTPS URLs.
"""
status_codes = status_codes
def __init__(
@@ -79,6 +86,7 @@ class API:
return self.whitenoise(environ, start_response)
def wsgi_app(self, environ, start_response):
"""Returns the WSGI app for this application (including all mounted WSGI apps)."""
apps = self.apps.copy()
main = apps.pop("/")
@@ -90,9 +98,13 @@ class API:
wrapped to applying middleware."""
return self.wsgi_app(environ, start_response)
def path_matches_route(self, url):
def path_matches_route(self, path):
"""Given a path portion of a URL, tests that it matches against any registered route.
:param path: The path portion of a URL, to test all known routes against.
"""
for (route, route_object) in self.routes.items():
if route_object.does_match(url):
if route_object.does_match(path):
return route
def _dispatch_request(self, req):
@@ -149,20 +161,40 @@ class API:
return resp
def add_route(self, route, view, *, check_existing=True, graphiql=False):
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.
:param endpoint: The endpoint for the route -- can be a callable, a class, a WSGI application, or graphene schema (GraphQL).
:param check_existing: If ``True``, an AssertionError will be raised, if the route is already defined.
"""
if check_existing:
assert route not in self.routes
# TODO: Support grpahiql.
self.routes[route] = Route(route, view)
self.routes[route] = Route(route, endpoint)
def default_response(self, req, resp):
resp.status_code = HTTP_404
resp.text = "Not found."
def redirect(self, resp, location, *, status_code=status_codes.HTTP_301):
def redirect(
self, resp, location, *, set_text=True, status_code=status_codes.HTTP_301
):
"""Redirects a given response to a given location.
:param resp: The Response to mutate.
:param location: The location of the redirect.
:param set_text: If ``True``, sets the Redirect body content automatically.
:param status_code: an `API.status_codes` attribute, or an integer, representing the HTTP status code of the redirect.
"""
assert resp.status_code.is_300(status_code)
resp.status_code = status_code
resp.text = f"Redirecting to: {location}"
if set_text:
resp.text = f"Redirecting to: {location}"
resp.headers.update({"Location": location})
@staticmethod
@@ -199,6 +231,16 @@ class API:
return (query, result, status_code)
def route(self, route, **options):
"""Decorator for creating new routes around function and class defenitions.
Usage::
@api.route("/hello")
def hello(req, resp):
req.text = "hello, world!"
"""
def decorator(f):
self.add_route(route, f, **options)
return f
@@ -206,9 +248,19 @@ class API:
return decorator
def mount(self, route, wsgi_app):
"""Mounts a WSGI application at a given route.
:param route: String representation of the route to be used (shouldn't be parameterized).
:param wsgi_app: The other WSGI app (e.g. a Flask app).
"""
self.apps.update({route: wsgi_app})
def session(self, base_url="http://;"):
"""Testing HTTP client. Returns a Requests session object, able to send HTTP requests to the WSGI application.
:param base_url: The URL to mount the connection adaptor to.
"""
if self._session is None:
session = RequestsSession()
session.mount(base_url, RequestsWSGIAdapter(self))
@@ -216,12 +268,26 @@ class API:
return self._session
def url_for(self, view, absolute_url=False, **params):
# TODO: Absolute_url
"""Given a view, returns the URL for that view.
:param view: The route endpoint you're searching for.
:param params: Data to pass into the URL generator (for parameterized URLs).
"""
for (route, route_object) in self.routes.items():
if route_object.endpoint == _view:
return route_object.url(**params)
raise ValueError
def template(self, name, auto_escape=True, **values):
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template, with provided values supplied.
Note: The current ``api`` instance is always passed into the view.
:param name: The filename of the jinja2 template, in ``templates_dir``.
:param auto_escape: If ``True``, HTML and XML will automatically be escaped.
:param params: Data to pass into the template.
"""
# Give reference to self.
values.update(api=self)
+1 -1
View File
@@ -1,2 +1,2 @@
from .api import API
from . import status_codes
from .models import Request, Response
+1 -1
View File
@@ -30,12 +30,12 @@ def flatten(d):
# TODO: add slots
class Request:
def __init__(self):
super().__init__()
self._wz = None
@classmethod
def from_environ(kls, environ, start_response=None):
self = kls()
#: The Werkzeug object, powering the Request.
self._wz = WerkzeugRequest(environ)
self.headers = CaseInsensitiveDict(self._wz.headers.to_wsgi_list())
self.method = self._wz.method.lower()
+24
View File
@@ -88,3 +88,27 @@ for number in codes:
for label in codes[number]:
locals()[label] = number
def _is_category(category, status_code):
return all([(status_code >= category), (status_code < category + 100)])
def is_100(status_code):
return _is_category(100, status_code)
def is_200(status_code):
return _is_category(200, status_code)
def is_300(status_code):
return _is_category(300, status_code)
def is_400(status_code):
return _is_category(400, status_code)
def is_500(status_code):
return _is_category(500, status_code)