my cheat sheet on flask

Home

1 Flask

Flask is a micro web framework written in Python. It is extended through supported extensions LinkedIn is built on Flask.

2 Flask REST API

My interest in Flask is to create a simple app that has an API. Given that, these notes are focused on the REST API features of Flask only.

Flask documentation is available at:

3 Installing flask

First off, pick your venv, for instance I have been using:sudo python3 -m venv venv-meraki so . venv-meraki/bin/activate

Then install flask:

  • pip install Flask

Flask is not in the usual repos, so you have to add the repo to your dnf repolist first.

On my CentOS vm1 host I ran this:

sudo dnf install python3.9
sudo dnf repolist
sudo dnf config-manager --set-enabled PowerTools
sudo dnf repolist
python3.9 -m pip install --upgrade pip
python3.9 -m pip install Flask
python3.9 -m pip install Flask-RESTful

4 Hello World

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello() -> str:
    return "Hello World"


if __name__ == "__main__":
    app.run(debug=False)

5 Flask Templates

There is a distinct difference between templates and Javascript that if understood makes developing flask apps easier.

5.0.1 Templates are rendered on the server

So a server runs the backend app, looks up the template, fills in all the variables on the template and ONLY THEN sends this to the client.

5.0.2 Javascript

Javascript runs in the user's/clients browser, AFTER the server has rendered the template and sent it on its way.

  1. tojson() filter

    To provide data to Javascript when rendering the template, use tojson() filter in a <script> block. This correclty converts the data to a valid javascript object. NOT using tojson filter will get you a syntax error.

    data = generate_report()
    return render_template("report.html", chart_data=data)
    

    Followed by this javascript block in the html file:

    <script>
    
    </script>
    

5.1 Jinja-2 templates

5.2 Javascript

6 Flask Dynamic pages based on a dictionary

A useful technique is to have Flask generate a webpage that is a series of url links that were generated based on all the entries in a dictionary.

An intriguing discussion to follow up on is: stackoverflow - Dynamically generate page endpoints and content

Another discussion to read on stackoverflow: Create dynamic URLs in Flask with urlfor()

6.1 urlfor

url_for in Flask is used for creating a URL to prevent the overhead of having to change URLs throughout an application (including in templates). Without url_for, if there is a change in the root URL of your app then you have to change it in every page where the link is present.

Syntax:

  • url_for('name of the function of the route','parameters (if required)')

Excerpts from help(url_for):

url_for(endpoint: 'str',
        ...
        **values: 't.Any') -> 'str'

    Generate a URL to the given endpoint with the given values.

    This requires an active request or application context, and calls
    :meth:`current_app.url_for() <flask.Flask.url_for>`. See that method
    for full documentation.

Based on the above stackoverflow links, here is an sample use case where the app wanted routes based on variables that looked like:

  • /<variable>/add
  • /<variable>/remove
url_for('add', variable=foo)
url_for('remove', variable=foo)

with routes being:

@app.route('/<variable>/add', methods=['GET', 'POST'])
def add(variable):

@app.route('/<variable>/remove', methods=['GET', 'POST'])
def remove(variable):

From the same stackoverflow discussion: if you have

@app.route("/<a>/<b>")
# and
def function(a,b): ... # as its function,

then you should use url_for and specify its keyword arguments like this:

url_for('function', a='somevalue', b='anothervalue')

This is well explained in flask.palletsprojects.com

6.2 form - response example (dynamic pages)

Sending data to a Flask template

In this example, explained in https://pythonbasics.org/flask-template-data/

''' routes.py  '''
from flask import Flask, render_template, request
app = Flask(name)
@app.route('/')
def student():
    '''
    This / URL presents the student URL page with a form.  The data from the
    form is published to the /result URL that triggered the result() function.
    '''
    return render_template('student.html')


@app.route('/result',methods = ['POST', 'GET'])
def result():
    '''
    collects form data present in the request.form in the dictionary object
    and sends it to the result.html page.
    '''
   # with logging set up, record the client's ip address in the log (
    ip_addr = request.remote_addr
    proxy_ip_addr = request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr)
    logger.info(f' ip address {ip_addr} or proxy address {proxy_ip_addr} accessed this website')


    if request.method == 'POST':
        result = request.form
        return render_template("result.html",result = result)


    if name == 'main':
        app.run(debug = True)

