PolarSPARC

Web Applications using Python Flask - Part I


Bhaskar S 08/30/2021


Overview

Flask is an elegant, popular, and simple micro-framework for building web applications in Python.


Installation and Setup

Installation and setup will be on a Linux desktop running Ubuntu 20.04 LTS. Note that the stable Python version on Ubuntu is 3.8. Also, we will assume the logged in user is alice with the home directory located at /home/alice.

For our demonstration, we will create a directory called MyFlask under the users home directory by executing the following command in a terminal window:

$ mkdir -p $HOME/MyFlask

Next, we will create a project specific Python virtual environment using the venv module. In order to do that, we first need to install the package for venv by executing the following command in a terminal window:

$ sudo apt install python3.8-venv

The Python venv module allows one to create a lightweight virtual environments, each with its own directory structure, that are isolated from the system specific directory structure. To create a Python virtual environment, execute the following command(s) in the terminal window:

$ cd $HOME/MyFlask

$ python3 -m venv venv

This will create a directory called venv under the current directory. On needs to activate the newly created virtual environment by executing the following command in the terminal window:

$ source venv/bin/activate

On successful virtual environment activation, the prompt will be prefixed with (venv).

We will now install the following Python modules:

Execute the following command(s) in the terminal window (with venv activated):

$ pip install flask flask-cors jinja2 gunicorn sqlalchemy

Next, we will install a small, fast, self-contained, highly-reliable, full-featured, open-source SQL database engine called sqlite by executing the following command in a terminal window:

$ sudo apt install sqlite3

For our web application, we will create a flask project directory called SecureNotes under the directory MyFlask by executing the following command(s) in the terminal window:

$ mkdir -p $HOME/MyFlask/SecureNotes

$ mkdir -p $HOME/MyFlask/SecureNotes/config

$ mkdir -p $HOME/MyFlask/SecureNotes/db

$ mkdir -p $HOME/MyFlask/SecureNotes/model

$ mkdir -p $HOME/MyFlask/SecureNotes/static/js

$ mkdir -p $HOME/MyFlask/SecureNotes/static/images

$ mkdir -p $HOME/MyFlask/SecureNotes/templates


Hands-on Python Flask

The following is the Python script called config.py that will be located in the directory SecureNotes/config:


config.py
#
# @Author: Bhaskar S
# @Blog:   https://www.polarsparc.com
# @Date:   30 Aug 2021
#

from flask import Flask

app_name = 'SecureNotes'

app = Flask(app_name)

Some aspects of the config.py from the above needs a little explanation.

The following is the Python script called main.py that will be located in the directory SecureNotes:


main.py
#
# @Author: Bhaskar S
# @Blog:   https://www.polarsparc.com
# @Date:   30 Aug 2021
#

from config.config import app

def index():
    return '<h3>Welcome to SecureNotes</h3>'

app.add_url_rule('/', view_func=index)

Some aspects of the main.py from the above needs a little explanation.

To test our the Python Flask web application, we need to start the gunicorn web server, which will connect with the WSGI enabled Flask application by executing the following command in the venv terminal window:

$ gunicorn --bind '127.0.0.1:8080' main:app

The first option of the command specifies the ip address and port to bind the web server to. The second option of the command specifies the Flask application module (which is main in our case) and the Flask object within the application module (which is app in our case that implements the WSGI middleware).

The following would be a typical output:

Output.1

[2021-08-30 12:27:18 -0400] [14826] [INFO] Starting gunicorn 20.1.0
[2021-08-30 12:27:18 -0400] [14826] [INFO] Listening at: http://127.0.0.1:8080 (14826)
[2021-08-30 12:27:18 -0400] [14826] [INFO] Using worker: sync
[2021-08-30 12:27:18 -0400] [14828] [INFO] Booting worker with pid: 14828

Launch a browser and access the URL http://127.0.0.1:8080/. The following illustration shows the response on the browser:

Request from Browser
Figure.2

The following is the modified version of the Python script main.py that is functionally the same as the previous case, except that it makes use of Flask defined Python decorator:


main.py
#
# @Author: Bhaskar S
# @Blog:   https://www.polarsparc.com
# @Date:   30 Aug 2021
#

from config.config import app

@app.route('/')
def index():
    return '<h3>Welcome to SecureNotes</h3>'

