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 usingipythonshell).RESTY_SHELL_IPY_EXTENSIONS: List of IPython extension names to load (must be usingipythonshell).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 usingptpythonshell).