The above example needed to have loggin setup prior. That can be done like this:

# to store who was accessing your web page, you can log the ip address
import logging

# setup logging
logger = logging.getLogger(__name__)
file_handler = logging.FileHandler("meraki_reports_logging.logger")
formatter = logging.Formatter('%(levelname)s,%(name)s,%(asctime)s %(message)s')
logger.setLevel(logging.INFO)
file_handler.setLevel(logging.INFO)    # setting to DEGUG will log DEBUG and higher
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)

Result.html

 <!doctype html>
 <table border = 1>
   {% for key, value in result.items() %}

   <tr>
      <th> {{ key }} </th>
      <td> {{ value }} </td>
   </tr>

   {% endfor %}
</table>
 <!-- result.html -->

Student.html (fragment)

<!-- student.html -->
  <form action = "http://localhost:5000/result" method = "POST">
     <p>Name <input type = "text" name = "Name" /></p>
     <p>Physics <input type = "text" name = "Physics" /></p>
     <p>Chemistry <input type = "text" name = "chemistry" /></p>
     <p>Maths <input type ="text" name = "Mathematics" /></p>
     <p><input type = "submit" value = "submit" /></p>
  </form>

6.3 dynamic table from dictionary

Another example is to display a html table based on data in a python dictionary.

''' routes.py  '''
from flask import Flask, render_template
app = Flask(name)
@app.route('/myemployees')
def myemployees():
    '''
    This /table URL presents 
    '''
    return render_template('table.html')


@app.route('/result',methods = ['POST', 'GET'])
def result():
    '''
    collects form data present in the request.form in the dictionary object
    and sends it to the result.html page.
    '''
    if request.method == 'POST':
        result = request.form
        return render_template("result.html",result = result)


    if name == 'main':
        app.run(debug = True)

table.html

 <!doctype html>
 <table border = 1>
   {% for key, value in result.items() %}

   <tr>
      <th> {{ key }} </th>
      <td> {{ value }} </td>
   </tr>

   {% endfor %}
</table>
 <!-- result.html -->

Student.html (fragment)

<!-- student.html -->
  <form action = "http://localhost:5000/result" method = "POST">
     <p>Name <input type = "text" name = "Name" /></p>
     <p>Physics <input type = "text" name = "Physics" /></p>
     <p>Chemistry <input type = "text" name = "chemistry" /></p>
     <p>Maths <input type ="text" name = "Mathematics" /></p>
     <p><input type = "submit" value = "submit" /></p>
  </form>

7 Flask in Production

When you run a flask program, such as the one above, you will most likely get a error This is a development server. Do not use it in a production deployment

That is because running Flask, is actually running Werkzeug's development WSGI server. (Web Server Gateway Interface). See wsgi.readthedocs.io Pronounced "whiskey".

WSGI is a python standard, described in PEPE 3333.

Flask documents the steps needed to deploy to production. Basically, you run apache or nginx production web server, that supports a wsgi component.

Follow these general steps:

  1. Build a distribution file. The current standard for python distribution is the wheel format, with a .whl extenstion. You need to have wheel installed, so pip install wheel
  2. Run setup.py This will give you command line tool with which to run build related commands. The bdist_wheel command will build a wheel distribution file. python setup.py bdist_wheel
    • You can find the file in dist/flaskr-1.0.0-py3-none0any.whl
    • file name has this format : {project name}-{version}-{python tag} -{abi tag}-{platform tag}
  3. Copy this file to your production server, in the correct venv and run:
    • pip install flaskr-1.0.0-py3-=none-any.whl

    pip will then install your project, along with its dependencies.

7.1 Using waitress:

From flask.palletsprojects.com tutorial:

When running publicly rather than in development, you should not use the built-in development server (flask run). The development server is provided by Werkzeug for convenience, but is not designed to be particularly efficient, stable, or secure.