Some aspects of the main.py from the above needs a little explanation.

We will now move into the next phase of introducing HTML web content into Flask.

The following is the HTML page called welcome.html that will be located in the directory SecureNotes/templates:


welcome.html
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Welcome to Secure Notes</title>
    </head>
    <body>
        <h3>Welcome to Secure Notes</h3>
        <p>This is a demo app using Flask</p>
        <br/>
        <hr/>
        <p style="text-align:center;"><img src="static/images/polarsparc.png" alt="PolarSPARC"></p>
    </body>
</html>

The following is the modified version of the Python script config.py:


config.py
#
# @Author: Bhaskar S
# @Blog:   https://www.polarsparc.com
# @Date:   30 Aug 2021
#

from flask import Flask
import logging

app_name = 'SecureNotes'

app = Flask(app_name)

gunicorn_logger = logging.getLogger('gunicorn.error')
app.logger.handlers = gunicorn_logger.handlers
app.logger.setLevel(gunicorn_logger.level)

app.logger.debug('Flask application root path: %s' % app.root_path)
app.logger.debug('Flask application static folder: %s' % app.static_folder)
app.logger.debug('Flask application template folder: %s' % os.path.join(app.root_path, app.template_folder))

Some aspects of the config.py from the above needs a little explanation.

The following is the modified version of the Python script main.py:


main.py
#
# @Author: Bhaskar S
# @Blog:   https://www.polarsparc.com
# @Date:   30 Aug 2021
#

from flask.templating import render_template
from config.config import app

@app.route('/')
def index():
    return render_template('welcome.html')

Some aspects of the main.py from the above needs a little explanation.

To test our the Python Flask web application, start the gunicorn web server by executing the following command in the venv terminal window:

$ gunicorn --bind '127.0.0.1:8080' --workers=2 --log-level=debug main:app

The second option of the command specifies the the number of worker processes to use to handle the web server requests. The third option of the command indicates the logging level to use.

The following would be a typical output:

Output.2

