Wilson Mar bio photo

Wilson Mar

Hello. Hire me!

Email me Calendar Skype call 310 320-7878

LinkedIn Twitter Gitter Google+ Youtube

Github Stackoverflow Pinterest

Step-by-step using the Flask REST API library


Overview

This is a hands-on tutorial showing how to quickly create a simple Python blog server program which processes REST API calls from a user’s browser. By hands-on I mean explanations are provided after you do each action.


  1. Open a Terminal shell window on Mac or cmd window on Windows.
  2. Create a folder where you hold various projects under your user home folder. On a Mac:

    cd ~ && mkdir gits

    Alternately, use the workspace you use with the Eclipse IDE.

  3. Create and/or navigate into a folder holding where git will create repositories:

    mkdir wilsonmar && cd wilsonmar

    The example here is a user account name on GitHub.com. The repository of interest is at:

    <a target=”_blank” https://github.com/wilsonmar/python-api-flask”> https://github.com/wilsonmar/python-api-flask</a>

    You can download a Zip file containing it and unzip it locally. But using Git also downloads a database containing the history of changes.

    Note the repository was forked from GitHub user “sagaragarwal94” = Sagar Chand Agarwal (@sagaragarwal94).

  4. Have Git create a folder and download code from GitHub:

    git clone <a target=”_blank” https://github.com/wilsonmar/python-api-flask”> https://github.com/wilsonmar/python-api-flask</a>

    Alternately, you can Fork to your own account and change the URL accordingly.

    View code

  5. In a text editor open a file server.py

    #!/usr/bin/python3

    means that we need to install the Python interpreter and environment. At version 3, print() functions are used.

    from flask import Flask, request, jsonify
    from flask_restful import Resource, Api
    from sqlalchemy import create_engine
    from json import dumps
    

    The above specifies install of libraries to provide utility functions used in the program.

    db_connect = create_engine('sqlite:///chinook.db')

    means that we need to install the sqlite3 database engine.

    class Employees(Resource):

    means that we need to use the sample database file chinook.db which contains an employee table.

    Python Environment

    PROTIP: The standup.sh file in the repo from GitHub performs the steps below automatically. The script can be run repeatedly.

  6. Install virtualenv.
  7. Create a simple virtual environment folder venv:

    virtualenv venv

    The response:

    New python executable in /Users/wilsonmar/gits/wilsonmar/python-api-flask/venv/bin/python
    Installing setuptools, pip, wheel...done.
    

    PROTIP</a>: The virtualenv program executes commands in the .env file every time we cd into the directory. An example:

    source env/bin/activate
    export FLASK_APP="server.py"
    export APP_SETTINGS="development"
    export DATABASE_URL="postgresql://localhost/flask_api"
    

    PROTIP: venv is specified in the .gitignore file so the folder isn’t uploaded up to GitHub, since it’s created each time.

  8. Manually activate if you do not use the .env file:

    source venv/bin/activate

    The response is a different prompt, such as:

    Wilsons-MacBook-Pro:python-api-flask wilsonmar$ source venv/bin/activate
    

    PROTIP: The prompt now changed to:

    (venv) Wilsons-MacBook-Pro:python-api-flask wilsonmar$ 
    
  9. BTW, to get out of a virtualenv environment:

    deactivate

    Alternately, to get out of an Anaconda enviornment:

    source deactivate

  10. Install dependencies within venv:

    pip install flask flask-jsonpify flask-sqlalchemy flask-restful

    The response:

    Collecting flask
      Downloading Flask-0.12.2-py2.py3-none-any.whl (83kB)
     100% |████████████████████████████████| 92kB 1.4MB/s 
    Collecting flask-jsonpify
      Downloading Flask-Jsonpify-1.5.0.tar.gz
    Collecting flask-sqlalchemy
      Downloading Flask_SQLAlchemy-2.3.0-py2.py3-none-any.whl
    Collecting flask-restful
      Downloading Flask_RESTful-0.3.6-py2.py3-none-any.whl
    Collecting itsdangerous>=0.21 (from flask)
      Downloading itsdangerous-0.24.tar.gz (46kB)
     100% |████████████████████████████████| 51kB 1.2MB/s 
    Collecting Werkzeug>=0.7 (from flask)
      Downloading Werkzeug-0.12.2-py2.py3-none-any.whl (312kB)
     100% |████████████████████████████████| 317kB 1.1MB/s 
    Collecting Jinja2>=2.4 (from flask)
      Downloading Jinja2-2.9.6-py2.py3-none-any.whl (340kB)
     100% |████████████████████████████████| 348kB 1.2MB/s 
    Collecting click>=2.0 (from flask)
      Downloading click-6.7-py2.py3-none-any.whl (71kB)
     100% |████████████████████████████████| 71kB 1.3MB/s 
    Collecting SQLAlchemy>=0.8.0 (from flask-sqlalchemy)
      Downloading SQLAlchemy-1.1.14.tar.gz (5.2MB)
     100% |████████████████████████████████| 5.2MB 228kB/s 
    Collecting six>=1.3.0 (from flask-restful)
      Downloading six-1.11.0-py2.py3-none-any.whl
    Collecting pytz (from flask-restful)
      Downloading pytz-2017.2-py2.py3-none-any.whl (484kB)
     100% |████████████████████████████████| 491kB 514kB/s 
    Collecting aniso8601>=0.82 (from flask-restful)
      Downloading aniso8601-1.3.0.tar.gz (57kB)
     100% |████████████████████████████████| 61kB 791kB/s 
    Collecting MarkupSafe>=0.23 (from Jinja2>=2.4->flask)
      Downloading MarkupSafe-1.0.tar.gz
    Collecting python-dateutil (from aniso8601>=0.82->flask-restful)
      Downloading python_dateutil-2.6.1-py2.py3-none-any.whl (194kB)
     100% |████████████████████████████████| 194kB 3.9MB/s 
    Building wheels for collected packages: flask-jsonpify, itsdangerous, SQLAlchemy, aniso8601, MarkupSafe
      Running setup.py bdist_wheel for flask-jsonpify ... done
      Stored in directory: /Users/wilsonmar/Library/Caches/pip/wheels/94/49/b1/376d04c3136a18e59dbda03d7c5dd5d242e1035372b6703076
      Running setup.py bdist_wheel for itsdangerous ... done
      Stored in directory: /Users/wilsonmar/Library/Caches/pip/wheels/fc/a8/66/24d655233c757e178d45dea2de22a04c6d92766abfb741129a
      Running setup.py bdist_wheel for SQLAlchemy ... done
      Stored in directory: /Users/wilsonmar/Library/Caches/pip/wheels/9f/cc/4b/d2645b72ec1ba3dd72d7ae384c431cf56bae03918f38c4e5e5
      Running setup.py bdist_wheel for aniso8601 ... done
      Stored in directory: /Users/wilsonmar/Library/Caches/pip/wheels/e3/6a/48/e4f2d89ff4146557cae20b77a9af7b4d09dd47b2004133cd7e
      Running setup.py bdist_wheel for MarkupSafe ... done
      Stored in directory: /Users/wilsonmar/Library/Caches/pip/wheels/88/a7/30/e39a54a87bcbe25308fa3ca64e8ddc75d9b3e5afa21ee32d57
    Successfully built flask-jsonpify itsdangerous SQLAlchemy aniso8601 MarkupSafe
    Installing collected packages: itsdangerous, Werkzeug, MarkupSafe, Jinja2, click, flask, flask-jsonpify, SQLAlchemy, flask-sqlalchemy, six, pytz, python-dateutil, aniso8601, flask-restful
    Successfully installed Jinja2-2.9.6 MarkupSafe-1.0 SQLAlchemy-1.1.14 Werkzeug-0.12.2 aniso8601-1.3.0 click-6.7 flask-0.12.2 flask-jsonpify-1.5.0 flask-restful-0.3.6 flask-sqlalchemy-2.3.0 itsdangerous-0.24 python-dateutil-2.6.1 pytz-2017.2 six-1.11.0
    
  11. Generate dependencies into a requirements.txt file:

    pip freeze </pre>

    The response:

    aniso8601==1.3.0
    click==6.7
    Flask==0.12.2
    Flask-Jsonpify==1.5.0
    Flask-RESTful==0.3.6
    Flask-SQLAlchemy==2.3.0
    itsdangerous==0.24
    Jinja2==2.9.6
    MarkupSafe==1.0
    python-dateutil==2.6.1
    pytz==2017.2
    six==1.11.0
    SQLAlchemy==1.1.14
    Werkzeug==0.12.2
    

    A description the Flask Framework is at http://flask.pocoo.org

    Additional information about the Flask framework is at:

    • http://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world
    • https://blog.miguelgrinberg.com/post/designing-a-restful-api-with-python-and-flask

    Flask works with:

    • aniso8601
    • click
    • Jinja2
    • MarkupSafe
    • Flask
    • Flash-Jsonify
    • Flask-SQLAlchemy is a Flask extension to support SQLAlchemy, an Object Relational Mapper (ORM).
    • Flask-Restful
    • Werkzeug

    Alternately, a more robust database is to runb (on default port 5432):

  12. PostgreSQL – Postgres database offers many advantages over others.
  13. Psycopg2 – A Python adapter for Postgres.
  14. Establish Database

  15. Install SQLite3 on MacOS from the internet:

    brew install sqlite3 -g

    This installs in another folder (not the pwd).

    The chinook.db is in the repository. But if it’s not there, download the “chinook.zip” file from:

    http://www.sqlitetutorial.net/download/sqlite-sample-database/?wpdmdl=94

    1. Unzip file to obtain file “chinook.db” which contains the database.
    2. Move the chinook.db file to the python_rest folder.
  16. Open the database using SQLite:

    sqlite3 chinook.db

    The response contains the date of the SQLite3 version being used:

    SQLite version 3.16.0 2016-11-04 19:09:39
    Enter ".help" for usage hints.
    sqlite> 
    
  17. List commands

    sqlite> .help

    The response is as shown on https://www.sqlite.org/cli.html

    .auth ON|OFF           Show authorizer callbacks
    .backup ?DB? FILE      Backup DB (default "main") to FILE
    .bail on|off           Stop after hitting an error.  Default OFF
    .binary on|off         Turn binary output on or off.  Default OFF
    .changes on|off        Show number of rows changed by SQL
    .check GLOB            Fail if output since .testcase does not match
    .clone NEWDB           Clone data into NEWDB from the existing database
    .databases             List names and files of attached databases
    .dbinfo ?DB?           Show status information about the database
    .dump ?TABLE? ...      Dump the database in an SQL text format
                          If TABLE specified, only dump tables matching
                          LIKE pattern TABLE.
    .echo on|off           Turn command echo on or off
    .eqp on|off|full       Enable or disable automatic EXPLAIN QUERY PLAN
    .exit                  Exit this program
    .explain ?on|off|auto? Turn EXPLAIN output mode on or off or to automatic
    .fullschema ?--indent? Show schema and the content of sqlite_stat tables
    .headers on|off        Turn display of headers on or off
    .help                  Show this message
    .import FILE TABLE     Import data from FILE into TABLE
    .imposter INDEX TABLE  Create imposter table TABLE on index INDEX
    .indexes ?TABLE?       Show names of all indexes
                          If TABLE specified, only show indexes for tables
                          matching LIKE pattern TABLE.
    .limit ?LIMIT? ?VAL?   Display or change the value of an SQLITE_LIMIT
    .log FILE|off          Turn logging on or off.  FILE can be stderr/stdout
    .mode MODE ?TABLE?     Set output mode where MODE is one of:
                          ascii    Columns/rows delimited by 0x1F and 0x1E
                          csv      Comma-separated values
                          column   Left-aligned columns.  (See .width)
                          html     HTML <table> code
                          insert   SQL insert statements for TABLE
                          line     One value per line
                          list     Values delimited by .separator strings
                          quote    Escape answers as for SQL
                          tabs     Tab-separated values
                          tcl      TCL list elements
    .nullvalue STRING      Use STRING in place of NULL values
    .once FILENAME         Output for the next SQL command only to FILENAME
    .open ?--new? ?FILE?   Close existing database and reopen FILE
                          The --new starts with an empty file
    .output ?FILENAME?     Send output to FILENAME or stdout
    .print STRING...       Print literal STRING
    .prompt MAIN CONTINUE  Replace the standard prompts
    .quit                  Exit this program
    .read FILENAME         Execute SQL in FILENAME
    .restore ?DB? FILE     Restore content of DB (default "main") from FILE
    .save FILE             Write in-memory database into FILE
    .scanstats on|off      Turn sqlite3_stmt_scanstatus() metrics on or off
    .schema ?PATTERN?      Show the CREATE statements matching PATTERN
                           Add --indent for pretty-printing
    .separator COL ?ROW?   Change the column separator and optionally the row
                          separator for both the output mode and .import
    .shell CMD ARGS...     Run CMD ARGS... in a system shell
    .show                  Show the current values for various settings
    .stats ?on|off?        Show stats or turn stats on or off
    .system CMD ARGS...    Run CMD ARGS... in a system shell
    .tables ?TABLE?        List names of tables
                          If TABLE specified, only list tables matching
                          LIKE pattern TABLE.
    .testcase NAME         Begin redirecting output to 'testcase-out.txt'
    .timeout MS            Try opening locked tables for MS milliseconds
    .timer on|off          Turn SQL timer on or off
    .trace FILE|off        Output each SQL statement as it is run
    .vfsinfo ?AUX?         Information about the top-level VFS
    .vfslist               List all available VFSes
    .vfsname ?AUX?         Print the name of the VFS stack
    .width NUM1 NUM2 ...   Set column widths for "column" mode
                          Negative values right-justify
     
  18. List the 11 custom data tables in the sample chinook database loaded:

    sqlite> .tables

    The response:

    albums          employees       invoices        playlists
    artists         genres          media_types     tracks
    customers       invoice_items   playlist_track
    

    PROTIP: These tables are a rather strang assortment that normally do not belong together. But it’s there as technical samples.

    sqlite-sample-database-color-650x327-82797

  19. For more information from the SQLite Tutorial website.

    • employees table stores employees data such as employee id, last name, first name, etc. It also has a field named ReportsTo to specify who reports to whom.
    • customers table stores customers data.
    • invoices & invoice_items tables: these two tables store invoice data. The invoices table stores invoice header data and the invoice_items table stores the invoice line items data.
    • artists table stores artists data. It is a simple table that contains only artist id and name.
    • albums table stores data about a list of tracks. Each album belongs to one artist. However, one artist may have multiple albums.
    • media_types table stores media types such as MPEG audio file, ACC audio file, etc.
    • genres table stores music types such as rock, jazz, metal, etc.
    • tracks table store the data of songs. Each track belongs to one album.
    • playlists & playlist_track tables: playlists table store data about playlists. Each playlist contains a list of tracks. Each track may belong to multiple playlists. The relationship between the playlists table and tracks table is many-to-many. The playlist_track table is used to reflect this relationship.
    • </ul> ### invoke
  20. Open another Terminal shell window so that the database remains running.
  21. Change to the directory you are using:

    cd ~/gits/wilsonmar/python-api-flask

  22. Ensure permissions: On a Mac:

    chmod a+x server.py

  23. Initiate the Python web service:

    python server.py

    Alternately:

    ./server.py

    The response:

    * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
    

    TODO: Add HATEOS to respond to URL with no resource (folder) specified.

    Routes Walkthough

  24. View the bottom of the program where code to invoke it is defined:

    if __name__ == '__main__':
      app.run
    

    Alternately, app.run(debug=True).

    Up from the bottom of the program are three api.add_resource statements that define routes the server processes.

    Note how modular each route is. This enables incremental addition of programming code capabilities over time.

    Get Tracks

  25. In your browser, go to the first URL:

    http://127.0.0.1:5000/tracks

    Alternately, on the command line:

    curl -i http://127.0.0.1:5000/tracks

    The response shows details for each media track:

    {
      "data": [
     {
       "Composer": "Angus Young, Malcolm Young, Brian Johnson", 
       "Name": "For Those About To Rock (We Salute You)", 
       "TrackId": 1, 
       "UnitPrice": 0.99
     }, 
    

    This output is from a database query to obtain a result returned.

    class Tracks(Resource):
     def get(self):
         conn = db_connect.connect()
         query = conn.execute("select trackid, name, composer, unitprice from tracks;")
         result = {'data': [dict(zip(tuple (query.keys()) ,i)) for i in query.cursor]}
         return jsonify(result)
    

    Note each class is self-contained.

    The conn object is defined before making the query.

    The execute function sends to the database connection SQL commands referencing fields defined in the database schema shown above.

    The query.cursor points to the whole query response object. The for keyword loops the various keys, using i as the index of each current item.

    tuple identifies the key in each item in the query object.

    zip takes one or more sequences, and returns a sequence of tuples.

    dict constructs dictionary objects from a sequence of key/value tuples.

    data is the high-level item in the response.

    The jsonify function formats the result object to JSON formatting for display.

    List Employees

  26. In your browser, go to the URL accessing the “employees” list route:

    http://127.0.0.1:5000/employees

    Alternately, on the command line:

    curl -i http://127.0.0.1:5000/employees

    The response shows ids of all the employees in the pre-populated database. All 8 in the sample database:

    {"employees": [1, 2, 3, 4, 5, 6, 7, 8]}
    

    TODO: Now we change the code to list employee names instead of just their numbers.

    List specific employee

  27. In your browser, go to the URL accessing the employees/item route:

    Alternately, on the command line:

    curl -i http://127.0.0.1:5000/employees/8

    The response shows details of employee whose employeeid is 8:

    {
      "data": [
     {
       "Address": "923 7 ST NW", 
       "BirthDate": "1968-01-09 00:00:00", 
       "City": "Lethbridge", 
       "Country": "Canada", 
       "Email": "laura@chinookcorp.com", 
       "EmployeeId": 8, 
       "Fax": "+1 (403) 467-8772", 
       "FirstName": "Laura", 
       "HireDate": "2004-03-04 00:00:00", 
       "LastName": "Callahan", 
       "Phone": "+1 (403) 467-3351", 
       "PostalCode": "T1H 1Y8", 
       "ReportsTo": 6, 
       "State": "AB", 
       "Title": "IT Staff"
     }
      ]
    }
    

    The output output is from this code which makes a database query:

    class Employees(Resource):
     def get(self):
         conn = db_connect.connect() # connect to database
         query = conn.execute("select * from employees") # This line performs query and returns json result
         return {'employees': [i[0] for i in query.cursor.fetchall()]} # Fetches first column that is Employee ID
    
  28. Instead of “8”, use annother number to obtain data for another employee.

    PROTIP: This is no longer considered a secure design. Services now use hashed values instead of allowing incrementable numbers to identify specific rows.

    Post new employee

    PROTIP: The previous code defines the response to “get” requests.

    The code to post a new entry is this:

    def post(self):
         conn = db_connect.connect()
         print(request.json)  # used during testing.
         LastName = request.json['LastName']
         FirstName = request.json['FirstName']
         Title = request.json['Title']
         ReportsTo = request.json['ReportsTo']
         BirthDate = request.json['BirthDate']
         HireDate = request.json['HireDate']
         Address = request.json['Address']
         City = request.json['City']
         State = request.json['State']
         Country = request.json['Country']
         PostalCode = request.json['PostalCode']
         Phone = request.json['Phone']
         Fax = request.json['Fax']
         Email = request.json['Email']
         query = conn.execute("insert into employees values(null,'{0}','{1}','{2}','{3}', \
                              '{4}','{5}','{6}','{7}','{8}','{9}','{10}','{11}','{12}', \
                              '{13}')".format(LastName,FirstName,Title,
                              ReportsTo, BirthDate, HireDate, Address,
                              City, State, Country, PostalCode, Phone, Fax,
                              Email))
         return {'status':'success'}
    

    def post(self): appears instead of a get in the definition.

    Unlike Get requests, which involve specification of just an URL, post requests also require the client to submit to the server additional fields in the HTTP header.

    The Python server program expects from the client to add into the database with this coding:

    LastName = request.json['LastName']

    If a curl utility program is used as the client, it would look like this:

    curl -X POST http://127.0.0.1:5000/employees/9 -d ‘{“LastName”:”Wayne”, “FirstName”:”Bruce”}’ -H “Content-Type: application/json”

    PROTIP: The simple sample code contains no editing of inputs which all “production worthy” code should have. Examples of edits include whether all required fields are supplied and that content are valid. Such edits would be in client software as well.

    The content following -d provides what is specified in server code such as:

    LastName = request.json['LastName']

    The application/json specifes the format expected back from the server.

    PROTIP: This starter program does not have logic to prevent duplicates from being added.

Next

TODO: Generation of code from database using Swagger (OpenAPI).

TODO: Lint code with default SonarQube rules.

TODO: GraphQL - https://github.com/graphql-python/flask-graphql or after buiding schemas using http://graphene-python.org/

Resources

This tutorial was originally described at https://www.codementor.io/sagaragarwal94/building-a-basic-restful-api-in-python-58k02xsiq

https://scotch.io/tutorials/build-a-restful-api-with-flask-the-tdd-way

http://python.apichecklist.com/ from Dan Bader.