Guide¶
Eager to get going? This page gives an introduction to getting started with Flask-Arrested.
First, make sure that:
- Arrested is installed
This tutorial steps through creating a simple Star Wars themed REST API using Arrested. We assume you have a working knowledge of both Python and Flask.
Example Application¶
The examples used in this guide can be found here https://github.com/mikeywaites/flask-arrested/tree/master/example.
Note
To follow along with the example you will need to install Docker for your operating system. Find out how here https://docker.com.
👋 Alternatively you can use the Arrested cookiecutter to create a working API in 4 simple commands. https://github.com/mikeywaites/arrested-cookiecutter.
APIs, Resources and Endpoints - Defining your first API¶
Flask-Arrested is split into 3 key concepts. ArrestedAPI
's, Resource
and Endpoint
. APIs contian multiple Resources. For example
Our API contains a Characters resource, a Planets resource and so on. Resources are a collection of Endpoints. Endpoints define
the urls inside of our Resources /v1/charcaters
/v1/planets/hfyf66775gjgjjf
etc.
from flask import Flask
from arrested import (
ArrestedAPI, Resource, Endpoint, GetListMixin, CreateMixin,
GetObjectMixin, PutObjectMixin, DeleteObjectMixin, ResponseHandler
)
from example.hanlders import DBRequestHandler, character_serializer
from example.models import db, Character
app = Flask(__name__)
api_v1 = ArrestedAPI(app, url_prefix='/v1')
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////opt/code/example/starwars.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db.init_app(app)
characters_resource = Resource('characters', __name__, url_prefix='/characters')
class CharactersIndexEndpoint(Endpoint, GetListMixin, CreateMixin):
name = 'list'
many = True
response_handler = DBResponseHandler
def get_response_handler_params(self, **params):
params['serializer'] = character_serializer
return params
def get_objects(self):
characters = db.session.query(Character).all()
return characters
def save_object(self, obj):
character = Character(**obj)
db.session.add(character)
db.session.commit()
return character
class CharacterObjectEndpoint(Endpoint, GetObjectMixin,
PutObjectMixin, DeleteObjectMixin):
name = 'object'
url = '/<string:obj_id>'
response_handler = DBResponseHandler
def get_response_handler_params(self, **params):
params['serializer'] = character_serializer
return params
def get_object(self):
obj_id = self.kwargs['obj_id']
obj = db.session.query(Character).filter(Character.id == obj_id).one_or_none()
if not obj:
payload = {
"message": "Character object not found.",
}
self.return_error(404, payload=payload)
return obj
def update_object(self, obj):
data = self.request.data
allowed_fields = ['name']
for key, val in data.items():
if key in allowed_fields:
setattr(obj, key, val)
db.session.add(obj)
db.session.commit()
return obj
def delete_object(self, obj):
db.session.delete(obj)
db.session.commit()
characters_resource.add_endpoint(CharactersIndexEndpoint)
characters_resource.add_endpoint(CharacterObjectEndpoint)
api_v1.register_resource(characters_resource)
Start the Docker container in the example/ directory.
$ docker-compose run --rm --service-ports api
Fetch a list of Character objects..
curl -X GET localhost:5000/v1/characters | python -m json.tool
{
"payload": [
{
"created_at": "2017-06-04T11:47:02.017094",
"id": 1,
"name": "Obe Wan"
}
]
}
Add a new Character..
curl -H "Content-Type: application/json" -d '{"name":"Darth Vader"}' -X POST localhost:5000/v1/characters | python -m json.tool
{
"payload": [
{
"created_at": "2017-09-01T04:51:45.456072",
"id": 2,
"name": "Darth Vader"
}
]
}
Fetch a Character by id..
curl -X GET localhost:5000/v1/characters/2 | python -m json.tool
{
"payload": {
"created_at": "2017-09-01T04:51:45.456072",
"id": 2,
"name": "Darth Vader"
}
}
Update a Character by id..
curl -H "Content-Type: application/json" -d '{"id": 2, "name":"Anakin Skywalker", "created_at": "2017-09-01T04:51:45.456072"}' -X PUT localhost:5000/v1/characters/2 | python -m json.tool
{
"payload": {
"created_at": "2017-09-01T04:51:45.456072",
"id": 2,
"name": "Anakin Skywalker"
}
}
And finally, Delete a Character by id..
curl -X DELETE localhost:5000/v1/characters/2
URLS && url_for¶
URLSs are automatically defined by Resoruces and Endpoints using Flask's built in url_mapping functionality. We optionally provide Resource with a url_prefix which is applied to all of it's registered Endponts.
We can also specify a URI segment for the Endpoint using the url
parameter. Endpoints require that the name attribute is provied. This is the name used when reversing the url using Flask's url_for
function. Ie url_for('news.list')
where new is the name given to the Resource and list of the name of one of its registered endpoints.
Getting objects¶
We defined an Endpoint within our characters Resource that accepts incoming GET requests to /v1/characters. This Endpoint fetches all the Character objects from the database and our custom DBRequestHandler handles converting them
into a format that can be serialized as JSON. The topic of Request and Response handling is covered in more detail below so for now let's take a closer look at the GetListMixin
mixin.
GetListMixin
provides automatic handling of GET requests. It requires that we define a single method GetListMixin.get_objects
. This method should return data that our specified ResponseHandler can serialize.
We tell Arrested that this endpoint returns many objects using the many class attribute. This setting is used by certain Response handlers when serializing the objects returned by Endpoints.
import redis
from arrested import Endpoint, GetListMixin
class NewsEndpoint(Endpoint, GetListMixin):
many = True
name = 'list'
def get_objects(self, obj):
r = redis.StrictRedis(host='localhost', port=6379, db=0)
return r.hmget('news')
Saving objects¶
The CharactersIndexEndpoint also inherits the CreateMixin
. This mixin provides functionality for handling POST requests. The CreateMixin
requires that the save_object
method be implemented.
The save_object method will be called with the obj or objects processed by the Endpoint's defined request_handler.
Here's an example Endpoint that store the incoming JSON data in Redis.
import redis
from arrested import Endpoint, GetListMixin, CreateMixin
class CustomEndpoint(Endpoint, GetListMixin, CreateMixin):
many = True
name = 'list'
def get_objects(self, obj):
r = redis.StrictRedis(host='localhost', port=6379, db=0)
return r.hmget('news')
def save_object(self, obj):
# obj will be a dict here as we're using the default RequestHandler
r.hmset('news', obj)
return obj
Object Endpoints¶
Object endpoints allow you to define APIS that typically let your users GET, PUT, PATCH and DELETE single objects. The Mixins can be combined to provide support for all the typical HTTP methods used when working with a single object.
Regardless of the HTTP methods you're supporting, your object endpoints must provide the get_object
method.
Getting a single object¶
To support GET requests that retrieve a single object from an Endpoint you should use the GetObjectMixin
. In addition to the get_object method, we have also specified a url class attribute. Arrested will populate a kwargs property on your Endpoint instance
which contains the named url paramaters from your Endpoint's url.
Below we use the obj_id passed as part of the url to fetch a new item from Redis by ID.
import redis
from arrested import Endpoint, GetObjectMixin
class CustomEndpoint(Endpoint, GetObjectMixin):
url = '/<str:obj_id>'
name = 'object'
def get_object(self, obj):
news_id = self.kwargs['obj_id']
r = redis.StrictRedis(host='localhost', port=6379, db=0)
return r.hmget('news:%s' % news_id)
Updating an object¶
Support for updating objects is provided by the PutObjectMixin
. PutObjectMixin requires two methods be implemented. get_object
and update_object
.
import redis
from arrested import Endpoint, PutObjectMixin
class CustomEndpoint(Endpoint, PutObjectMixin):
url = '/<str:obj_id>'
name = 'object'
def get_object(self, obj):
news_id = self.kwargs['obj_id']
r = redis.StrictRedis(host='localhost', port=6379, db=0)
return r.hmget('news:%s' % news_id)
def update_object(self, obj):
news_id = self.kwargs['obj_id']
r = redis.StrictRedis(host='localhost', port=6379, db=0)
return r.hmset('news:%s' % news_id, obj)
When a PUT request is handled by our CustomEndpoint the get_object
method is called first to retrieve the existing object. If an object is found
the PutObjectMixin.update_object
method is then called.
To support updating objects via PATCH requests all we need to do is use the PatchObjectMixin
. It works in same way as PutObjectMixin
except that we the patch_object
method is called when an object is returned by get_object.
import redis
from arrested import Endpoint, PutObjectMixin, PatchObjectMixin
class CustomEndpoint(Endpoint, PutObjectMixin, PatchObjectMixin):
url = '/<str:obj_id>'
name = 'object'
def get_object(self, obj):
news_id = self.kwargs['obj_id']
r = redis.StrictRedis(host='localhost', port=6379, db=0)
return r.hmget('news:%s' % news_id)
def do_update(self, obj):
news_id = self.kwargs['obj_id']
r = redis.StrictRedis(host='localhost', port=6379, db=0)
return r.hmset('news%s' % news_id, obj)
def update_object(self, obj):
self.do_update(obj)
def patch_object(self, obj):
self.do_update(obj)
Deleting objects¶
Support for deleting objects is provided by the DeleteObjectMixin
. DeleteObjectMixin requires two methods be implemented. get_object
and delete_object
.
import redis
from arrested import Endpoint, DeleteObjectMixin
class CustomEndpoint(Endpoint, DeleteObjectMixin):
url = '/<str:obj_id>'
name = 'object'
def get_object(self, obj):
news_id = self.kwargs['obj_id']
r = redis.StrictRedis(host='localhost', port=6379, db=0)
return r.hmget('news:%s' % news_id)
def delete_object(self, obj):
news_id = self.kwargs['obj_id']
return r.delete('news:%s' % news_id)
Middleware¶
Flask comes with a great system for defining request middleware. Arrested builds on top of this system to allow more fine grained control of where and when your middleware is run.
API Middleware¶
Middleware can be applied at each level of the Arrested stack. You will often want a piece middleware to be applied across every resource and every endpoint defined in an API. An example of this might be authentication.
The ArrestedAPI
object supports two middleware hooks, before_all_hooks
and after_all_hooks
. Let's create a basic example that demonstrates how authentication can be applied across APIs.
def authenticated(endpoint):
token_valid = request.args.get('token') == 'test-token'
if not token_valid:
endpoint.return_error(401)
api_v1 = ArrestedAPI(app, url_prefix='/v1', before_all_hooks=[authenticated])
Hit the http://localhost:5000/v1/characters
url in your browser. We now get a 401 status code when requesting the characters API.
A second request, this time providng our API token should return our character objects. http://localhost:5000/v1/characters?token=test-token
Resource Middleware¶
Middleware can also be applied on a per Resource basis. Resource
, Like the ArrestedAPI
object also has two options for injecting middleware into the request/response cycle. before_all_hooks
and after_request_hook
. Let's add some logging code to our characters resource using an after request hook.
def log_request(endpoint, response):
app.logger.debug('request to characters resource made')
return response
characters_resource = Resource('characters', __name__, url_prefix='/characters', after_all_hooks=[log_request])
Our middleware is slightly different from the authenication example. When we're dealing with an after request hook we are also passed the response object as well as the endpoint instance. The response object should be returned from every after request hook defined on our APIs and Resources.
Endpoint Middleware¶
Lastly we come to the Endpoint
object. Endpoint
supports defining middleware using the following hooks:
- before_all_hooks
- before_get_hooks
- after_get_hooks
- before_post_hooks
- after_post_hooks
- before_put_hooks
- after_put_hooks
- before_patch_hooks
- after_patch_hooks
- before_delete_hooks
- after_delete_hooks
- after_all_hooks
As you can see, not only can we dfine the before_all_hooks and after_all_hooks like we have on the ArrestedAPI
and Resource
, we can also inject middleware before and after each HTTP method.
Let's update our CharacterObjectEndpoint to require an admin for PUT requests.
def is_admin(endpoint):
endpoint.return_error(403)
class CharacterObjectEndpoint(Endpoint, GetObjectMixin,
PutObjectMixin, DeleteObjectMixin):
name = 'object'
url = '/<string:obj_id>'
response_handler = DBResponseHandler
before_put_hooks = [is_admin, ]
def get_response_handler_params(self, **params):
params['serializer'] = character_serializer
return params
def get_object(self):
obj_id = self.kwargs['obj_id']
obj = db.session.query(Character).filter(Character.id == obj_id).one_or_none()
if not obj:
payload = {
"message": "Character object not found.",
}
self.return_error(404, payload=payload)
return obj
def update_object(self, obj):
data = self.request.data
allowed_fields = ['name']
for key, val in data.items():
if key in allowed_fields:
setattr(obj, key, val)
db.session.add(obj)
db.session.commit()
return obj
def delete_object(self, obj):
db.session.delete(obj)
db.session.commit()
Making a PUT request to http://localhost:5000/v1/characters/1
using curl now returns a 403
Handling Requests and Responses¶
Arrested provides a flexible API for handling the data flowing into, and out from your APIs. Each endpoint can have a custom RequestHandler
and ResponseHandler
. This system provides support for any concievable way of processing data. Arrested also provides some out of the box integrations with popular
serialization libraries, such as Kim and Marshmallow.
Request Handling¶
HTTP requests that process data require that a RequestHandler
is defined on the Endpoint using the request_handler property. The default RequestHandler
simply pulls the json data from the Flask request object, deserialises it into a dict and returns it verbatim.
Let's suppose we want to apply some very basic validation ensuring that certain keys are present within the request payload. To do this we will implement a custom RequestHandler
that takes a list of field names and ensures all the keys are present in the request data.
from arrested.handlers import RequestHandler
class ValidatingRequestHandler(RequestHandler):
def __init__(self, endpoint, fields=None, *args, **kwargs):
super(ValidatingRequestHandler, self).__init__(endpoint, *args, **params)
self.fields = fields
def handle(self, data, **kwargs):
if self.fields and not sorted(data.keys()) == sorted(self.fields):
payload = {
"message": "Missing required fields",
}
self.endpoint.return_error(422, payload=payload)
return super(ValidatingRequestHandler, self).handle(data, **kwargs)
class CustomEndpoint(Endpoint, GetListMixin, CreateMixin):
many = True
name = 'list'
request_handler = ValidatingRequestHandler
def get_objects(self, obj):
r = redis.StrictRedis(host='localhost', port=6379, db=0)
return r.hmget('news')
def save_object(self, obj):
# obj will be a dict here as we're using the default RequestHandler
return r.hmset('news', obj)
def get_request_handler_params(self, **params):
params = super(KimEndpoint, self).get_request_handler_params(**params)
params['fields'] = ['field_one', 'field_two']
return params
This simple examples demonstrates the flexibility the handler system offers. We can define handlers to accomodate any use case imaginable in Python. We can use the Endpoint.get_request_handler_params
to configure the handler
on an endpoint by endpoint basis.
Accessing the Request object¶
We've seen how to define a custom handler and how we configure it to process incoming data. So what does arrested do with all this stuff? Whenever a POST, PUT, PATCH request is made to one of your Endpoint
arrested will instantiate the request
object and set it on the Endpoint.
This allows users to access the handler instance used to process the incoming request. An example of this in practice is the CreateMixin.handle_post_request
method.
def handle_post_request(self):
"""Handle incoming POST request to an Endpoint and marshal the request data
via the specified RequestHandler. :meth:`.CreateMixin.save_object`. is then
called and must be implemented by mixins implementing this interfce.
.. seealso::
:meth:`CreateMixin.save_object`
:meth:`Endpoint.post`
"""
self.request = self.get_request_handler()
self.obj = self.request.process().data
self.save_object(self.obj)
return self.create_response()
Response Handling¶
Endpoints that return data will typically require that a ResponseHandler
be defined on the Endpoint using the response_handler property. The default ResponseHandler
simply attempts to serialize the obj passed to it using json.dumps
.
This works fine in simple cases but when we're dealing with more complex types like SQLAlchemy Models we need something a bit smarter.
Let's look at implementing a simple ResponseHandler
that removes some fields from response data.
from arrested.handlers import RequestHandler
class ValidatingResponseHandler(RequestHandler):
def __init__(self, endpoint, fields=None, *args, **kwargs):
super(ValidatingRequestHandler, self).__init__(endpoint, *args, **params)
self.fields = fields
def handle(self, data, **kwargs):
new_data = {}
for key, value in data.items():
if key in self.fields:
new_data[key] = value
return super(ValidatingResponseHandler, self).handle(new_data, **kwargs)
class CustomEndpoint(Endpoint, GetListMixin, CreateMixin):
many = True
name = 'list'
response_handler = ValidatingResponseHandler
def get_objects(self, obj):
r = redis.StrictRedis(host='localhost', port=6379, db=0)
return r.hmget('news')
def save_object(self, obj):
# obj will be a dict here as we're using the default RequestHandler
return r.hmset('news', obj)
def get_response_handler_params(self, **params):
params = super(KimEndpoint, self).get_response_handler_params(**params)
params['fields'] = ['field_one', ]
return params
Accessing the Response object¶
As we saw with the Request object, Arrested will store the response handler instance against a property on the Endpoint called response
. By default a ResponseHandler is created for any request handled by an Endpoint.
This means that the data generated by a RequestHandler will later be processed and returned by the provided ResponseHandler. We can see this in action in the PutObjectMixin
mixin shown below.
class PutObjectMixin(HTTPMixin, ObjectMixin):
"""Base PutObjectMixins class that defines the expected API for all PutObjectMixin
"""
def object_response(self, status=200):
"""Generic response generation for Endpoints that return a single
serialized object.
:param status: The HTTP status code returned with the response
:returns: Response object
"""
self.response = self.get_response_handler()
self.response.process(self.obj)
return self._response(self.response.get_response_data(), status=status)
def put_request_response(self, status=200):
"""Pull the processed data from the response_handler and return a response.
:param status: The HTTP status code returned with the response
.. seealso:
:meth:`ObjectMixin.object_response`
:meth:`Endpoint.handle_put_request`
"""
return self.object_response(status=status)
def handle_put_request(self):
"""
"""
obj = self.obj
self.request = self.get_request_handler()
self.request.process()
self.update_object(obj)
return self.put_request_response()
def update_object(self, obj):
"""Called by :meth:`PutObjectMixin.handle_put_request` ater the incoming data has
been marshalled by the RequestHandler.
:param obj: The marhsaled object from RequestHandler.
"""
return obj
Handling Errors¶
Returning specific HTTP status codes under certain conditions is an important part of building REST APIs. Arrested provides users with a simple, and consistent way to handle generating error responses from their Endpoints. An example of this might be returning a 404 when an object is not found.
Our CharacterObjectEndpoint has already demonstrated this above in the get_object method. When we fail to find the object we're looking for from the database, we call the Endpoint.return_error
method
to have Flask abort execution of the request and immediately return an error.
We simply provide the status code we want to return along with an optional request payload that will be serialized as JSON and retured as the response body.
def get_object(self):
obj_id = self.kwargs['obj_id']
obj = db.session.query(Character).filter(Character.id == obj_id).one_or_none()
if not obj:
payload = {
"message": "Character object not found.",
}
self.return_error(404, payload=payload)
return obj