RESTful APIs

API Extension

The extension is instantiated in backend/extensions/api.py and should be used in views via from backend.extensions.api import api.

class backend.api.Api(name, app=None, prefix='', default_mediatype='application/json', decorators=None, catch_all_404s=False, serve_challenge_on_401=False, url_part_order='bae', errors=None)[source]

Extends flask_restful.Api to support integration with Flask-Marshmallow serializers, along with a few other minor enhancements:

  • can register individual view functions ala blueprints, via @api.route()
  • supports using flask.jsonify() in resource methods
model_resource(*args, **kwargs)[source]

Decorator to wrap a backend.api.ModelResource class, adding it to the api. There are two supported method signatures:

Api.model_resource(model, *urls, **kwargs)

and

Api.model_resource(blueprint, model, *urls, *kwargs)

Example without blueprint:

from backend.extensions.api import api
from models import User

@api.model_resource(User, '/users', '/users/<int:id>')
class UserResource(Resource):
    def get(self, user):
        return user

    def list(self, users):
        return users

Example with blueprint:

from backend.extensions.api import api
from models import User
from views import bp

@api.model_resource(bp, User, '/users', '/users/<int:id>')
class UserResource(Resource):
    def get(self, user):
        return user

    def list(self, users):
        return users
serializer(*args, many=False)[source]

Decorator to wrap a ModelSerializer class, registering the wrapped serializer as the specific one to use for the serializer’s model.

For example:

from backend.extensions.api import api
from backend.api import ModelSerializer
from models import Foo

@api.serializer  # @api.serializer() works too
class FooSerializer(ModelSerializer):
    class Meta:
        model = Foo

@api.serializer(many=True)
class FooListSerializer(ModelSerializer):
    class Meta:
        model = Foo
route(*args, **kwargs)[source]

Decorator for registering individual view functions.

Usage without blueprint:

api = Api('api', prefix='/api/v1')

@api.route('/foo')  # resulting url: /api/v1/foo
def get_foo():
    # do stuff

Usage with blueprint:

api = Api('api', prefix='/api/v1')
team = Blueprint('team', url_prefix='/team')

@api.route(team, '/users')  # resulting url: /api/v1/team/users
def users():
    # do stuff

ModelResource

class backend.api.ModelResource[source]

Base class for database model resource views. Includes a bit of configurable magic to automatically support the basic CRUD endpoints.

For example:

from backend.api import ModelResource
from backend.extensions.api import api       # url_prefix='/api/v1'
from backend.security.models import User
from backend.security.views import security  # url_prefix='/auth'

@api.model_resource(security, User, '/users', '/users/<int:id>')
class UserResource(ModelResource):
    pass

This results in the following URL endpoints:

GET    /api/v1/auth/users       # list all Users
POST   /api/v1/auth/users       # create new User
GET    /api/v1/auth/users/<id>  # get User.id == <id>
PUT    /api/v1/auth/users/<id>  # update User.id == <id>
PATCH  /api/v1/auth/users/<id>  # partial update User.id == <id>
DELETE /api/v1/auth/users/<id>  # delete User.id == <id>

To limit which endpoints are included, override the include_methods or exclude_methods class attributes:

from backend.api import CREATE, DELETE, GET, LIST, PATCH, PUT

# only support list & create
@api.model_resource(security, User, '/users')
class UserResource(ModelResource):
    include_methods = (CREATE, LIST)

# only support get & update
@api.model_resource(security, User, '/users/<int:id>')
class UserResource(ModelResource):
    include_methods = (GET, PATCH, PUT)

# all except delete
@api.model_resource(security, User, '/users', '/users/<int:id>')
class UserResource(ModelResource):
    exclude_methods = (DELETE,)

To customize the implementation of any of the methods, implement the lower-cased method name (shown below are the default implementations):

@api.model_resource(security, User, '/users', '/users/<int:id>')
class UserResource(ModelResource):
    def list(self, users):
        return users

    def create(self, user, errors):
        if errors:
            return self.errors(errors)
        return self.created(user)

    def get(self, user):
        return user

    def put(self, user, errors):
        if errors:
            return self.errors(errors)
        return self.updated(user)

    def patch(self, user, errors):
        if errors:
            return self.errors(errors)
        return self.updated(user)

    def delete(self, user):
        return self.deleted(user)

As you can see from above, there’s still a bit more magic happening behind the scenes to convert the request parameters/data into models. This happens via method decorators (and if applicable, the respective serializer for the model assigned to this resource). The default decorators effectively look like this:

from backend.api import (
    ALL_METHODS,
    param_converter,
    list_loader,
    patch_loader,
    post_loader,
    put_loader,
)

