Difference between revisions of "Python - Flask"

(The RESTful server)
(Rest APIs Development with Flask-RESTful)
Line 22: Line 22:
 
* '''description''': long task description. Text type.
 
* '''description''': long task description. Text type.
 
* '''done''': task completion state. Boolean type.
 
* '''done''': task completion state. Boolean type.
 +
 +
==Routing==
 +
 +
Flask-RESTful provides a Resource base class that can define the routing for one or more HTTP methods for a given URL. For example, to define a User resource with GET, PUT and DELETE methods you would write:
 +
 +
<syntaxhighlight lang="python">
 +
from flask import Flask
 +
from flask_restful import Api, Resource
 +
 +
app = Flask(__name__)
 +
api = Api(app)
 +
 +
class UserAPI(Resource):
 +
    def get(self, id):
 +
        pass
 +
 +
    def put(self, id):
 +
        pass
 +
 +
    def delete(self, id):
 +
        pass
 +
 +
api.add_resource(UserAPI, '/users/<int:id>', endpoint = 'user')
 +
</syntaxhighlight>
 +
 +
 +
The add_resource function registers the routes with the framework using the given endpoint. If an endpoint isn't given then Flask-RESTful generates one for you from the class name, but since sometimes the endpoint is needed for functions such as url_for I prefer to make it explicit.
 +
 +
My ToDo API defines two URLs: '''/todo/api/v1.0/tasks''' for the list of tasks, and '''/todo/api/v1.0/tasks/<int:id>''' for an individual task. Since Flask-RESTful's Resource class can wrap a single URL this server will need two resources:
 +
 +
<syntaxhighlight lang="python">
 +
class TaskListAPI(Resource):
 +
    def get(self):
 +
        pass
 +
 +
    def post(self):
 +
        pass
 +
 +
class TaskAPI(Resource):
 +
    def get(self, id):
 +
        pass
 +
 +
    def put(self, id):
 +
        pass
 +
 +
    def delete(self, id):
 +
        pass
 +
 +
api.add_resource(TaskListAPI, '/todo/api/v1.0/tasks', endpoint = 'tasks')
 +
api.add_resource(TaskAPI, '/todo/api/v1.0/tasks/<int:id>', endpoint = 'task')
 +
</syntaxhighlight>
 +
 +
Note that while the method views of TaskListAPI receive no arguments the ones in TaskAPI all receive the id, as specified in the URL under which the resource is registered.
 +
 +
==Request Parsing and Validation==
 +
When you implemented your own validation of the request data. For example, look at how long the PUT handler is in that version:
 +
<syntaxhighlight lang="python">
 +
@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods = ['PUT'])
 +
@auth.login_required
 +
def update_task(task_id):
 +
    task = filter(lambda t: t['id'] == task_id, tasks)
 +
    if len(task) == 0:
 +
        abort(404)
 +
    if not request.json:
 +
        abort(400)
 +
    if 'title' in request.json and type(request.json['title']) != unicode:
 +
        abort(400)
 +
    if 'description' in request.json and type(request.json['description']) is not unicode:
 +
        abort(400)
 +
    if 'done' in request.json and type(request.json['done']) is not bool:
 +
        abort(400)
 +
    task[0]['title'] = request.json.get('title', task[0]['title'])
 +
    task[0]['description'] = request.json.get('description', task[0]['description'])
 +
    task[0]['done'] = request.json.get('done', task[0]['done'])
 +
    return jsonify( { 'task': make_public_task(task[0]) } )
 +
</syntaxhighlight>
 +
 +
Here you have to make sure the data given with the request is valid before using it, and that makes the function pretty long.
 +
 +
Flask-RESTful provides a much better way to handle this with the RequestParser class. This class works in a similar way as '''argparse''' for command line arguments.
 +
 +
First, for each resource I define the arguments and how to validate them:
 +
 +
<syntaxhighlight lang="python">
 +
