others-how to upload file using python flask-restful api ?

1. Purpose

In this post, I would demo how to upload file using flask-restful api when using python. I would write a python script which accept a HTTP get request to list all the objects and a HTTP post request to upload file. And I would also demo how to read both the uploaded file and a normal field in the HTTP request.

2. The Environment

  • Python 3.7.6
  • Flask 1.1.2
  • Flask_restful 0.3.8

You can check your python dependencies version as follows:

$ pip show flask

Name: Flask
Version: 1.1.2
Summary: A simple framework for building complex web applications.
Home-page: https://palletsprojects.com/p/flask/
Author: Armin Ronacher
Author-email: [email protected]
License: BSD-3-Clause
Location: /Users/bswen/.pyenv/versions/3.7.6/lib/python3.7/site-packages
Requires: Werkzeug, click, itsdangerous, Jinja2
Required-by: Flask-RESTful, Flask-Bootstrap

And this:

$ pip show flask_restful     
Name: Flask-RESTful
Version: 0.3.8
Summary: Simple framework for creating REST APIs
Home-page: https://www.github.com/flask-restful/flask-restful/
Author: Twilio API Team
Author-email: help@twilio.com
License: BSD
Location: /Users/bswen/.pyenv/versions/3.7.6/lib/python3.7/site-packages
Requires: aniso8601, six, Flask, pytz
Required-by: 

3. Code

3.1 The domain object: The Book

Here we suppose we have a books store, and want to supply an API for uploading new books to the store. Here is the domain object that represents the Book:

Books = {
    'book1': {'title': 'build an API'},
    'book2': {'title': 'test 1'},
    'book3': {'title': 'profit!'},
}

3.2 The book list function

We should provide the users to ability to list all the books in the store, here is the class:

class BookList(Resource):
    def get(self):
        return Books

3.3 Configure the Flask_restful http request route

Then , we should configure the flask_rest app to route the http requests to our BookList object, just as follows:

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

api.add_resource(BookList, '/books')

The api.add_resource function just map an url to our class:

add_resource(resource, *urls, **kwargs)

Adds a resource to the api.

  • resource (Type[Resource]) – the class name of your resource
  • urls (str) – one or more url routes to match for the resource, standard flask routing rules apply. Any url variables will be passed to the resource method as args.

We should metion that the first parameter is a Class Name, the second is the url that we want to map.

3.4 The main function

if __name__ == '__main__':
    app.run(debug=True, port=8080, host="0.0.0.0")

3.5 Run the app

Now we can run the app , just type python upload_file_demo.py or run it in your pycharm, the result is:

/Users/bswen/.pyenv/versions/3.7.6/bin/python /Users/bswen/work/python/myutils/learn_flask/app_upload_demo.py
 * Serving Flask app "app_upload_demo" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://0.0.0.0:8080/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 717-204-539
127.0.0.1 - - [16/Mar/2021 20:54:44] "POST /books HTTP/1.1" 201 -
 * Detected change in '/Users/bswen/work/python/myutils/learn_flask/app_upload_demo.py', reloading
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 717-204-539

Then we can test the ‘/books’ url as follows:

$ curl http://localhost:8080/books                    
{
    "book1": {
        "title": "build an API"
    },
    "book2": {
        "title": "test 1"
    },
    "book3": {
        "title": "profit!"
    }
}

3.6 Let’s add the upload file function

We can add this function to the class ‘BookList’:

    UPLOAD_DIR="/Users/bswen/work/python/myutils/learn_flask/uploaded_files"
    
    def __init__(self):
        self.parser = reqparse.RequestParser()
        
    def post(self):
        # Part 1: add arguments in the post request
        self.parser.add_argument("bookfile", type=werkzeug.datastructures.FileStorage, location='files')
        self.parser.add_argument('title')
        
        # Part 2: parse the request
        args = self.parser.parse_args()
        
        # Part 3: compute and set the next id of the new book
        book_id = int(max(Books.keys()).lstrip('book')) + 1
        bookfile = args.get("bookfile")
        book_id = 'book%i' % book_id
        Books[book_id] = {'title': args['title']}
        
        # Part 4: save the uploaded file to UPLOAD_DIR
        bookfile.save(os.path.join(UPLOAD_DIR, bookfile.filename))
        return Books, 201

Let’s explain the above codes :

  • Part 1: The ‘self.parser.add_argument’ line declares two fields in the request, a file object and a normal text field in the request, here we used the type ‘werkzeug.datastructures.FileStorage’ to represent the file object in the request:
    • The Werkzeug is a comprehensive WSGI web application library, it began as a simple collection of various utilities for WSGI applications and has become one of the most advanced WSGI utility libraries. … A threaded WSGI server for use while developing applications locally. Werkzeug provides some subclasses of common Python objects to extend them with additional features. Some of them are used to make them immutable, others are used to change some semantics to better work with HTTP.
    • The FileStorage class is a thin wrapper over incoming files. It is used by the request object to represent uploaded files. All the attributes of the wrapper stream are proxied by the file storage so it’s possible to do storage.read() instead of the long form storage.stream.read().
  • Part 2: here we parse the request using the reqparser object , we read both the form data and the uploaded file (multipart/form-data) from the request, you should pay attention to the line:
    • bookfile = args.get(“bookfile”), this code retrieve the file object from the request
  • Part 3: We compute the next ID of the book , just add one based on the max ID of current books
  • Part 4: We can use the file object to save the uploaded file to the specified UPLOAD_DIR

3.7 Run the code and test the upload file function

Now we use the curl to test the file upload function:

(There should exist a file named test.zip in the current working directory where we call the curl command)

$ curl -v -X POST -H "Content-Type: multipart/form-data" -F "title=aabb" -F "[email protected]" http://localhost:8080/books

we get this result:

{
    "book1": {
        "title": "build an API"
    },
    "book2": {
        "title": "test 1"
    },
    "book3": {
        "title": "profit!"
    },
    "book4": {
        "title": "aabb"
    }
}

And the uploaded file:

➜  uploaded_files git:(master) ✗ ls -l        
total 32
-rw-r--r--  1 bswen  staff  13819 Mar 16 22:12 test.zip
➜  uploaded_files git:(master)pwd
/Users/bswen/work/python/myutils/learn_flask/uploaded_files

It works!

4. The whole codes

The code has been uploaded to github as gist,you can click the url to get all the code:

5. Summary

In this post, we demonstrated how to upload file using flask_restful and how to read both the uploaded file and the fields in the http request using flask . Thanks for your reading. Regards.