Flask-Restless-NG¶
Flask-Restless provides simple generation of ReSTful APIs for database models defined using SQLAlchemy (or Flask-SQLAlchemy). The generated APIs satisfy the requirements of the JSON API specification.
User’s guide¶
How to use Flask-Restless in your own projects. Much of the documentation in this chapter assumes some familiarity with the terminology and interfaces of the JSON API specification.
Downloading and installing Flask-Restless-NG¶
Flask-Restless can be downloaded from the Python Package Index. The
development version can be downloaded from GitHub. However, it is better to
install with pip
(in a virtual environment provided by virtualenv
):
pip install Flask-Restless-NG
Flask-Restless supports Python 3.6+
Flask-Restless has the following dependencies (which will be automatically
installed if you use pip
):
Flask version 1.0 or greater
SQLAlchemy version 1.3 or greater
python-dateutil version strictly greater than 2.2
Flask-SQLAlchemy, only if you want to define your models using Flask-SQLAlchemy
Quickstart¶
For the restless:
import flask
import flask_restless
import flask_sqlalchemy
# Create the Flask application and the Flask-SQLAlchemy object.
app = flask.Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = flask_sqlalchemy.SQLAlchemy(app)
class Person(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode)
birth_date = db.Column(db.Date)
class Article(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.Unicode)
published_at = db.Column(db.DateTime)
author_id = db.Column(db.Integer, db.ForeignKey('person.id'))
author = db.relationship(Person, backref=db.backref('articles'))
# Create the database tables.
db.create_all()
# Create the Flask-Restless API manager.
manager = flask_restless.APIManager(app, session=db.session)
# Create API endpoints, which will be available at /api/<tablename> by
# default. Allowed HTTP methods can be specified as well.
manager.create_api(Person, methods=['GET', 'POST', 'DELETE'])
manager.create_api(Article, methods=['GET'])
# start the flask loop
app.run()
You may find this example at examples/quickstart.py
in the source
distribution; you may also view it online. Further examples can be found in
the examples/
directory in the source distribution or on the web
Creating API endpoints¶
To use this extension, you must have defined your database models using either SQLAlchemy or Flask-SQLALchemy. The basic setup in either case is nearly the same.
If you have defined your models with Flask-SQLAlchemy, first, create your
Flask
object, SQLAlchemy
object,
and model classes as usual but with one additional restriction: each model must
have a primary key column named id
of type sqlalchemy.Integer
or
type sqlalchemy.Unicode
.
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy(app)
class Person(db.Model):
id = db.Column(db.Integer, primary_key=True)
class Article(db.Model):
id = db.Column(db.Integer, primary_key=True)
author_id = db.Column(db.Integer, db.ForeignKey('person.id'))
author = db.relationship(Person, backref=db.backref('articles'))
db.create_all()
If you are using pure SQLAlchemy:
from flask import Flask
from sqlalchemy import Column, Integer, Unicode
from sqlalchemy import ForeignKey
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import backref, relationship
from sqlalchemy.orm import scoped_session, sessionmaker
app = Flask(__name__)
engine = create_engine('sqlite:////tmp/testdb.sqlite', convert_unicode=True)
Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)
mysession = scoped_session(Session)
Base = declarative_base()
Base.metadata.bind = engine
class Person(Base):
id = Column(Integer, primary_key=True)
class Article(Base):
id = Column(Integer, primary_key=True)
author_id = Column(Integer, ForeignKey('person.id'))
author = relationship(Person, backref=backref('articles'))
Base.metadata.create_all()
Second, instantiate an APIManager
object with the
Flask
and SQLAlchemy
objects:
from flask_restless import APIManager
manager = APIManager(app, session=db.session)
Or if you are using pure SQLAlchemy, specify the session you created above instead:
manager = APIManager(app, session=mysession)
Third, create the API endpoints that will be accessible to web clients:
person_blueprint = manager.create_api(Person, methods=['GET', 'POST'])
article_blueprint = manager.create_api(Article)
You can specify which HTTP methods are available for each API endpoint. In this example, the client can fetch and create people, but only fetch articles (the default if no methods are specified). There are many options for customizing the endpoints created at this step; for more information, see Customizing the ReSTful interface.
Due to the design of Flask, these APIs must be created before your application
handles any requests. The return value of APIManager.create_api()
is the
blueprint in which the endpoints for the specified database model live. The
blueprint has already been registered on the Flask
application,
so you do not need to register it yourself. It is provided so that you can
examine its attributes, but if you don’t need it then just ignore it:
methods = ['GET', 'POST']
manager.create_api(Person, methods=methods)
manager.create_api(Article)
If you wish to create the blueprint for the API without registering it (for
example, if you wish to register it manually later in your code), use the
create_api_blueprint()
method instead. You must provide an
additional positional argument, name, to this method:
blueprint = manager.create_api_blueprint('person', Person, methods=methods)
# later...
someapp.register_blueprint(blueprint)
By default, the API for Person
in the above code samples will be accessible
at <base_url>/api/person
, where the person
part of the URL is the value
of Person.__tablename__
:
>>> import json
>>> # The python-requests library is installable from PyPI.
>>> import requests
>>> # Let's create a new person resource with the following fields.
>>> newperson = {'type': 'person', 'name': u'Lincoln', 'age': 23}
>>> # Our requests must have the appropriate JSON API headers.
>>> headers = {'Content-Type': 'application/vnd.api+json',
... 'Accept': 'application/vnd.api+json'}
>>> # Assume we have a Flask application running on localhost.
>>> r = requests.post('http://localhost/api/person',
... data=json.dumps(newperson), headers=headers)
>>> r.status_code
201
>>> document = json.loads(r.data)
>>> dumps(document, indent=2)
{
"data": {
"id": "1",
"type": "person",
"relationships": {
"articles": {
"data": [],
"links": {
"related": "http://localhost/api/person/1/articles",
"self": "http://localhost/api/person/1/relationships/articles"
}
},
},
"links": {
"self": "http://localhost/api/person/1"
}
}
"meta": {},
"jsonapi": {
"version": "1.0"
}
}
>>> newid = document['data']['id']
>>> r = requests.get('/api/person/{0}'.format(newid), headers=headers)
>>> r.status_code
200
>>> document = loads(r.data)
>>> dumps(document, indent=2)
{
"data": {
"id": "1",
"type": "person",
"relationships": {
"articles": {
"data": [],
"links": {
"related": "http://localhost/api/person/1/articles",
"self": "http://localhost/api/person/1/relationships/articles"
}
},
},
"links": {
"self": "http://localhost/api/person/1"
}
}
"meta": {},
"jsonapi": {
"version": "1.0"
}
}
If the primary key is a Unicode
instead of an
Integer
, the instances will be accessible at URL endpoints
like http://<host>:<port>/api/person/foo
instead of
http://<host>:<port>/api/person/1
.
Deferred API registration¶
If you only wish to create APIs on a single Flask application and have access
to the Flask application before you create the APIs, you can provide a Flask
application as an argument to the constructor of the APIManager
class,
as described above. However, if you wish to create APIs on multiple Flask
applications or if you do not have access to the Flask application at the time
you create the APIs, you can use the APIManager.init_app()
method.
If a APIManager
object is created without a Flask application,
manager = APIManager(session=session)
then you can create your APIs without registering them on a particular Flask application:
manager.create_api(Person)
manager.create_api(Article)
Later, you can call the init_app()
method with any
Flask
objects on which you would like the APIs to be
available:
app1 = Flask('app1')
app2 = Flask('app2')
manager.init_app(app1)
manager.init_app(app2)
The manager creates and stores a blueprint each time
create_api()
is invoked, and registers those blueprints each
time init_app()
is invoked. (The name of each blueprint will
be a uuid.UUID
.)
Changed in version 1.0.0: The behavior of the init_app()
method was strange and
incorrect before version 1.0.0. It is best not to use earlier versions.
Requests and responses¶
Requests and responses are all in the JSON API format, so each request must include an Accept header whose value is application/vnd.api+json and any request that contains content must include a Content-Type header whose value is application/vnd.api+json. If they do not, the client will receive an error response.
This section of the documentation assumes some familiarity with the JSON API specification.
Fetching resources and relationships¶
For the purposes of concreteness in this section, suppose we have executed the following code on the server:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_restless import APIManager
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy(app)
class Person(db.Model):
id = db.Column(db.Integer, primary_key=True)
class Article(db.Model):
id = db.Column(db.Integer, primary_key=True)
author_id = db.Column(db.Integer, db.ForeignKey('person.id'))
author = db.relationship(Person, backref=db.backref('articles'))
db.create_all()
manager = APIManager(app, session=db.session)
manager.create_api(Person)
manager.create_api(Article)
By default, all columns and relationships will appear in the resource object representation of an instance of your model. See Specifying which fields appear in responses for more information on specifying which values appear in responses.
To fetch a collection of resources, the request
GET /api/person HTTP/1.1
Host: example.com
Accept: application/vnd.api+json
yields the response
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"id": "1",
"links": {
"self": "http://example.com/api/person/1"
},
"relationships": {
"articles": {
"data": [],
"links": {
"related": "http://example.com/api/person/1/articles",
"self": "http://example.com/api/person/1/relationships/articles"
}
}
},
"type": "person"
}
],
"links": {
"first": "http://example.com/api/person?page[number]=1&page[size]=10",
"last": "http://example.com/api/person?page[number]=1&page[size]=10",
"next": null,
"prev": null,
"self": "http://example.com/api/person"
},
"meta": {
"total": 1
}
}
To fetch a single resource, the request
GET /api/person/1 HTTP/1.1
Host: example.com
Accept: application/vnd.api+json
yields the response
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"id": "1",
"links": {
"self": "http://example.com/api/person/1"
},
"relationships": {
"articles": {
"data": [],
"links": {
"related": "http://example.com/api/person/1/articles",
"self": "http://example.com/api/person/1/relationships/articles"
}
}
},
"type": "person"
}
}
To fetch a resource from a to-one relationship, the request
GET /api/article/1/author HTTP/1.1
Host: example.com
Accept: application/vnd.api+json
yields the response
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"id": "1",
"links": {
"self": "http://example.com/api/person/1"
},
"relationships": {
"articles": {
"data": [
{
"id": "1",
"type": "article"
}
],
"links": {
"related": "http://example.com/api/person/1/articles",
"self": "http://example.com/api/person/1/relationships/articles"
}
}
},
"type": "person"
}
}
To fetch a resource from a to-many relationship, the request
GET /api/person/1/articles HTTP/1.1
Host: example.com
Accept: application/vnd.api+json
yields the response
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"id": "2",
"links": {
"self": "http://example.com/api/articles/2"
},
"relationships": {
"author": {
"data": {
"id": "1",
"type": "person",
},
"links": {
"related": "http://example.com/api/articles/2/author",
"self": "http://example.com/api/articles/2/relationships/author"
}
}
},
"type": "article"
}
],
"links": {
"first": "http://example.com/api/person/1/articles?page[number]=1&page[size]=10",
"last": "http://example.com/api/person/1/articles?page[number]=1&page[size]=10",
"next": null,
"prev": null,
"self": "http://example.com/api/person/1/articles"
},
"meta": {
"total": 1
}
}
To fetch a single resource from a to-many relationship, the request
GET /api/person/1/articles/2 HTTP/1.1
Host: example.com
Accept: application/vnd.api+json
yields the response
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"id": "2",
"links": {
"self": "http://example.com/api/articles/2"
},
"relationships": {
"author": {
"data": {
"id": "1",
"type": "person"
},
"links": {
"related": "http://example.com/api/articles/2/author",
"self": "http://example.com/api/articles/2/relationships/author"
}
}
},
"type": "article"
}
}
To fetch the link object for a to-one relationship, the request
GET /api/article/1/relationships/author HTTP/1.1
Host: example.com
Accept: application/vnd.api+json
yields the response
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"id": "1",
"type": "person"
}
}
To fetch the link objects for a to-many relationship, the request
GET /api/person/1/relationships/articles HTTP/1.1
Host: example.com
Accept: application/vnd.api+json
yields the response
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"id": "1",
"type": "article"
},
{
"id": "2",
"type": "article"
}
]
}
Specifying which fields appear in responses¶
For more information on client-side sparse fieldsets, see Sparse Fieldsets in the JSON API specification.
Warning
The server-side configuration for specifying which fields appear in resource objects as described in this section is simplistic; a better way to specify which fields are included in your responses is to use a Python object serialization library and specify custom serialization and deserialization functions as described in Custom serialization.
By default, all fields of your model will be exposed by the API. A client can
request that only certain fields appear in the resource object in a response to
a GET request by using the only
query parameter. On the
server side, you can specify which fields appear in the resource object
representation of an instance of the model by setting the only
, exclude
and additional_attributes
keyword arguments to the
APIManager.create_api()
method.
If only
is an iterable of column names or actual column attributes, only
those fields will appear in the resource object that appears in responses to
fetch instances of this model. If instead exclude
is specified, all fields
except those specified in that iterable will appear in responses. If
additional_attributes
is an iterable of column names, the values of these
attributes will also appear in the response; this is useful if you wish to see
the value of some attribute that is not a column or relationship.
Attention
The type
and id
elements will always appear in the resource object,
regardless of whether the server or the client tries to exclude them.
For example, if your models are defined like this (using Flask-SQLAlchemy):
class Person(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode)
birthday = db.Column(db.Date)
articles = db.relationship('Article')
# This class attribute is not a column.
foo = 'bar'
class Article(db.Model):
id = db.Column(db.Integer, primary_key=True)
author_id = db.Column(db.Integer, db.ForeignKey('person.id'))
and you want your resource objects to include only the values of the name
and birthday
columns, create your API with the following arguments:
apimanager.create_api(Person, only=['name', 'birthday'])
Now a request like
GET /api/person/1 HTTP/1.1
Host: example.com
Accept: application/vnd.api+json
yields the response
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"id": "1",
"links": {
"self": "http://example.com/api/person/1"
},
"attributes": {
"birthday": "1969-07-20",
"name": "foo"
},
"type": "person"
}
}
If you want your resource objects to exclude the birthday
and name
columns:
apimanager.create_api(Person, exclude=['name', 'birthday'])
Now the same request yields the response
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"id": "1",
"links": {
"self": "http://example.com/api/person/1"
}
"relationships": {
"articles": {
"data": [],
"links": {
"related": "http://example.com/api/person/1/articles",
"self": "http://example.com/api/person/1/links/articles"
}
},
},
"type": "person"
}
}
If you want your resource objects to include the value for the class attribute
foo
:
apimanager.create_api(Person, additional_attributes=['foo'])
Now the same request yields the response
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"attributes": {
"birthday": "1969-07-20",
"foo": "bar",
"name": "foo"
},
"id": "1",
"links": {
"self": "http://example.com/api/person/1"
}
"relationships": {
"articles": {
"data": [],
"links": {
"related": "http://example.com/api/person/1/articles",
"self": "http://example.com/api/person/1/links/articles"
}
}
},
"type": "person"
}
}
Sorting¶
Clients can sort according to the sorting protocol described in the Sorting section of the JSON API specification. Sorting by a nullable attribute will cause resources with null attributes to appear first.
If sort
parameter is not specified, data is sorted using the primary key
Clients can disable default sorting by using sort=0
Pagination¶
Pagination works as described in the JSON API specification, via the
page[number]
and page[size]
query parameters. Pagination respects
sorting and filtering. The first page is page one. If no page number
is specified by the client, the first page will be returned. By default,
pagination is enabled and the page size is ten. If the page size specified by
the client is greater than the maximum page size as configured on the server,
then the query parameter will be ignored.
To set the default page size for collections of resources, use the
page_size
keyword argument to the APIManager.create_api()
method. To
set the maximum page size that the client can request, use the
max_page_size
argument. Even if page_size
is greater than
max_page_size
, at most max_page_size
resources will be returned in a
page. If max_page_size
is set to anything but a positive integer, the
client will be able to specify arbitrarily large page sizes. If, further,
page_size
is set to anything but a positive integer, pagination will be
disabled by default, and any GET request that does not specify a
page size in its query parameters will get a response with all matching
results.
Attention
Disabling pagination can result in arbitrarily large responses!
For example, to set each page to include only two results:
apimanager.create_api(Person, page_size=2)
Then a GET request to /api/person?page[number]=2
would yield
the response
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"id": "3",
"type": "person",
"attributes": {
"name": "John"
}
}
{
"id": "4",
"type": "person",
"attributes": {
"name": "Paul"
}
}
],
"links": {
"first": "http://example.com/api/person?page[number]=1&page[size]=2",
"last": "http://example.com/api/person?page[number]=3&page[size]=2",
"next": "http://example.com/api/person?page[number]=3&page[size]=2",
"prev": "http://example.com/api/person?page[number]=1&page[size]=2",
"self": "http://example.com/api/person"
},
"meta": {
"total": 6
}
}
Filtering¶
Requests that would normally return a collection of resources can be filtered
so that only a subset of the resources are returned in a response. If the
client specifies the filter[objects]
query parameter, it must be a
URL encoded JSON list of filter objects, as described below.
Quick client examples for filtering¶
The following are some quick examples of making filtered GET
requests from different types of clients. More complete documentation is in
subsequent sections. In these examples, each client will filter by instances of
the model Person
whose names contain the letter “y”.
Using the Python requests library:
import requests
import json
url = 'http://127.0.0.1:5000/api/person'
headers = {'Accept': 'application/vnd.api+json'}
filters = [dict(name='name', op='like', val='%y%')]
params = {'filter[objects]': json.dumps(filters)}
response = requests.get(url, params=params, headers=headers)
assert response.status_code == 200
print(response.json())
Using jQuery:
var filters = [{"name": "id", "op": "like", "val": "%y%"}];
$.ajax({
data: {"filter[objects]": JSON.stringify(filters)},
headers: {
"Accept": JSONAPI_MIMETYPE
},
success: function(data) { console.log(data.objects); },
url: 'http://127.0.0.1:5000/api/person'
});
Using curl:
curl \
-G \
-H "Accept: application/vnd.api+json" \
-d "filter[objects]=[{\"name\":\"name\",\"op\":\"like\",\"val\":\"%y%\"}]" \
http://127.0.0.1:5000/api/person
The examples/
directory has more complete versions of these examples.
Filter objects¶
A filter object is a JSON object. Filter objects are defined recursively as follows. A filter object may be of the form
{"name": <field_name>, "op": <unary_operator>}
where <field_name>
is the name of a field on the model whose instances are
being fetched and <unary_operator>
is the name of one of the unary
operators supported by Flask-Restless. For example,
{"name": "birthday", "op": "is_null"}
A filter object may be of the form
{"name": <field_name>, "op": <binary_operator>, "val": <argument>}
where <binary_operator>
is the name of one of the binary operators
supported by Flask-Restless and <argument>
is the second argument to that
binary operator. For example,
{"name": "age", "op": "gt", "val": 23}
A filter object may be of the form
{"name": <field_name>, "op": <binary_operator>, "field": <field_name>}
The field
element indicates that the second argument to the binary operator
should be the value of that field. For example, to filter by resources that
have a greater width than height,
{"name": "width", "op": "gt", "field": "height"}
A filter object may be of the form
{"name": <relation_name>, "op": <relation_operator>, "val": <filter_object>}
where <relation_name>
is the name of a relationship on the model whose
resources are being fetched, <relation_operator>
is either "has"
, for a
to-one relationship, or "any"
, for a to-many relationship, and
<filter_object>
is another filter object. For example, to filter person
resources by only those people that have authored an article dated before
January 1, 2010,
{
"name": "articles",
"op": "any",
"val": {
"name": "date",
"op": "lt",
"val": "2010-01-01"
}
}
For another example, to filter article resources by only those articles that have an author of age at most fifty,
{
"name": "author",
"op": "has",
"val": {
"name": "age",
"op": "lte",
"val": 50
}
}
A filter object may be a conjunction (“and”) or disjunction (“or”) of other filter objects:
{"or": [<filter_object>, <filter_object>, ...]}
or
{"and": [<filter_object>, <filter_object>, ...]}
For example, to filter by resources that have width greater than height, and length of at least ten,
{
"and": [
{"name": "width", "op": "gt", "field": "height"},
{"name": "length", "op": "lte", "val": 10}
]
}
How are filter objects used in practice? To get a response in which only those resources that meet the requirements of the filter objects are returned, clients can make requests like this:
GET /api/person?filter[objects]=[{"name":"age","op":"<","val":18}] HTTP/1.1
Host: example.com
Accept: application/vnd.api+json
Operators¶
Flask-Restless understands the following operators, which correspond to the appropriate SQLAlchemy column operators.
==
,eq
,equals
,equals_to
!=
,neq
,does_not_equal
,not_equal_to
>
,gt
,<
,lt
>=
,ge
,gte
,geq
,<=
,le
,lte
,leq
in
,not_in
is_null
,is_not_null
like
,ilike
,not_like
has
any
Flask-Restless also understands the PostgreSQL network address operators
<<
, <<=
, >>
, >>=
, <>
, and &&
.
Warning
If you use a percent sign in the argument to the like
operator (for
example, %somestring%
), make sure it is percent-encoded, otherwise
the server may interpret the first few characters of that argument as a
percent-encoded character when attempting to decode the URL.
Filter object examples¶
Attribute greater than a value¶
On request
GET /api/person?filter[objects]=[{"name":"age","op":"gt","val":18}] HTTP/1.1
Host: example.com
Accept: application/vnd.api+json
the response will include only those Person
instances that have age
attribute greater than or equal to 18:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"attributes": {
"age": 19
},
"id": "2",
"links": {
"self": "http://example.com/api/person/2"
},
"type": "person"
},
{
"attributes": {
"age": 29
},
"id": "5",
"links": {
"self": "http://example.com/api/person/5"
},
"type": "person"
},
],
"links": {
"self": "/api/person?filter[objects]=[{\"name\":\"age\",\"op\":\"gt\",\"val\":18}]"
},
"meta": {
"total": 2
}
}
Arbitrary Boolean expression of filters¶
On request
GET /api/person?filter[objects]=[{"or":[{"name":"age","op":"lt","val":10},{"name":"age","op":"gt","val":20}]}] HTTP/1.1
Host: example.com
Accept: application/vnd.api+json
the response will include only those Person
instances that have age
attribute either less than 10 or greater than 20:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"attributes": {
"age": 9
},
"id": "1",
"links": {
"self": "http://example.com/api/person/1"
},
"type": "person"
},
{
"attributes": {
"age": 25
},
"id": "3",
"links": {
"self": "http://example.com/api/person/3"
},
"type": "person"
}
],
"links": {
"self": "/api/person?filter[objects]=[{\"or\":[{\"name\":\"age\",\"op\":\"lt\",\"val\":10},{\"name\":\"age\",\"op\":\"gt\",\"val\":20}]}]"
},
"meta": {
"total": 2
}
}
Comparing two attributes¶
On request
GET /api/box?filter[objects]=[{"name":"width","op":"ge","field":"height"}] HTTP/1.1
Host: example.com
Accept: application/vnd.api+json
the response will include only those Box
instances that have width
attribute greater than or equal to the value of the height
attribute:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"attributes": {
"height": 10,
"width": 20
}
"id": "1",
"links": {
"self": "http://example.com/api/box/1"
},
"type": "box"
},
{
"attributes": {
"height": 15,
"width": 20
}
"id": "2",
"links": {
"self": "http://example.com/api/box/2"
},
"type": "box"
}
],
"links": {
"self": "/api/box?filter[objects]=[{\"name\":\"width\",\"op\":\"ge\",\"field\":\"height\"}]"
},
"meta": {
"total": 100
}
}
Using has
and any
¶
On request
GET /api/person?filter[objects]=[{"name":"articles","op":"any","val":{"name":"date","op":"lt","val":"2010-01-01"}}] HTTP/1.1
Host: example.com
Accept: application/vnd.api+json
the response will include only those people that have authored an article dated before January 1, 2010 (assume in the example below that at least one of the article linkage objects refers to an article that has such a date):
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"id": "1",
"links": {
"self": "http://example.com/api/person/1"
},
"relationships": {
"articles": {
"data": [
{
"id": "1",
"type": "article"
},
{
"id": "2",
"type": "article"
}
],
"links": {
"related": "http://example.com/api/person/1/articles",
"self": "http://example.com/api/person/1/relationships/articles"
}
}
},
"type": "person"
}
],
"links": {
"self": "/api/person?filter[objects]=[{\"name\":\"articles\",\"op\":\"any\",\"val\":{\"name\":\"date\",\"op\":\"lt\",\"val\":\"2010-01-01\"}}]"
},
"meta": {
"total": 1
}
}
On request
GET /api/article?filter[objects]=[{"name":"author","op":"has","val":{"name":"age","op":"lte","val":50}}] HTTP/1.1
Host: example.com
Accept: application/vnd.api+json
the response will include only those articles that have an author of age at most fifty (assume in the example below that the author linkage objects refers to a person that has such an age):
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"id": "1",
"links": {
"self": "http://example.com/api/article/1"
},
"relationships": {
"author": {
"data": {
"id": "7",
"type": "person"
},
"links": {
"related": "http://example.com/api/article/1/author",
"self": "http://example.com/api/article/1/relationships/author"
}
}
},
"type": "article"
}
],
"links": {
"self": "/api/article?filter[objects]=[{\"name\":\"author\",\"op\":\"has\",\"val\":{\"name\":\"age\",\"op\":\"lte\",\"val\":50}}]"
},
"meta": {
"total": 1
}
}
Creating resources¶
For the purposes of concreteness in this section, suppose we have executed the following code on the server:
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.restless import APIManager
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy(app)
class Person(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode)
db.create_all()
manager = APIManager(app, session=db.session)
manager.create_api(Person, methods=['POST'])
To create a new resource, the request
POST /api/person HTTP/1.1
Host: example.com
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "person",
"attributes": {
"name": "foo"
}
}
}
yields the response
HTTP/1.1 201 Created
Location: http://example.com/api/person/1
Content-Type: application/vnd.api+json
{
"data": {
"attributes": {
"name": "foo"
},
"id": "1",
"jsonapi": {
{"version": "1.0"}
},
"links": {
"self": "http://example.com/api/person/bd34b544-ad39-11e5-a2aa-4cbb58b9ee34"
},
"meta": {},
"type": "person"
}
}
To create a new resource with a client-generated ID (if enabled by setting
allow_client_generated_ids
to True
in APIManager.create_api()
),
the request
POST /api/person HTTP/1.1
Host: example.com
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "person",
"id": "bd34b544-ad39-11e5-a2aa-4cbb58b9ee34",
"attributes": {
"name": "foo"
}
}
}
yields the response
HTTP/1.1 201 Created
Location: http://example.com/api/person/bd34b544-ad39-11e5-a2aa-4cbb58b9ee34
Content-Type: application/vnd.api+json
{
"data": {
"attributes": {
"name": "foo"
},
"id": "bd34b544-ad39-11e5-a2aa-4cbb58b9ee34",
"links": {
"self": "http://example.com/api/person/bd34b544-ad39-11e5-a2aa-4cbb58b9ee34"
},
"meta": {},
"jsonapi": {
{"version": "1.0"}
},
"type": "person"
}
}
The server always responds with 201 Created and a complete resource object on a request with a client-generated ID.
The server will respond with 400 Bad Request if the request specifies a field that does not exist on the model.
Deleting resources¶
For the purposes of concreteness in this section, suppose we have executed the following code on the server:
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.restless import APIManager
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy(app)
class Person(db.Model):
id = db.Column(db.Integer, primary_key=True)
db.create_all()
manager = APIManager(app, session=db.session)
manager.create_api(Person, method=['DELETE'])
To delete a resource, the request
DELETE /api/person/1 HTTP/1.1
Host: example.com
Accept: application/vnd.api+json
yields a 204 No Content response.
Updating resources¶
For the purposes of concreteness in this section, suppose we have executed the following code on the server:
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.restless import APIManager
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy(app)
class Person(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode)
class Article(db.Model):
id = db.Column(db.Integer, primary_key=True)
author_id = db.Column(db.Integer, db.ForeignKey('person.id'))
author = db.relationship(Person, backref=db.backref('articles'))
db.create_all()
manager = APIManager(app, session=db.sesion)
manager.create_api(Person, methods=['PATCH'])
manager.create_api(Article)
To update an existing resource, the request
PATCH /api/person/1 HTTP/1.1
Host: example.com
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "person",
"id": 1,
"attributes": {
"name": "foo"
}
}
}
yields a 204 No Content response.
If you set the allow_to_many_replacement
keyword argument of
APIManager.create_api()
to True
, you can replace a to-many
relationship entirely by making a request to update a resource. To update a
to-many relationship, the request
PATCH /api/person/1 HTTP/1.1
Host: example.com
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "person",
"id": 1,
"relationships": {
"articles": {
"data": [
{
"id": "1",
"type": "article"
},
{
"id": "2",
"type": "article"
}
]
}
}
}
}
yields a 204 No Content response.
The server will respond with 400 Bad Request if the request specifies a field that does not exist on the model.
Updating relationships¶
For the purposes of concreteness in this section, suppose we have executed the following code on the server:
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.restless import APIManager
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy(app)
class Person(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode)
class Article(db.Model):
id = db.Column(db.Integer, primary_key=True)
author_id = db.Column(db.Integer, db.ForeignKey('person.id'))
author = db.relationship(Person, backref=db.backref('articles'))
db.create_all()
manager = APIManager(app, session=db.session)
manager.create_api(Person, methods=['PATCH'])
manager.create_api(Article)
To update a to-one relationship, the request
PATCH /api/articles/1/relationships/author HTTP/1.1
Host: example.com
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "person",
"id": 1
}
}
yields a 204 No Content response.
To update a to-many relationship (if enabled by setting
allow_to_many_replacement
to True
in APIManager.create_api()
),
the request
PATCH /api/people/1/relationships/articles HTTP/1.1
Host: example.com
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": [
{
"type": "article",
"id": 1
},
{
"type": "article",
"id": 2
}
]
}
yields a 204 No Content response.
To add to a to-many relationship, the request
POST /api/person/1/relationships/articles HTTP/1.1
Host: example.com
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": [
{
"type": "article",
"id": 1
},
{
"type": "article",
"id": 2
}
]
}
yields a 204 No Content response.
To remove from a to-many relationship, the request
DELETE /api/person/1/links/articles HTTP/1.1
Host: example.com
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": [
{
"type": "article",
"id": 1
},
{
"type": "article",
"id": 2
}
]
}
yields a 204 No Content response.
To remove from a to-many relationship (if enabled by setting
allow_delete_from_to_many_relationships
to True
in
APIManager.create_api()
), the request
DELETE /api/person/1/relationships/articles HTTP/1.1
Host: example.com
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": [
{
"type": "article",
"id": 1
},
{
"type": "article",
"id": 2
}
]
}
yields a 204 No Content response.
Resource ID must be a string¶
As required by the JSON API, the ID (and type) of a resource must be a string in request and response documents. This does not mean that the primary key in the database must be a string, only that it will appear as a string in communications between the client and the server. For more information, see the Identification section of the JSON API specification.
Trailing slashes in URLs¶
API endpoints do not have trailing slashes. A GET request to,
for example, /api/person/
will result in a 404 Not Found response.
Date and time fields¶
Flask-Restless will automatically parse and convert date and time strings into the corresponding Python objects. Flask-Restless also understands intervals (also known as durations), if you specify the interval as an integer representing the number of units that the interval spans.
If you want the server to set the value of a date or time field of a model as
the current time (as measured at the server), use one of the special strings
"CURRENT_TIMESTAMP"
, "CURRENT_DATE"
, or "LOCALTIMESTAMP"
. When the
server receives one of these strings in a request, it will use the
corresponding SQL function to set the date or time of the field in the model.
Errors and error messages¶
Flask-Restless returns the error responses required by the JSON API
specification, and most other server errors yield a
400 Bad Request. Errors are included in the errors
element in the
top-level JSON document in the response body.
If a request triggers certain types of errors, the SQLAlchemy session will be rolled back. Currently these errors are
Cross-Origin Resource Sharing (CORS)¶
Cross-Origin Resource Sharing (CORS) is a protocol that allows JavaScript
HTTP clients to make HTTP requests across Internet domain boundaries while
still protecting against cross-site scripting (XSS) attacks. If you have access
to the HTTP server that serves your Flask application, I recommend configuring
CORS there, since such concerns are beyond the scope of Flask-Restless.
However, in case you need to support CORS at the application level, you should
create a function that adds the necessary HTTP headers after the request has
been processed by Flask-Restless (that is, just before the HTTP response is
sent from the server to the client) using the
flask.Blueprint.after_request()
method:
from flask import Flask
from flask_restless import APIManager
def add_cors_headers(response):
response.headers['Access-Control-Allow-Origin'] = 'example.com'
response.headers['Access-Control-Allow-Credentials'] = 'true'
# Set whatever other headers you like...
return response
app = Flask(__name__)
manager = APIManager(app)
blueprint = manager.create_api_blueprint('mypersonapi', Person)
blueprint.after_request(add_cors_headers)
app.register_blueprint(blueprint)
Customizing the ReSTful interface¶
HTTP methods¶
By default, the APIManager.create_api()
method creates a read-only
interface; requests with HTTP methods other than GET will cause
a response with 405 Method Not Allowed. To explicitly specify which methods
should be allowed for the endpoint, pass a list as the value of keyword
argument methods
:
apimanager.create_api(Person, methods=['GET', 'POST', 'DELETE'])
This creates an endpoint at /api/person
which responds to
GET, POST, and DELETE methods, but
not to PATCH.
If you allow GET requests, you will have access to endpoints of the following forms.
- GET /api/person¶
- GET /api/person/1¶
- GET /api/person/1/comments¶
- GET /api/person/1/relationships/comments¶
- GET /api/person/1/comments/2¶
The first four are described explicitly in the JSON API specification. The last is particular to Flask-Restless; it allows you to access a particular related resource via a relationship on another resource.
If you allow DELETE requests, you will have access to endpoints of the form
- DELETE /api/person/1¶
If you allow POST requests, you will have access to endpoints of the form
- POST /api/person¶
Finally, if you allow PATCH requests, you will have access to endpoints of the following forms.
- PATCH /api/person/1¶
- POST /api/person/1/relationships/comments¶
- PATCH /api/person/1/relationships/comments¶
- DELETE /api/person/1/relationships/comments¶
The last three allow the client to interact with the relationships of a
particular resource. The last two must be enabled explicitly by setting the
allow_to_many_replacement
and allow_delete_from_to_many_relationships
,
respectively, to True
when creating an API using the
APIManager.create_api()
method.
API prefix¶
To create an API at a prefix other than the default /api
, use the
url_prefix
keyword argument:
apimanager.create_api(Person, url_prefix='/api/v2')
Then your API for Person
will be available at /api/v2/person
.
Collection name¶
By default, the name of the collection that appears in the URLs of the API will
be the name of the table that backs your model. If your model is a SQLAlchemy
model, this will be the value of its __table__.name
attribute. If your
model is a Flask-SQLAlchemy model, this will be the lowercase name of the model
with camel case changed to all-lowercase with underscore separators. For
example, a class named MyModel
implies a collection name of
'my_model'
. Furthermore, the URL at which this collection is accessible by
default is /api/my_model
.
To provide a different name for the model, provide a string to the
collection_name keyword argument of the APIManager.create_api()
method:
apimanager.create_api(Person, collection_name='people')
Then the API will be exposed at /api/people
instead of /api/person
.
Note
According to the JSON API specification,
Note: This spec is agnostic about inflection rules, so the value of type can be either plural or singular. However, the same value should be used consistently throughout an implementation.
It’s up to you to make sure your collection names are either all plural or all singular!
Specifying one of many primary keys¶
If your model has more than one primary key (one called id
and one called
username
, for example), you should specify the one to use:
manager.create_api(User, primary_key='username')
If you do this, Flask-Restless will create URLs like /api/user/myusername
instead of /api/user/123
.
Enable bulk operations¶
Bulk operations via the JSON API Bulk extension are not yet supported.
Custom serialization¶
Flask-Restless-NG provides serialization and deserialization that work with the
JSON API specification. If you wish to have more control over the way
instances of your models are converted to Python dictionary representations,
you can specify a custom serialization class by providing it to
APIManager.create_api()
via the serializer
keyword argument.
Similarly, to provide a deserialization function that converts a Python
dictionary representation to an instance of your model, use the
deserializer
keyword argument. However, if you provide a serializer that
fails to produce resource objects that satisfy the JSON API specification, your
client will receive non-compliant responses!
Define your serialization functions like this:
from flask_restless import Serializer
class CustomSerializer(Serializer):
def __init__(attributes, relationships):
self.attributes = attributes
self.relationships = relationships
@property
def attributes_columns(self):
return set(self.attributes)
@property
def relationship_columns(self):
return set(self.relationships)
def serialize(self, instance, only=None):
return {'id': instance.id, 'type': 'custom', 'attributes': {}}
instance
is an instance of a SQLAlchemy model and the only
argument is
a list; only the fields (that is, the attributes and relationships) whose names
appear as strings in only should appear in the returned dictionary. The only
exception is that the keys 'id'
and 'type'
must always appear,
regardless of whether they appear in only. The function must return a
dictionary representation of the resource object.
For deserialization, define your custom deserialization function like this:
from flask_restless import Deserializer
class DefaultDeserializer(Deserializer):
def deserialize(self, document):
return Person(...)
document
is a dictionary representation of the complete incoming JSON API
document, where the data
element contains the primary resource object. The
function must return an instance of the model that has the requested fields.
For example, if you create schema for your database models using Marshmallow, then you use that library’s built-in serialization functions as follows:
class PersonSchema(Schema):
id = fields.Integer()
name = fields.String()
def make_object(self, data):
return Person(**data)
person_schema = PersonSchema()
class PersonSerializer(Serializer):
@property
def attributes_columns(self):
return {'name'}
@property
def relationship_columns(self):
return set()
def serialize(instance, only=None):
return person_schema.dump(instance).data
class PersonDeserializer(Deserializer):
def deserialize(self, document):
return person_schema.load(data).data
person_serializer = PersonSerializer()
person_deserializer = PersonDeserializer()
manager = APIManager(app, session=session)
manager.create_api(Person, methods=['GET', 'POST'],
serializer=person_serializer,
deserializer=person_deserializer)
Per-model serialization¶
The correct serialization function will be used for each type of SQLAlchemy
model for which you invoke APIManager.create_api()
. For example, if you
create two APIs, one for Person
objects and one for Article
objects,
manager.create_api(Person, serializer=person_serializer)
manager.create_api(Article, serializer=article_serializer)
and then make a request like
GET /api/article/1?include=author HTTP/1.1
Host: example.com
Accept: application/vnd.api+json
then Flask-Restless-NG will use the article_serializer
instance to serialize
the primary data (that is, the top-level data
element in the response
document) and the person_serializer
to serialize the included Person
resource.
Capturing validation errors¶
By default, no validation is performed by Flask-Restless; if you want validation, implement it yourself in your database models. However, by specifying a list of exceptions raised by your backend on validation errors, Flask-Restless-NG will forward messages from raised exceptions to the client in an error response.
For example, if your validation framework includes an exception called
ValidationError
, then call the APIManager.create_api()
method with
the validation_exceptions
keyword argument:
from cool_validation_framework import ValidationError
apimanager.create_api(Person, validation_exceptions=[ValidationError],
methods=['PATCH', 'POST'])
Note
Currently, Flask-Restless expects that an instance of a specified validation
error will have a errors
attribute, which is a dictionary mapping field
name to error description (note: one error per field). If you have a better,
more general solution to this problem, please visit our issue tracker.
Now when you make POST and PATCH requests with invalid fields, the JSON response will look like this:
HTTP/1.1 400 Bad Request
{
"errors": [
{
"status": 400,
"title": "Validation error",
"detail": "age: must be an integer"
}
]
}
Request preprocessors and postprocessors¶
To apply a function to the request parameters and/or body before the request is
processed, use the preprocessors
keyword argument. To apply a function to
the response data after the request is processed (immediately before the
response is sent), use the postprocessors
keyword argument. Both
preprocessors
and postprocessors
must be a dictionary which maps HTTP
method names as strings (with exceptions as described below) to a list of
functions. The specified functions will be applied in the order given in the
list.
There are many different routes on which you can apply preprocessors and postprocessors, depending on HTTP method type, whether the client is accessing a resource or a relationship, whether the client is accessing a collection or a single resource, etc.
This table states the preprocessors that apply to each type of endpoint.
preprocessor name
applies to URLs like…
GET_COLLECTION
/api/person
GET_RESOURCE
/api/person/1
GET_RELATION
/api/person/1/articles
GET_RELATED_RESOURCE
/api/person/1/articles/2
DELETE_RESOURCE
/api/person/1
POST_RESOURCE
/api/person
PATCH_RESOURCE
/api/person/1
GET_RELATIONSHIP
/api/person/1/relationships/articles
DELETE_RELATIONSHIP
/api/person/1/relationships/articles
POST_RELATIONSHIP
/api/person/1/relationships/articles
PATCH_RELATIONSHIP
/api/person/1/relationships/articles
This table states the postprocessors that apply to each type of endpoint.
postprocessor name
applies to URLs like…
GET_COLLECTION
/api/person
GET_RESOURCE
/api/person/1
GET_TO_MANY_RELATION
/api/person/1/articles
GET_TO_ONE_RELATION
/api/articles/1/author
GET_RELATED_RESOURCE
/api/person/1/articles/2
DELETE_RESOURCE
/api/person/1
POST_RESOURCE
/api/person
PATCH_RESOURCE
/api/person/1
GET_TO_MANY_RELATIONSHIP
/api/person/1/relationships/articles
GET_TO_ONE_RELATIONSHIP
/api/articles/1/relationships/author
GET_RELATIONSHIP
/api/person/1/relationships/articles
DELETE_RELATIONSHIP
/api/person/1/relationships/articles
POST_RELATIONSHIP
/api/person/1/relationships/articles
PATCH_RELATIONSHIP
/api/person/1/relationships/articles
Each type of preprocessor or postprocessor requires different arguments. For preprocessors:
preprocessor name
keyword arguments
GET_COLLECTION
filters
,sort
GET_RESOURCE
resource_id
GET_RELATION
resource_id
,relation_name
,filters
,sort
GET_RELATED_RESOURCE
resource_id
,relation_name
,related_resource_id
DELETE_RESOURCE
resource_id
POST_RESOURCE
data
PATCH_RESOURCE
resource_id
,data
GET_RELATIONSHIP
resource_id
,relation_name
DELETE_RELATIONSHIP
resource_id
,relation_name
POST_RELATIONSHIP
resource_id
,relation_name
,data
PATCH_RELATIONSHIP
resource_id
,relation_name
,data
For postprocessors:
postprocessor name
keyword arguments
GET_COLLECTION
result
,filters
,sort
GET_RESOURCE
result
GET_TO_MANY_RELATION
result
,filters
,sort
GET_TO_ONE_RELATION
result
GET_RELATED_RESOURCE
result
DELETE_RESOURCE
was_deleted
POST_RESOURCE
result
PATCH_RESOURCE
result
GET_TO_MANY_RELATIONSHIP
result
,filters
,sort
GET_TO_ONE_RELATIONSHIP
result
DELETE_RELATIONSHIP
was_deleted
POST_RELATIONSHIP
none
PATCH_RELATIONSHIP
none
How can one use these tables to create a preprocessor or postprocessor? If you
want to create a preprocessor that will be applied on GET
requests to /api/person
, first define a function that accepts the keyword
arguments you need, and has a **kw
argument for any additional keyword
arguments (and any new arguments that may appear in future versions of
Flask-Restless):
def fetch_preprocessor(filters=None, sort=None, **kw):
# Here perform any application-specific code...
Next, instruct these preprocessors to be applied by Flask-Restless by using the
preprocessors
keyword argument to APIManager.create_api()
. The value
of this argument must be a dictionary in which each key is a string containing
a processor name and each value is a list of functions to be applied for that
request:
preprocessors = {'GET_COLLECTION': [fetch_preprocessor]}
manager.create_api(Person, preprocessors=preprocessors)
For preprocessors for endpoints of the form /api/person/1
, a returned value
will be interpreted as the resource ID for the request. (Remember, as described
in Resource ID must be a string, the returned ID must be a string.) For example, if a
preprocessor for a GET request to /api/person/1
returns the
string 'foo'
, then Flask-Restless will behave as if the request were
originally for the URL /api/person/foo
. For preprocessors for endpoints of
the form /api/person/1/articles
or
/api/person/1/relationships/articles
, the function can return either one
value, in which case the resource ID will be replaced with the return value, or
a two-tuple, in which case both the resource ID and the relationship name will
be replaced. Finally, for preprocessors for endpoints of the form
/api/person/1/articles/2
, the function can return one, two, or three
values; if three values are returned, the resource ID, the relationship name,
and the related resource ID are all replaced. (If multiple preprocessors are
specified for a single HTTP method and each one has a return value,
Flask-Restless will only remember the value returned by the last preprocessor
function.)
Those preprocessors and postprocessors that accept dictionaries as parameters
can (and should) modify their arguments in-place. That means the changes made
to, for example, the result
dictionary will be seen by the Flask-Restless
view functions and ultimately returned to the client.
Note
For more information about the filters
keyword arguments,
see Filtering. For more information about sort
keyword argument, see Sorting.
In order to halt the preprocessing or postprocessing and return an error
response directly to the client, your preprocessor or postprocessor functions
can raise a ProcessingException
. If a function raises this exception, no
preprocessing or postprocessing functions that appear later in the list
specified when the API was created will be invoked. For example, an
authentication function can be implemented like this:
def check_auth(resource_id=None, **kw):
# Here, get the current user from the session.
current_user = ...
# Next, check if the user is authorized to modify the specified
# instance of the model.
if not is_authorized_to_modify(current_user, instance_id):
raise ProcessingException(detail='Not Authorized', status=401)
manager.create_api(Person, preprocessors=dict(GET_RESOURCE=[check_auth]))
The ProcessingException
allows you to specify as keyword arguments to
the constructor the elements of the JSON API error object. If no arguments
are provided, the error is assumed to have status code 400 Bad Request.
Universal preprocessors and postprocessors¶
New in version 0.13.0.
The previous section describes how to specify a preprocessor or postprocessor
on a per-API (that is, a per-model) basis. If you want a function to be
executed for all APIs created by a APIManager
, you can use the
preprocessors
or postprocessors
keyword arguments in the constructor of
the APIManager
class. These keyword arguments have the same format as
the corresponding ones in the APIManager.create_api()
method as described
above. Functions specified in this way are prepended to the list of
preprocessors or postprocessors specified in the APIManager.create_api()
method.
This may be used, for example, if all POST requests require authentication:
from flask import Flask
from flask_restless import APIManager
from flask_restless import ProcessingException
from flask.ext.login import current_user
from mymodels import User
from mymodels import session
def auth_func(*args, **kw):
if not current_user.is_authenticated():
raise ProcessingException(detail='Not authenticated', status=401)
app = Flask(__name__)
preprocessors = {'POST_RESOURCE': [auth_func]}
api_manager = APIManager(app, session=session, preprocessors=preprocessors)
api_manager.create_api(User)
Preprocessors for collections¶
When the server receives, for example, a GET request for
/api/person
, Flask-Restless interprets this request as a search with no
filters (that is, a search for all instances of Person
without
exception). In other words, a GET request to /api/person
is
roughly equivalent to the same request to
/api/person?filter[objects]=[]
. Therefore, if you want to filter the set of
Person
instances returned by such a request, you can create a
GET_COLLECTION
preprocessor that appends filters to the filters
keyword argument. For example:
def preprocessor(filters=None, **kw):
# This checks if the preprocessor function is being called before a
# request that does not have search parameters.
if filters is None:
return
# Create the filter you wish to add; in this case, we include only
# instances with ``id`` not equal to 1.
filt = dict(name='id', op='neq', val=1)
# *Append* your filter to the list of filters.
filters.append(filt)
preprocessors = {'GET_COLLECTION': [preprocessor]}
manager.create_api(Person, preprocessors=preprocessors)
Custom queries¶
In cases where it is not possible to use preprocessors or postprocessors
(Request preprocessors and postprocessors) efficiently, you can provide a custom query
attribute
to your model instead. The attribute can either be a SQLAlchemy query
expression or a class method that returns a SQLAlchemy query
expression. Flask-Restless will use this query
attribute internally,
however it is defined, instead of the default session.query(Model)
(in the
pure SQLAlchemy case) or Model.query
(in the Flask-SQLAlchemy
case). Flask-Restless uses a query during most GET and
PATCH requests to find the model(s) being requested.
You may want to use a custom query attribute if you want to reveal only certain information to the client. For example, if you have a set of people and you only want to reveal information about people from the group named “students”, define a query class method this way:
class Group(Base):
__tablename__ = 'group'
id = Column(Integer, primary_key=True)
groupname = Column(Unicode)
people = relationship('Person')
class Person(Base):
__tablename__ = 'person'
id = Column(Integer, primary_key=True)
group_id = Column(Integer, ForeignKey('group.id'))
group = relationship('Group')
@classmethod
def query(cls):
original_query = session.query(cls)
condition = (Group.groupname == 'students')
return original_query.join(Group).filter(condition)
Then GET requests to, for example, /api/person
will only
reveal instances of Person
who also are in the group named “students”.
Requiring authentication for some methods¶
If you want certain HTTP methods to require authentication, use preprocessors:
from flask import Flask
from flask.ext.restless import APIManager
from flask.ext.restless import ProcessingException
from flask.ext.login import current_user
from mymodels import User
def auth_func(*args, **kwargs):
if not current_user.is_authenticated():
raise ProcessingException(detail='Not authenticated', status=401)
app = Flask(__name__)
api_manager = APIManager(app)
# Set `auth_func` to be a preprocessor for any type of endpoint you want to
# be guarded by authentication.
preprocessors = {'GET_RESOURCE': [auth_func], ...}
api_manager.create_api(User, preprocessors=preprocessors)
For a more complete example using Flask-Login, see the
examples/server_configurations/authentication
directory in the source
distribution, or view the authentication example online.
API reference¶
A technical description of the classes, functions, and idioms of Flask-Restless.
API¶
This part of the documentation documents all the public classes and functions in Flask-Restless-NG.
The API Manager class¶
- class flask_restless.APIManager(app=None, session=None, preprocessors=None, postprocessors=None, url_prefix='/api', include_links: bool = False)¶
Provides a method for creating a public ReSTful JSON API with respect to a given
Flask
application object.The
Flask
object can either be specified in the constructor, or after instantiation time by calling theinit_app()
method.app is the
Flask
object containing the user’s Flask application.session is the
Session
object in which changes to the database will be made.For example, to use this class with models defined in pure SQLAlchemy:
from flask import Flask from flask.ext.restless import APIManager from sqlalchemy import create_engine from sqlalchemy.orm.session import sessionmaker engine = create_engine('sqlite:////tmp/mydb.sqlite') Session = sessionmaker(bind=engine) my_session = Session() app = Flask(__name__) api_manager = APIManager(app, session=my_session)
url_prefix is the URL prefix at which each API created by this instance will be accessible. For example, if this is set to
'foo'
, then this method creates endpoints of the form/foo/<collection_name>
whencreate_api()
is called. If the url_prefix is set in thecreate_api()
, the URL prefix set in the constructor will be ignored for that endpoint.postprocessors and preprocessors must be dictionaries as described in the section Request preprocessors and postprocessors. These preprocessors and postprocessors will be applied to all requests to and responses from APIs created using this APIManager object. The preprocessors and postprocessors given in these keyword arguments will be prepended to the list of processors given for each individual model when using the
create_api_blueprint()
method (more specifically, the functions listed here will be executed before any functions specified in thecreate_api_blueprint()
method). For more information on using preprocessors and postprocessors, see Request preprocessors and postprocessors.include_links controls whether to include link objects in resource objects https://jsonapi.org/format/#document-links
- init_app(app)¶
Registers any created APIs on the given Flask application.
This function should only be called if no Flask application was provided in the app keyword argument to the constructor of this class.
When this function is invoked, any blueprint created by a previous invocation of
create_api()
will be registered on app by calling theregister_blueprint()
method.To use this method with pure SQLAlchemy, for example:
from flask import Flask from flask_restless import APIManager from sqlalchemy import create_engine from sqlalchemy.orm.session import sessionmaker engine = create_engine('sqlite:////tmp/mydb.sqlite') Session = sessionmaker(bind=engine) mysession = Session() # Here create model classes, for example User, Comment, etc. ... # Create the API manager and create the APIs. api_manager = APIManager(session=mysession) api_manager.create_api(User) api_manager.create_api(Comment) # Later, call `init_app` to register the blueprints for the # APIs created earlier. app = Flask(__name__) api_manager.init_app(app)
and with models defined with Flask-SQLAlchemy:
from flask import Flask from flask_restless import APIManager from flask_sqlalchemy import SQLAlchemy db = SQLALchemy(app) # Here create model classes, for example User, Comment, etc. ... # Create the API manager and create the APIs. api_manager = APIManager(session=db.session) api_manager.create_api(User) api_manager.create_api(Comment) # Later, call `init_app` to register the blueprints for the # APIs created earlier. app = Flask(__name__) api_manager.init_app(app)
- create_api(*args, **kw)¶
Creates and possibly registers a ReSTful API blueprint for the given SQLAlchemy model.
If a Flask application was provided in the constructor of this class, the created blueprint is immediately registered on that application. Otherwise, the blueprint is stored for later registration when the
init_app()
method is invoked. In that case, the blueprint will be registered each time theinit_app()
method is invoked.The keyword arguments for this method are exactly the same as those for
create_api_blueprint()
, and are passed directly to that method. However, unlike that method, this method accepts only a single positional argument, model, the SQLAlchemy model for which to create the API. A UUID will be automatically generated for the blueprint name.For example, if you only wish to create APIs on a single Flask application:
app = Flask(__name__) session = ... # create the SQLAlchemy session manager = APIManager(app=app, session=session) manager.create_api(User)
If you want to create APIs before having access to a Flask application, you can call this method before calling
init_app()
:session = ... # create the SQLAlchemy session manager = APIManager(session=session) manager.create_api(User) # later... app = Flask(__name__) manager.init_app(app)
If you want to create an API and register it on multiple Flask applications, you can call this method once and
init_app()
multiple times with different app arguments:session = ... # create the SQLAlchemy session manager = APIManager(session=session) manager.create_api(User) # later... app1 = Flask('application1') app2 = Flask('application2') manager.init_app(app1) manager.init_app(app2)
- create_api_blueprint(name: str, model, methods=frozenset({'GET'}), url_prefix: Optional[str] = None, collection_name: Optional[str] = None, only=None, exclude=None, additional_attributes=None, validation_exceptions=None, page_size: int = 10, max_page_size: int = 100, preprocessors=None, postprocessors=None, primary_key: Optional[str] = None, serializer: Optional[Serializer] = None, deserializer: Optional[Deserializer] = None, includes=None, allow_to_many_replacement: bool = False, allow_delete_from_to_many_relationships: bool = False, allow_client_generated_ids: bool = False)¶
Creates and returns a ReSTful API interface as a blueprint, but does not register it on any
flask.Flask
application.The endpoints for the API for
model
will be available at<url_prefix>/<collection_name>
. If collection_name isNone
, the lowercase name of the provided model class will be used instead, as accessed bymodel.__table__.name
. (If any black magic was performed onmodel.__table__
, this will be reflected in the endpoint URL.) For more information, see Collection name.This function must be called at most once for each model for which you wish to create a ReSTful API. Its behavior (for now) is undefined if called more than once.
This function returns the
flask.Blueprint
object that handles the endpoints for the model. The returnedBlueprint
has not been registered with theFlask
application object specified in the constructor of this class, so you will need to register it yourself to make it available on the application. If you don’t need access to theBlueprint
object, usecreate_api_blueprint()
instead, which handles registration automatically.name is the name of the blueprint that will be created.
model is the SQLAlchemy model class for which a ReSTful interface will be created.
app is the
Flask
object on which we expect the blueprint created in this method to be eventually registered. If not specified, the Flask application specified in the constructor of this class is used.methods is a list of strings specifying the HTTP methods that will be made available on the ReSTful API for the specified model.
If
'GET'
is in the list, GET requests will be allowed at endpoints for collections of resources, resources, to-many and to-one relations of resources, and particular members of a to-many relation. Furthermore, relationship information will be accessible. For more information, see Fetching resources and relationships.If
'POST'
is in the list, POST requests will be allowed at endpoints for collections of resources. For more information, see Creating resources.If
'DELETE'
is in the list, DELETE requests will be allowed at endpoints for individual resources. For more information, see Deleting resources.If
'PATCH'
is in the list, PATCH requests will be allowed at endpoints for individual resources. Replacing a to-many relationship when issuing a request to update a resource can be enabled by settingallow_to_many_replacement
toTrue
.Furthermore, to-one relationships can be updated at the relationship endpoints under an individual resource via PATCH requests. This also allows you to add to a to-many relationship via the POST method, delete from a to-many relationship via the DELETE method (if
allow_delete_from_to_many_relationships
is set toTrue
), and replace a to-many relationship via the PATCH method (ifallow_to_many_replacement
is set toTrue
). For more information, see Updating resources and Updating relationships.
The default set of methods provides a read-only interface (that is, only GET requests are allowed).
url_prefix is the URL prefix at which this API will be accessible. For example, if this is set to
'/foo'
, then this method creates endpoints of the form/foo/<collection_name>
. If not set, the default URL prefix specified in the constructor of this class will be used. If that was not set either, the default'/api'
will be used.collection_name is the name of the collection specified by the given model class to be used in the URL for the ReSTful API created. If this is not specified, the lowercase name of the model will be used. For example, if this is set to
'foo'
, then this method creates endpoints of the form/api/foo
,/api/foo/<id>
, etc.If only is not
None
, it must be a list of columns and/or relationships of the specified model, given either as strings or as the attributes themselves. If it is a list, only these fields will appear in the resource object representation of an instance of model. In other words, only is a allowlist of fields. Theid
andtype
elements of the resource object will always be present regardless of the value of this argument. If only contains a string that does not name a column in model, it will be ignored.If additional_attributes is a list of strings, these attributes of the model will appear in the JSON representation of an instance of the model. This is useful if your model has an attribute that is not a SQLAlchemy column but you want it to be exposed. If any of the attributes does not exist on the model, a
AttributeError
is raised.If exclude is not
None
, it must be a list of columns and/or relationships of the specified model, given either as strings or as the attributes themselves. If it is a list, all fields except these will appear in the resource object representation of an instance of model. In other words, exclude is an blocklist of fields. Theid
andtype
elements of the resource object will always be present regardless of the value of this argument. If exclude contains a string that does not name a column in model, it will be ignored.If either only or exclude is not
None
, exactly one of them must be specified; if both are notNone
, then this function will raise aIllegalArgumentError
.See Specifying which fields appear in responses for more information on specifying which fields will be included in the resource object representation.
validation_exceptions is the tuple of possible exceptions raised by validation of your database models. If this is specified, validation errors will be captured and forwarded to the client in the format described by the JSON API specification. For more information on how to use validation, see Capturing validation errors.
page_size must be a positive integer that represents the default page size for responses that consist of a collection of resources. Requests made by clients may override this default by specifying
page_size
as a query parameter. max_page_size must be a positive integer that represents the maximum page size that a client can request. Even if a client specifies that greater than max_page_size should be returned, at most max_page_size results will be returned. For more information, see Pagination.serializer and deserializer are custom serialization classes. See Custom serialization.
preprocessors is a dictionary mapping strings to lists of functions. Each key represents a type of endpoint (for example,
'GET_RESOURCE'
or'GET_COLLECTION'
). Each value is a list of functions, each of which will be called before any other code is executed when this API receives the corresponding HTTP request. The functions will be called in the order given here. The postprocessors keyword argument is essentially the same, except the given functions are called after all other code. For more information on preprocessors and postprocessors, see Request preprocessors and postprocessors.primary_key is a string specifying the name of the column of model to use as the primary key for the purposes of creating URLs. If the model has exactly one primary key, there is no need to provide a value for this. If model has two or more primary keys, you must specify which one to use. For more information, see Specifying one of many primary keys.
includes must be a list of strings specifying which related resources will be included in a compound document by default when fetching a resource object representation of an instance of model. Each element of includes is the name of a field of model (that is, either an attribute or a relationship). For more information, see Inclusion of related resources.
If allow_to_many_replacement is
True
and this API allows PATCH requests, the server will allow two types of requests. First, it allows the client to replace the entire collection of resources in a to-many relationship when updating an individual instance of the model. Second, it allows the client to replace the entire to-many relationship when making a PATCH request to a to-many relationship endpoint. This isFalse
by default. For more information, see Updating resources and Updating relationships.If allow_delete_from_to_many_relationships is
True
and this API allows PATCH requests, the server will allow the client to delete resources from any to-many relationship of the model. This isFalse
by default. For more information, see Updating relationships.If allow_client_generated_ids is
True
and this API allows POST requests, the server will allow the client to specify the ID for the resource to create. JSON API recommends that this be a UUID. This isFalse
by default. For more information, see Creating resources.If allow_functions is
True
, then GET requests to/api/eval/<collection_name>
will return the result of evaluating SQL functions specified in the body of the request. For information on the request format, see functionevaluation. This isFalse
by default.Warning
This is deprecated and going to be removed in the next major version
Warning
If
allow_functions
isTrue
, you must not create an API for a model whose name is'eval'
.
Serialization helpers¶
- class flask_restless.Serializer¶
An object that, when called, returns a dictionary representation of a given instance of a SQLAlchemy model.
- class flask_restless.Deserializer(session, model, api_manager)¶
An object that, when called, returns an instance of the SQLAlchemy model specified at instantiation time.
session is the SQLAlchemy session in which to look for any related resources.
model is the class of which instances will be created by the
__call__()
method.This is a base class with no implementation.
- class flask_restless.SerializationException(instance, message=None, resource=None, resource_type=None, resource_id=None, *args, **kw)¶
Raised when there is a problem serializing an instance of a SQLAlchemy model to a dictionary representation.
instance is the (problematic) instance on which
Serializer.__call__()
was invoked.message is an optional string describing the problem in more detail.
resource is an optional partially-constructed serialized representation of
instance
.Each of these keyword arguments is stored in a corresponding instance attribute so client code can access them.
- class flask_restless.DeserializationException(*args, **kw)¶
Raised when there is a problem deserializing a Python dictionary to an instance of a SQLAlchemy model.
Subclasses that wish to provide more detailed about the problem should set the
detail
attribute to be a string, either as a class-level attribute or as an instance attribute.
Pre- and postprocessor helpers¶
- class flask_restless.ProcessingException(id_=None, links=None, status=400, code=None, title=None, detail=None, source=None, meta=None, *args, **kw)¶
Raised when a preprocessor or postprocessor encounters a problem.
This exception should be raised by functions supplied in the
preprocessors
andpostprocessors
keyword arguments toAPIManager.create_api
. When this exception is raised, all preprocessing or postprocessing halts, so any processors appearing later in the list will not be invoked.The keyword arguments
id_
,href
status
,code
,title
,detail
,links
,paths
correspond to the elements of the JSON API error object; the values of these keyword arguments will appear in the error object returned to the client.Any additional positional or keyword arguments are supplied directly to the superclass,
werkzeug.exceptions.HTTPException
.
Additional information¶
Meta-information on Flask-Restless.
Similar projects¶
If Flask-Restless doesn’t work for you, here are some similar Python packages that intend to simplify the creation of ReSTful APIs (in various combinations of Web frameworks and database backends):
Copyright and license¶
Flask-Restless is copyright 2011 Lincoln de Sousa and copyright 2012, 2013, 2014, 2015, 2016 Jeffrey Finkelstein and contributors, and is dual-licensed under the following two copyright licenses:
the GNU Affero General Public License, either version 3 or (at your option) any later version
the 3-clause BSD License
For more information, see the files LICENSE.AGPL
and
LICENSE.BSD
in top-level directory of the source distribution.
The artwork for Flask-Restless is copyright 2012 Jeffrey Finkelstein. The couch logo is licensed under the Creative Commons Attribute-ShareAlike 4.0 license. The original image is a scan of a (now public domain) illustration by Arthur Hopkins in a serial edition of “The Return of the Native” by Thomas Hardy published in October 1878. The couch logo with the “Flask-Restless” text is licensed under the Flask Artwork License.
The documentation is licensed under the Creative Commons Attribute-ShareAlike 4.0 license.
Changelog¶
Changes in Flask-Restless-NG¶
Unreleased¶
Dropped savalidation support
Version 3.1.0 (2023-10-14):¶
Added support for Flask 3.0
Added support for Python 3.11
Added support for Python 3.12
Dropped support for Python 3.7
Version 3.0.0 (2023-03-19):¶
Added support for SQLAlchemy 2.0
Minimum required SQLAlchemy version: 1.4.18
Minimum required Flask version: 2.2
Drop Functions API support
Version 2.5.1 (2023-03-15):¶
Restricted SQLAlchemy to <2.0. Support of 2.0 requires significant changes and will be a major release
Version 2.5.0 (2022-12-24):¶
Added support for X-Forwarded- headers (pagination links now use the original host/proto) (#38)
Version 2.4.0 (2022-11-11):¶
Clients can disable default sorting by using sort=0 query parameter
Version 2.3.1:¶
Fix for an incorrect error message
Returns 500 instead 400 response code in case of serialization errors in POST requests
Fix for pagination Flask-SQLAlchemy (now works also with 3.0+) (#37)
Version 2.3.0¶
Allow sorting of nested fields
Version 2.2.9¶
Do not erase type field from attributes (#31)
Version 2.2.8¶
Make sure that POST response contains actual values from the DB
Version 2.2.7¶
Fix Server Error for null relationship in POST (#29)
Allow session rollback in PATCH_RESOURCE, PATCH_RELATIONSHIP, POST_RELATIONSHIP post-processors (#28)
Version 2.2.6¶
Escape user input in error messages to prevent potential server-side cross-site scripting
‘status’ field in error objects is now a string, as required by JSON API standard https://jsonapi.org/format/#error-objects
Returns ‘400’ status if page number is provided but page size is 0
Returns ‘400’ status if unknown field was used for sorting
Allow rolling back the current session in POST_RESOURCE postprocessors (#28)
Version 2.2.5¶
Fix for #27 ‘relationship with secondary generates incorrect query’
Version 2.2.4¶
Do not log exceptions for user related errors (bad query, etc)
Update safe check for selectinload for `includes
Update SQLAlchemy dependency to 1.3.6+
Version 2.2.3¶
Add safe check for selectinload for includes
Version 2.2.2¶
Fix an incorrect selectinload query for models with custom select queries
Version 2.2.1¶
Minor improvements and fixes
Version 2.2.0¶
Serialize To-One relationships using foreign key, instead of trying to fetch the whole
relationship object from the database
Version 2.1.1¶
Only fetch primary keys from a database for relationships when no filtering is required
Version 2.1.0¶
Re-added FunctionsAPI until the next major release to let users to implement an alternative #23
Version 2.0.3¶
- Fix: #26 - selectinload is broken for models that have primary keys other than ‘id’. Disabled until a new schema is
implemented
Make ‘primary_key’ optional again #25 (by @tanj)
Version 2.0.2¶
Fixed import for SQLAlchemy 1.3 #22
Version 2.0.0¶
Refactored fetching resource collections: - SQL query optimizations for ‘include’ and ‘relationship’ objects, using selectinload
(3x-5x performance improvement when tested on large datasets)
New parameter ‘include_links’ which controls should relationship objects include links. They are not required by JSON API, and disabling them significantly improves performance
New interfaces for Serializer and Deserializer classes.
APIManager requires Serializer/Deserializer objects instead of functions for serializer/deserializer options
Deprecations: - ‘single’ parameter is no longer supported - makes code complicated, is not defined in JSON API specs and can be easily
replicated on a client side
‘group’ parameter is not longer supported - not defined in JSON API specifications, confusing and broken for PostrgeSQL
JSONP callbacks are no longer supported - please reach out if you have a use case for them
Version 1.0.6¶
Prevent redundant SQL queries during pagination and resource inclusion
Version 1.0.5¶
#16 - Fix: including child of empty relationship (by @sharky98)
Version 1.0.4¶
#15: Support SQLAlchemy 1.4.x
Version 1.0.2¶
#1, #13: Fix for relationship updates not being committed (by @sharky98)
#12: Fix for 500 when trying to include Null/None relationship
Added TSQuery operator (by @augustin)
Version 1.0.1¶
#4: id is an optional attribute as long as Model has a primary key
#6: Fix for flask_restless.views not being included in the installed package.
Version 1.0.0¶
Performance improvement: url_for() changed to build url locally instead of delegating it to Flask
This is the last release that is backward compatible with the original Flask-Restless API.
Version 0.0.2¶
New serializer (2-3x faster)
Added lru_cache to helpers to reduce number of recursive calls (better performance)
Version 0.0.1¶
Fixed 1.0+ compatibility
Fix for hybrid_property
Original Flask-Restless¶
You can find the full changelog in the original repo