from flask_restful import reqparse
 +
 +
class TaskListAPI(Resource):
 +
    def __init__(self):
 +
        self.reqparse = reqparse.RequestParser()
 +
        self.reqparse.add_argument('title', type = str, required = True,
 +
            help = 'No task title provided', location = 'json')
 +
        self.reqparse.add_argument('description', type = str, default = "", location = 'json')
 +
        super(TaskListAPI, self).__init__()
 +
 +
    # ...
 +
 +
class TaskAPI(Resource):
 +
    def __init__(self):
 +
        self.reqparse = reqparse.RequestParser()
 +
        self.reqparse.add_argument('title', type = str, location = 'json')
 +
        self.reqparse.add_argument('description', type = str, location = 'json')
 +
        self.reqparse.add_argument('done', type = bool, location = 'json')
 +
        super(TaskAPI, self).__init__()
 +
 +
    # ...
 +
 +
</syntaxhighlight>
 +
 +
In the TaskListAPI resource the POST method is the only one the receives arguments. The title argument is required here, so I included an error message that Flask-RESTful will send as a response to the client when the field is missing. The description field is optional, and when it is missing a default value of an empty string will be used. One interesting aspect of the RequestParser class is that by default it looks for fields in request.values, so the location optional argument must be set to indicate that the fields are coming in request.json.
 +
 +
The request parser for the TaskAPI is constructed in a similar way, but has a few differences. In this case it is the PUT method that will need to parse arguments, and for this method all the arguments are optional, including the done field that was not part of the request in the other resource.
 +
 +
Now that the request parsers are initialized, parsing and validating a request is pretty easy. For example, note how much simpler the TaskAPI.put() method becomes:
 +
 +
<syntaxhighlight lang="python">
 +
def put(self, id):
 +
        task = filter(lambda t: t['id'] == id, tasks)
 +
        if len(task) == 0:
 +
            abort(404)
 +
        task = task[0]
 +
        args = self.reqparse.parse_args()
 +
        for k, v in args.iteritems():
 +
            if v != None:
 +
                task[k] = v
 +
        return jsonify( { 'task': make_public_task(task) } )
 +
</syntaxhighlight>
 +
 +
A side benefit of letting Flask-RESTful do the validation is that now there is no need to have a handler for the bad request code 400 error, this is all taken care of by the extension.
 +
 +
==Generating Responses==

Revision as of 07:18, 22 September 2020

Rest APIs Development with Flask-RESTful

Flask extension that simplifies the creation of APIs.

The RESTful server

Here is the definition of the ToDo List web service that has been serving as an example:

HTTP Method  URI                                                Action
GET          http://[hostname]/todo/api/v1.0/tasks              Retrieve list of tasks
GET          http://[hostname]/todo/api/v1.0/tasks/[task_id]    Retrieve a task
POST         http://[hostname]/todo/api/v1.0/tasks              Create a new task
PUT          http://[hostname]/todo/api/v1.0/tasks/[task_id]    Update an existing task
DELETE       http://[hostname]/todo/api/v1.0/tasks/[task_id]    Delete a task

The only resource exposed by this service is a "task", which has the following data fields:

  • uri: unique URI for the task. String type.
  • title: short task description. String type.
  • description: long task description. Text type.
  • done: task completion state. Boolean type.

Routing

Flask-RESTful provides a Resource base class that can define the routing for one or more HTTP methods for a given URL. For example, to define a User resource with GET, PUT and DELETE methods you would write:

from flask import Flask
from flask_restful import Api, Resource

app = Flask(__name__)
api = Api(app)

class UserAPI(Resource):
    def get(self, id):
        pass

    def put(self, id):
        pass

    def delete(self, id):
        pass

api.add_resource(UserAPI, '/users/<int:id>', endpoint = 'user')


The add_resource function registers the routes with the framework using the given endpoint. If an endpoint isn't given then Flask-RESTful generates one for you from the class name, but since sometimes the endpoint is needed for functions such as url_for I prefer to make it explicit.

