Jun 06 2022 · Aleksandra Płońska, Piotr Płoński

Build dashboard in Python with automatic updates and email notifications

Build Python Dashboard in Python banner Do you need to monitor data from multiple sources and act based on their values? If you answered yes, then it means that you need a dashboard. I will show you how to create a dashboard in Python. It will automatically update the values in the dashboard and send email notifications based on monitored values. The dashboard will be deployed to the Amazon AWS cloud service.

The dashboard will be created in Python with Jupyter Notebook. It will monitor data from multiple sources:

  • stock exchange (data from Yahoo finance),
  • cryptocurrency exchange (data from Binance REST API),
  • weather (data from open-meteo.com).

The data will be displayed on charts and value boxes. The data will be updated daily. The dashboard will be served with an open-source Mercury framework. It converts Jupyter Notebook into a web application and schedules notebook execution. The web application will be deployed to AWS EC2 with docker-compose. The screenshot of the dashboard:

Screenshot of Python Dashboard

Environment setup

The first step is to set up the environment for development. The project's code will be in the GitHub repository. The link to the repo: github.com/pplonski/python-dashboard - keeping code in the repository will be a handful for deployment. Let's clone the empty repo to the local machine:

git clone git@github.com:pplonski/python-dashboard.git

I will use the virtualenv to create a virtual environment for my project:

virtualenv pdenv --python=python3.8

The name of the virtual environment is pdenv. Let's define all needed packages in the requirements.txt file:

mljar-mercury
python_dotenv
yfinance
matplotlib
numpy
pandas
requests
bloxs

Install them after environment activation:

# activate the env
source pdenv/bin/activate
# install all packages
pip install -r requirements.txt

The important step is to add our virtual environment as a kernel in the Jupyter Notebook:

python -m ipykernel install --user --name=pdenv

After executing the above command, the pdenv will be available as a kernel in the Jupyter Notebook.

Data Sources

All data will be fetched during notebook execution. Please start the Jupyter Notebook:

jupyter notebook

Please create a new notebook with the pdenv kernel. My notebook is named dashboard.ipynb.

The stock data will be loaded with the yfinance package. It uses the Yahoo Finance REST API to get information about stock data. The stock loading code:

import yfinance as yf

stock_symbol = "MSFT" # MSFT is Microsoft symbol on stock exchange

stock  = yf.Ticket(stock_symbol)
stock_hist = stock.history() 

The stock_hist is a DataFrame with 30 days of stock history with 1-day interval.

To get the data about cryptocurrency, we will use the Binance REST API and requests package:

import requests

crypto_symbol = "BTC" # Bitcoin

# get crypto data from Binance REST API
def get_crypto_data(symbol):
    try:
        response = requests.get(f"https://www.binance.com/api/v1/klines?symbol={symbol}&interval=1d&limit=30")
        candles = response.json()
        df = pd.DataFrame(candles, columns=["Open time", "Open", "High", "Low", "Close", "Volume", 
                                        "Close time", "Quote", "Trades", "BaseVol", "QuoteVol", "Igonre"])
        df["Date"] = pd.to_datetime(df["Open time"], unit='ms')
        for col in ["Open", "High", "Low", "Close", "Volume"]:
            df[col] = df[col].astype(float)
        df = df.set_index('Date')
        return df
    except Exception as e:
        print(str(e))
        return None
    
crypto = get_crypto_data(f"{crypto_symbol}USDT")

The crypto variable keeps the information about BTC to USDT 30 days price history with 1 day interval.

For the weather data, I will use open-meteo.com. The service is free to use and has an amazing link builder interface in the documentation. To get weather, we need to pass latitude and longitude. You can get them from Google Maps.

# get weather data from open meteo REST API
def get_meteo_data(lat = 52.9314055, lon=23.7737443):
    try:
        url = f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}&hourly=temperature_2m,windspeed_10m,windgusts_10m&windspeed_unit=kn&timezone=Europe%2FBerlin"
        data = requests.get(url).json()
        meteo = pd.DataFrame(data["hourly"])
        meteo.time = pd.to_datetime(meteo.time)
        return meteo
    except Exception as e:
        print(str(e))
        return None

meteo = get_meteo_data()

The meteo is a DataFrame with hourly information about the weather for the next 7 days. It has information about temperature and wind speed. My kids are training sailing, so the wind speed is important for us ;)

We have the data in 3 DataFrames. The data is fetched from the internet resources during the notebook execution.

Plot data

There will be 4 charts in the dashboard. The first two charts will be with financial data:

  • chart with stock price for the last 30 days,
  • chart with crypto price for the last 30 days.
# import packages
from matplotlib import dates as mdates
from matplotlib import pyplot as plt
plt.style.use('ggplot')


# plot finance charts
fig, ax = plt.subplots(1, 2, figsize=(17,5))

_ = ax[0].plot(stock_hist.Close,  linewidth=3)
ax[0].xaxis.set_major_formatter(mdates.DateFormatter('%b-%d'))
_=ax[0].set_ylabel("USD")
ax[0].title.set_text(stock_symbol)

