Skip to main content

A modern Python web framework filled with asynchronous salsa.

Project description

A modern Python web framework filled with asynchronous salsa.


travis python pypi license

Bocadillo

Inspired by Responder, Bocadillo is a web framework that combines ideas from Falcon and Flask while leveraging modern Python async capabilities.

Under the hood, it uses the Starlette ASGI toolkit and the uvicorn ASGI server.

Contents

Quick start

Write your first app:

# app.py
import bocadillo

api = bocadillo.API()

@api.route('/add/{x:d}/{y:d}')
async def add(req, resp, x: int, y: int):
    resp.media = {'result': x + y}

if __name__ == '__main__':
    api.run()

Run it:

python app.py
INFO: Started server process [81910]
INFO: Waiting for application startup.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

Make some requests!

curl "http://localhost:8000/add/1/2"
{"result": 3}

🌯💥

Install

Bocadillo is available on PyPI:

pip install bocadillo

Usage

It all starts with an import:

import bocadillo
api = bocadillo.API()

API

The main object you'll manipulate in Bocadillo is the API object, an ASGI-compliant application.

Running a Bocadillo app

To run a Bocadillo app, either run the application directly:

# myapp.py
import bocadillo

api = bocadillo.API()

if __name__ == '__main__':
    api.run()
python myapp.py

Or give it to uvicorn (an ASGI server installed with Bocadillo):

uvicorn myapp:api

Configuring host and port

By default, Bocadillo serves your app on 127.0.0.1:8000, i.e. localhost on port 8000.

To customize the host and port, you can:

  • Specify them on app.run():
api.run(host='mydomain.org', port=5045)
  • Set the PORT environment variable. Bocadillo will pick it up and automatically use the host 0.0.0.0 to accept all existing hosts on the machine. This is especially useful when running the app in a container or on a cloud hosting service. If needed, you can still specify the host on app.run().

Debug mode

You can toggle debug mode (full display of traceback in responses + hot reload) by passing debug=True to app.run():

api.run(debug=True)

or passing the --debug flag to uvicorn:

uvicorn myapp:api --debug

Configuring allowed hosts

By default, a Bocadillo API can run on any host. To specify which hosts are allowed, use allowed_hosts:

api = bocadillo.API(allowed_hosts=['mysite.com'])

If a non-allowed host is used, all requests will return a 400 error.

Views

In Bocadillo, views are functions that take at least a request and a response as arguments, and mutate those objects as necessary.

Views can be asynchronous or synchronous, function-based or class-based.

Asynchronous views

The recommended way to define views in Bocadillo is using the async/await syntax. This allows you to call arbitrary async/await Python code:

from asyncio import sleep

async def find_post_content(slug: str):
    await sleep(1)  # perhaps query a database here?
    return 'My awesome post'

async def retrieve_post(req, res, slug: str):
    res.text = await find_post_content(slug)

Synchronous views

While Bocadillo is asynchronous at its core, you can also use plain Python functions to define synchronous views:

def index(req, res):
    res.html = '<h1>My website</h1>'

Note: it is generally more efficient to use asynchronous views rather than synchronous ones. This is because, when given a synchronous view, Bocadillo needs to perform a sync-to-async conversion, which might add extra overhead.

Class-based views

The previous examples were function-based views, but Bocadillo also supports class-based views.

Class-based views are regular Python classes (there is no base View class). Each HTTP method gets mapped to the corresponding method on the class. For example, GET gets mapped to .get(), POST gets mapped to .post(), etc.

Other than that, class-based view methods are just regular views:

class Index:

    async def get(self, req, res):
        res.text = 'Classes, oh my!'

    def post(self, req, res):
        res.text = 'Roger that'

A catch-all .handle() method can also be implemented to process all incoming requests — resulting in other methods being ignored.

class Index:

    async def handle(self, req, res):
        res.text = 'Post it, get it, put it, delete it.'

Routing

Route declaration

To declare and register a new route, use the @api.route() decorator:

@api.route('/')
async def index(req, res):
    res.text = 'Hello, Bocadillo!'

Route parameters

Bocadillo allows you to specify route parameters as named template literals in the route pattern (which uses the F-string syntax). Route parameters are passed as additional arguments to the view:

@api.route('/posts/{slug}')
async def retrieve_post(req, res, slug: str):
    res.text = 'My awesome post'

Route parameter validation

You can leverage F-string specifiers to add lightweight validation to routes:

# Only decimal integer values for `x` will be accepted
@api.route('/negation/{x:d}')
async def negate(req, res, x: int):
    res.media = {'result': -x}
curl "http://localhost:8000/negation/abc"
HTTP/1.1 404 Not Found
server: uvicorn
date: Wed, 07 Nov 2018 20:24:31 GMT
content-type: text/plain
transfer-encoding: chunked

Not Found

Named routes

You can specify a name for a route by passing name to @api.route():

