diff --git a/docs/source/index.rst b/docs/source/index.rst index 9cb1eac..16b3c84 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -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 ================== diff --git a/responder/api.py b/responder/api.py index 99e775f..71997e4 100644 --- a/responder/api.py +++ b/responder/api.py @@ -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 `_ 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) diff --git a/responder/core.py b/responder/core.py index c1487e0..ac22195 100644 --- a/responder/core.py +++ b/responder/core.py @@ -1,2 +1,2 @@ from .api import API -from . import status_codes +from .models import Request, Response diff --git a/responder/models.py b/responder/models.py index 2403d0d..5fb280d 100644 --- a/responder/models.py +++ b/responder/models.py @@ -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() diff --git a/responder/status_codes.py b/responder/status_codes.py index 445d99d..71e371a 100644 --- a/responder/status_codes.py +++ b/responder/status_codes.py @@ -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)