_=ax[1].plot(crypto.Close, color="orange", linewidth=3)
ax[1].title.set_text(crypto_symbol)
ax[1].xaxis.set_major_formatter(mdates.DateFormatter('%b-%d'))
_=ax[1].set_ylabel("USDT")

Finance Charts in Python Dashboard

Plot weather charts:

  • one chart with temperature for the next 7 days,
  • wind speed for the next 7 days.
# plot meteo charts
fig, ax = plt.subplots(1, 2, figsize=(17,5))

_=ax[0].plot(meteo.time, meteo.temperature_2m, color="black")
ax[0].xaxis.set_major_formatter(mdates.DateFormatter('%b-%d'))
ax[0].set_ylabel("Celsius")
ax[0].title.set_text("Temperature")

_=ax[1].plot(meteo.time, meteo.windspeed_10m, label="Speed", linewidth=3)
_=ax[1].plot(meteo.time, meteo.windgusts_10m, label="Gusts", linewidth=3)
_=ax[1].title.set_text("Wind")
_=ax[1].set_ylabel("Knots")
ax[1].xaxis.set_major_formatter(mdates.DateFormatter('%b-%d'))
_=ax[1].legend()

Meteo Charts in Python Dashboard

Boxes with values

I will compute some additional values that will be displayed in value boxes. I will use the Bloxs package to display values in boxes. I will display current prices, max temperature, and max wind gusts for the next 7 days.

Let's compute the maximum price change during the last 7 days:

# week max change for stock
stock_change = (stock_hist.tail(7).Close.max() - stock_hist.tail(7).Close.min())/stock_hist.tail(7).Close.max()

# week max change for crypto
crypto_change = (crypto.tail(7).Close.max() - crypto.tail(7).Close.min())/crypto.tail(7).Close.max()

I will display data with the Bloxs package:

# remember to import needed packages
from bloxs import B

# display boxes
B([
    B(f"{np.round(stock_change*100, 2)}%", f"{stock_symbol} week change"),
    B(f"{np.round(crypto_change*100, 2)}%", f"{crypto_symbol} week change"),
    B(meteo.windgusts_10m.max(), f"Max wind gusts"),
])

Boxes values part 1 in Python Dashboard

The next rows of 'Bloxs':

B([
    B(stock_hist.Close[-1].astype(int), f"{stock_symbol}"),
    B(crypto.Close[-1].astype(int), f"{crypto_symbol}"),
    B(meteo.windgusts_10m.max(), f"Max Temp"),
])

Boxes values part 2 in Python Dashboard

Email notifications

We have current data values. Let's define alerts with email notifications. I will use smtplib and email packages. I've constructed a very simple EmailService class. It loads email address and password from environment variables.

I'm using my Gmail account to send an email. The password is the app password generated to use in the code (the docs how to generate an app password for Gmail account). The email address and password can't be hardcoded in the code! I'm using python-dotenv to load environment variables from .env file. The .env shouldn't be committed to the GitHub repository.

import smtplib
from email.message import EmailMessage
from dotenv import load_dotenv

_ = load_dotenv() 


class EmailService:
    def __init__(self):
        self.address = os.environ.get("EMAIL_ADDRESS")
        self.password = os.environ.get("EMAIL_PASSWORD")
        
    def send(self, to_address, subject, message):
        msg = EmailMessage()
        msg['Subject'] = subject
        msg['From'] = self.address
        msg['To'] = to_address
        msg.set_content(message)
        
        with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp:
            smtp.login(self.address, self.password)
            smtp.send_message(msg)

Setting the alert for wind max speed. If the maximum wind gusts are larger than 25 knots, let's send an email notification:

if meteo.windgusts_10m.max() > 25:
    EmailService().send("piotr@mljar.com", 
        "Strong wind gusts alert", 
        f"There might be strong wind gusts in the next 7 days. Up to {meteo.windgusts_10m.max()} knots.")

Email notifications for financial changes:

if stock_change > 0.3: # stock change over 30% send email notification
    EmailService().send("piotr@mljar.com", 
        "Stock large price change alert", 
        f"Large price change ({np.round(stock_change*100.0,2)}%) of {stock_symbol} in the last 7 days.")


if crypto_change > 0.3: # crypto change over 30% send email notification
    EmailService().send("piotr@mljar.com", 
        "Crypto price large change alert", 
        f"Large price change ({np.round(crypto_change*100.0,2)}%) of {crypto_symbol} in the last 7 days.")

Turn notebook to web application

The notebook can be turned into a web application with Mercury framework. It requires a YAML header in the first raw cell in the notebook. The header defines title and description. The code can be easily hidden with show-code: False. Additionally, the interactive widgets can be added to the notebook. In the below YAML header the two widgets are added stock_symbol and crypto_symbol. The widgets are directly connected to variables in the Python code.

The YAML header:

