Python Responder
Python Responder
Release 1.3.0
Kenneth Reitz
1 Features 3
2 Testimonials 5
3 User Guides 7
3.1 Quick Start! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
3.2 Feature Tour . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
3.3 Deploying Responder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
3.4 Building and Testing with Responder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
3.5 API Documentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
4 Installing Responder 23
6 Ideas 27
i
ii
responder Documentation, Release 1.3.0
import responder
api = responder.API()
@api.route("/{greeting}")
async def greet_world(req, resp, *, greeting):
resp.text = f"{greeting}, world!"
if __name__ == '__main__':
api.run()
Contents 1
responder Documentation, Release 1.3.0
2 Contents
CHAPTER 1
Features
3
responder Documentation, Release 1.3.0
4 Chapter 1. Features
CHAPTER 2
Testimonials
5
responder Documentation, Release 1.3.0
6 Chapter 2. Testimonials
CHAPTER 3
User Guides
This section of the documentation exists to provide an introduction to the Responder interface, as well as educate the
user on basic functionality.
import responder
api = responder.API()
@api.route("/")
def hello_world(req, resp):
resp.text = "hello, world!"
api.run()
7
responder Documentation, Release 1.3.0
This will spin up a production web server on port 5042, ready for incoming HTTP requests.
Note: you can pass port=5000 if you want to customize the port. The PORT environment variable for established
web service providers (e.g. Heroku) will automatically be honored and will set the listening address to 0.0.0.0
automatically (also configurable through the address keyword argument).
If you want dynamic URLs, you can use Python’s familiar f-string syntax to declare variables in your routes:
@api.route("/hello/{who}")
def hello_to(req, resp, *, who):
resp.text = f"hello, {who}!"
If you want your API to send back JSON, simply set the resp.media property to a JSON-serializable Python object:
@api.route("/hello/{who}/json")
def hello_to(req, resp, *, who):
resp.media = {"hello": who}
If you want to render a template, simply use api.template. No need for additional imports:
@api.route("/hello/{who}/html")
def hello_html(req, resp, *, who):
resp.html = api.template('hello.html', who=who)
If you want to set the response status code, simply set resp.status_code:
@api.route("/416")
def teapot(req, resp):
resp.status_code = api.status_codes.HTTP_416 # ...or 416
If you want to set a response header, like X-Pizza: 42, simply modify the resp.headers dictionary:
@api.route("/pizza")
def pizza_pizza(req, resp):
resp.headers['X-Pizza'] = '42'
That’s it!
If you’re expecting to read any request data, on the server, you need to declare your view as async and await the
content.
Here, we’ll process our data in the background, while responding immediately to the client:
import time
@api.route("/incoming")
async def receive_incoming(req, resp):
@api.background.task
def process_data(data):
"""Just sleeps for three seconds, as a demo."""
time.sleep(3)
@api.route("/{greeting}")
class GreetingResource:
def on_request(self, req, resp, *, greeting): # or on_get...
resp.text = f"{greeting}, world!"
resp.headers.update({'X-Life': '42'})
resp.status_code = api.status_codes.HTTP_416
Here, you can spawn off a background thread to run any function, out-of-request:
@api.route("/")
def hello(req, resp):
@api.background.task
def sleep(s=10):
time.sleep(s)
print("slept!")
sleep()
resp.content = "processing"
3.2.3 GraphQL
class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.String(default_value="stranger"))
schema = graphene.Schema(query=Query)
view = responder.ext.GraphQLView(api=api, schema=schema)
api.add_route("/graph", view)
api = responder.API(
title="Web Service",
version="1.0",
(continues on next page)
@api.schema("Pet")
class PetSchema(Schema):
name = fields.Str()
@api.route("/")
def route(req, resp):
"""A cute furry animal endpoint.
---
get:
description: Get a random pet
responses:
200:
description: A pet to be returned
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
"""
resp.media = PetSchema().dump({"name": "little orange"})
>>> r = api.session().get("http://;/schema.yml")
>>> print(r.text)
components:
parameters: {}
responses: {}
schemas:
Pet:
properties:
name: {type: string}
type: object
securitySchemes: {}
info:
contact: {email: [email protected], name: API Support, url: 'http://www.example.
˓→com/support'}
Responder can automatically supply API Documentation for you. Using the example above:
api = responder.API(
title="Web Service",
version="1.0",
openapi="3.0.2",
docs_route='/docs',
description=description,
terms_of_service=terms_of_service,
contact=contact,
license=license,
)
This will make /docs render interactive documentation for your API.
Responder gives you the ability to mount another ASGI / WSGI app at a subroute:
import responder
from flask import Flask
api = responder.API()
flask = Flask(__name__)
@flask.route('/')
def hello():
return 'hello'
api.mount('/flask', flask)
That’s it!
If you have a single-page webapp, you can tell Responder to serve up your static/index.html at a route, like
so:
api.add_route("/", static=True)
This will make index.html the default response to all undefined routes.
Responder makes it very easy to interact with cookies from a Request, or add some to a Response:
>>> req.cookies
{"hello": "world"}
Supported directives:
• key - Required
• value - [OPTIONAL] - Defaults to "".
• expires - Defaults to None.
• max_age - Defaults to None.
• domain - Defaults to None.
• path - Defaults to "/".
• secure - Defaults to False.
• httponly - Defaults to True.
For more information see directives
Responder has built-in support for cookie-based sessions. To enable cookie-based sessions, simply add something to
the resp.session dictionary:
A cookie called Responder-Session will be set, which contains all the data in resp.session. It is signed, for
verification purposes.
You can easily read a Request’s session data, that can be trusted to have originated from the API:
>>> req.session
{'username': 'kennethreitz'}
Note: if you are using this in production, you should pass the secret_key argument to API(...):
api = responder.API(secret_key=os.environ['SECRET_KEY'])
If you’d like a view to be executed before every request, simply do the following:
@api.route(before_request=True)
def prepare_response(req, resp):
resp.headers["X-Pizza"] = "42"
Now all requests to your HTTP Service will include an X-Pizza header.
For websockets:
@api.route(before_request=True, websocket=True)
def prepare_response(ws):
await ws.accept()
@api.route('/ws', websocket=True)
async def websocket(ws):
await ws.accept()
while True:
name = await ws.receive_text()
await ws.send_text(f"Hello {name}!")
await ws.close()
await websocket.accept()
await websocket.send_{format}(data)
await websocket.receive_{format}(data)
await websocket.close()
Responder comes with a first-class, well supported test client for your ASGI web services: Requests.
Here’s an example of a test (written with pytest):
import myapi
@pytest.fixture
def api():
return myapi.api
def test_response(api):
hello = "hello, world!"
@api.route('/some-url')
def some_view(req, resp):
resp.text = hello
r = api.requests.get(url=api.url_for(some_view))
assert r.text == hello
Boom.
3.2.14 CORS
Want CORS ?
api = responder.API(cors=True)
The default parameters used by Responder are restrictive by default, so you’ll need to explicitly enable particular
origins, methods, or headers, in order for browsers to be permitted to use them in a Cross-Domain context.
In order to set custom parameters, you need to set the cors_params argument of api, a dictionary containing the
following entries:
• allow_origins - A list of origins that should be permitted to make cross-origin requests. eg. ['https:/
/example.org', 'https://www.example.org']. You can use ['*'] to allow any origin.
• allow_origin_regex - A regex string to match against origins that should be permitted to make cross-
origin requests. eg. 'https://.*\.example\.org'.
• allow_methods - A list of HTTP methods that should be allowed for cross-origin requests. Defaults to
[‘GET’]. You can use ['*'] to allow all standard methods.
• allow_headers - A list of HTTP request headers that should be supported for cross-origin requests. Defaults
to []. You can use ['*'] to allow all headers. The Accept, Accept-Language, Content-Language
and Content-Type headers are always allowed for CORS requests.
• allow_credentials - Indicate that cookies should be supported for cross-origin requests. Defaults to
False.
• expose_headers - Indicate any response headers that should be made accessible to the browser. Defaults to
[].
• max_age - Sets a maximum time in seconds for browsers to cache CORS responses. Defaults to 60.
Make sure that all the incoming requests headers have a valid host, that matches one of the provided patterns in the
allowed_hosts attribute, in order to prevent HTTP Host Header attacks.
A 400 response will be raised, if a request does not match any of the provided patterns in the allowed_hosts
attribute.
api = responder.API(allowed_hosts=[example.com, tenant.example.com])
You can deploy Responder anywhere you can deploy a basic Python application.
FROM kennethreitz/pipenv
ENV PORT '80'
COPY . /app
CMD python3 api.py
EXPOSE 80
That’s it!
The basics:
$ mkdir my-api
$ cd my-api
$ git init
$ heroku create
...
Install Responder:
import responder
api = responder.API()
@api.route("/")
async def hello(req, resp):
resp.text = "hello, world!"
if __name__ == "__main__":
api.run()
$ git add -A
$ git commit -m 'initial commit'
$ git push heroku master
Responder comes with a first-class, well supported test client for your ASGI web services: Requests.
Here, we’ll go over the basics of setting up a proper Python package and adding testing to it.
$ cat api.py:
import responder
api = responder.API()
@api.route("/")
def hello_world(req, resp):
resp.text = "hello, world!"
if __name__ == "__main__":
api.run()
$ cat Pipfile:
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
responder = "*"
[dev-packages]
pytest = "*"
[requires]
python_version = "3.7"
[pipenv]
allow_prereleases = true
$ cat test_api.py:
import pytest
import api as service
@pytest.fixture
def api():
return service.api
(continues on next page)
def test_hello_world(api):
r = api.requests.get("/")
assert r.text == "hello, world!"
$ pytest:
...
========================== 1 passed in 0.10 seconds ==========================
Optionally, you can not rely on relative imports, and instead install your api as a proper package. This requires:
1. A proper setup.py file.
2. $ pipenv install -e . --dev
This will allow you to only specify your dependencies once: in setup.py. $ pipenv lock will automatically
lock your transitive dependencies (e.g. Responder), even if it’s not specified in the Pipfile.
This will ensure that your application gets installed in every developer’s environment, using Pipenv.
• terms_of_service – A URL to the Terms of Service for the API (OpenAPI Info Ob-
ject)
• contact – The contact dictionary of the application (OpenAPI Contact Object)
• license – The license information of the exposed API (OpenAPI License Object)
add_event_handler(event_type, handler)
Adds an event handler to the API.
Parameters
• event_type – A string in (“startup”, “shutdown”)
• handler – The function to run. Can be either a function or a coroutine.
add_route(route=None, endpoint=None, *, default=False, static=False, check_existing=True, web-
socket=False, before_request=False)
Adds a route to the API.
Parameters
• route – A string representation of the route.
• endpoint – The endpoint for the route – can be a callable, or a class.
• default – If True, all unknown requests will route to this view.
• static – If True, and no endpoint was passed, render “static/index.html”, and it will
become a default route.
• check_existing – If True, an AssertionError will be raised, if the route is already
defined.
add_schema(name, schema, check_existing=True)
Adds a mashmallow schema to the API specification.
mount(route, app)
Mounts an WSGI / ASGI application at a given route.
Parameters
• route – String representation of the route to be used (shouldn’t be parameterized).
• app – The other WSGI / ASGI app.
on_event(event_type: str, **args)
Decorator for registering functions or coroutines to run at certain events Supported events: startup, cleanup,
shutdown, tick
Usage:
@api.on_event('startup')
async def open_database_connection_pool():
...
@api.on_event('tick', seconds=10)
async def do_stuff():
...
@api.on_event('cleanup')
async def close_database_connection_pool():
...
path_matches_route(path)
Given a path portion of a URL, tests that it matches against any registered route.
Parameters path – The path portion of a URL, to test all known routes against.
redirect(resp, location, *, set_text=True, status_code=301)
Redirects a given response to a given location.
Parameters
• resp – The Response to mutate.
• location – The location of the redirect.
• set_text – If True, sets the Redirect body content automatically.
• status_code – an API.status_codes attribute, or an integer, representing the HTTP
status code of the redirect.
requests = None
A Requests session that is connected to the ASGI app.
route(route=None, **options)
Decorator for creating new routes around function and class definitions.
Usage:
@api.route("/hello")
def hello(req, resp):
resp.text = "hello, world!"
schema(name, **options)
Decorator for creating new routes around function and class definitions.
Usage:
@api.schema("Pet")
class PetSchema(Schema):
name = fields.Str()
template(name_, **values)
Renders the given jinja2 template, with provided values supplied.
Note: The current api instance is by default passed into the view. This is set in the dict api.
jinja_values_base.
Parameters
• name – The filename of the jinja2 template, in templates_dir.
• values – Data to pass into the template.
template_string(s_, **values)
Renders the given jinja2 template string, with provided values supplied.
Note: The current api instance is by default passed into the view. This is set in the dict api.
jinja_values_base.
Parameters
• s – The template to use.
• values – Data to pass into the template.
url_for(endpoint, **params)
Given an endpoint, returns a rendered URL for its route.
Parameters
• endpoint – The route endpoint you’re searching for.
• params – Data to pass into the URL generator (for parameterized URLs).
accepts(content_type)
Returns True if the incoming Request accepts the given content_type.
apparent_encoding
The apparent encoding, provided by the chardet library. Must be awaited.
content
The Request body, as bytes. Must be awaited.
cookies
The cookies sent in the Request, as a dictionary.
encoding
The encoding of the Request’s body. Can be set, manually. Must be awaited.
full_url
The full URL of the Request, query parameters and all.
headers
A case-insensitive dictionary, containing all headers sent in the Request.
media(format=None)
Renders incoming json/yaml/form data as Python objects. Must be awaited.
Parameters format – The name of the format being used. Alternatively accepts a custom
callable for the format type.
method
The incoming HTTP method used for the request, lower-cased.
params
A dictionary of the parsed query parameters used for the Request.
session
The session data, in dict form, from the Request.
text
The Request body, as unicode. Must be awaited.
url
The parsed URL of the Request.
class responder.Response(req, *, formats)
content
A bytes representation of the response body.
cookies
The cookies set in the Response
headers
A Python dictionary of {key: value}, representing the headers of the response.
media
A Python object that will be content-negotiated and sent back to the client. Typically, in JSON formatting.
session
The cookie-based session data, in dict form, to add to the Response.
status_code
The HTTP Status Code to use for the Response.
responder.API.status_codes.is_100(status_code)
responder.API.status_codes.is_200(status_code)
responder.API.status_codes.is_300(status_code)
responder.API.status_codes.is_400(status_code)
responder.API.status_codes.is_500(status_code)
Installing Responder
23
responder Documentation, Release 1.3.0
The primary concept here is to bring the niceties that are brought forth from both Flask and Falcon and unify them
into a single framework, along with some new ideas I have. I also wanted to take some of the API primitives that
are instilled in the Requests library and put them into a web framework. So, you’ll find a lot of parallels here with
Requests.
• Setting resp.content sends back bytes.
• Setting resp.text sends back unicode, while setting resp.html sends back HTML.
• Setting resp.media sends back JSON/YAML (.text/.html/.content override this).
• Case-insensitive req.headers dict (from Requests directly).
• resp.status_code, req.method, req.url, and other familiar friends.
25
responder Documentation, Release 1.3.0
Ideas
• Flask-style route expression, with new capabilities – all while using Python 3.6+’s new f-string syntax.
• I love Falcon’s “every request and response is passed into each view and mutated” methodology, especially
response.media, and have used it here. In addition to supporting JSON, I have decided to support YAML
as well, as Kubernetes is slowly taking over the world, and it uses YAML for all the things. Content-negotiation
and all that.
• A built in testing client that uses the actual Requests you know and love.
• The ability to mount other WSGI apps easily.
• Automatic gzipped-responses.
• In addition to Falcon’s on_get, on_post, etc methods, Responder features an on_request method, which
gets called on every type of request, much like Requests.
• A production static files server is built-in.
• Uvicorn built-in as a production web server. I would have chosen Gunicorn, but it doesn’t run on Windows.
Plus, Uvicorn serves well to protect against slowloris attacks, making nginx unnecessary in production.
• GraphQL support, via Graphene. The goal here is to have any GraphQL query exposable at any route, magically.
27
responder Documentation, Release 1.3.0
28 Chapter 6. Ideas
CHAPTER 7
• genindex
• modindex
• search
29
responder Documentation, Release 1.3.0
r
responder, 18
31
responder Documentation, Release 1.3.0
A P
accepts() (responder.Request method), 21 params (responder.Request attribute), 22
add_event_handler() (responder.API method), 19 path_matches_route() (responder.API method), 19
add_route() (responder.API method), 19
add_schema() (responder.API method), 19 R
API (class in responder), 18 redirect() (responder.API method), 20
apparent_encoding (responder.Request attribute), 21 Request (class in responder), 21
requests (responder.API attribute), 20
C responder (module), 18
content (responder.Request attribute), 21 Response (class in responder), 22
content (responder.Response attribute), 22 route() (responder.API method), 20
cookies (responder.Request attribute), 21
cookies (responder.Response attribute), 22 S
schema() (responder.API method), 20
E serve() (responder.API method), 20
encoding (responder.Request attribute), 21 session (responder.Request attribute), 22
session (responder.Response attribute), 22
F session() (responder.API method), 20
full_url (responder.Request attribute), 21 static_url() (responder.API method), 20
status_code (responder.Response attribute), 22
H
headers (responder.Request attribute), 21 T
headers (responder.Response attribute), 22 template() (responder.API method), 20
template_string() (responder.API method), 21
I text (responder.Request attribute), 22
is_100() (in module responder.API.status_codes), 22
is_200() (in module responder.API.status_codes), 22 U
is_300() (in module responder.API.status_codes), 22 url (responder.Request attribute), 22
is_400() (in module responder.API.status_codes), 22 url_for() (responder.API method), 21
is_500() (in module responder.API.status_codes), 22
M
media (responder.Response attribute), 22
media() (responder.Request method), 21
method (responder.Request attribute), 21
mount() (responder.API method), 19
O
on_event() (responder.API method), 19
33