my cheat sheet on flask
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.
- 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 usingtojson
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:
- Build a distribution file. The current standard for python distribution
is the wheel format, with a
.whl
extenstion. You need to havewheel
installed, sopip install wheel
- Run
setup.py
This will give you command line tool with which to run build related commands. Thebdist_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}
- You can find the file in
- 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):
orapplication = 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