Building an Application¶
Project Structure¶
When building applications with Flask-RESTy, we recommend starting with the following project structure.
example
├── __init__.py
├── settings.py # App settings
├── models.py # SQLAlchemy models
├── schemas.py # marshmallow schemas
├── auth.py # Authn and authz classes
├── views.py # View classes
└── routes.py # Route declarations
The __init__.py
file initializes the Flask
app and hooks up the routes.
# example/__init__.py
from flask import Flask
from . import settings
app = Flask(__name__)
app.config.from_object(settings)
from . import routes # noqa: F401 isort:skip
Note
# noqa: F401 isort:skip
prevents Flake8 and isort from reporting a misplaced import.
Models¶
Models are created using Flask-SQLAlchemy .
# example/models.py
import datetime as dt
from flask_sqlalchemy import SQLAlchemy
from . import app
db = SQLAlchemy(app)
class Author(db.Model):
__tablename__ = "example_authors"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Text, nullable=False)
created_at = db.Column(
db.DateTime, default=dt.datetime.utcnow, nullable=False
)
class Book(db.Model):
__tablename__ = "example_books"
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.Text, nullable=False)
author_id = db.Column(db.Integer, db.ForeignKey(Author.id), nullable=False)
author = db.relationship(Author, backref=db.backref("books"))
published_at = db.Column(db.DateTime, nullable=False)
created_at = db.Column(
db.DateTime, default=dt.datetime.utcnow, nullable=False
)
See also
See the Flask-SQLAlchemy documentation for more information on defining models.
Schemas¶
Schemas are used to validate request input and format response outputs.
# example/schemas.py
from marshmallow import Schema, fields
class AuthorSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.String(required=True)
created_at = fields.DateTime(dump_only=True)
class BookSchema(Schema):
id = fields.Int(dump_only=True)
title = fields.String(required=True)
author_id = fields.Int(required=True)
published_at = fields.DateTime(required=True)
created_at = fields.DateTime(dump_only=True)
See also
See the marshmallow documentation for more information on defining schemas.
Views¶
Most view classes will extend flask_resty.GenericModelView
which
provides standard CRUD behavior.
Typically, you will expose a model with a list endpoint (/api/authors/
), and a detail
endpoint (/api/authors/<id>
). To keep your code DRY, we recommend using a common base class for both endpoints.
For example:
# example/views.py
from flask_resty import GenericModelView
from . import models, schemas
class AuthorViewBase(GenericModelView):
model = models.Author
schema = schemas.AuthorSchema()
# authentication, authorization, pagination,
# sorting, and filtering would also go here
The concrete view classes simply call the appropriate CRUD methods from GenericModelView
.
class AuthorListView(AuthorViewBase):
def get(self):
return self.list()
def post(self):
return self.create()
class AuthorView(AuthorViewBase):
def get(self, id):
return self.retrieve(id)
def patch(self, id):
return self.update(id, partial=True)
def delete(self, id):
return self.destroy(id)
Note
Unimplemented HTTP methods will return 405 Method not allowed
.
Pagination¶
Add pagination to your list endpoints by setting the pagination
attribute on the base class.
The following will allow clients to pass a page
parameter in the query string, e.g. ?page=2
.
class AuthorViewBase(GenericModelView):
model = models.Author
schema = schemas.AuthorSchema()
pagination = PagePagination(page_size=10)
See also
See the Pagination section of the API docs for a listing of available pagination classes.
Sorting¶
Add sorting to your list endpoints by setting the sorting
attribute on the base class.
The following will allow clients to pass a sort
parameter in the query string, e.g. ?sort=-created_at
.
class AuthorViewBase(GenericModelView):
model = models.Author
schema = schemas.AuthorSchema()
pagination = PagePagination(page_size=10)
sorting = Sorting("created_at", default="-created_at")
See also
See the Sorting section of the API docs for a listing of available sorting classes.
Filtering¶
Add filtering to your list endpoints by setting the filtering
attribute on the base class.
Filtering natively handles multiple values. Specify values in a comma separated string to the query parameter, e.g., /books?author_id=1,2
.
class BookViewBase(GenericModelView):
model = models.Book
schema = schemas.BookSchema()
pagination = PagePagination(page_size=10)
sorting = Sorting("published_at", default="-published_at")
# An error is returned if author_id is omitted from the query string
filtering = Filtering(author_id=ColumnFilter(operator.eq, required=True))
See also
See the Filtering section of the API docs for a listing of available filtering classes.
Authentication¶
Add authentication by setting the authentication
attribute on the base class. We’ll use NoOpAuthentication
for this example. Flask-RESTy also includes a JwtAuthentication
class for authenticating with
JSON Web Tokens .
class AuthorViewBase(GenericModelView):
model = models.Author
schema = schemas.AuthorSchema()
authentication = NoOpAuthentication()
pagination = PagePagination(page_size=10)
sorting = Sorting("created_at", default="-created_at")
See also
See the Authentication section of the API docs for a listing of available authentication classes.
Routes¶
The routes.py
file contains the Api
instance with which we can connect
our view classes to URL patterns.
# example/routes.py
from flask_resty import Api
from . import app, views
api = Api(app, prefix="/api")
api.add_resource("/authors/", views.AuthorListView, views.AuthorView)
api.add_resource("/books/", views.BookListView, views.BookView)
api.add_ping("/ping/")
Testing¶
Flask-RESTy includes utilities for writing integration tests for your applications. Here’s how you can use them with pytest.
from unittest.mock import ANY
import pytest
from flask_resty.testing import ApiClient, assert_response, assert_shape
from . import app
@pytest.fixture(scope="session")
def db():
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
database = app.extensions["sqlalchemy"].db
database.create_all()
return database
@pytest.fixture(autouse=True)
def clean_tables(db):
for table in reversed(db.metadata.sorted_tables):
db.session.execute(table.delete())
db.session.commit()
yield
db.session.rollback()
@pytest.fixture
def client(monkeypatch):
monkeypatch.setattr(app, "testing", True)
monkeypatch.setattr(app, "test_client_class", ApiClient)
return app.test_client()
The first two fixtures ensure that we start with a clean database for each test.
The third fixture constructs an ApiClient
for
sending requests within our tests.
Let’s test that we can create an author. Here we use assert_response
to check the status code of the response and assert_shape
to
validate the shape of the response body.
def test_create_author(client):
response = client.post("/authors/", data={"name": "Fred Brooks"})
data = assert_response(response, 201)
assert_shape(data, {"id": ANY, "name": "Fred Brooks", "created_at": ANY})
We can test both the response code and the data shape using a single call. The following snippet is equivalent to the above.
def test_create_author(client):
response = client.post("/authors/", data={"name": "Fred Brooks"})
data = assert_response(response, 201)
assert_response(
response, 201, {"id": ANY, "name": "Fred Brooks", "created_at": ANY}
)
Running the Example Application¶
To run the example application, clone the Flask-RESTy repo.
$ git clone https://github.com/4Catalyzer/flask-resty.git
$ cd flask-resty
Populate the database with some dummy data.
$ python -m example.populate_db
Then serve the app on localhost:5000
.
$ FLASK_APP=example FLASK_ENV=development flask run
You can make requests using the httpie utility.
$ pip install httpie
$ http ":5000/api/books/?author_id=2"
HTTP/1.0 200 OK
Content-Length: 474
Content-Type: application/json
Date: Sun, 16 Jun 2019 01:39:04 GMT
Server: Werkzeug/0.14.1 Python/3.7.3
{
"data": [
{
"author_id": 2,
"created_at": "2019-06-16T01:09:33.450768",
"id": 2,
"published_at": "2013-11-05T00:00:00",
"title": "The Design of Everyday Things"
},
{
"author_id": 2,
"created_at": "2019-06-16T01:09:33.450900",
"id": 3,
"published_at": "2010-10-29T00:00:00",
"title": "Living With Complexity"
}
],
"meta": {
"has_next_page": false
}
}
The naive datetimes in the response are only because the example uses SQLite. A real application would use a timezone-aware datetime column in the database, and would have a UTC offset in the response.
Running the Shell¶
Flask-RESTy includes an enhanced flask shell
command that automatically imports all SQLAlchemy models and marshmallow schemas.
It will also automatically use IPython, BPython, or ptpython if they are installed.
$ FLASK_APP=example flask shell
3.8.5 (default, Jul 24 2020, 12:48:45)
[Clang 11.0.3 (clang-1103.0.32.62)]
_____ _ _ ____ _____ ____ _____
| ___| | __ _ ___| | __ | _ \| ____/ ___|_ _| _
| |_ | |/ _` / __| |/ /____| |_) | _| \___ \ | || | | |
| _| | | (_| \__ \ <_____| _ <| |___ ___) || || |_| |
|_| |_|\__,_|___/_|\_\ |_| \_\_____|____/ |_| \__, |
|___/
Flask app: example, Database: sqlite:///example.db
Flask:
app, g
Schemas:
AuthorSchema, BookSchema, Schema
Models:
Author, Book, commit, db, flush, rollback, session
In [1]: Author
Out[1]: example.models.Author
In [2]: AuthorSchema
Out[2]: example.schemas.AuthorSchema
Note
Pass the --sqlalchemy-echo
option to see database queries printed within your shell session.
The following app configuration options are available for customizing flask shell
:
RESTY_SHELL_CONTEXT
: Dictionary of additional variables to include in the shell context.RESTY_SHELL_LOGO
: Custom logo.RESTY_SHELL_PROMPT
: Custom input prompt.RESTY_SHELL_OUTPUT
: Custom output prompt.RESTY_SHELL_SETUP
: Additional shell setup, a function.RESTY_SHELL_CONTEXT_FORMAT
: Format to display shell context. May be'full'
,'short'
, or a function that receives the context dictionary as input and returns a string.RESTY_SHELL_IPY_AUTORELOAD
: Whether to load and enable the IPython autoreload extension (must be usingipython
shell).RESTY_SHELL_IPY_EXTENSIONS
: List of IPython extension names to load (must be usingipython
shell).RESTY_SHELL_IPY_COLORS
: IPython color style.RESTY_SHELL_IPY_HIGHLIGHTING_STYLE
: IPython code highlighting style.RESTY_SHELL_PTPY_VI_MODE
: Enable vi mode (must be usingptpython
shell).