---
title: My dashboard
description: Dashboard with stock, crypto and meteo
show-code: False
params:
    stock_symbol:
        input: select
        label: Select stock
        value: TSLA
        choices: [TSLA, MSFT, GME, PLTR, SNOW]
    crypto_symbol:
        input: select
        label: Select crypto
        value: BTC
        choices: [BTC, ETH, DOT, NMR]
---

The variables that are controlled by Mercury widgets should be in a separate cell:

stock_symbol = "TSLA"
crypto_symbol = "BTC"

To see the notebook as a web application, you need to run the Mercury server. Please use the command:

mercury run

It will load your Python notebook and serve locally at 127.0.0.1. You can open the web browser and see the dashboard running:

Screenshot of Python Dashboard

You can change the widget values and execute the notebook with new values.

Schedule notebook

The notebook can be easily executed in the schedule with the Mercury framework. You just need to add schedule parameter in the YAML with crontab string. I will set the execution for every day between Monday and Friday at 8.30 in the morning. The additional parameter is schedule: '30 8 * * 1-5', and the full YAML in the notebook is presented in the picture below:

Notebook with YAML header

Deployment

I will deploy the dashboard created in Python to the AWS EC2 machine. I will use t3a.small machine with 2 vCPU and 2 GB of RAM and Ubuntu operating system:

Starting AWS EC2 for Python Dashboard

It is important to allow HTTPS and HTTP traffic to the machine:

AWS EC2 Security Network for Python Dashboard

After machine start I set a route in Route 53 service with A record pointing to the IP of the machine. The A record has the python-dashboard.mljar.com address.

Please login to the machine (there should be Connect button in AWS instances view). The machine should have the git already installed. We will need to install docker and docker-compose:

# install docker
sudo apt-get update
sudo apt install docker.io
# check if installed
sudo docker --version

# install docker-compose 
sudo curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
# check if installed
sudo docker-compose version

OK, we can now clone the python-dashboard repository to the server:

git clone https://github.com/pplonski/python-dashboard.git

The next step is to setup the Mercury server. We need to clone the Mercury repository:

git clone https://github.com/mljar/mercury.git

Please enter the mercury directory and copy the .env.example file:

cp .env.example .env

Please edit the .env file with your values (remember not to commit the .env file to the GitHub):

DEBUG=False
SERVE_STATIC=False
DJANGO_SUPERUSER_USERNAME=piotr-super-user
DJANGO_SUPERUSER_PASSWORD=a-password-top-secret
DJANGO_SUPERUSER_EMAIL=piotr@piotr.pl
SECRET_KEY="x3%q8fs(-q3i(m&=e1g%9xtvcn*q!c%i@v0*ha4-some-secred"
ALLOWED_HOSTS=python-dashboard.mljar.com
NOTEBOOKS_PATH=../python-dashboard/
TIME_ZONE=Europe/Warsaw
WELCOME=/app/notebooks/welcome.md
EMAIL_ADDRESS=my-gmail-address@gmail.com
EMAIL_PASSWORD=my-app-password-generated-in-gmail

The important variables that need to be set:

  • ALLOWED_HOSTS - the comma-separated hosts, I used here the domain name. If you don't have a domain, you can use the IP address.
  • NOTEBOOKS_PATH - the path to the folder with notebooks. In our case, it is ../python-dashboard/.
  • TIME_ZONE - the timezone that will be used for scheduling (you can read more about scheduling in the docs).
  • WELCOME - the path to the welcome message. The welcome message is in Markdown in the same directory as notebooks.
  • EMAIL_ADDRESS and EMAIL_PASSWORD - credentials used in notebook EmailService class.

I will use the docker-compose-pro.yaml for starting the Mercury server. You can use it if you have a domain name available. It will set you a Let's Encrypt HTTPS certificate. It will work without Pro License. However, the Pro features won't be available.

There is a setup-pro.sh script to start the docker-compose and initialize certificates. You need to pass the domain name at the start:

./setup-pro.sh python-dashboard.mljar.com

It will issue the certificate and start docker-compose with docker-compose-pro.yaml configuration in the background. That's all. The server should be running after a short while. You can access it at https://python-dashboard.mljar.com.

Python Dashboard Running on AWS EC2

You can stop the server with:

sudo docker-compose down

To start the server, please run:

sudo docker-compose up --build -d

It might be needed to update the notebook's code.

Summary

The Python language is amazing. It can be used to create so many different things. The Jupyter Notebook mixes the Python power with Markdown text and plots. It makes endless possibilities for creation. The data dashboard with live data from the Internet sources was created with Python only - no additional markup and web languages like HTML, JS, or CSS. The dashboard is published as a web application thanks to the open-source Mercury framework. What is more, the dashboard is automatically executed on the schedule. The publishing tools like Mercury makes it easy to share Jupyter Notebooks with non-technical users because code can be hidden.

The Mercury is an open-source framework. If you are a business user looking for a commercial-friendly license with additional features (like authentication) and dedicated support, please check our website for more details.