My ToDo API defines two URLs: /todo/api/v1.0/tasks for the list of tasks, and /todo/api/v1.0/tasks/<int:id> for an individual task. Since Flask-RESTful's Resource class can wrap a single URL this server will need two resources:

class TaskListAPI(Resource):
    def get(self):
        pass

    def post(self):
        pass

class TaskAPI(Resource):
    def get(self, id):
        pass

    def put(self, id):
        pass

    def delete(self, id):
        pass

api.add_resource(TaskListAPI, '/todo/api/v1.0/tasks', endpoint = 'tasks')
api.add_resource(TaskAPI, '/todo/api/v1.0/tasks/<int:id>', endpoint = 'task')

Note that while the method views of TaskListAPI receive no arguments the ones in TaskAPI all receive the id, as specified in the URL under which the resource is registered.

Request Parsing and Validation

When you implemented your own validation of the request data. For example, look at how long the PUT handler is in that version:

@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods = ['PUT'])
@auth.login_required
def update_task(task_id):
    task = filter(lambda t: t['id'] == task_id, tasks)
    if len(task) == 0:
        abort(404)
    if not request.json:
        abort(400)
    if 'title' in request.json and type(request.json['title']) != unicode:
        abort(400)
    if 'description' in request.json and type(request.json['description']) is not unicode:
        abort(400)
    if 'done' in request.json and type(request.json['done']) is not bool:
        abort(400)
    task[0]['title'] = request.json.get('title', task[0]['title'])
    task[0]['description'] = request.json.get('description', task[0]['description'])
    task[0]['done'] = request.json.get('done', task[0]['done'])
    return jsonify( { 'task': make_public_task(task[0]) } )

Here you have to make sure the data given with the request is valid before using it, and that makes the function pretty long.

Flask-RESTful provides a much better way to handle this with the RequestParser class. This class works in a similar way as argparse for command line arguments.

First, for each resource I define the arguments and how to validate them:

from flask_restful import reqparse

class TaskListAPI(Resource):
    def __init__(self):
        self.reqparse = reqparse.RequestParser()
        self.reqparse.add_argument('title', type = str, required = True,
            help = 'No task title provided', location = 'json')
        self.reqparse.add_argument('description', type = str, default = "", location = 'json')
        super(TaskListAPI, self).__init__()

    # ...

class TaskAPI(Resource):
    def __init__(self):
        self.reqparse = reqparse.RequestParser()
        self.reqparse.add_argument('title', type = str, location = 'json')
        self.reqparse.add_argument('description', type = str, location = 'json')
        self.reqparse.add_argument('done', type = bool, location = 'json')
        super(TaskAPI, self).__init__()

    # ...

In the TaskListAPI resource the POST method is the only one the receives arguments. The title argument is required here, so I included an error message that Flask-RESTful will send as a response to the client when the field is missing. The description field is optional, and when it is missing a default value of an empty string will be used. One interesting aspect of the RequestParser class is that by default it looks for fields in request.values, so the location optional argument must be set to indicate that the fields are coming in request.json.

The request parser for the TaskAPI is constructed in a similar way, but has a few differences. In this case it is the PUT method that will need to parse arguments, and for this method all the arguments are optional, including the done field that was not part of the request in the other resource.

Now that the request parsers are initialized, parsing and validating a request is pretty easy. For example, note how much simpler the TaskAPI.put() method becomes:

 def put(self, id):
        task = filter(lambda t: t['id'] == id, tasks)
        if len(task) == 0:
            abort(404)
        task = task[0]
        args = self.reqparse.parse_args()
        for k, v in args.iteritems():
            if v != None:
                task[k] = v
        return jsonify( { 'task': make_public_task(task) } )

A side benefit of letting Flask-RESTful do the validation is that now there is no need to have a handler for the bad request code 400 error, this is all taken care of by the extension.

Generating Responses