@api.model_resource(security, User, '/users', '/users/<int:id>')
class UserResource(ModelResource):
    exclude_decorators = ALL_METHODS  # disable the default decorators
    include_decorators = ()  # alternative way to disable them

    @list_loader(User)
    def list(self, users):
        return users

    @post_loader(UserResource.serializer_create)  # <- invalid syntax
    def create(self, user, errors):
        if errors:
            return self.errors(errors)
        return self.created(user)

    @param_converter(id=User)
    def get(self, user):
        return user

    @put_loader(UserResource.serializer)  # <- invalid syntax
    def put(self, user, errors):
        if errors:
            return self.errors(errors)
        return self.updated(user)

    @patch_loader(UserResource.serializer)  # <- invalid syntax
    def patch(self, user, errors):
        if errors:
            return self.errors(errors)
        return self.updated(user)

    @param_converter(id=User)
    def delete(self, user):
        return self.deleted(user)

Furthermore, you can add extra decorators to methods using method_decorators:

from backend.security import auth_required_same_user

@api.model_resource(security, User, '/users/<int:id>')
class UserResource(ModelResource):
    include_methods = (PATCH, PUT)
    method_decorators = (auth_required_same_user,)

Or on a per-method basis:

from backend.security import (
    anonymous_user_required,
    auth_required_same_user,
)

@api.model_resource(security, User, '/users/<int:id>')
class UserResource(ModelResource):
    include_methods = (CREATE, PATCH, PUT)
    method_decorators = {CREATE: [anonymous_user_required],
                         PATCH: [auth_required_same_user],
                         PUT: [auth_required_same_user]}
model = None

The database model class for a ModelResource (automatically set by the backend.api.Api extension instance)

serializer = None

The serializer to be used for serializing single instances of self.model (automatically set by the backend.api.Api extension instance)

serializer_create = None

The serializer to be used when creating an instance of self.model (automatically set by the backend.api.Api extension instance)

include_methods = ('create', 'delete', 'get', 'list', 'patch', 'put')

Override to limit methods supported by ModelResource

exclude_methods = ()

Override to exclude methods supported by ModelResource

include_decorators = ('create', 'delete', 'get', 'list', 'patch', 'put')

Override to limit automatic decorators applied by ModelResource

exclude_decorators = ()

Override to exclude automatic decorators applied by ModelResource

created(obj, save=True)[source]

Convenience method for saving a model (automatically commits it to the database and returns the object with an HTTP 201 status code)

deleted(obj)[source]

Convenience method for deleting a model (automatically commits the delete to the database and returns with an HTTP 204 status code)

errors(errors)[source]

Convenience method for returning a dictionary of errors with an HTTP 400 status code

updated(obj)[source]

Convenience method for updating a model (automatically commits it to the database and returns the object with with an HTTP 200 status code)

ModelSerializer

class backend.api.ModelSerializer(*args, **kwargs)[source]

Base class for database model serializers. This is pretty much a stock flask_marshmallow.sqla.ModelSchema: it will automatically create fields from the attached database Model, the only difference being that it will automatically dump to (and load from) the camel-cased variants of the field names.

For example:

from backend.api import ModelSerializer
from backend.security.models import Role

class RoleSerializer(ModelSerializer):
    class Meta:
        model = Role

Is roughly equivalent to:

from marshmallow import Schema, fields

class RoleSerializer(Schema):
    id = fields.Integer()
    name = fields.String()
    description = fields.String()
    created_at = fields.DateTime(dump_to='createdAt',
                                 load_from='createdAt')
    updated_at = fields.DateTime(dump_to='updatedAt',
                                 load_from='updatedAt')

Obviously you probably shouldn’t be loading created_at or updated_at from JSON; it’s just an example to show the automatic snake-to-camelcase field naming conversion.

WrappedSerializer

class backend.api.WrappedSerializer(*args, **kwargs)[source]

Extends backend.api.ModelSchema to automatically wrap serialized results with the model name, and automatically unwrap it when loading.

NOTE: this might not behave as you’d expect if your serializer uses nested fields (if a nested object’s serializer is also a WrappedSerializer, then the nested objects will also end up wrapped, which probably isn’t what you want…)

Example usage:

class Foo(PrimaryKeyMixin, BaseModel):
    name = Column(String)

class FooSerializer(WrappedSerializer):
    class Meta:
        model = Foo

foo_serializer = FooSerializer()
foo = Foo(id=1, name='FooBar')
foo_json = foo_serializer.dump(foo).data
# results in:
foo_json == {
   "foo": {  # <- added by self.wrap_with_envelope on @post_dump
      "id": 1,
      "name": "FooBar"
   }
}

# and on deserialization, self.unwrap_envelope loads it correctly:
foo = foo_serializer.load(foo_json).data
isinstance(foo, Foo) == True