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:
flask :: the micro-framework for web development
flask-cors :: a Flask extension for handling Cross Origin Resource Sharing (CORS)
jinja2 :: the fast, expressive, and extensible templating engine
gunicorn :: the HTTP web server
sqlalchemy :: the popular SQL database abstraction layer
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:
# # @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.
WSGI :: stands for Web Server Gateway Interface that defines a well-defined standard interface for executing Python applications via the web server. Note that the regular web servers have no way of executing any Python code. The standrad WSGI interface could be implemented by any web server provider as a module for intercepting and routing the web requests to a WSGI server that can invoke any Python code in a standard way. In other words, WSGI has two sides - the server side that is implemented as an extension by the web server provider and the application side that is implemented like a middleware for executing the Python application code
The following illustration shows the high-level architectural components of WSGI:
View Function :: is the method that will respond to request in a web application. It returns content that flask will return as the outgoing response
Flask :: an object that implements the WSGI interface and acts as the core object. It is passed the name of the module or package of the application. Once an instance is created, it acts as a central registry for all the view functions and much more. Flask uses the passed in name argument to determine the root directory path of the application so that it can be used to find resource files relative to the location of the web application
The following is the Python script called main.py that will be located in the directory SecureNotes:
# # @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.
index() :: defines the view function that returns some basic HTML text
app.add_url_rule(...) :: the method that allows one to register a view function that corresponds to a URL path. In our case, the view function index() will be invoked when the web client (such as a browser accesses the URL path '/' on the web server
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:
[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:
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:
# # @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.
@app.route(...) :: the decorator that allows one to register the decorated method as the view function that corresponds to the specified URL path '/'. This is a more elegant way of registering view functions corresponding to the various URLs instead of using the app.add_url_rule(...) method. Going forward, we will use this approach to register view functions
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:
<!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:
# # @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.
logging.getLogger('gunicorn.error') :: get the Logger object for gunicorn
app.logger.handlers = gunicorn_logger.handlers :: associates the Flask application log handler with the log handler of gunicorn so the logs for the Flask application appears along with gunicorn's logs
app.logger.setLevel(gunicorn_logger.level) :: sets the Flask application logging level to be the same as the one for gunicorn so the logs are consistent
The following is the modified version of the Python script 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.
render_template(...) :: returns the contents of the specified HTML file from the template directory located at SecureNotes/templates
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:
[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:
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:
<!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:
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:
# # @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:
<!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:
<!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:
# # @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:
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:
<!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:
# # @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.
@app.route('/signup', methods=['GET', 'POST']) :: the URL route decorator indicates the HTTP methods (GET and POST) this view function will handle
request :: when the Flask application handles a request, it creates a instance of the flask.Request object based on the environment it received from the WSGI server. Because a worker (coroutine, thread, or process) handles a single request at a time, the request data is considered global to that worker during that request. This is referred to as the request context. Flask automatically pushes the request context in the variable called request to the view function that is handling the request. The request context is unique to each worker. The request context cannot be passed to another worker as the other worker will have a different context stack and will not know about the request the parent worker is pointing to. When the request ends, Flask will automatically pop the request context
request.method :: returns the HTTP method used in making the current request, such as GET, POST, PUT, DELETE, etc
request.form :: returns an immutable dictionary containing the key value pairs, where the keys contain all the name attributes of the input elements from the HTML form
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:
<!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:
References