Instead, use a production WSGI server. For example, to use Waitress, first install it in the virtual environment:

$ pip install waitress You need to tell Waitress about your application, but it doesn’t use FLASKAPP like flask run does. You need to tell it to import and call the application factory to get an application object.

$ waitress-serve --call 'flaskr:create_app'

Serving on http://0.0.0.0:8080 See Deployment Options for a list of many different ways to host your application. Waitress is just an example, chosen for the tutorial because it supports both Windows and Linux. There are many more WSGI servers and deployment options that you may choose for your project.

7.2 Using modwsgi on Apache

Since I already have Apache running, I will use mod_wsgi on that server.

Steps are described here: werkzeug.palletsprojects.com

And a good tutorial on Werkzeug is on werkzeug.palletsproject.com/tutorial

I started with installing modwsgi to my production server:

  • sudo dnf install python3-mod_wsgi

Then configure the python3wsgi.conf file in /etc/httpd/conf.d directory

I followed the steps listed in tecadmin.net and got my first test wgi page running: https://zintis.net/test_wsgi

8 Migrating from a Flask app to a WSGI app

Unordered notes:

  • to run an application, you need a myapplication.wsgi file. This file has the code that modwsgi is executing on startup to get the applicaiton object

    The object called application in that file is then used as the application.

    For example:

    from myapplication import app as application
    
    # if a factory function is used in a __init__.py file then the function
    # should be imported, i.e.:
    from myapplication import create_app
    application = create_app()
    
    

    I need to investigate this creating a .wsgi file some more.

8.1 .wsgi file

This file is found in /etc/httpd/conf.d directory. No it is not. It is /var/www/pie/pie.wsgi

To run your application you need a your application.wsgi file. This file is found in /etc/httpd/conf.d directory. It contains the code modwsgi is executing on startup to get the application object. The object called application in that file is then used as application.

For most applications the following file should be sufficient:

from yourapplication import make_app
application = make_app()

This is our application object. It could have any name, except when using modwsgi where it must be "application" so:

  • def application(environ, start_response): or
  • application = make_app()

If you don’t have a factory function for application creation but a singleton instance you can directly import that one as application.

Store that file somewhere where you will find it again.

I have examples where that is in /etc/httpd/conf.d and others where that is in /var/www/pi So, which one is best????

(eg: /var/www/yourapplication) and make sure that yourapplication and all the libraries that are in use are on the python load path. If you don’t want to install it system wide consider using a virtual python instance.

9 _init_.py

In a wsgi setup, you must have init.py file in the modules/ directory to tell python that modules is a package. It can be an empty file.

9.1 My test .wsgi file

Here is test .wsgi file. This will change when in production. See below, section .wsgi file for production

# create new web resource (server/test_wsgi) that points to the python wsgi.py
# script. i.e. to [/test_wsgi] from [/var/www/html/test_wsgi.py]
WSGIScriptAlias /test_wsgi /var/www/html/test_wsgi.py


<Directory /var/www/html>
   # WSGIProcessGroup myapplication
   # WSGIApplicaitonGroup %{GLOBAL}
   Order allow,deny
   Allow from all
</Directory>

10 Configure Apache for wsgi

Configuring Apache The last thing you have to do is to create an Apache configuration file for your application. In this example we are telling mod_wsgi to execute the application under a different user for security reasons:

<VirtualHost *>
    ServerName example.com

    WSGIDaemonProcess yourapplication user=user1 group=group1 processes=2 threads=5
    WSGIScriptAlias / /var/www/yourapplication/yourapplication.wsgi

    <Directory /var/www/yourapplication>
        WSGIProcessGroup yourapplication
        WSGIApplicationGroup %{GLOBAL}
        Order deny,allow
        Allow from all
    </Directory>
</VirtualHost>

I setup "yourapplication" to be "pi" so, like this:

<VirtualHost *>
    ServerName example.com

    WSGIDaemonProcess pi user=apache group=apache processes=1
    WSGIScriptAlias / /var/www/pi/pi.wsgi

    <Directory /var/www/pi>
        WSGIProcessGroup pi
        WSGIApplicationGroup %{GLOBAL}
        Order deny,allow
        Allow from all
    </Directory>
