A modern Python web framework filled with asynchronous salsa.
Project description
A modern Python web framework filled with asynchronous salsa.
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 host0.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 thehost
onapp.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'shttp
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file bocadillo-0.4.0.tar.gz
.
File metadata
- Download URL: bocadillo-0.4.0.tar.gz
- Upload date:
- Size: 24.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/1.12.1 pkginfo/1.4.2 requests/2.20.1 setuptools/40.5.0 requests-toolbelt/0.8.0 tqdm/4.28.1 CPython/3.7.1
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | f7573c0867894d5e34be78f82e7c2df17f09a6b4216fd04e702a6d414ec5a6c2 |
|
MD5 | a010faf662e86514faae1095b3645119 |
|
BLAKE2b-256 | 9789d6e850c8c0628c5a3fac9decd02ee5f07f8fd0bdc4fed45b91cfcbf80ba6 |
File details
Details for the file bocadillo-0.4.0-py3-none-any.whl
.
File metadata
- Download URL: bocadillo-0.4.0-py3-none-any.whl
- Upload date:
- Size: 20.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/1.12.1 pkginfo/1.4.2 requests/2.20.1 setuptools/40.5.0 requests-toolbelt/0.8.0 tqdm/4.28.1 CPython/3.7.1
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 02db4676d97ee07bade0b77feb38643dd46cb708ab956fc6105305d85b3f51d9 |
|
MD5 | 205e661515e26b2a9314f241d1b90bdd |
|
BLAKE2b-256 | cc6d7af5b4935790a84d401d131bc1b827d4a89f50b3e0acb88d36ac2e8650d0 |