@api.route('/about/{who}', name='about')
async def about(req, res, who):
    res.html = f'<h1>About {who}</h1>'

In code, you can get the full URL path to a route using api.url_for():

>>> api.url_for('about', who='them')
'/about/them'

In templates, you can use the url_for() global:

<h1>Hello, Bocadillo!</h1>
<p>
    <a href="{{ url_for('about', who='me') }}">About me</a>
</p>

Note: referencing to a non-existing named route with url_for() will return a 404 error page.

Specifying HTTP methods (function-based views only)

By default, a route accepts all HTTP methods. On function-based views, you can use the methods argument to @api.route() to specify the set of HTTP methods being exposed:

@api.route('/', methods=['get'])
async def index(req, res):
    res.text = "Come GET me, bro"

Note: the methods argument is ignored on class-based views. You should instead decide which methods are implemented on the class to control the exposition of HTTP methods.

Requests

Request objects in Bocadillo expose the same interface as the Starlette Request. Common usage is documented here.

Method

The HTTP method of the request is available at req.method.

curl -X POST "http://localhost:8000"
req.method  # 'POST'

URL

The full URL of the request is available as req.url:

curl "http://localhost:8000/foo/bar?add=sub"
req.url  # 'http://localhost:8000/foo/bar?add=sub'

It is in fact a string-like object that also exposes individual parts:

req.url.path  # '/foo/bar'
req.url.port  # 8000
req.url.scheme  # 'http'
req.url.hostname  # '127.0.0.1'
req.url.query  # 'add=sub'
req.url.is_secure  # False

Headers

Request headers are available at req.headers, an immutable, case-insensitive Python dictionary.

curl -H 'X-App: Bocadillo' "http://localhost:8000"
req.headers['x-app']  # 'Bocadillo'
req.headers['X-App']  # 'Bocadillo'

Query parameters

Query parameters are available at req.query_params, an immutable Python dictionary-like object.

curl "http://localhost:8000?add=1&sub=2&sub=3"
req.query_params['add']  # '1'
req.query_params['sub']  # '2'  (first item)
req.query_params.getlist('sub')  # ['2', '3']

Body

In Bocadillo, response body is always accessed using async/await. You can retrieve it through different means depending on the expected encoding:

  • Bytes : await req.body()
  • Form data: await req.form()
  • JSON: await req.json()
  • Stream (advanced usage): async for chunk in req.chunk(): ...

Responses

Bocadillo passes the request and the response object to each view, much like Falcon does. To send a response, the idiomatic process is to mutate the res object directly.

Sending content

Bocadillo has built-in support for common types of responses:

res.text = 'My awesome post'  # text/plain
res.html = '<h1>My awesome post</h1>'  # text/html
res.media = {'title': 'My awesome post'}  # application/json

Setting a response type attribute automatically sets the appropriate Content-Type, as depicted above.

If you need to send another content type, use .content and set the Content-Type header yourself:

res.content = 'h1 { color; gold; }'
res.headers['Content-Type'] = 'text/css'

Status codes

You can set the numeric status code on the response using res.status_code:

@api.route('/jobs', methods=['post'])
async def create_job(req, res):
    res.status_code = 201

Bocadillo does not provide an enum of HTTP status codes. If you prefer to use one, you'd be safe enough going for HTTPStatus, located in the standard library's http module.

from http import HTTPStatus
res.status_code = HTTPStatus.CREATED.value

Headers

You can access and modify a response's headers using res.headers, which is a standard Python dictionary object:

res.headers['Cache-Control'] = 'no-cache'

Redirections

Inside a view, you can redirect to another page using api.redirect(), which can be used in a few ways.

By route name

Use the name argument:

@api.route('/home', name='home')
async def home(req, res):
    res.text = f'This is home!'

@api.route('/')
async def index(req, res):
    api.redirect(name='home')

Note: route parameters can be passed as additional keyword arguments.

By URL

