Quick Start Guide¶
This introductory guide describes how to set up an API using SQLAlchemy with Flask-Resone, query it, and attach routes to resources.
A minimal Flask-Resone API looks like this:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_restone import Api, ModelResource
app = Flask(__name__)
db = SQLAlchemy(app)
class Book(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.Str(), nullable=False)
year_published = db.Column(db.Integer)
db.create_all()
class BookResource(ModelResource):
class Meta:
model = Book
api = Api(app)
api.add_resources(BookResource)
if __name__ == '__main__':
app.run()
Save this as server.py and run it using your Python interpreter. The application will create an in-memory SQLite database, so the state of the application will reset every time the server is restarted.
$ python server.py
* Running on http://127.0.0.1:5000/
What did we do here? We used a ModelResource and defined a model in its Meta property.
Meta and Schema are the two of the primary ways to describe resources (a third being route,
which we’ll go into later).
Meta class attributes¶
The Meta class is how the basic functions of a resource are defined. Besides model, there
are a few other properties that control how the ModelResource maps to the SQLAlchemy model:
Attribute name |
Default |
Description |
|---|---|---|
model |
— |
The Flask-SQLAlchemy model |
name |
— |
Name of the resource; defaults to the lower-case of the model’s table name |
id_attribute |
|
With SQLAlchemy models, defaults to the name of the primary key of model. |
id_converter |
–– |
Flask URL converter for resource routes. Typically this is inferred from id_field_class. |
id_field_class |
|
Field class to use for |
include_id |
|
Whether to include the id of the item as an |
include_fields |
— |
A list of fields that should be imported from the model. By default, all
columns other than foreign key and primary key columns are imported.
|
exclude_fields |
— |
A list of fields that should not be imported from the model. |
required_fields |
— |
Fields that are automatically imported from the model are automatically required if their columns are not nullable and do not have a default. |
read_only_fields |
— |
A list of fields that are returned by the resource but are ignored in POST and PATCH requests. Useful for e.g. timestamps. |
filters |
|
Used to configure what properties of an item can be filtered and what filters can be used. |
write_only_fields |
— |
A list of fields that can be written to but are not returned. For secret stuff. |
title |
— |
JSON-schema title declaration |
description |
— |
JSON-schema description declaration |
manager |
|
A |
key_converters |
|
A list of |
natural_key |
|
A string, or tuple of strings, corresponding to schema field names, for a natural key. |
exclude_routes |
— |
A list of rel-strings for any previously defined routes that should not be published for this resource. |
Schema class attributes¶
Schema is used to define a default schema for a resource. The Schema class contains a set of fields
that inherit from Field
Using ModelResource with a SQLAlchemy model, the schema is for the most part auto-generated for us. Yet it still on
occasion makes sense to manually describe a field. The reference field types, Res and Many, also
need to be set by hand.
For instance, our book resource only stores books produced by the printing press. Let’s acknowledge this by setting a
sensible minimum for year_published:
from flask_restone import fields
class BookResource(ModelResource):
class Meta:
model = Book
class Schema:
year_published = Int[1400:]
Relationships¶
RESTful relationships create a variety of API client design and caching problems that Restone has been written to address. To preface what you will see now, it needs to be said that Restone should be used with SPDY or the upcoming HTTP/2 as it generates more requests than some alternative approaches.
We now have both an author and a book resource:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import backref
from flask_restone.routes import Relation
from flask_restone import ModelResource, fields, Api
app = Flask(__name__)
db = SQLAlchemy(app)
class Author(db.Model):
id = db.Column(db.Integer, primary_key=True)
first_name = db.Column(db.String(), nullable=False)
last_name = db.Column(db.Str(), nullable=False)
class Book(db.Model):
id = db.Column(db.Integer, primary_key=True)
author_id = db.Column(db.Integer, db.ForeignKey(Author.id), nullable=False)
title = db.Column(db.String(), nullable=False)
year_published = db.Column(db.Integer)
author = db.relationship(Author, backref=backref('books', lazy='dynamic'))
db.create_all()
class BookResource(ModelResource):
class Meta:
model = Book
class Schema:
author = Res('author')
class AuthorResource(ModelResource):
books = Relation('book')
class Meta:
model = Author
api = Api(app)
api.add_resources(BookResource,AuthorResource)
if __name__ == '__main__':
app.run()
We’re going to add two authors and books:
http :5000/author first_name=Charles last_name=Darwin
HTTP/1.0 200 OK
Content-Length: 69
Content-Type: application/json
Date: Sat, 07 Feb 2015 12:11:33 GMT
Server: Werkzeug/0.9.6 Python/3.3.2
{
"$uri": "/author/1",
"first_name": "Charles",
"last_name": "Darwin"
}
Note
At the moment, references always need to be declared as json-ref objects. This is tedious during command-line use, and an enhancement to Restone to support using ids and natural keys in requests is already in the works.
http :5000/book title="On the Origin of Species" author:=1 year_published:=1859
HTTP/1.0 200 OK
Content-Length: 113
Content-Type: application/json
Date: Sat, 07 Feb 2015 12:16:11 GMT
Server: Werkzeug/0.9.6 Python/3.3.2
{
"$uri": "/book/1",
"author": {
"$ref": "/author/1"
},
"title": "On the Origin of Species",
"year_published": 1859
}
http :5000/author first_name=James last_name=Watson > /dev/null
http :5000/book title="The Double Helix" author:=2 year_published:=1968 > /dev/null
As you can see, references in Restone are JSON Reference draft reference
objects. These objects always have the same format — {"$ref": 'target-uri'} — and can easily be recognized by an API client
when deserializing JSON. An API client can first check its cache for the target item and, if necessary, query it from the server.
Requests allow both plain ids and json-ref objects — it’s all the same to the server.
There are now two ways available to us for querying the relationship between the resources. The first is the author’s
Relation('book'), which created a new route on the author resource with references to the book resource. Let’s query Charles’ books:
http :5000/author/1/books
HTTP/1.0 200 OK
Content-Length: 21
Content-Type: application/json
Date: Sat, 07 Feb 2015 12:18:45 GMT
Link: </author/1/books?page=1&per_page=20>; rel="self",</author/1/books?page=1&per_page=20>; rel="last"
Server: Werkzeug/0.9.6 Python/3.3.2
X-Total-Count: 1
[
{
"$ref": "/book/1"
}
]
This is not a particularly good example for using Relation, and in fact there are few at all. There is a more
RESTful way for querying a one-to-many relation:
http GET :5000/book where=='{"author": {"$ref": "/author/1"}}'
HTTP/1.0 200 OK
Content-Length: 115
Content-Type: application/json
Date: Sat, 07 Feb 2015 12:34:18 GMT
Link: </book?page=1&per_page=20>; rel="self",</book?page=1&per_page=20>; rel="last"
Server: Werkzeug/0.9.6 Python/3.3.2
X-Total-Count: 1
[
{
"$uri": "/book/1",
"author": {
"$ref": "/author/1"
},
"title": "On the Origin of Species",
"year_published": 1859
}
]
So far, in our queries, we have used item ids and json-ref objects to refer to items. These surrogate keys can be difficult to remember and tedious to work with on the command line — but Restone has a solution:
Natural Keys¶
A natural key is a unique identifier that exists in the real world and is often more memorable than a surrogate key. Restone ships with support for declaring natural keys.
The author model has both a first name and a last name. Together, these two names form a natural key for the author resource. We’ll update both our database model and our resource to reflect this:
class Author(db.Model):
id = db.Column(db.Integer, primary_key=True)
first_name = db.Column(db.String(), nullable=False)
last_name = db.Column(db.String(), nullable=False)
__table_args__ = (
UniqueConstraint('first_name', 'last_name'), # unique constraint added here
)
class AuthorResource(ModelResource):
class Meta:
model = Author
natural_key = ('first_name', 'last_name') # natural key declaration added here
Now our earlier query can be written using the full name of the author:
http GET :5000/book where=='{"author": ["Charles", "Darwin"]}'
Natural keys can be declared as either a single unique field or a tuple of fields that are unique together.
Filtering & Sorting¶
Instances of a ModelResource can be filtered using the where query and sorted using sort.
We were interested in relations, so we filtered a Res field for equality. Most other field types can also be filtered and support custom comparators. Here are some examples of where queries:
http :5000/book where=='{"year_published": {"$gt": 1900}}' # Book.year_published > 1900
http :5000/author where=='{"first_name": {"$sw": "C"}}' # Author.first_name starts with 'C'
http :5000/author where=='{"first_name": {"$in": ["Charles", "James"]}}' # Author.first_name in ['Charles', 'James']
http :5000/book where=='{"title": "The Double Helix", "year_published": {"$lt": 2000}}'
Here are some examples of sort queries:
http :5000/book sort=='{"year_published": false}' # Book.year_published ascending
http :5000/book sort=='{"year_published": false, "title": true}' # Book.year_published ascending, Book.title descending
Both where and sort need to be valid JSON, so use double quotes.
See Filters for a full list of possible filters.
Pagination¶
Restone pagination is borrowed from the GitHub API. Pages are requested
using the page and per_page query string arguments. The Link header lists links to the current, first, previous, next, and last page.
In addition, the X-Total-Count header contains a count of the total number of items.
HTTP/1.0 200 OK
Content-Type: application/json
Link: </book?page=1&per_page=20>; rel="self",
</book?page=3&per_page=20>; rel="last",
</book?page=2&per_page=20>; rel="next"
X-Total-Count: 55
ModelResource items are paginated automatically.
The default and maximum number of items per page can be configured using the
'RESTONE_DEFAULT_PER_PAGE' and 'RESTONE_MAX_PER_PAGE' configuration variables.
routes¶
routes are added using decorators named after the HTTP methods, declared either with or without arguments. The format for the route decorators is:
route.method(rule = None,
rel=None,
attribute=None,
schema=None,
response_schema=None)
A route instance itself also has decorators for each method, so that they can define different functions
for different HTTP methods on the same endpoint.
Each method has its own schema and response_schema used to decode, verify, and encode requests and responses.
If schema is a FieldSet, its properties are
spread over the route function as keyword arguments.
itemroute is a special route, used with ModelResource, whose rule is prefixed '/<id_converter:id>' and
that passes the item as the first function argument.
Here is a slightly different Book model (a rating has been added) and a book resource with some of the different
kinds of routes:
class Book(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.Str(), nullable=False)
year_published = db.Column(db.Integer)
rating = db.Column(db.Integer, default=5)
class BookResource(ModelResource):
class Meta:
model = Book
excluded_fields = ['rating']
@itemroute.get('/rating')
def rating(self, book) -> Int:
return book.rating
@rating.post
def rate(self, book, value: Int[1:10]) -> Int:
self.manager.update(book, {"rating": value})
return value
@itemroute.get
def is_recent(self, book) -> Bool:
return datetime.date.today().year <= book.year_published + 10
@route.get
def genres(self) -> List(Str, description="A list of genres"):
return ['biography', 'history', 'essay', 'law', 'philosophy']
Note
This example makes use of function annotations, which appeared in Python 3.0.
After adding a book, we can give these routes a spin:
http GET :5000/book/1/rating
HTTP/1.0 200 OK
Content-Length: 3
Content-Type: application/json
Date: Sat, 07 Feb 2015 16:16:37 GMT
Server: Werkzeug/0.9.6 Python/3.3.2
5
http POST :5000/book/1/rating value:=7
HTTP/1.0 200 OK
Content-Length: 1
Content-Type: application/json
Date: Sat, 07 Feb 2015 16:17:59 GMT
Server: Werkzeug/0.9.6 Python/3.3.2
7
http GET :5000/book/1/is-recent
HTTP/1.0 200 OK
Content-Length: 5
Content-Type: application/json
Date: Sat, 07 Feb 2015 16:20:19 GMT
Server: Werkzeug/0.9.6 Python/3.3.2
false
http GET :5000/book/genres
HTTP/1.0 200 OK
Content-Length: 54
Content-Type: application/json
Date: Sat, 07 Feb 2015 16:20:44 GMT
Server: Werkzeug/0.9.6 Python/3.3.2
[
"biography",
"history",
"essay",
"law",
"philosophy"
]
It is worth noting that ModelResource is not much more than the empty Resource type with a few custom
routes. route and Resource are the backbone of Restone.
route Sets & Mixins¶
In the example above, we have one property — rating — which can be read and updated by accessing
a specific route. Restone provides a shortcut for this common pattern. Let’s use AttrRoute to rewrite the rating getter and setter:
class BookResource(ModelResource):
rating = AttrRoute(Float)
# ...
Done. Now, this isn’t strictly a set of routes — but it implements _RouteSet, which can be used
to write reusable groups of routes. (Relation is also a route set,:class:TaskRoute is also a route set).
A second pattern for reusability is the mixin. They can augment the Schema and Meta attributes and
add new routes and route sets to the resources. Here is an example mixin, adding two new fields to the schema:
class MetaMixin(object):
class Schema:
created_at = DateTime(io='r')
updated_at = DateTime(io='r', nullable=True)
class BookResource(MetaMixin, ModelResource):
# ...
Mixin and Resource base classes are evaluated left-to-right.
Self-documenting API¶
It can be a huge hassle to write and maintain the documentation of an API—not with Restone! In fact, every API you saw in this quick start guide was fully documented.
It uses flasgger to generate documentation automatically. You just need to set the autodoc parameter of the API to True. Then you can read the doc and test your apis at http://<ip>:<post>/apidocs.
Next steps…¶
This guide has only skimmed the surface of what Restone can do for you.
In particular you may be interested in Permissions with Flask-Principal, a guide to a fully-fledged permissions system for SQLAlchemy using Flask-Principal.