Python and Flask design RESTful API

In recent years REST (REpresentational State Transfer) has become a web services and web APIs as standard.

In this article I will show you how easy it is to use Python and Flask framework to create a RESTful web service.

What is REST?

Six design specification defines the characteristics of a REST system:

  • Client - Server : isolation between the client and the server, the server provides services to clients to consume.
  • Stateless : each request from the client to the server the request must contain the information necessary for understanding. In other words, information is not stored on the server a client request for a next use.
  • Cacheable : The server must express the client request can cache.
  • Layer system : a communication between the client and the server should be in a standard manner, that is, the intermediate layer in place when the server responds, the client does not need to make any changes.
  • Unified interface : server and client communication methods must be unified.
  • Demand coding : The server can provide executable code or script for the client to perform in their environment. This is the only constraint is optional.

What is a RESTful web service?

The original purpose of REST architecture is adapted to the World Wide Web HTTP protocol.

The core RESTful web services concept is "resource." Resources can be URI to represent. HTTP protocol defines client uses the method to send a request to these URIs, of course, can lead to changes in the state of "resource" is accessed.

HTTP standard methods are as follows:

==========  =====================  ==================================
HTTP method behavior example
==========  =====================  ==================================
GET access to information resources http://example.com/api/orders
GET get information http://example.com/api/orders/123 a particular resource
POST create a new resource http://example.com/api/orders
PUT updates the resource http://example.com/api/orders/123
DELETE delete resources http://example.com/api/orders/123
==========  ====================== ==================================

REST design does not require a particular data format. In the request data can be JSON form or url query term is sometimes used as parameters.

Design a simple web service

Adhere to the guidelines for the design of a REST web service API, or a task becomes a resource identifier is shown out and how they are affected by different request methods of practice.

For example, we want to write a to-do application for it and we want to design a web service. The first thing to do is to decide what kind of access the service using the root URL. For example, we can access this by:

http://[hostname]/todo/api/v1.0/

Here I have decided to include the name and version number of the API application in the URL. The application name included in the URL helps provide a namespace to distinguish other services on the same system. Can help contain future updates version number in the URL, if new and potentially incompatible functions exist in the new version, you can not rely on the influence of the older features of the application.

The next step is to select the service by exposure to (show) resources. This is a very simple application, we only have the task, so the only resource is the task of our backlog.

Our task will be to use the HTTP resource as follows:

==========  ===============================================  =============================
HTTP Method URL action
==========  ===============================================  ==============================
GET http: // [hostname] /todo/api/v1.0/tasks retrieval task list
GET http: // [hostname] /todo/api/v1.0/tasks/ [task_id] to 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 tasks
DELETE http: // [hostname] /todo/api/v1.0/tasks/ [task_id] Delete Task
==========  ================================================ =============================

We define the task has the following attributes:

  • the above mentioned id : unique identifier for the task. Digital type.
  • title : a brief description of the task. String type.
  • the Description : specific tasks described. Text type.
  • DONE : the task is completed. Boolean value.

So far about our web service designed basically completed. The remaining thing is to realize it!

About Flask framework

If you read Flask Mega-Tutorial series , you know Flask is a simple but very powerful Python web framework.

Before we delve into the details of web services, let's look at the structure of an ordinary Flask Web applications.

I will first assume you know the basics of Python works on your platform. I will explain examples of work in a Unix-like operating system. In short, this means they can work in Linux, Mac OS X and Windows (if you use Cygwin). If you're using Python version of Windows natively, then the command will be different.

Let us begin the installation Flask in a virtual environment. If you do not virtualenv on your system, you can from https://pypi.python.org/pypi/virtualenv download:

$ Mkdir todo-fire
$ Cd all-api
$ virtualenv flask
New python executable in flask/bin/python
Installing setuptools............................done.
Installing pip...................done.
$ flask/bin/pip install flask

Now that you have installed Flask, now simply create a web application, we put it in a file called app.py in:

#!flask/bin/python
from flask import Flask

app = Flask(__name__) @app.route('/') def index(): return "Hello, World!" if __name__ == '__main__': app.run(debug=True) 

To run this program we must perform app.py:

$ chmod a+x app.py
$ ./app.py
 * Running on http://127.0.0.1:5000/
 * Restarting with reloader

Now you can start your web browser, type http: // localhost: 5000 to see the effect of this small application.

Simple, right? Now we will convert this application into our RESTful service!

Python and Flask implement RESTful services

Flask use to build web services is very simple, than I do in Mega-Tutorial for more than simply a complete application server build in.

There are many extensions with Flask to help us build RESTful services, but in my opinion this task is very simple, no need to use Flask extension.

