Wednesday 4 November 2020

PyBloom coding project part 10: Setting up the web site

Previously I've shown how I've collected the data, set the Philips Hue bulbs, and written the weather readings to a SQLite database. Here in part 10 I start work on the web site that will show the readings.

The web server

Part of the rationale for this project is to leverage my burgeoning Python knowledge in as many places as possible. So it seemed sensible to me to try out one of the Python web frameworks for my website. I’ve tried Django before, and it seemed quite complicated for my needs, so I went with the simpler Flask to see if it could do what I wanted. And indeed it could.

Setting up the web server with Flask consists of creating some simple initiator scripts. The file structure from earlier lists all the files we need to make. We’ll focus on the scripts here, and get to the templates and static resources in the next section.

./Project/pybloom

├── app.py
├── app/
│ ├── __init__.py
│ ├── content.py
│ ├── routes.py
│ ├── templates/
│ │ ├── base.html
│ │ ├── index.html
│ │ └── colours.html
│ └── static/
│ ├── favicon.ico
│ ├── brand.svg
│ ├── logo.svg
│ ├── bar_graph.svg
│ └── pie_chart.svg



The technologies

  • Flask

  • Advanced Python Scheduler

  • HTTP

The code

The app entity

Flask looks for an app.py script in the root of the project directory to tell it where to get the rest of the files. (You can use a different filename, but then you need to declare that in a different .flaskenv variable - why bother.)

from app import app

That’s all you need, at least in our simple project.


A rudimentary CMS

We’ve generated our weather observation data and visualisations in the Python script, but we need a way of declaring them to the web pages. Rather than sprinkling references to filenames and file paths throughout the HTML, I decided to collect them in a content management script.

from db_utils import get_rows

We’re going to need our helpful database utility, so let’s import it first.

def graphs():
all_graphs = {
    'lastday': 'last_day_bar.svg',
    'lastweek': 'last_week_pie.svg',
    'lastmonth': 'last_month_bar.svg'
}
return all_graphs

This function collects all the graphs in one dictionary.

def colours_table():

    rows = get_rows('colours')

    return rows


This second function fetches our temperature-colour lookup table.


We’re using functions to define the content instead of variables because I like to keep variables associated with a single namespace - to me it’s just cleaner and less prone to error.


Setting up the page routes in python

 

Flask takes care of responding to all the HTTP requests with the correct web pages (web serving) and calls them routes.

from flask import render_template
from app import app
from app import content

First let’s import all the modules that we’re going to need in this routing script.

CONTENT = content.graphs()
COLOURS_TABLE = content.colours_table()

Second, we have a couple of global variables that tell our pages where to get their data, defined in our CMS.

@app.route('/')
@app.route('/index')
def index():
    return render_template('index.html', title='Home', content=CONTENT)


Flask uses function decorators to associate HTTP requests with HTML files. These statements are saying that if the web server receives a request for <IP address of server:5000>/ or <IP address of server:5000>/index it should render the index.html file.

Additionally, the render_template() method passes a couple of parameters into the file in order to complete its rendering, the title of the page and the graph content.

@app.route('/colours')
def colours():
    return render_template('colours.html', title='Colours', rows=COLOURS_TABLE)


<IP address of server:5000>/colours takes us to the colours.html page. Again the render_template() method passes a couple of parameters as defined in the CMS.

@app.after_request
def after_request_func(response):
    response.headers['Cache-Control'] = 'no-store'
    return response

While testing, I noticed that the graphs were old, and that refreshing the page just re-showed the old graphs. I figured that the browser was serving a cached page, but as the data refreshes every 10 minutes, the page was old. Furthermore, clicking on the refresh button in modern browsers just re-fetches the same page from the browser cache. In order to force the browser to re-send its HTTP request, I need to make sure the HTTP response indicates that the browser should not store the page.

Let’s unpick this final decorator:

  • @app.after_request : tells Flask that this function is to be called after every web page request is responded to
  • after_request_func(response) : works on Flask’s internal response object, which normally you wouldn’t interact with directly
  • response.headers['Cache-Control'] = 'no-store' : this adds a new header to the HTTP response which tells the browser not to cache the page


Initialising and running the code

We now have two processes that need to run simultaneously: the web server, and fetching the observations. There’s a library that makes the scheduling straightforward:

from apscheduler.schedulers.background import BackgroundScheduler

The Advanced Python Scheduler has a background scheduling function that is perfect for my needs.

schedule = BackgroundScheduler(daemon=True)
schedule.add_job(lambda: weather(), 'interval', minutes=10)
schedule.start()

We instantiate a schedule object as a daemon, which means it closes once it finishes (contrast with a thread that just keeps going). The job itself is a lambda function that calls the weather function every 10 minutes. (We should remember to import this function with from pybloom import weather at the top of the file.)


from flask import Flask
app = Flask(__name__)
from app import routes

The remaining initialisation command tells Flask what the app is, and where it can find the routing.

In dev, I’m using the same machine to run the server as to browse it, so the correct command is flask run. By default, this runs a server in the localhost, so isn’t accessible outside of the dev machine. However, in prod, I want the RPi to serve the site to other browsers to view it, so the correct command is:

flask run --host=0.0.0.0

To access the site, point the browser to http://<IP address of RPi>:5000/ (specify the port, otherwise the RPi will reject the connection to the browser).

Putting it together


app.py

from app import app



_init_py

from flask import Flask

app = Flask(__name__)

from app import routes

from apscheduler.schedulers.background import BackgroundScheduler

from pybloom import weather


schedule = BackgroundScheduler(daemon=True)

schedule.add_job(lambda: weather(), 'interval', minutes=10)

schedule.start()



routes.py

from flask import render_template

from app import app

from app import content


CONTENT = content.graphs()

COLOURS_TABLE = content.colours_table()



@app.route('/')

@app.route('/index')

def index():

    return render_template('index.html', title='Home', content=CONTENT)

@app.route('/colours')

def colours():

    return render_template('colours.html', title='Colours', rows=COLOURS_TABLE)

@app.after_request

def after_request_func(response):

    response.headers['Cache-Control'] = 'no-store'

    return response



content.py

from db_utils import get_rows



def graphs():

    all_graphs = {

        'lastday': 'last_day_bar.svg',

        'lastweek': 'last_week_pie.svg',

        'lastmonth': 'last_month_bar.svg'

    }

    return all_graphs



def colours_table():

    rows = get_rows('colours')

    return rows




Here in part 10 I've taken us through using Flask to create my web server. In the next part 11, we'll see how I created the web pages. Also visit https://github.com/Schmoiger/pybloom for the full story.

No comments:

Post a Comment

It's always great to hear what you think. Please leave a comment, and start a conversation!