Python Rest Framework
Python Rest Framework
Documentation
Release 0.1
Yohann Gabory
i
ii
Python Rest Api Framework Documentation, Release 0.1
Python REST API framework is a set of utilities based on werkzeug to easily build Restful API with a MVC pattern.
Main features includes: Pagination, Authentication, Authorization, Filters, Partials Response, Error handling, data
validators, data formaters... and more...
Contents:
Contents 1
Python Rest Api Framework Documentation, Release 0.1
2 Contents
CHAPTER 1
Python REST API framework is a set of utilities based on werkzeug to easily build Restful API. It keep a clean
codebase and is easy to configure and extends.
It does not decide how you want to render your data, or where they lives, or other decisions.
Instead it give you a good start with an extensible architecture to build your own API.
Python REST API Framework has not been create for the usual case. Instead it give you some hook to your very
special ressource provider, your very special view management and your very special way of displaying data.
Python REST API Framework is fully REST compilant; It implement the common verbs:
• GET
• POST
• UPDATE
• DELETE
• HEAD
It also implement:
• PAGINATION
• AUTHENTICATION
• RATE-LIMIT
• DATA VALIDATION
• PARTIAL RESPONSE
Architecture
Python REST API Framework is base on the MVC pattern. You define some endpoints defining a Ressource, a
Controller and a View with a set of options to configure them.
3
Python Rest Api Framework Documentation, Release 0.1
Controller
Manage the way you handle request. Controller create the urls endpoints for you. List, Unique and Autodocumented
endpoints.
Controller also manage pagination, formaters, authentication, authorization, rate-limit and allowed method.
DataStore
Each method of a Controller call the DataStore to interact with data. The DataStore must be able to retreive data from
a ressource.
Each datastore act on a particular type of ressource (database backend, api backend, csv backend etc...). It must be
able to validate data, create new ressources, update existing ressources, manage filters and pagination.
Optional configuration option, that can be unique for a particular datastore like Ressource level validation (unique
together and so), ForeignKey management...
View
Views defines how the data must be send to the client. It send a Response object and set the needed headers, mime-type
and other presentation options like formaters.
How To use it
To create as many endpoint as you need. Each endpoints defining a ressource, a controller and a view. Then add them
to the rest_api_framework.controllers.WSGIDispatcher
See QuickStart for an example or the Tutorial: building an adressebook API for the whole picture.
QuickStart
A Simple API
For this example, we will use a python list containing dicts. This is our data:
ressources = [
{"name": "bob",
"age": a,
"id": a
} for a in range(100)
]
Then we have to describe this ressource. To describe a ressouce, you must create a Model class inheriting from base
Model class:
from rest_api_framework import models
class ApiModel(models.Model):
models.PkField(name="id")
]
Each Field contain validators. When you reuse an existing Field class you get his validators for free.
There is already a datastore to handle this type of data: PythonListDataStore. We can reuse this store:
class ApiApp(Controller):
ressource = {
"ressource_name": "address",
"ressource": ressources,
"model": ApiModel,
"datastore": PythonListDataStore
}
controller = {
"list_verbs": ["GET", "POST"],
"unique_verbs": ["GET", "PUT", "DELETE"],
}
Ressource
Ressource define your data. Where are your data ? How can they be accessed ? What they look likes?
• ressource_name: will be used to build the url endpoint to your ressource.
• ressource: where your ressource lies.this argument tell the datastore how they can be accessed. It can be the
database name and the database table for a SQL datastore or the url endpoint to a distant API for exemple.
• model: describe how your data look like. Wich field it show, how to validate data and so on.
• datastore: the type of your data. There is datastore for simple Python list of dict and SQLite datastore.
They are exemple on how to build your own datastore depending on your needs.
Controller
The controller define the way your data should be accessed. Should the results be paginated ? Authenticated ? Rate-
limited ? Wich it the verbs you can use on the resource ? and so on.
• list_verbs: define the verbs you can use on the main endpoint of your ressource. If you dont’ use “POST”,
a user cannot create new ressources on your datastore.
1.3. QuickStart 5
Python Rest Api Framework Documentation, Release 0.1
• unique_verbs: define the verbs you can use on the unique identifier of the ressource. actions depending on
the verbs follows the REST implementation: PUT to modify an existing ressource, DELETE to delete a
ressource.
View
view define How your ressoources should be rendered to the user. It can be a Json format, XML, or whatever. It can
also render pagination token, first page, last page, number of objects and other usefull informations for your users.
• response_class: the response class you use to render your data.
To test you application locally, you can add:
if __name__ == '__main__':
from werkzeug.serving import run_simple
from rest_api_framework.controllers import WSGIDispatcher
app = WSGIDispatcher([ApiApp])
run_simple('127.0.0.1', 5000, app, use_debugger=True, use_reloader=True)
Options
Each of this dicts can take an optional parameter: “option”. This parameter is a dict containing all the options you
want to use with either the datastore, the view or the controller.
You can learn more about optional parameters in the documentation of each topic : datastore, view, controller
Using a database
Instead of using a python dict, you may want to actualy save your data in a database. To do so, you just have to change
your datastore and define your ressources in a way SQL datastore can understand.
SQLiteDataStore use sqlite3 as database backend. ressources will be a dict with database name and table name. The
rest of the configuration is the same as with the PythonListDataStore.
Note: if the sqlite3 database does not exist, REST API Framework create it for you
class ApiModel(models.Model):
fields = [models.StringField(name="message", required=True),
models.StringField(name="user", required=True),
models.PkField(name="id", required=True),
]
class ApiApp(Controller):
ressource = {
"ressource_name": "tweets",
"ressource": {"name": "twitter.db", "table": "tweets"},
"datastore": SQLiteDataStore,
"model": ApiModel
}
controller = {
"list_verbs": ["GET", "POST"],
"unique_verbs": ["GET", "PUT", "DELETE"]
"options": {"pagination": Pagination(20)}
}
view = {"response_class": JsonResponse}
if __name__ == '__main__':
from werkzeug.serving import run_simple
from rest_api_framework.controllers import WSGIDispatcher
app = WSGIDispatcher([ApiApp])
run_simple('127.0.0.1', 5000, app, use_debugger=True, use_reloader=True
For this project we need users. Users will be helpfull for our adress book and for our authentication process.
Users will be define with at least a first name and a last name. We also need an unique identifier to retreive the user.
Note: For this tutorial the file yyou create will be named app.py To launch your application then just type in a
terminal:
python app.py
Define a model
class UserModel(models.Model):
The use of required_true will ensure that a user without this field cannot be created
Chose a DataStore
We also need a datastore to get a place where we can save our users. For instance we will use a sqlite3 database. The
SQLiteDataStore is what we need
9
Python Rest Api Framework Documentation, Release 0.1
Chose a view
We want results to be rendered as Json. We use the JsonResponse view for that:
from rest_api_framework.views import JsonResponse
To create an endpoint, we need a controller. This will manage our endpoint in a RESTFUL fashion.
from rest_api_framework.controllers import Controller
class UserEndPoint(Controller):
ressource = {
"ressource_name": "users",
"ressource": {"name": "adress_book.db", "table": "users"},
"model": UserModel,
"datastore": SQLiteDataStore
}
controller = {
"list_verbs": ["GET", "POST"],
"unique_verbs": ["GET", "PUT", "DELETE"]
}
Summary
class UserModel(models.Model):
class UserEndPoint(Controller):
ressource = {
"ressource_name": "users",
"ressource": {"name": "adress_book.db", "table": "users"},
"model": UserModel,
"datastore": SQLiteDataStore
}
controller = {
"list_verbs": ["GET", "POST"],
"unique_verbs": ["GET", "PUT", "DELETE"]
}
if __name__ == '__main__':
from werkzeug.serving import run_simple
from rest_api_framework.controllers import WSGIDispatcher
app = WSGIDispatcher([UserEndPoint])
run_simple('127.0.0.1', 5000, app, use_debugger=True, use_reloader=True)
python app.py
curl -i "http://localhost:5000/users/"
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 44
Server: Werkzeug/0.8.3 Python/2.7.2
Date: Tue, 15 Oct 2013 11:13:44 GMT
{
"meta": {
"filters": {}
},
"object_list": []
}
Your endpoint is responding but does not have any data. Let’s add some:
Create a user
If you look carfully at the response, you can see the header “Location” giving you the ressource uri of the ressource
you just created. This is usefull if you want to retreive your object. Let’s get a try:
curl -i "http://localhost:5000/users/1/"
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 51
Server: Werkzeug/0.8.3 Python/2.7.2
Date: Mon, 14 Oct 2013 16:53:19 GMT
{
"first_name": "John",
"id": 1,
"last_name": "Doe",
"ressource_uri": "/users/1/"
}
You can see that ressource_uri was not part of the ressource. It have been added by the View itself. View can add
multiple metadata, remove or change some fields and so on. More on that in Show data to users
The list of users is also updated:
curl -i "http://localhost:5000/users/"
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 83
Server: Werkzeug/0.8.3 Python/2.7.2
Date: Mon, 14 Oct 2013 17:03:00 GMT
{
"meta": {
"filters": {}
},
"object_list": [
{
"first_name": "John",
"id": 1,
"last_name": "Doe",
"ressource_uri": "/users/1/"
}
]
}
Delete a user
curl -i "http://localhost:5000/users/2/"
HTTP/1.0 404 NOT FOUND
Content-Type: application/json
Connection: close
Server: Werkzeug/0.8.3 Python/2.7.2
Date: Tue, 15 Oct 2013 11:16:33 GMT
curl -i "http://localhost:5000/users/"
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 125
Server: Werkzeug/0.8.3 Python/2.7.2
Date: Tue, 15 Oct 2013 11:17:46 GMT
{
"meta": {
"filters": {}
},
"object_list": [
{
"first_name": "John",
"id": 1,
"last_name": "Doe",
"ressource_uri": "/users/1/"
}
]
}
Update a User
But well everybody now that Steve Roger real name is Captain America. Let’s update this user:
Argh! Thats a typo. the fist name is “Captain”, not “Capitain”. Let’s correct this:
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 59
Server: Werkzeug/0.8.3 Python/2.7.2
Date: Mon, 14 Oct 2013 21:08:04 GMT
Filtering
curl -i "http://localhost:5000/users/?last_name=America"
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 236
Server: Werkzeug/0.8.3 Python/2.7.2
Date: Tue, 15 Oct 2013 12:07:21 GMT
curl -i "http://localhost:5000/users/?last_name=America&first_name=Joe"
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 171
Server: Werkzeug/0.8.3 Python/2.7.2
Date: Tue, 15 Oct 2013 12:09:32 GMT
Error handling
Of course, If data is not formated as expected by the API, the base error handling take place.
Missing data
If you don’t provide a last_name, the API will raise a BAD REQUEST explaining your error:
curl -i -H "Content-type: application/json" -X POST -d '{"first_name":"John"}' http:/
˓→/localhost:5000/users/
Invalid Data
however, there is no duplicate check. So you can create as many “John Doe” you want. This could be a huge problem
if your not able to validate uniqueness of a user. For the API, this is not a problem because each user is uniquely
identified by his id.
If you need to ensure it can be only one John Doe, you must add a validator on your datastore.
Autodocumentation
{
"users": {
"allowed list_verbs": [
"GET",
"POST"
],
"allowed unique ressource": [
"GET",
"PUT",
"DELETE"
],
"list_endpoint": "/users/",
"schema_endpoint": "/schema/users/"
}
}
{
"first_name": {
"example": "Hello World",
"required": "true",
"type": "string"
},
"last_name": {
"example": "Hello World",
"required": "true",
"type": "string"
}
}
In this exemple, you want to check that a user with the same last_name and same first_name does not exist in your
datastore before creating a new user.
For this you can use UniqueTogether:
UniqueTogether
class UserEndPoint(Controller):
ressource = {
"ressource_name": "users",
"ressource": {"name": "adress_book.db", "table": "users"},
"model": UserModel,
"datastore": SQLiteDataStore,
"options":{"validators": [UniqueTogether("first_name", "last_name")]}
}
controller = {
"list_verbs": ["GET", "POST"],
"unique_verbs": ["GET", "PUT", "DELETE"]
}
each of ressource, controller and views can have various options to add new functionality to them. The “validators”
option of ressource enable some datastore based validators. As you can see, validators are a list. This meen that you
can add many validators for a single datastore.
UniqueTogether will ensure that a user with first_name: John and last_name: Doe cannot be created.
Let’s try:
The view you have used so far just added a ressource_uri. But preserve the id attribut. As id is an internal representation
of the data you may wich to remove it.
To do so you’ll have to write a simple function to plug on the view. This function is a formater. When the View
instanciate the formater, it give you access to the response object and the object to be rendered.
Because you want to remove the id of the reprensentaion of your ressource, you can write:
add_ressource_uri is the default formatter for this View. You dont need to remove it for now. But if you try, then it
will work as expected. The ressource_uri field will be removed.
The idea behind Python REST API Framework is to always get out of your way.
You can check that it work as expected:
curl -i "http://localhost:5000/users/1/"
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 80
Server: Werkzeug/0.8.3 Python/2.7.2
Date: Mon, 14 Oct 2013 23:41:55 GMT
This implementation work on your endpoint because you each object has an id. But, if later you create another endpoint
with ressources lacking the “id” key, you’ll have to re-write your function.
Instead, you can take advantage of the response wich is part of the parameters of your function.
response object carry the attribut model who define your ressources fields. You can then get the name of the Pk field
used with this ressource with:
response.model.pk_field.name
Creating fixtures
When your address book will be full of entry, you will need to add a pagination on your API. As it is a common need,
REST API Framework implement a very easy way of doing so.
Before you can play with the pagination process, you will need to create more data. You can create those records the
way you want:
• direct insert into the database
sqlite3 adress_book.db
INSERT INTO users VALUES ("Nick", "Furry", 6);
each on of those methods have advantages and disavantages but they all make the work done. For this example, I
propose to use the well know requests package with a script to create a bunch of random records:
For this to work you need to install resquests : http://docs.python-requests.org/en/latest/user/install/#install
import json
import requests
import random
import string
def get_random():
return ''.join(
random.choice(
string.ascii_letters) for x in range(
int(random.random() * 20)
)
)
for i in range(200):
requests.post("http://localhost:5000/users/", data=json.dumps({"first_name": get_
˓→random(), "last_name": get_random()}))
Pagination
Now your datastore is filled with more than 200 records, it’s time to paginate. To do so import Pagination and change
the controller part of your app.
class UserEndPoint(Controller):
ressource = {
"ressource_name": "users",
"ressource": {"name": "adress_book.db", "table": "users"},
"model": UserModel,
"datastore": SQLiteDataStore,
"options": {"validators": [UniqueTogether("first_name", "last_name")]}
}
controller = {
"list_verbs": ["GET", "POST"],
"unique_verbs": ["GET", "PUT", "DELETE"],
"options": {"pagination": Pagination(20)}
}
curl -i "http://localhost:5000/users/"
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 1811
Server: Werkzeug/0.8.3 Python/2.7.2
Date: Tue, 15 Oct 2013 11:32:55 GMT
{
"meta": {
"count": 20,
"filters": {},
"next": "?offset=20",
"offset": 0,
"previous": "null",
"total_count": 802
},
"object_list": [
{
"first_name": "Captain",
"last_name": "America",
"ressource_uri": "/users/1/"
},
{
"first_name": "Captain",
"last_name": "America",
"ressource_uri": "/users/3/"
},
{
"first_name": "John",
"last_name": "Doe",
"ressource_uri": "/users/4/"
},
{
"first_name": "arRFOSYZT",
"last_name": "",
"ressource_uri": "/users/5/"
},
{
"first_name": "iUJsYORMuYeMUDy",
"last_name": "TqFpmcBQD",
"ressource_uri": "/users/6/"
},
{
"first_name": "EU",
"last_name": "FMSAbcUJBSBDPaF",
"ressource_uri": "/users/7/"
},
{
"first_name": "mWAwamrMQARXW",
"last_name": "yMNpEnYOPzY",
"ressource_uri": "/users/8/"
},
{
"first_name": "y",
"last_name": "yNiKP",
"ressource_uri": "/users/9/"
},
{
"first_name": "s",
"last_name": "TRT",
"ressource_uri": "/users/10/"
},
{
"first_name": "",
"last_name": "zFUaBd",
"ressource_uri": "/users/11/"
},
{
"first_name": "WA",
"last_name": "priJ",
"ressource_uri": "/users/12/"
},
{
"first_name": "XvpLttDqFmR",
"last_name": "liU",
"ressource_uri": "/users/13/"
},
{
"first_name": "ZhJqTgYoEUzmcN",
"last_name": "KKDqHJwJMxPSaTX",
"ressource_uri": "/users/14/"
},
{
"first_name": "qvUxiKIATdKdkC",
"last_name": "wIVzfDlKCkjkHIaC",
"ressource_uri": "/users/15/"
},
{
"first_name": "YSSMHxdDQQsW",
"last_name": "UaKCKgKsgEe",
"ressource_uri": "/users/16/"
},
{
"first_name": "EKLFTPJLKDINZio",
"last_name": "nuilPTzHqattX",
"ressource_uri": "/users/17/"
},
{
"first_name": "SPcDBtmDIi",
"last_name": "MrytYqElXiIxA",
"ressource_uri": "/users/18/"
},
{
"first_name": "OHxNppXiYp",
"last_name": "AUvUXFRPICsJIB",
"ressource_uri": "/users/19/"
},
{
"first_name": "WBFGxnoe",
"last_name": "KG",
"ressource_uri": "/users/20/"
},
{
"first_name": "i",
"last_name": "ggLOcKPpMfgvVGtv",
"ressource_uri": "/users/21/"
}
]
}
Of course you get 20 records but the most usefull part is the meta key:
{"meta":
{"count": 20,
"total_count": 802,
"next": "?offset=20",
"filters": {},
"offset": 0,
"previous": "null"}
}
You can use the “next” key to retreive the 20 next rows:
curl -i "http://localhost:5000/users/?offset=20"
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 1849
Server: Werkzeug/0.8.3 Python/2.7.2
Date: Tue, 15 Oct 2013 11:38:59 GMT
Note: The count and offset keywords can be easily changed to match your needs. pagination class may take an
offset_key and count_key parameters. So if you prefer to use first_id and limit, you can change your Paginator class
to do so:
"options": {"pagination": Pagination(20,
offset_key="first_id",
count_key="limit")
curl -i "http://localhost:5000/users/"
{"meta": {"first_id": 0, "total_count": 802, "next": "?first_id=20",
"limit": 20, "filters": {}, "previous": "null"}, "object_list": [<snip
for readability>]
curl -i "http://localhost:5000/users/?last_name=America"
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 298
Server: Werkzeug/0.8.3 Python/2.7.2
Date: Tue, 15 Oct 2013 12:14:59 GMT
Now that your fist endpoint work as expected, you will need to add an address field on the user model. But as some
users can have the same address, and because you want to retreive some user using an address, you will need to create
an new endpoint:
class AddressModel(models.Model):
models.PkField(name="id", required=True)
]
The only thing that change here in comparisson to the UserEndPoint you created earlier is the ressource dict. So
instead of copy pasting a lot of lines, let’s heritate from your first app:
class AddressEndPoint(UserEndPoint):
ressource = {
"ressource_name": "address",
"ressource": {"name": "adress_book.db", "table": "address"},
"model": AddressModel,
"datastore": SQLiteDataStore
}
All the options already defined in the UserEndPoint will be available with this new one. Pagination, formater and so
on.
Of course, if you change the controller or the view of UserEndPoint, AddressEndPoint will change too. If it become a
problem, you’ll have to create a base class with common options and configurations and each of your endpoints will
inherit from this base class. Each endpoint will be able to change some specifics settings.
The last thing to do to enable your new endpoint is to add it to the WSGIDispatcher
if __name__ == '__main__':
from werkzeug.serving import run_simple
from rest_api_framework.controllers import WSGIDispatcher
app = WSGIDispatcher([AddressEndPoint, UserEndPoint])
run_simple('127.0.0.1', 5000, app, use_debugger=True, use_reloader=True)
Note: For now the order you register AddressEndPoint and UserEndPoint doesn’t make a difference. But we will add
a reference from the user table to the address table. At this point, you will need to reference AddressEndPoint before
UserEndPoint.
curl -i "http://localhost:5000/address/"
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 124
Server: Werkzeug/0.8.3 Python/2.7.2
Date: Tue, 15 Oct 2013 15:45:34 GMT
{
"meta": {
"count": 20,
"filters": {},
"next": "null",
"offset": 0,
"previous": "null",
"total_count": 0
},
"object_list": []
}
Now that you have users and address, you want to link them together. Adding a reference from a user to his user.
Not all the datastore can handle this type of relation but hopefully, the SQLiteDataStore does.
First you will need to change your UserModel definition:
models.IntForeign(name="address",
foreign={"table": "address",
"column": "id",
}
),
This will add a foreign key constrain on the user ensuring the address id you give corresspond to an existing address.
• table : is the table of the ressource your are linking
• column: is the column you will check for the constrain
Note: unfortunately, at the time of writing, there is no way to update the schema automaticaly. You will need either
to destroy your database (Python Rest Framework will create a fresh one) or do an alter table by hands. As this is just
a tutorial, we will choose the second option and delete the file “adress.db”
It’s also important to note the your endpoints must be listed in the Wrapper in the order of foreing keys. First the
model to link to, then the model that will be linked
Adding an adress
Because, as the API developper you know that http://localhost:5000/address/1/ corresond to the address with the “id”
1 you can create a user:
curl -i http://localhost:5000/users/1/
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 90
Server: Werkzeug/0.8.3 Python/2.7.2
Date: Tue, 15 Oct 2013 17:42:18 GMT
{
"address": 1,
"first_name": "Super",
"last_name": "Dupont",
"ressource_uri": "/users/1/"
}
curl -i http://localhost:5000/address/1/
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 112
Server: Werkzeug/0.8.3 Python/2.7.2
Date: Tue, 15 Oct 2013 17:44:07 GMT
{
"city": "Paris",
"country": "France",
"number": 45,
"ressource_uri": "/address/1/",
"street": "quais de Valmy"
}
The same apply in the other side. As we know the adress id:
curl -i http://localhost:5000/users/?address=1
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 228
Server: Werkzeug/0.8.3 Python/2.7.2
Date: Tue, 15 Oct 2013 17:46:13 GMT
{
"meta": {
"count": 20,
"filters": {
"address": "1"
},
"next": "null",
"offset": 0,
"previous": "null",
"total_count": 1
},
"object_list": [
{
"address": 1,
"first_name": "Super",
"last_name": "Dupont",
"ressource_uri": "/users/1/"
}
]
}
Representing relations
Even if now can query adress from a user and users from an adress, your users cannot know that the field “address”: 1
correspond to /address/1/ plus it break a common rule. The id of the relation correspond to yor internal logic. Users
doesn’t have to know how it work, they just have to use it.
What we will try to do in this part of the tutorial is the following:
• http://localhost:5000/users/1/ should return:
{
"address": /address/1/,
"first_name": "Super",
"last_name": "Dupont",
"ressource_uri": "/users/1/"
}
This is the simplest task because you already changed the response result by adding remove_id function to the list of
View formater in Show data to users
Sure this method will work but if you get a close look on how ForeignKeyField (IntForeign inherit from this class)
You will see that the ForeignKeyField is filled with th options parameter you gave at the foreign key creation. You can
so write:
This function can then be used in all your project when you need to translate a foreignkey into a meaning full ressource
uri
For now, you can add this function to the list of formaters in your UserEndPoint views:
remove_id,
format_foreign_key
]}}
curl -i http://localhost:5000/users/
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 226
Server: Werkzeug/0.8.3 Python/2.7.2
Date: Tue, 15 Oct 2013 21:21:44 GMT
{
"meta": {
"count": 20,
"filters": {},
"next": "null",
"offset": 0,
"previous": "null",
"total_count": 1
},
"object_list": [
{
"address": "/address/1/",
"first_name": "Super",
"last_name": "Dupont",
"ressource_uri": "/users/1/"
}
]
}
Because you hide the internal implementation of your API to your user, you have to give him a way to interact with
your API.
To do so, you need to create a formater, exactly like you have done for the View. But this time you must do it for the
Controller.
controller = {
"list_verbs": ["GET", "POST"],
"unique_verbs": ["GET", "PUT", "DELETE"],
Now, each time the endpoint will deal with a data fields corresponding to a ForeignKeyField it will retreive the id from
the url supplied
“/address/1/” will be translated in 1
curl -i http://localhost:5000/users/?address=/adress/1/
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 341
Server: Werkzeug/0.8.3 Python/2.7.2
Date: Tue, 15 Oct 2013 22:33:47 GMT
{
"meta": {
"count": 20,
"filters": {
"address": 1
},
"next": "null",
"offset": 0,
"previous": "null",
"total_count": 2
},
"object_list": [
{
"address": "/address/1/",
"first_name": "Super",
"last_name": "Dupont",
"ressource_uri": "/users/1/"
},
{
"address": "/address/1/",
"first_name": "Supe",
"last_name": "Dupont",
"ressource_uri": "/users/2/"
}
]
}
Authentication and Authorization are different topics as you can implement Authentication without Authorization (For
rate-limiting or loggin for example).
Authentication
The fist thing you can do is to add an Authentication backend. Authentication backend needs a datastore to retreive
the user accessing the API. This datastore can be used by another endpoint of your API or a datastore aimed for this
purpose only.
In this example, we will use a very simple datastore, meant for testing purpose: the PythonListDataStore.
Define a backend
The PythonListDataStore is just a list of python dictionnary. So let’s first create this list:
ressources = [{"accesskey": "hackme"}, {"accesskey": "nopassword"}]
Like any other datastore, you need a Model to describe your datastore:
class KeyModel(models.Model):
fields = [
models.StringPkField(name="accesskey", required=True)
]
To keep this example simple we will use another testing tool, the ApiKeyAuthentication
ApiKeyAuthentication will inspect the query for an “apikey” parameter. If the “apikey” correspond to an existing
object in the datastore, it will return this object. Otherwise, the user is anonymous.
from rest_api_framework.authentication import ApiKeyAuthentication
authentication = ApiKeyAuthentication(datastore, identifier="accesskey")
The Authorization backend relies on the Authentication backend to retreive a user. With this user and the request, it
will grant access or raise an Unauthorized error.
For this example we will use the base Authentication class. This class tries to authenticate the user. If the user is
authenticated, then access is granted. Otherwise, it is not.
from rest_api_framework.authentication import Authorization then add it to the controller options:
controller = {
"list_verbs": ["GET", "POST"],
"unique_verbs": ["GET", "PUT", "DELETE"],
"options": {"pagination": Pagination(20),
"formaters": [foreign_keys_format],
"authentication": authentication,
"authorization": Authorization,
}
}
Now that your users are authenticated and that you put an authorization backend, you can add a rate limit on your api.
Rate limit will prevent your users to over use your endpoints.
With rate limit, a user can call your API at a certain rate. A number of calls per an interval. You have to decide how
many call and wich interval.
For this example, let say something like 100 call per 10 minutes. For Python REST Framework, interval are counted
in seconds so 10 minutes equals 10*60 = 600
The rate-limit implementation need a datastore to store rate-limit. Let’s create one:
class RateLimitModel(models.Model):
fields = [models.StringPkField(name="access_key"),
models.IntegerField(name="quota"),
models.TimestampField(name="last_request")]
You can then add your new datastore to the list of options of you controller:
controller = {
"list_verbs": ["GET", "POST"],
"unique_verbs": ["GET", "PUT", "DELETE"],
"options": {"pagination": Pagination(20),
"formaters": [foreign_keys_format],
"authentication": authentication,
"authorization": Authorization,
"ratelimit": RateLimit(
PythonListDataStore([],RateLimitModel),
interval=10*60,
quota=100)
}
}
Test!
Content-Type: application/json
Content-Length: 23
Server: Werkzeug/0.8.3 Python/2.7.2
Date: Wed, 16 Oct 2013 15:22:14 GMT
You can give your user the ability to retreive only the data they need instead of all of an object representation. For the
adress field, some can want to retreive only the country and the city field but do not care about the others.
with Python REST API Framework, it’s easy to make this happend.
First import the Partial base class:
class AddressEndPoint(UserEndPoint):
ressource = {
"ressource_name": "address",
"ressource": {"name": "adress_book.db", "table": "address"},
"model": AddressModel,
"datastore": SQLiteDataStore,
"options": {"partial": Partial()}
}
{
"meta": {
"count": 20,
"filters": {
"accesskey": "hackme",
"fields": "city,country"
},
"next": "null",
"offset": 0,
"previous": "null",
"total_count": 1
},
"object_list": [
{
"city": "Paris",
"country": "France",
"ressource_uri": "/address/1/"
}
]
}
To let you have a look on the application you have build so far, here is the whole application you have build:
from rest_api_framework import models
from rest_api_framework.models.fields import ForeignKeyField
from rest_api_framework.datastore import SQLiteDataStore, PythonListDataStore
from rest_api_framework.datastore.validators import UniqueTogether
from rest_api_framework.controllers import Controller
from rest_api_framework.pagination import Pagination
from rest_api_framework.authentication import (ApiKeyAuthentication,
Authorization)
from rest_api_framework.ratelimit import RateLimit
from rest_api_framework.partials import Partial
from rest_api_framework.views import JsonResponse
class KeyModel(models.Model):
fields = [
models.StringPkField(name="accesskey", required=True)
]
class RateLimitModel(models.Model):
fields = [models.StringPkField(name="access_key"),
models.IntegerField(name="quota"),
models.TimestampField(name="last_request")]
class UserModel(models.Model):
class AddressModel(models.Model):
for f in response.model.get_fields():
if isinstance(f, ForeignKeyField):
obj[f.name] = "/{0}/{1}/".format(f.options["foreign"]["table"],
obj[f.name])
return obj
class UserEndPoint(Controller):
ressource = {
"ressource_name": "users",
"ressource": {"name": "adress_book.db", "table": "users"},
"model": UserModel,
"datastore": SQLiteDataStore,
"options": {"validators": [UniqueTogether("first_name", "last_name")],
}
}
controller = {
"list_verbs": ["GET", "POST"],
"unique_verbs": ["GET", "PUT", "DELETE"],
"options": {"pagination": Pagination(20),
"formaters": [foreign_keys_format],
"authentication": authentication,
"authorization": Authorization,
"ratelimit": RateLimit(
PythonListDataStore([],RateLimitModel),
interval=100,
quota=200),
}
}
class AddressEndPoint(UserEndPoint):
ressource = {
"ressource_name": "address",
"ressource": {"name": "adress_book.db", "table": "address"},
"model": AddressModel,
"datastore": SQLiteDataStore,
"options": {"partial": Partial()}
}
if __name__ == '__main__':
Main modules
Controllers
39
Python Rest Api Framework Documentation, Release 0.1
create(request)
Try to load the data received from json to python, format each field if a formater has been set and call the
datastore for saving operation. Validation will be done on the datastore side
If creation is successfull, add the location the the headers of the response and render a 201 response with
an empty body
Parameters request (werkzeug.wrappers.Request) –
update_list(request)
Try to mass update the data.
update(request, identifier)
Try to retreive the object identified by the identifier. Try to load the incomming data from json to python.
Call the datastore for update.
If update is successfull, return the object updated with a status of 200
Parameters request (werkzeug.wrappers.Request) –
delete(request, identifier)
try to retreive the object from the datastore (will raise a NotFound Error if object does not exist) call the
delete
method on the datastore.
return a response with a status code of 204 (NO CONTENT)
Parameters request (werkzeug.wrappers.Request) –
class rest_api_framework.controllers.Controller(*args, **kwargs)
Controller configure the application. Set all configuration options and parameters on the Controller, the View
and the Ressource
load_urls()
Parameters urls (list) – A list of tuple in the form (url(string), view(string), permitted Http
verbs(list))
return a werkzeug.routing.Map
this method is automaticaly called by __init__ to build the Controller urls mapping
make_options(options)
Make options enable Pagination, Authentication, Authorization, RateLimit and all other options an appli-
cation need.
class rest_api_framework.controllers.WSGIWrapper
Base Wsgi application loader. WSGIWrapper is an abstract class. Herited by ApiController it make the
class callable and implement the request/response process
wsgi_app(environ, start_response)
instanciate a Request object, dispatch to the needed method, return a response
dispatch_request(request)
Using the werkzeug.routing.Map constructed by load_urls() call the view method with the
request object and return the response object.
class rest_api_framework.controllers.AutoDocGenerator(apps)
Auto generate a documentation endpoint for each endpoints registered.
schema(request)
Generate the schema url of each endpoints
ressource_schema(request, ressource)
Generate the main endpoint of schema. Return the list of all print app.datastore.modelendpoints
available
DataStores
create(data)
data is a dict containing the representation of the ressource. This method should call validate(), create
the data in the datastore and return the ressource identifier
Not implemented by base DataStore class
update(obj, data)
should be able to call get() to retreive the object to be updated, validate_fields() and return the
updated object
delete(identifier)
should be able to validate the existence of the object in the ressource and remove it from the datastore
filter(**kwargs)
should return a way to filter the ressource according to kwargs. It is not mandatory to actualy retreive the
ressources as they will be paginated just after the filter call. If you retreive the wole filtered ressources you
loose the pagination advantage. The point here is to prepare the filtering. Look at SQLiteDataStore.filter
for an example.
validate(data)
Check if data send are valid for object creation. Validate Chek that each required fields are in data and
check for their type too.
Used to create new ressources
validate_fields(data)
Validate only some fields of the ressource. Used to update existing objects
class rest_api_framework.datastore.simple.PythonListDataStore(ressource_config,
model, **options)
Bases: rest_api_framework.datastore.base.DataStore
a datastore made of list of dicts
get(identifier)
return an object matching the uri or None
get_list(offset=0, count=None, **kwargs)
return all the objects. paginated if needed
update(obj, data)
Update a single object
class rest_api_framework.datastore.sql.SQLiteDataStore(ressource_config, model, **op-
tions)
Bases: rest_api_framework.datastore.base.DataStore
Define a sqlite datastore for your ressource. you have to give __init__ a data parameter containing the informa-
tion to connect to the database and to the table.
example:
data={"table": "tweets",
"name": "test.db"}
model = ApiModel
datastore = SQLiteDataStore(data, **options)
SQLiteDataStore implement a naive wrapper to convert Field types into database type.
•int will be saved in the database as INTEGER
•float will be saved in the database as REAL
•basestring will be saved in the database as TEXT
•if the Field type is PKField, is a will be saved as PRIMARY KEY AUTOINCREMENT
As soon as the datastore is instanciated, the database is create if it does not exists and table is created too
Note:
•It is not possible to use :memory database either. The connection is closed after each operations
get_connector()
return a sqlite3 connection to communicate with the table define in self.db
filter(**kwargs)
Change kwargs[”query”] with “WHERE X=Y statements”. The filtering will be done with the actual
evaluation of the query in paginate() the sql can then be lazy
paginate(data, **kwargs)
paginate the result of filter using ids limits. Obviously, to work properly, you have to set the start to the
last ids you receive from the last call on this method. The max number of row this method can give back
depend on the paginate_by option.
get_list(**kwargs)
return all the objects, paginated if needed, fitered if filters have been set.
get(identifier)
Return a single row or raise NotFound
create(data)
Validate the data with base.DataStore.validate() And, if data is valid, create the row in database
and return it.
update(obj, data)
Retreive the object to be updated (get() will raise a NotFound error if the row does not exist)
Validate the fields to be updated and return the updated row
delete(identifier)
Retreive the object to be updated
(get() will raise a NotFound error if the row does not exist)
Return None on success, Raise a 400 error if foreign key constrain prevent delete.
Views
format(objs)
Format the output using formaters listed in self.formaters
Optional modules
Authentication
class rest_api_framework.authentication.Authentication
Manage the authentication of a request. Must implement the get_user method
get_user(identifier)
Must return a user if authentication is successfull, None otherwise
class rest_api_framework.authentication.ApiKeyAuthentication(datastore, identi-
fier=’apikey’)
Authentication based on an apikey stored in a datastore.
get_user(request)
return a user or None based on the identifier found in the request query parameters.
class rest_api_framework.authentication.BasicAuthentication(datastore)
Implement the Basic Auth authentication http://fr.wikipedia.org/wiki/HTTP_Authentification
get_user(request)
return a user or None based on the Authorization: Basic header found in the request. login and password
are Base64 encoded string : “login:password”
Authorization
class rest_api_framework.authentication.Authorization(authentication)
Check if an authenticated request can perform the given action.
check_auth(request)
Return None if the request user is authorized to perform this action, raise Unauthorized otherwise
Parameters request (werkzeug.wrappers.Request) –
class rest_api_framework.authentication.Authorization(authentication)
Check if an authenticated request can perform the given action.
check_auth(request)
Return None if the request user is authorized to perform this action, raise Unauthorized otherwise
Parameters request (werkzeug.wrappers.Request) –
Pagination
Partials
Enable partials response from the api. With partials response, only a subset of fields are send back to the request user.
DataStore are responsible for implementing partial options
class rest_api_framework.partials.Partial(partial_keyword=’fields’)
The base implementation of partial response.
get_partials(**kwargs)
This partial implementation wait for a list of fields separated by comma. Other implementations are possi-
ble. Just inherit from this base class and implement your own get_partials method.
get_partials does not check that the fields are part of the model. Datastore get_list will check for it and
raise an error if needed.
Rate Limit
class UserModel(models.Model):
"""
Define how to handle and validate your data.
"""
fields = [models.StringField(name="first_name", required=True),
models.StringField(name="last_name", required=True),
models.PkField(name="id", required=True)
]
class UserEndPoint(Controller):
ressource = {
"ressource_name": "users",
"ressource": {"name": "adress_book.db", "table": "users"},
"model": UserModel,
"datastore": SQLiteDataStore,
"options": {"validators": [UniqueTogether("first_name", "last_name")]}
}
47
Python Rest Api Framework Documentation, Release 0.1
controller = {
"list_verbs": ["GET", "POST"],
"unique_verbs": ["GET", "PUT", "DELETE"],
"options": {"pagination": Pagination(20)}
}
if __name__ == '__main__':
• genindex
• modindex
• search
49
Python Rest Api Framework Documentation, Release 0.1
r
rest_api_framework.partials, 44
rest_api_framework.ratelimit, 44
51
Python Rest Api Framework Documentation, Release 0.1
A dispatch_request() (rest_api_framework.controllers.WSGIWrapper
ApiController (class in rest_api_framework.controllers), method), 40
39
ApiKeyAuthentication (class in F
rest_api_framework.authentication), 43 filter() (rest_api_framework.datastore.base.DataStore
Authentication (class in method), 41
rest_api_framework.authentication), 43 filter() (rest_api_framework.datastore.sql.SQLiteDataStore
Authorization (class in method), 42
rest_api_framework.authentication), 44 format() (rest_api_framework.views.JsonResponse
AutoDocGenerator (class in method), 43
rest_api_framework.controllers), 40
G
B get() (rest_api_framework.controllers.ApiController
BasicAuthentication (class in method), 39
rest_api_framework.authentication), 43 get() (rest_api_framework.datastore.base.DataStore
method), 41
C get() (rest_api_framework.datastore.simple.PythonListDataStore
check_auth() (rest_api_framework.authentication.Authorization method), 42
method), 44 get() (rest_api_framework.datastore.sql.SQLiteDataStore
check_limit() (rest_api_framework.ratelimit.RateLimit method), 43
method), 45 get_connector() (rest_api_framework.datastore.sql.SQLiteDataStore
Controller (class in rest_api_framework.controllers), 40 method), 42
create() (rest_api_framework.controllers.ApiController get_list() (rest_api_framework.controllers.ApiController
method), 39 method), 39
create() (rest_api_framework.datastore.base.DataStore get_list() (rest_api_framework.datastore.base.DataStore
method), 41 method), 41
create() (rest_api_framework.datastore.sql.SQLiteDataStoreget_list() (rest_api_framework.datastore.simple.PythonListDataStore
method), 43 method), 42
get_list() (rest_api_framework.datastore.sql.SQLiteDataStore
D method), 43
get_partials() (rest_api_framework.partials.Partial
DataStore (class in rest_api_framework.datastore.base),
method), 44
41
get_user() (rest_api_framework.authentication.ApiKeyAuthentication
delete() (rest_api_framework.controllers.ApiController
method), 43
method), 40
get_user() (rest_api_framework.authentication.Authentication
delete() (rest_api_framework.datastore.base.DataStore
method), 43
method), 41
get_user() (rest_api_framework.authentication.BasicAuthentication
delete() (rest_api_framework.datastore.sql.SQLiteDataStore
method), 44
method), 43
53
Python Rest Api Framework Documentation, Release 0.1
I update() (rest_api_framework.datastore.simple.PythonListDataStore
index() (rest_api_framework.controllers.ApiController method), 42
method), 39 update() (rest_api_framework.datastore.sql.SQLiteDataStore
method), 43
J update_list() (rest_api_framework.controllers.ApiController
JsonResponse (class in rest_api_framework.views), 43 method), 40
L V
validate() (rest_api_framework.datastore.base.DataStore
load_urls() (rest_api_framework.controllers.Controller
method), 41
method), 40
validate_fields() (rest_api_framework.datastore.base.DataStore
M method), 42
make_options() (rest_api_framework.controllers.Controller W
method), 40
wsgi_app() (rest_api_framework.controllers.WSGIWrapper
P method), 40
WSGIWrapper (class in rest_api_framework.controllers),
paginate() (rest_api_framework.controllers.ApiController 40
method), 39
paginate() (rest_api_framework.datastore.base.DataStore
method), 41
paginate() (rest_api_framework.datastore.sql.SQLiteDataStore
method), 42
paginate() (rest_api_framework.pagination.Pagination
method), 44
Pagination (class in rest_api_framework.pagination), 44
Partial (class in rest_api_framework.partials), 44
PythonListDataStore (class in
rest_api_framework.datastore.simple), 42
R
RateLimit (class in rest_api_framework.ratelimit), 44
ressource_schema() (rest_api_framework.controllers.AutoDocGenerator
method), 40
rest_api_framework.partials (module), 44
rest_api_framework.ratelimit (module), 44
S
schema() (rest_api_framework.controllers.AutoDocGenerator
method), 40
SQLiteDataStore (class in
rest_api_framework.datastore.sql), 42
T
TooManyRequest, 44
U
unique_uri() (rest_api_framework.controllers.ApiController
method), 39
update() (rest_api_framework.controllers.ApiController
method), 40
update() (rest_api_framework.datastore.base.DataStore
method), 41
54 Index