Our web service clients need to add, delete and modify tasks of service, so obviously we need a way to store tasks. The most direct way is to build a small database, but the database is not the subject of this article. Learn to use the appropriate database Flask, I strongly recommend reading the Mega-the Tutorial .

Here we store directly to the task list, task list, so these will only work in the web server is running in memory, at the end of it fail. This applies only way to develop our own web server, and does not apply to the production environment web server, this is a suitable database structures is necessary.

We now realize the first entry in a web service:

#!flask/bin/python
from flask import Flask, jsonify app = Flask(__name__) tasks = [ { 'id': 1, 'title': u'Buy groceries', 'description': u'Milk, Cheese, Pizza, Fruit, Tylenol', 'done': False }, { 'id': 2, 'title': u'Learn Python', 'description': u'Need to find a good Python tutorial on the web', 'done': False } ] @app.route('/todo/api/v1.0/tasks', methods=['GET']) def get_tasks(): return jsonify({'tasks': tasks}) if __name__ == '__main__': app.run(debug=True) 

As you can see, there is not much change. Our task is to create a memory database, this is nothing more than a dictionary and arrays. Properties task in the array with each element defined above.

Replaced the home page, we now have a get_tasks function, URI access to /todo/api/v1.0/tasks, and only allow the HTTP GET method.

This response is not a function of the text, we used data format in response JSON, in the Flask jsonify function generation from our data structure.

Use a Web browser to test our web service is not the best attention, because the method can not simulate all HTTP requests easily on the web browser. Instead, we will use the curl. If you have not curl installed, install it immediately.

By executing app.py, start web service. Then open a new console window, run the following command:

$ curl -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 294
Server: tool / 0.8.3 Python / 2.7.3
Date: Mon, 20 May 2013 04:53:53 GMT

{
  "tasks": [
    {
      "description": "Milk, Cheese, Pizza, Fruit, Tylenol",
      "done": false,
      "id": 1,
      "title": "Buy groceries"
    },
    {
      "description": "Need to find a good Python tutorial on the web",
      "done": false,
      "id": 2,
      "title": "Learn Python"
    }
  ]
}

We have successfully call a function of our RESTful service!

Now we start writing the second version of the GET method request our mission resources. This is a function that returns a single task:

from flask import abort

@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['GET']) def get_task(task_id): task = filter(lambda t: t['id'] == task_id, tasks) if len(task) == 0: abort(404) return jsonify({'task': task[0]}) 

The second function some meaning. Here we get the id URL in the task, and then convert it into Flask task_id parameters of function.

We use this parameter to search for our task array. If there is no id search our database, we will return an error similar to 404, according to the HTTP specification means "Resource not found."

If we find the task, so we just use it jsonify packaged into JSON format and sends it as a response, just as we handle the entire task set before.

The results call curl request are as follows:

$ curl -i http://localhost:5000/todo/api/v1.0/tasks/2
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 151
Server: tool / 0.8.3 Python / 2.7.3
Date: Mon, 20 May 2013 05:21:50 GMT

{
  "task": {
    "description": "Need to find a good Python tutorial on the web",
    "done": false,
    "id": 2,
    "title": "Learn Python"
  }
}
$ curl -i http://localhost:5000/todo/api/v1.0/tasks/3
HTTP/1.0 404 NOT FOUND
Content-Type: text/html
Content-Length: 238
Server: tool / 0.8.3 Python / 2.7.3
Date: Mon, 20 May 2013 05:21:52 GMT

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server.</p><p>If you     entered the URL manually please check your spelling and try again.</p>

When we requested resource id # 2, we get to, but when we requested # 3 returns a 404 error. The strange thing is about the error returns HTML information rather than JSON, because Flask generated 404 response according to the default mode. Since this is a Web service clients want us to always respond in JSON format, so we need to improve our 404 error handler:

from flask import make_response

@app.errorhandler(404) def not_found(error): return make_response(jsonify({'error': 'Not found'}), 404) 

We will get a friendly error message:

$ curl -i http://localhost:5000/todo/api/v1.0/tasks/3
HTTP/1.0 404 NOT FOUND
Content-Type: application/json
Content-Length: 26
Server: tool / 0.8.3 Python / 2.7.3
Date: Mon, 20 May 2013 05:36:54 GMT

{
  "error": "Not found"
}

Then there is the POST method, we used to insert a new task in our job database:

from flask import request

