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.

Authorization

Add authorization by setting the authorization attribute on the base class. We’ll use NoOpAuthorization for this example. You will likely need to implement your own subclasses of AuthorizationBase for your applications.

class AuthorViewBase(GenericModelView):
    model = models.Author
    schema = schemas.AuthorSchema()

    authentication = NoOpAuthentication()
    authorization = NoOpAuthorization()

    pagination = PagePagination(page_size=10)
    sorting = Sorting("created_at", default="-created_at")

See also

See the Authorization section of the API docs for a listing of available authorization 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 using ipython shell).

  • RESTY_SHELL_IPY_EXTENSIONS: List of IPython extension names to load (must be using ipython 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 using ptpython shell).