Commit 08651c2b authored by David Larlet's avatar David Larlet

Merge branch 'documentation' into 'master'

️ First iteration on documentation

See merge request !25
parents b728d249 1b2d3df0
Pipeline #23342 passed with stages
in 2 minutes and 24 seconds
# Roll changelog
A changelog:
* is breaking-change-oriented
* links to related issues
* is concise
*Analogy: git blame :p*
## In progress
* Add documentation.
* **Breaking change**:
order of parameters in events is always `request`, `response` and
optionnaly `error` if any, in that particular order.
## 0.4.0 — 2017-09-21
* Switch routes from kua to autoroutes for performances.
* **Breaking change**:
routes placeholder syntax changed from `:parameter` to `{parameter}`
## 0.3.0 — 2017-09-21
* Improve benchmarks and overall performances.
* **Breaking change**:
`cors` extension parameter is no longer `value` but `origin`
## 0.2.0 — 2017-08-25
* Resolve HTTP status only at response write time.
## 0.1.1 — 2017-08-25
* Fix `setup.py`.
## 0.1.0 — 2017-08-25
* First release
# Discussions
A discussion:
* is understanding-oriented
* explains
* provides background and context
*Analogy: an article on culinary social history*
## Why Roll?
It all started with big waves (hence the name ;-)) in a little retreat
with a new project that required good performances.
We tried Sanic but were not satisfied with synchronuous tests and did
not understand why such a complexity so we assembled a few classes and
Roll was born!
Lately we realized that it has pretty good preformances *for our usage*
and decided to add some benchmarks and then iterate to better understand
bottlenecks and strategies to tackle these. It was fun and not
ridiculous at the end so we gave some love to the documentation and
here we are.
# How-to guides
A how-to guide:
* is goal-oriented
* shows how to solve a specific problem
* is a series of steps
*Analogy: a recipe in a cookery book*
## How to install Roll
You can install Roll through [pip](https://pip.pypa.io/en/stable/):
pip install roll
It is recommended to install it within
[a pipenv or virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/) though.
## How to create an extension
You can use extensions to achieve a lot of enhancements of the base
framework.
Basically, an extension is a function listening to
[events](reference.md#events), for instance:
```python
def cors(app, value='*'):
@app.listen('response')
async def add_cors_headers(response, request):
response.headers['Access-Control-Allow-Origin'] = value
```
Here the `cors` extension can be applied to the Roll `app` object.
It listens to the `response` event and for each of those add a custom
header. The name of the inner function is not relevant but explicit is
always a bonus. The `response` object is modified in place.
*Note: more [extensions](reference.md#events) are available by default.
Make sure to check these out!*
## How to return an HTTP error
There are many reasons to return an HTTP error, with Roll you have to
raise an HttpError instance. Remember our
[base example from tutorial](tutorials.md#your-first-roll-application)?
What if we want to return an error to the user:
```python
from http import HTTPStatus
from roll import Roll, HttpError
from roll.extensions import simple_server
app = Roll()
@app.route('/hello/{parameter}')
async def hello(request, response, parameter):
if parameter == 'foo':
raise HttpError(HTTPStatus.BAD_REQUEST, 'Run, you foo(l)!')
response.body = f'Hello {parameter}'
if __name__ == '__main__':
simple_server(app)
```
Now when we try to reach the view with the `foo` parameter:
```
$ http :3579/hello/foo
HTTP/1.1 400 Bad Request
Content-Length: 16
Run, you foo(l)!
```
One advantage of using the exception mechanism is that you can raise an
HttpError from anywhere and let Roll handle it!
## How to return JSON content
There is a shortcut to return JSON content from a view. Remember our
[base example from tutorial](tutorials.md#your-first-roll-application)?
```python
from roll import Roll
from roll.extensions import simple_server
app = Roll()
@app.route('/hello/{parameter}')
async def hello(request, response, parameter):
response.json = {'hello': parameter}
if __name__ == '__main__':
simple_server(app)
```
Setting a `dict` to `response.json` will automagically dump it to
regular JSON and set the appropriated content type:
```
$ http :3579/hello/world
HTTP/1.1 200 OK
Content-Length: 17
Content-Type: application/json
{
"hello": "world"
}
```
Especially useful for APIs.
## How to subclass Roll itself
Let’s say you want your own [Query](reference.md#core-objects) parser
to deal with GET parameters that should be converted as `datetime.date`
objects.
What you can do is subclass both the Roll class and the Protocol one
to set your custom Query class:
```python
from datetime import date
from roll import Roll, Query
from roll.extensions import simple_server
class MyQuery(Query):
@property
def date(self):
return date(int(self.get('year')),
int(self.get('month')),
int(self.get('day')))
class MyProtocol(Roll.Protocol):
Query = MyQuery
class MyRoll(Roll):
Protocol = MyProtocol
app = MyRoll()
@app.route('/hello/')
async def hello(request, response):
response.body = request.query.date.isoformat()
if __name__ == '__main__':
simple_server(app)
```
And now when you pass appropriated parameters (for the sake of brievety,
no error handling is performed but hopefully you get the point!):
```
$ http :3579/hello/ year==2017 month==9 day==20
HTTP/1.1 200 OK
Content-Length: 10
2017-09-20
```
## How to deploy Roll into production
The recommended way to deploy Roll is using
[Gunicorn](http://docs.gunicorn.org/).
First install gunicorn in your virtualenv:
pip install gunicorn
To run it, you need to pass it the pythonpath to your roll project
application. For example, if you have created a module `core.py`
in your package `mypackage`, where you create your application
with `app = Roll()`, then you need to issue this command line:
gunicorn mypackage.core:app --worker roll.worker.Worker
See [gunicorn documentation](http://docs.gunicorn.org/en/stable/settings.html)
for more details about the available arguments.
# Let’s Roll!
The documentation structure is based on that
[excellent article from Divio](https://www.divio.com/en/blog/documentation/).
Please provide us any feedback as an early reader!
## [Tutorials](tutorials.md)
A tutorial:
* is learning-oriented
* allows the newcomer to get started
* is a lesson
*Analogy: teaching a small child how to cook*
* [Your first Roll application](tutorials.md#your-first-roll-application)
* [Your first Roll test](tutorials.md#your-first-roll-test)
* [Using extensions](tutorials.md#using-extensions)
* [Using events](tutorials.md#using-events)
## [How-to guides](how-to-guides.md)
A how-to guide:
* is goal-oriented
* shows how to solve a specific problem
* is a series of steps
*Analogy: a recipe in a cookery book*
* [How to install Roll](how-to-guides.md#how-to-install-roll)
* [How to create an extension](how-to-guides.md#how-to-create-an-extension)
* [How to return an HTTP error](how-to-guides.md#how-to-return-an-http-error)
* [How to return JSON content](how-to-guides.md#how-to-return-json-content)
* [How to subclass Roll itself](how-to-guides.md#how-to-subclass-roll-itself)
* [How to deploy Roll into production](how-to-guides.md#how-to-deploy-roll-into-production)
## [Discussions](discussions.md)
A discussion:
* is understanding-oriented
* explains
* provides background and context
*Analogy: an article on culinary social history*
* [Why Roll?](discussions.md#why-roll)
## [Reference](reference.md)
A reference guide:
* is information-oriented
* describes the machinery
* is accurate and complete
*Analogy: a reference encyclopaedia article*
* [Core objects](reference.md#core-objects)
* [Extensions](reference.md#extensions)
* [Events](reference.md#events)
## [Changelog](changelog.md)
A changelog:
* is breaking-change-oriented
* links to related issues
* is concise
*Analogy: git blame :p*
# Reference
A reference guide:
* is information-oriented
* describes the machinery
* is accurate and complete
*Analogy: a reference encyclopaedia article*
## Core objects
### `Roll`
Roll provides an asyncio protocol.
You can subclass it to set your own `Protocol` or `Routes` class.
### `HttpError`
The object to raise when an error must be returned.
Accepts a `status` and a `message`.
The `status` can be either a `http.HTTPStatus` instance or an integer.
### `Request`
A container for the result of the parsing on each request made by
`httptools.HttpRequestParser`.
### `Response`
A container for `status`, `headers` and `body`.
*Note: there is a shortcut to set JSON content as a dict (`json`).*
### `Query`
Handy parsing of GET HTTP parameters (`get`, `list`, `bool`, `int`,
`float`).
### `Protocol`
You can subclass it to set your own `Query`, `Request` or `Response`
classes.
### `Routes`
Responsible for URL-pattern matching. Allows to switch to your own
parser. Default routes use [autoroutes](https://github.com/pyrates/autoroutes),
please refers to that documentation for available patterns.
## Extensions
Please read
[How to create an extension](how-to-guides.md#how-to-create-an-extension)
for usage.
### `cors`
Add [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS)-related headers. Especially useful for APIs.
You can set the `Access-Control-Allow-Origin` header with the `origin`
parameter and the `Access-Control-Allow-Methods` header with the
`methods` parameter (list).
### `logger`
Log each and every request by default. You can pass a `level` parameter
and a `handler`. Both are classic `logging` module objects.
### `options`
Performant return in case of `OPTIONS` HTTP request.
Combine it with the `cors` extension to handle the preflight request.
### `traceback`
Print the traceback on the server side if any. Handy for debugging.
### `igniter`
Display a BIG message when running the server.
Quite useless, hence so essential!
### `simple_server`
Special extension that does not rely on the events’ mechanism.
Launch a local server on `127.0.0.1:3579` by default. `port` and `host`
parameters allow you to customize. The `quiet` parameter does not
display any message on startup.
## Events
Please read
[How to create an extension](how-to-guides.md#how-to-create-an-extension)
for usage.
### `startup`
Fired once when launching the server.
### `shutdown`
Fired once when shutting down the server.
### `request`
Fired at each request before any dispatching/route matching.
Receives `request` and `response` parameters.
Returning `True` allows to shortcut everything and return the current
response object directly, see the [options extension](#extensions) for
an example.
### `response`
Fired at each request after all processing.
Receives `request` and `response` parameters.
### `error`
Fired in case of error, can be at each request.
Use it to customize HTTP error formatting for instance.
Receives `request`, `response` and `error` parameters.
# Tutorials
A tutorial:
* is learning-oriented
* allows the newcomer to get started
* is a lesson
*Analogy: teaching a small child how to cook*
## Your first Roll application
Make sure you [installed Roll first](how-to-guides.md#how-to-install-roll).
The tinyest application you can make is this one:
```python
from roll import Roll
from roll.extensions import simple_server
app = Roll()
@app.route('/hello/{parameter}')
async def hello(request, response, parameter):
response.body = f'Hello {parameter}'
if __name__ == '__main__':
simple_server(app)
```
Roll provides an asyncio protocol dealing with routes, requests and
responses. Everything else is done via extensions. Default routing is
done by [autoroutes](https://github.com/pyrates/autoroutes).
*Note: if you are not familiar with that `f''` thing, it is Python 3.6
shortcut for `.format()`.*
To launch that application, run it with `python yourfile.py`. You should
be able to perform HTTP requests against it:
```
$ curl localhost:3579/hello/world
Hello world
```
*Note: [HTTPie](https://httpie.org/) is definitely a nicer replacement
for curl so we will use it from now on. You can `pip install` it too.*
```
$ http :3579/hello/world
HTTP/1.1 200 OK
Content-Length: 11
Hello world
```
That’s it! Celebrate that first step and… wait!
We need to test that view before :-).
## Your first Roll test
First install `pytest` and `pytest-asyncio`.
Then create a `tests.py` file and copy-paste:
```python
from http import HTTPStatus
import pytest
from yourfile import app as app_
pytestmark = pytest.mark.asyncio
@pytest.fixture(scope='function')
def app():
return app_
async def test_hello_view(client, app):
resp = await client.get('/hello/world')
assert resp.status == HTTPStatus.OK
assert resp.body == 'Hello world'
```
You will have to adapt the import of your `app` given the filename
you gave during the previous part of the tutorial.
Once it’s done, you can launch `py.test tests.py`.
*Note: in case the `client` fixture is not found, you probably did not
install `Roll` correctly.*
## Using extensions
There are a couple of extensions available to “enrich” your application.
These extensions have to be applied to your Roll app, for instance:
```python
from roll import Roll
from roll.extensions import logger, simple_server
app = Roll()
logger(app) # <- This is the only change we made! (+ import)
@app.route('/hello/{parameter}')
async def hello(request, response, parameter):
response.body = f'Hello {parameter}'
if __name__ == '__main__':
simple_server(app)
```
Once you had that `logger` extension, each and every request will be
logged on your server-side. Try it by yourself!
Relaunch the server `$ python yourfile.py` and perform a new request with
httpie: `$ http :3579/hello/world`. On your server terminal you should
have something like that:
```
python yourfile.py
Rolling on http://127.0.0.1:3579
GET /hello/world
```
Notice the `GET` line, if you perform another HTTP request, a new line
will appear. Quite handy for debugging!
Another extension is very useful for debugging: `traceback`. Try to add
it by yourself and raise any error *within* your view to see it in
application (do not forget to restart your server!).
See the [reference documentation](reference.md#extensions) for all
built-in extensions.
## Using events
Last but not least, you can directly use registered events to alter the
behaviour of Roll at runtime.
*Note: this is how extensions are working internally.*
Let’s say you want to display a custom message when you launch your
server:
```python
from roll import Roll
from roll.extensions import simple_server
app = Roll()
@app.route('/hello/{parameter}')
async def hello(request, response, parameter):
response.body = f'Hello {parameter}'
@app.listen('startup') # <- This is the part we added (3 lines)
async def on_startup():
print('Example message')
if __name__ == '__main__':
simple_server(app)
```
Now restart your server and you should see the message printed.
Wonderful.
See the [reference documentation](reference.md#events) for all available
events.
from http import HTTPStatus
import pytest
from . import app as app_
pytestmark = pytest.mark.asyncio
@pytest.fixture(scope='function')
def app():
return app_
async def test_hello_view(client, app):
resp = await client.get('/hello/world')
assert resp.status == HTTPStatus.OK
assert resp.body == 'Hello world'
site_name: Roll
pages:
- Home: index.md
- Tutorials: tutorials.md
- How-to guides: how-to-guides.md
- Discussions: discussions.md
- Reference: reference.md
- Changelog: changelog.md
theme: readthedocs
mkdocs==0.16.3
pytest==3.1.2
pytest-asyncio==0.6.0
......@@ -70,10 +70,49 @@ class Query(dict):
"Key '{}' must be castable to float".format(key))
class Request:
__slots__ = ('url', 'path', 'query_string', 'query', 'method', 'kwargs',
'body', 'headers')
def __init__(self):
self.kwargs = {}
self.headers = {}
self.body = b''
class Response:
__slots__ = ('_status', 'headers', 'body')
def __init__(self):
self._status = None
self.body = b''
self.status = HTTPStatus.OK
self.headers = {}
@property
def status(self):
return self._status
@status.setter
def status(self, code):
# Allow to pass either the HttpStatus or the numeric value.
self._status = HTTPStatus(code)
def json(self, value):
self.headers['Content-Type'] = 'application/json'
self.body = json.dumps(value)
json = property(None, json)
class Protocol(asyncio.Protocol):
__slots__ = ('app', 'req', 'parser', 'resp', 'writer')
Query = Query
Request = Request
Response = Response
def __init__(self, app):
self.app = app
......@@ -85,9 +124,9 @@ class Protocol(asyncio.Protocol):
except HttpParserError:
# If the parsing failed before on_message_begin, we don't have a
# response.
self.resp = Response()
self.resp.status = HTTPStatus.BAD_