@app.route('/todo/api/v1.0/tasks', methods=['POST']) def create_task(): if not request.json or not 'title' in request.json: abort(400) task = { 'id': tasks[-1]['id'] + 1, 'title': request.json['title'], 'description': request.json.get('description', ""), 'done': False } tasks.append(task) return jsonify({'task': task}), 201 

Add a new task is quite easy. Only when the request form in JSON format, request.json will have the requested data. If there is no data, or data but there is a lack of title item, we will return 400, which is a request was invalid.

Then we will create a new task dictionary, using the last of a task id + 1 as the task id. We allow description field is missing, and it is assumed done field to False.

We add a new task to our task array, and to add new tasks and state 201 in response to the client.

Use the following curl command to test the new functions:

$ curl -i -H "Content-Type: application/json" -X POST -d '{"title":"Read a book"}' http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 201 Created
Content-Type: application/json
Content-Length: 104
Server: tool / 0.8.3 Python / 2.7.3
Date: Mon, 20 May 2013 05:56:21 GMT

{
  "task": {
    "description": "",
    "done": false,
    "id": 3,
    "title": "Read a book"
  }
}

Note: If you and run Cygwin version of curl on Windows, the above command will not have any problems. However, if you use the curl native, command a little different:

curl -i -H "Content-Type: application/json" -X POST -d "{"""title""":"""Read a book"""}" http://localhost:5000/todo/api/v1.0/tasks

Of course, after the completion of this request, we can get the updated list of tasks:

$ curl -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 423
Server: tool / 0.8.3 Python / 2.7.3
Date: Mon, 20 May 2013 05:57:44 GMT

{
  "tasks": [
    {
      "description": "Milk, Cheese, Pizza, Fruit, Tylenol",
      "done": false,
      "id": 1,
      "title": "Buy groceries"
    },
    {
      "description": "Need to find a good Python tutorial on the web",
      "done": false,
      "id": 2,
      "title": "Learn Python"
    },
    {
      "description": "",
      "done": false,
      "id": 3,
      "title": "Read a book"
    }
  ]
}

The remaining two functions are as follows:

@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['PUT']) 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': task[0]}) @app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['DELETE']) def delete_task(task_id): task = filter(lambda t: t['id'] == task_id, tasks) if len(task) == 0: abort(404) tasks.remove(task[0]) return jsonify({'result': True}) 

delete_task function nothing special. For update_task function, we need to strictly check the input parameters in order to prevent possible problems. We need to make sure before we put it to update the database, any client is to provide our expected format.

Task # 2 update function call as follows:

$ curl -i -H "Content-Type: application/json" -X PUT -d '{"done":true}' http://localhost:5000/todo/api/v1.0/tasks/2
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 170
Server: tool / 0.8.3 Python / 2.7.3
Date: Mon, 20 May 2013 07:10:16 GMT

{
  "task": [
    {
      "description": "Need to find a good Python tutorial on the web",
      "done": true,
      "id": 2,
      "title": "Learn Python"
    }
  ]
}

Optimization of web service interfaces

The current issue of the API is designed to force the client to return after the task ID to construct URIs. This server is very simple to know, but indirectly forcing the client how these URIs are constructed, which will prevent us from future changes to these URIs.

Not directly return the task ids, we have direct control returns the full URI these tasks, so that clients can always use these URIs. To this end, we can write a small helper function to generate a send a "public" version of the task to the client:

from flask import url_for

def make_public_task(task): new_task = {} for field in task: if field == 'id': new_task['uri'] = url_for('get_task', task_id=task['id'], _external=True) else: new_task[field] = task[field] return new_task 

Here all the things to do is removed from our database tasks and create a new task, id field of this task was replaced uri field generated by the url_for Flask.

When we return all of the task list when before being sent to the client for processing by this function:

@app.route('/todo/api/v1.0/tasks', methods=['GET']) def get_tasks(): return jsonify({'tasks': map(make_public_task, tasks)}) 

Here is a list of client tasks get time to get the data:

$ curl -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 406
Server: tool / 0.8.3 Python / 2.7.3
Date: Mon, 20 May 2013 18:16:28 GMT

{
  "tasks": [
    {
      "title": "Buy groceries",
      "done": false,
      "description": "Milk, Cheese, Pizza, Fruit, Tylenol",
      "uri": "http://localhost:5000/todo/api/v1.0/tasks/1"
    },
    {
      "title": "Learn Python",
      "done": false,
      "description": "Need to find a good Python tutorial on the web",
      "uri": "http://localhost:5000/todo/api/v1.0/tasks/2"
    }
  ]
}

We will apply the above-mentioned manner to all of the other functions to ensure that clients continue to see URIs instead of ids.

Strengthening of security RESTful web service

We have completed most of the functionality of our web service, but there is still a problem. Our web service is open to anyone, this is not a good idea.

We have a can-do we manage a complete web service, but in the current state of the web service is open to all clients. If a stranger figure out how our API works, he or she can write a client visit our web service and destroyed our data.

Most primary tutorial will ignore this problem and stop here. In my opinion this is a very serious issue, I must point out.

To ensure that our web service security services easiest way is to ask the client to provide a user name and password. In a conventional web application will provide a login form to authenticate, and the server will create a session for the operational use after the logged-on user, id session cookie stored in the form of the client browser. However, one of the rules of REST is "stateless", therefore we must require the client to provide authentication information in every request.

We have always tried to adhere to the standard HTTP protocol as possible. Since we need to realize that we need to complete certification in the context of HTTP, HTTP protocol provides two authentication mechanisms: Basic and Digest .

There is a small Flask extensions can help us, we can install Flask-HTTPAuth:

$ flask/bin/pip install flask-httpauth

Let's say we want our web service only allows access to the user name and password miguel python client access. We can set up a basic HTTP authentication as follows:

from flask.ext.httpauth import HTTPBasicAuth
auth = HTTPBasicAuth() @auth.get_password def get_password(username): if username == 'miguel': return 'python' return None @auth.error_handler def unauthorized(): return make_response(jsonify({'error': 'Unauthorized access'}), 401) 

get_password function is a callback function, Flask-HTTPAuth use it to get to a given user's password. In a more complex system, this function is required to check a user database, but in our case only a single user it is not necessary.

error_handler callback function for transmitting to the client an unauthorized error code. As we deal with other error codes, here we customize a response JSON data format instead of HTML include.

With the establishment of certification systems, have left to do is add @ auth.login_required decorator require authentication function. E.g:

@app.route('/todo/api/v1.0/tasks', methods=['GET']) @auth.login_required def get_tasks(): return jsonify({'tasks': tasks}) 

If you now want to try to use curl to call this function we get:

$ curl -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 401 UNAUTHORIZED
Content-Type: application/json
Content-Length: 36
WWW-Authenticate: Basic realm="Authentication Required"
Server: tool / 0.8.3 Python / 2.7.3
Date: Mon, 20 May 2013 06:41:14 GMT

{
  "error": "Unauthorized access"
}

To be able to call this function we have to send our certification credentials:

$ curl -u miguel:python -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 316
Server: tool / 0.8.3 Python / 2.7.3
Date: Mon, 20 May 2013 06:46:45 GMT

{
  "tasks": [
    {
      "title": "Buy groceries",
      "done": false,
      "description": "Milk, Cheese, Pizza, Fruit, Tylenol",
      "uri": "http://localhost:5000/todo/api/v1.0/tasks/1"
    },
    {
      "title": "Learn Python",
      "done": false,
      "description": "Need to find a good Python tutorial on the web",
      "uri": "http://localhost:5000/todo/api/v1.0/tasks/2"
    }
  ]
}

Authentication extension gives us a lot of freedom to choose which function needs to be protected, which functions need to expose.

In order to secure the logon information should use the HTTP secure server (for example: HTTPS: // ...), so that the communication between the client and server are encrypted to prevent transmission during a third-party certification to see credentials.

Uncomfortable when requesting receive a 401 error, web browser will jump out of an ugly login box, even if the request is happening in the background. So if we are to achieve a perfect web server, we need to jump to prohibit the browser displays the authentication dialog box, so that our client application to deal with their own login.

A simple way is to not return a 401 error. Error 403 is a very popular alternative, 403 error means "forbidden" error:

@auth.error_handler
def unauthorized():
    return make_response(jsonify({'error': 'Unauthorized access'}), 403) 

Possible improvements

We've written a small web service can also be improved in many aspects.

For starters, a real web service requires a real database support. Memory data structure we now have to use a lot of restrictions should not be used in real applications.

Another area that can be improved to handle multiple users. If the system supports multiple users, different clients can send different authentication credentials to obtain the user's task list. In such a system, we need a second resource is the user. POST requests on behalf of user resources on registering for a new user. A GET request indicates that the client access to information a user. A PUT request indicates that the update user information, such as may be updated email address. It represents a DELETE request to delete a user account.

GET request to retrieve the task list can be extended in several ways. First, you can carry an optional parameter page so that part of the task of client requests. In addition, this expansion is more useful: allow screening according to certain criteria. For example, a user only wants to see the completion of the task, or just want to see the title of the task A letter. All of these items can be used as a parameter of the URL.

Guess you like

Origin www.cnblogs.com/momoyan/p/11027572.html