You can redirect by URL by passing url. The URL can be internal (path relative to the server's host) or external (absolute URL).

@api.route('/')
async def index(req, res):
    # internal:
    api.redirect(url='/home')
    # external:
    api.redirect(url='http://localhost:8000/home')

Permanent redirections

Redirections are temporary (302) by default. To return a permanent (301) redirection, pass permanent = True:

api.redirect(url='/home', permanent=True)

Templates

Bocadillo allows you to render Jinja2 templates. You get all the niceties of the Jinja2 template engine: a familiar templating language, automatic escaping, template inheritance, etc.

Rendering templates

You can render a template using await api.template():

async def post_detail(req, res):
    res.html = await api.template('index.html', title='Hello, Bocadillo!')

In synchronous views, use api.template_sync() instead:

def post_detail(req, res):
    res.html = api.template_sync('index.html', title='Hello, Bocadillo!')

Context variables can also be given as a dictionary:

await api.template('index.html', {'title': 'Hello, Bocadillo!'})

Lastly, you can render a template directly from a string:

>>> api.template_string('<h1>{{ title }}</h1>', title='Hello, Bocadillo!')
'<h1>Hello, Bocadillo!</h1>'

Templates location

By default, Bocadillo looks for templates in the templates/ folder relative to where the app is executed. For example:

.
├── app.py
└── templates
    └── index.html

You can change the template directory using the templates_dir option:

api = bocadillo.API(templates_dir='path/to/templates')

Static files

Bocadillo uses WhiteNoise to serve static assets for you in an efficient manner.

Basic usage

Place files in the static folder at the root location, and they will be available at the corresponding URL:

/* static/css/styles.css */
h1 { color: red; }
curl "http://localhost:8000/static/css/styles.css"
h1 { color: red; }

Static files location

By default, static assets are served at the /static/ URL root and are searched for in a static/ directory relative to where the app is executed. For example:

.
├── app.py
└── static
    └── css
        └── styles.css

You can modify the static files directory using the static_dir option:

api = bocadillo.API(static_dir='staticfiles')

To modify the root URL path, use static_root:

api = bocadillo.API(static_root='assets')

Extra static files directories

You can serve other static directories using app.mount() and the static helper:

import bocadillo

api = bocadillo.API()

# Serve more static files located in the assets/ directory
api.mount(prefix='assets', app=bocadillo.static('assets'))

Disabling static files

To prevent Bocadillo from serving static files altogether, you can use:

api = bocadillo.API(static_dir=None)

Error handling

Returning error responses

To return an error HTTP response, you can raise an HTTPError exception. Bocadillo will catch it and return an appropriate response:

from bocadillo.exceptions import HTTPError

@api.route('/fail/{status_code:d}')
def fail(req, res, status_code: int):
    raise HTTPError(status_code)
curl -SD - "http://localhost:8000/fail/403"
HTTP/1.1 403 Forbidden
server: uvicorn
date: Wed, 07 Nov 2018 19:55:56 GMT
content-type: text/plain
transfer-encoding: chunked

Forbidden

Customizing error handling

You can customize error handling by registering your own error handlers. This can be done using the @api.error_handler() decorator:

@api.error_handler(KeyError)
def on_key_error(req, res, exc: KeyError):
    res.status = 400
    res.text = f"You fool! We didn't find the key '{exc.args[0]}'."

For convenience, a non-decorator syntax is also available:

def on_attribute_error(req, res, exc: AttributeError):
    res.status = 500
    res.media = {'error': {'attribute_not_found': exc.args[0]}}

api.add_error_handler(AttributeError, on_attribute_error)

Middleware

This feature is experimental; the middleware API may be subject to changes.

Bocadillo provides a simple middleware architecture in the form of middleware classes.

Middleware classes provide behavior for the entire application. They act as an intermediate between the ASGI layer and the Bocadillo API object. In fact, they implement the ASGI protocol themselves.

Routing middleware

Routing middleware is a high-level API for performing operations before and after a request is routed to the Bocadillo application.

To define a custom routing middleware class, create a subclass of bocadillo.RoutingMiddleware and implement .before_dispatch() and .after_dispatch() as necessary:

import bocadillo

class PrintUrlMiddleware(bocadillo.RoutingMiddleware):

    def before_dispatch(self, req):
        print(req.url)

    def after_dispatch(self, req, res):
        print(res.url)

Note: the underlying application (which is either another routing middleware or the API object) is available on the .app attribute.

You can then register the middleware using add_middleware():

api = bocadillo.API()
api.add_middleware(PrintUrlMiddleware)

CORS

Bocadillo has built-in support for Cross-Origin Resource Sharing (CORS). Adding CORS headers to responses is typically required when your API is to be accessed by web browsers.

To enable CORS, simply use:

api = bocadillo.API(enable_cors=True)

Bocadillo has restrictive defaults to prevent security issues: empty Allow-Origins, only GET for Allow-Methods. To customize the CORS configuration, use cors_config, e.g.:

api = bocadillo.API(
    enable_cors=True,
    cors_config={
        'allow_origins': ['*'],
        'allow_methods': ['*'],
    }
)

Please refer to Starlette's CORSMiddleware documentation for the full list of options and defaults.

HSTS

If you want enable HTTP Strict Transport Security (HSTS) and redirect all HTTP traffic to HTTPS, simply use:

api = bocadillo.API(enable_hsts=True)

Testing

TODO

Deployment

TODO

Contributing

See CONTRIBUTING for contribution guidelines.

Changelog

See CHANGELOG for a chronological log of changes to Bocadillo.

Roadmap

If you are interested in the future features that may be implemented into Bocadillo, take a look at our milestones.

To see what has already been implemented for the next release, see the Unreleased section of our changelog.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bocadillo-0.4.0.tar.gz (24.3 kB view hashes)

Uploaded Source

Built Distribution

bocadillo-0.4.0-py3-none-any.whl (20.8 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page