[2021-08-30 15:04:19 -0400] [18347] [DEBUG] Current configuration:
  config: ./gunicorn.conf.py
  wsgi_app: None
  bind: ['127.0.0.1:8080']
  backlog: 2048
  workers: 2
  worker_class: sync
  threads: 1
  worker_connections: 1000
  max_requests: 0
  max_requests_jitter: 0
  timeout: 30
  graceful_timeout: 30
  keepalive: 2
  limit_request_line: 4094
  limit_request_fields: 100
  limit_request_field_size: 8190
  reload: False
  reload_engine: auto
  reload_extra_files: []
  spew: False
  check_config: False
  print_config: False
  preload_app: False
  sendfile: None
  reuse_port: False
  chdir: /home/alice/MyFlask/SecureNotes
  daemon: False
  raw_env: []
  pidfile: None
  worker_tmp_dir: None
  user: 1000
  group: 1000
  umask: 0
  initgroups: False
  tmp_upload_dir: None
  secure_scheme_headers: {'X-FORWARDED-PROTOCOL': 'ssl', 'X-FORWARDED-PROTO': 'https', 'X-FORWARDED-SSL': 'on'}
  forwarded_allow_ips: ['127.0.0.1']
  accesslog: None
  disable_redirect_access_to_syslog: False
  access_log_format: %(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"
  errorlog: -
  loglevel: debug
  capture_output: False
  logger_class: gunicorn.glogging.Logger
  logconfig: None
  logconfig_dict: {}
  syslog_addr: udp://localhost:514
  syslog: False
  syslog_prefix: None
  syslog_facility: user
  enable_stdio_inheritance: False
  statsd_host: None
  dogstatsd_tags: 
  statsd_prefix: 
  proc_name: None
  default_proc_name: main:app
  pythonpath: None
  paste: None
  on_starting: <function OnStarting.on_starting at 0x7f951d4aa790>
  on_reload: <function OnReload.on_reload at 0x7f951d4aa8b0>
  when_ready: <function WhenReady.when_ready at 0x7f951d4aa9d0>
  pre_fork: <function Prefork.pre_fork at 0x7f951d4aaaf0>
  post_fork: <function Postfork.post_fork at 0x7f951d4aac10>
  post_worker_init: <function PostWorkerInit.post_worker_init at 0x7f951d4aad30>
  worker_int: <function WorkerInt.worker_int at 0x7f951d4aae50>
  worker_abort: <function WorkerAbort.worker_abort at 0x7f951d4aaf70>
  pre_exec: <function PreExec.pre_exec at 0x7f951d43f0d0>
  pre_request: <function PreRequest.pre_request at 0x7f951d43f1f0>
  post_request: <function PostRequest.post_request at 0x7f951d43f280>
  child_exit: <function ChildExit.child_exit at 0x7f951d43f3a0>
  worker_exit: <function WorkerExit.worker_exit at 0x7f951d43f4c0>
  nworkers_changed: <function NumWorkersChanged.nworkers_changed at 0x7f951d43f5e0>
  on_exit: <function OnExit.on_exit at 0x7f951d43f700>
  proxy_protocol: False
  proxy_allow_ips: ['127.0.0.1']
  keyfile: None
  certfile: None
  ssl_version: 2
  cert_reqs: 0
  ca_certs: None
  suppress_ragged_eofs: True
  do_handshake_on_connect: False
  ciphers: None
  raw_paste_global_conf: []
  strip_header_spaces: False
[2021-08-30 15:04:19 -0400] [18347] [INFO] Starting gunicorn 20.1.0
[2021-08-30 15:04:19 -0400] [18347] [DEBUG] Arbiter booted
[2021-08-30 15:04:19 -0400] [18347] [INFO] Listening at: http://127.0.0.1:8080 (18347)
[2021-08-30 15:04:19 -0400] [18347] [INFO] Using worker: sync
[2021-08-30 15:04:19 -0400] [18349] [INFO] Booting worker with pid: 18349
[2021-08-30 15:04:19 -0400] [18350] [INFO] Booting worker with pid: 18350
[2021-08-30 15:04:19 -0400] [18347] [DEBUG] 2 workers
[2021-08-30 15:04:19 -0400] [18350] [DEBUG] Flask application root path: /home/bswamina/MyProjects/MyFlask/SecureNotes
[2021-08-30 15:04:19 -0400] [18350] [DEBUG] Flask application static folder: /home/bswamina/MyProjects/MyFlask/SecureNotes/static
[2021-08-30 15:04:19 -0400] [18350] [DEBUG] Flask application template folder: /home/bswamina/MyProjects/MyFlask/SecureNotes/templates
[2021-08-30 15:04:19 -0400] [18349] [DEBUG] Flask application root path: /home/bswamina/MyProjects/MyFlask/SecureNotes
[2021-08-30 15:04:19 -0400] [18349] [DEBUG] Flask application static folder: /home/bswamina/MyProjects/MyFlask/SecureNotes/static
[2021-08-30 15:04:19 -0400] [18349] [DEBUG] Flask application template folder: /home/bswamina/MyProjects/MyFlask/SecureNotes/templates

Launch a browser and access the URL http://127.0.0.1:8080/. The following illustration shows the response on the browser:

Request from Browser
Figure.3

We will now move into the next phase by modifying the HTML page welcome.html into a login page and make it look more slick using Bootstrap . Download bootstrap.min.css and save it in the directory located at SecureNotes/static.

The following is the modified version of the HTML page welcome.html:


welcome.html
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <link rel="stylesheet" href="static/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
        <title>Welcome to Secure Notes - Login</title>
    </head>
    <body>
        <div class="container">
            <div class="alert alert-secondary" role="alert">
                <div class="text-center">
                    <h3>Welcome to Secure Notes - Login</h3>
                </div>
            </div>
            <form>
                <div class="form-group">
                    <label for="emailInput">Email</label>
                    <input type="email" class="form-control" id="email" name="email" required placeholder="Enter email...">
                </div>
                <div class="form-group">
                    <label for="passwordInput">Password</label>
                    <input type="password" class="form-control" id="password" name="password" required placeholder="Enter password...">
                </div>
                <button type="submit" class="btn btn-primary">Login</button>
            </form>
            <br/>
            <div class="alert alert-primary" role="alert">
                Don't have an account - <a href="#" class="alert-link">Sign Up</a>
            </div>
            <div class="text-center">
                <hr/>
                <img class="img-thumbnail" src="static/images/polarsparc.png" alt="PolarSPARC">
            </div>
        </div>        
    </body>
</html>

Restart the gunicorn server and launch a browser and access the URL http://127.0.0.1:8080/. The following illustration shows the response on the browser:

Request from Browser
Figure.4

This is looking much better now !!!

One can define all the options used to start the gunicorn server in a Python script called gunicorn_config.py that is located in the SecureNotes/config directory and use that to start the server:


gunicorn_config.py
#
# @Author: Bhaskar S
# @Blog:   https://www.polarsparc.com
# @Date:   30 Aug 2021
#

bind = ['127.0.0.1:8080']
workers = 2
reload = True
loglevel = 'debug'
wsgi_app = 'main:app'

Going forward, one can start the gunicorn web server by executing the following command in the venv terminal window:

$ gunicorn --config config/gunicorn_config.py

We will now create a new HTML page for sign-up and link it from the HTML page welcome.html.

The following is the HTML sign-up page called signup.html that will be located in the directory SecureNotes/templates:


signup.html
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <link rel="stylesheet" href="static/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
        <title>Welcome to Secure Notes - Sign Up</title>
    </head>
    <body>
        <div class="container">
            <div class="alert alert-secondary" role="alert">
                <div class="text-center">
                    <h3>Welcome to Secure Notes - Sign Up</h3>
                </div>
            </div>
            <form>
                <div class="form-group">
                    <label for="emailInput">Email</label>
                    <input type="email" class="form-control" id="email" name="email" required placeholder="Enter email...">
                </div>
                <div class="form-group">
                    <label for="passwordInput">Password</label>
                    <input type="password" class="form-control" id="password1" name="password1" required placeholder="Enter password...">
                </div>
                <div class="form-group">
                    <label for="passwordInput">Confirm Password</label>
                    <input type="password" class="form-control" id="password2" name="password2" required placeholder="Confirm password...">
                </div>
                <button type="submit" class="btn btn-primary">Register</button>
            </form>
            <div class="text-center">
                <hr/>
                <img class="img-thumbnail" src="static/images/polarsparc.png" alt="PolarSPARC">
            </div>
        </div>        
    </body>
</html>

The following is the modified version of the HTML page welcome.html that links the HTML page signup.html via the URL /signup:


welcome.html
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <link rel="stylesheet" href="static/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
        <title>Welcome to Secure Notes - Login</title>
    </head>
    <body>
        <div class="container">
            <div class="alert alert-secondary" role="alert">
                <div class="text-center">
                    <h3>Welcome to Secure Notes - Login</h3>
                </div>
            </div>
            <form>
                <div class="form-group">
                    <label for="emailInput">Email</label>
                    <input type="email" class="form-control" id="email" name="email" required placeholder="Enter email...">
                </div>
                <div class="form-group">
                    <label for="passwordInput">Password</label>
                    <input type="password" class="form-control" id="password" name="password" required placeholder="Enter password...">
                </div>
                <button type="submit" class="btn btn-primary">Login</button>
            </form>
            <br/>
            <div class="alert alert-primary" role="alert">
                Don't have an account - <a href="/signup" class="alert-link">Sign Up</a>
            </div>
            <div class="text-center">
                <hr/>
                <img class="img-thumbnail" src="static/images/polarsparc.png" alt="PolarSPARC">
            </div>
        </div>        
    </body>
</html>

The following is the modified version of the Python script main.py to handle the URL endpoint for /signup:


main.py
#
# @Author: Bhaskar S
# @Blog:   https://www.polarsparc.com
# @Date:   30 Aug 2021
#

from flask.templating import render_template
from config.config import app

@app.route('/')
def index():
    return render_template('welcome.html')

@app.route('/signup')
def signup():
    return render_template('signup.html')

Restart the gunicorn server and launch a browser and access the URL http://127.0.0.1:8080/. Once the login page loads, click on the 'Sign Up' link. The following illustration shows the response on the browser:

Sign Up from Browser
Figure.5

Note that when the 'Sign Up' link on the HTML page welcome.html is clicked on, it makes a HTTP GET request to the URL /signup, which is handled by the Flask application main.py in the method signup() to render the sign-up HTML page signup.html on the browser. Currently, when the 'Register' button on the HTML page signup.html is clicked, nothing happens. We will fix that so that it will make a HTTP POST of the form data to the URL at /signup and handled by the same method signup() in the main Flask application.

The following is the modified version of the HTML page signup.html to POST the form to the URL at /signup:


signup.html
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <link rel="stylesheet" href="static/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
        <title>Welcome to Secure Notes - Sign Up</title>
    </head>
    <body>
        <div class="container">
            <div class="alert alert-secondary" role="alert">
                <div class="text-center">
                    <h3>Welcome to Secure Notes - Sign Up</h3>
                </div>
            </div>
            <form action="/signup" method="POST">
                <div class="form-group">
                    <label for="emailInput">Email</label>
                    <input type="email" class="form-control" id="email" name="email" required placeholder="Enter email...">
                </div>
                <div class="form-group">
                    <label for="passwordInput">Password</label>
                    <input type="password" class="form-control" id="password1" name="password1" required placeholder="Enter password...">
                </div>
                <div class="form-group">
                    <label for="passwordInput">Confirm Password</label>
                    <input type="password" class="form-control" id="password2" name="password2" required placeholder="Confirm password...">
                </div>
                <button type="submit" class="btn btn-primary">Register</button>
            </form>
            <div class="text-center">
                <hr/>
                <img class="img-thumbnail" src="static/images/polarsparc.png" alt="PolarSPARC">
            </div>
        </div>        
    </body>
</html>

Notice the use of action="/signup" and method="POST".

The following is the modified version of the Python script main.py to handle the HTTP POST request at the URL endpoint for /signup:


main.py
#
# @Author: Bhaskar S
# @Blog:   https://www.polarsparc.com
# @Date:   30 Aug 2021
#

from flask import request
from flask.templating import render_template
from config.config import app

@app.route('/')
def index():
    return render_template('welcome.html')

@app.route('/signup', methods=['GET', 'POST'])
def signup():
    if request.method == 'GET':
        return render_template('signup.html')
    email = None
    if 'email' in request.form:
        email = request.form['email']
    if email is None or len(email.strip()) == 0:
        return render_template('signup_error.html', message='Invalid email !!!')
    password1 = None
    if 'password1' in request.form:
        password1 = request.form['password1']
    if password1 is None or len(password1.strip()) == 0:
        return render_template('signup_error.html', message='Invalid password !!!')
    password2 = None
    if 'password2' in request.form:
        password2 = request.form['password2']
    if password1 != password2:
        return render_template('signup_error.html', message='Password confirmation failed !!!')
    return render_template('welcome.html')

Some aspects of the main.py from the above needs a little explanation.

Notice that we perform validation of the various input elements retrieved from the request.form dictionary. If any of the validation fails, we respond with a sign-up error HTML page.

The following is the HTML sign-up error page called signup_error.html that will be located in the directory SecureNotes/templates:


signup_error.html
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <link rel="stylesheet" href="static/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
        <title>Welcome to Secure Notes - Sign Up Error</title>
    </head>
    <body>
        <div class="container">
            <div class="alert alert-secondary" role="alert">
                <div class="text-center">
                    <h3>Welcome to Secure Notes - Sign Up Error</h3>
                </div>
            </div>
            <br/>
            <div class="alert alert-danger" role="alert">
                <p>{{ message }}</p>
            </div>
            <br/>
            <span class="p-1 rounded-sm border border-primary">
                <a href="/signup" class="alert-link">Sign Up</a>
            </span>
            <div class="text-center">
                <hr/>
                <img class="img-thumbnail" src="static/images/polarsparc.png" alt="PolarSPARC">
            </div>
        </div>        
    </body>
</html>

Notice the use of the syntax {{ message }} in the HTML page above. This is the Jinja syntax in action. The 'message' is the passed in variable whose value is substituted for when the response is returned back to the web client (the browser in our case).

Restart the gunicorn server and launch a browser and access the URL http://127.0.0.1:8080/. Once the login page loads, click on the 'Sign Up' link. When the sign-up page loads, enter the email-id, and different values for the password input and then click on the 'Register' button. The following illustration shows the response on the browser:

Sign Up Error from Browser
Figure.6

References

Flask 2.0.x Documentation

Bootstrap Documentation


© PolarSPARC