</VirtualHost>

10.1 WSGI settings in conf.d

zintis@zintis.net /etc/httpd/conf.d[1079]:
$ !g
grep WSGI *.*
pie.conf:    WSGIDaemonProcess pieapp user=apache group=apache threads=2 home=/var/www/pie
pie.conf:    WSGIScriptAlias /pie /var/www/pie/pie.wsgi
pie.conf:        WSGIProcessGroup pieapp
pie.conf:        WSGIApplicationGroup %{GLOBAL}
python3_wsgi.conf-holding:WSGIScriptAlias /test_wsgi /var/www/html/test_wsgi.py
python3_wsgi.conf-holding:   # WSGIProcessGroup myapplication
python3_wsgi.conf-holding:   # WSGIApplicaitonGroup %{GLOBAL}

10.2 Expert on WSGI

I think I might become an expert on WSGI to get my Flask app to work. Take this documentaiton as a start: modwsgi.readthedocs.io

  • WSGIDaemonProcess
  • WSGIProcessGroup
  • WSGIScriptAlias # that may have a process-group option in place of the WSGiprocessgroup

A good blog discusses several options: blog.dscpl.com.au

11 Trouble with apache and seLinux

I tried to create a virtual apache host, that listened to port 7927, for a flask app, that was being moved into a production apache modwsgi app.

Everything was correct in my virtual host config, but ss -tulpn would NOT ever say it was listening on port 7927.

I then issued three semanage commands:

  • semanage port -l | grep http
  • semanage port -a -t http_port_t -p tcp 7927
  • semanage port -l | grep http

After that every was copasetic!

root@zintis /etc/httpd/conf[1152]$

semanage port -l  | grep http
http_cache_port_t              tcp      8080, 8118, 8123, 10001-10010
http_cache_port_t              udp      3130
http_port_t                    tcp      80, 81, 443, 488, 8008, 8009, 8443, 9000
pegasus_http_port_t            tcp      5988
pegasus_https_port_t           tcp      5989
root@zintis /etc/httpd/conf[1153]$

semanage port -a -t http_port_t -p tcp 7927
root@zintis /etc/httpd/conf[1154]$

semanage port -l  | grep http
http_cache_port_t              tcp      8080, 8118, 8123, 10001-10010
http_cache_port_t              udp      3130
http_port_t                    tcp      7927, 80, 81, 443, 488, 8008, 8009, 8443, 9000
pegasus_http_port_t            tcp      5988
pegasus_https_port_t           tcp      5989
root@zintis /etc/httpd/conf[1155]$

12 Jinga use cases:

Since Flask uses jinja2, you can also benefit from jinja2 filters

See the file jinja templates and flask for detailed discussion on jinja filters on jinja variables.

12.1 Reminder of variables in jinja templates

13 Flask Login

Flask also has some login methods that you can use. No need to create a login function from scratch. rtfm: https://flask-login.readthedocs.io/en/latest/

13.1 Sample Flask Basic Auth

The following example shows how to limit attempted logins even when using basic authentication. This example comes from stackoverflow.com

@app.route('/',methods=['GET'])
def home():
    session['attempt'] = 5
@app.route('/login')
def login():
    username = request.form.get('username')
    session['username'] = username
    password = request.form.get('password')
    if username and password and username in users and users[username] == password:
        session['logged_in'] = True
        return redirect(url_for('index'))
    attempt= session.get('attempt')
    attempt -= 1
    session['attempt']=attempt
    #print(attempt,flush=True)
    if attempt==1:
        client_ip= session.get('client_ip')
        flash('This is your last attempt, %s will be blocked for 24hr, Attempt %d of 5'  % (client_ip,attempt), 'error')
    else:
        flash('Invalid login credentials, Attempts %d of 5'  % attempt, 'error')
    return redirect(url_for('login'))

13.2 Users database

For a login to work, you must have a user database, and optionally a new user registration system. For just a select few users, it is simpler to hard code the users.

13.3 login modules

Flask has these modules related to user logins:

from flask_login import LoginManager

13.4 Home