A simple guide to automate a Python script using GitHub Actions for free

Before we dive into the code let’s put some ground rules. GitHub Actions is a platform for automating software development workflows directly within GitHub repositories. Its primary purpose is to streamline tasks like continuous integration, continuous deployment, and other automation processes. So, it sounds more like a DevOps tool. However, we can use it to automate any (or almost any) Python script that doesn’t necessarily have to do with a DevOps process.

In this article, I will show you how to:

  • Set up a Python script as a recurring GitHub action
  • Get the weather forecast from Weatherapi
  • Store in a csv file the current weather conditions in the repository and commit the changes
  • Send an email with the weather forecast using jinja2 templates

We will need to register for accounts on three platforms at their free tier:

  • GitHub Actions, currently the free tier is 2,000 minutes per month. Our daily process is less than a minute of execution time.
  • Weatherapi, the free tier is 1 million calls per month and we only call it once per day…
  • Brevo, for sending emails. The free tier is 300 email per day so one a day should be fine…

Please note that you should check your selves the above tiers before using their service in order not to occure unwanted charges.

Repository structure

The repository at the end of the tutorial should have the below structure:

root
--> main.py (the main script)
--> requirements.txt (important so github understands how to setup the python environment)
--> sendemail.py (the script that sends the email)
--> template_forecast.html (the html template to be used for the email)
--> .github (folder)
-------> workflows (folder)
-------------> main.yml (the yml file that will be used for the action)

Set up main.py

As always let’s do our imports and set up some variables.

import pandas as pd
import requests, json, pprint
from sendemail import send_email
from datetime import datetime
from jinja2 import Template
import os

weather_api_key = os.environ["WEATHER_API_KEY"]
location = 'London'
csv_file = "temperature_log.csv"

Now we will make an API call to get the weather forecast

# Get the weather forcast
url= f'http://api.weatherapi.com/v1/forecast.json?key={weather_api_key}&q={location}&days=4&aqi=no&alerts=no'
response = requests.get(url)
data = json.loads(response.text)

Next, we will save the current temperature in a pandas dataframe

# Create a new entry for the csv in a dataframe
new_entry = {
    'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M'),
    'last_updated': data['current']['last_updated'],
    'location': f'{data['location']['name']}/{data['location']['country']}',
    'condition': data['current']['condition']['text'],
    'temperature': data['current']['temp_c'],
    'feels_like': data['current']['feelslike_c'],
    'humidity': data['current']['humidity'],
    'wind_speed': data['current']['wind_kph']
}
new_entry_df = pd.DataFrame([new_entry])

We stored it in a dataframe, to be able to add it to the csv file where we keep the current temperature. We will do it with the following code

# Append the new entry to the csv and save it
# Use try except in case the file does not exist (first time)
try:
    df = pd.read_csv(csv_file)
    df = pd.concat([df, new_entry_df])
except FileNotFoundError:
    df = new_entry_df.copy()
df.to_csv(csv_file, index=False)

Finally, we are going to send an email with the current temperature and the weather forecast.

#read html template and render the template
with open('template_forecast.html', 'r') as file:
    html_template = file.read()
template = Template(html_template)
html_output = template.render(data=data)

#send the email
send_email("Temprature Forecast Daily Email", html_output)

Sending the email (sendemail.py)

In the main.py, I use a custom function to send the emails. The code is below:

import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import os


def send_email(subject, html_body):
    try:
        smtp_user = os.environ["SMTP_USER"]
        smtp_password = os.environ["SMTP_PASSWORD"]
        smtp_server = os.environ["SMTP_SERVER"]
        smtp_port = int(os.environ["SMTP_PORT"])
        sender_email = os.environ["SENDER_EMAIL"]
        to_email = os.environ["TO_EMAIL"]

        # Construct the email message
        message = MIMEMultipart()
        message['From'] = sender_email
        message['To'] = to_email
        message['Subject'] = subject
        message.attach(MIMEText(html_body, 'html'))

        # Connect to the SMTP server
        with smtplib.SMTP(smtp_server, smtp_port) as server:
            # Start TLS for security (optional)
            server.starttls()

            # Log in to the email account
            server.login(smtp_user, smtp_password)

            # Send the email
            server.sendmail(sender_email, to_email, message.as_string())

        print('Email sent successfully!')
    except Exception as e:
        print(e)

Email template (template_forecast.html)

I am using the jinja2 package to render the response of the weatherapi into HTML format. For those of you who know a bit of Django, the below structure should be familiar. I am not going to go any deeper into this, but there are plenty of tutorials over the web.

<html>
<body>
<h2>Current Condition for {{ data.location.name }} - {{ data.location.country }}</h2>

<p>The weather is {{ data.current.condition.text }} and the temperature is {{ data.current.temp_c }}°C</p>

<h2>4-day Forecast</h2>

<table border="1">
    <thead>
        <tr>
            <th>Date</th>
            <th>Condition</th>
            <th>Min</th>
            <th>Max</th>
            <th>Chance of rain</th>
        </tr>
    </thead>
    <tbody>
        {% for forecast in data.forecast.forecastday %}
        <tr>
            <td>{{ forecast.date }}</td>
            <td>{{ forecast.day.condition.text }}</td>
            <td>{{ forecast.day.mintemp_c }}</td>
            <td>{{ forecast.day.maxtemp_c }}</td>
            <td>{{ forecast.day.daily_chance_of_rain }} %</td>
        </tr>
        {% endfor %}
    </tbody>
</table>
</body>
</html>

requirements.txt

We will need also a requirements txt file so github understands how to set up the environment. Note that this is done every time the action runs. Don’t expect that there will be a dedicated environment for your repository. This means billable time, and if you plan to have a complex script running often, you should always check if you are still in the free tier.

pandas
requests
jinja2




Setting up the GitHub action

Assuming that the repository is already set up on GitHub we should select the Actions tab and “set up a workflow yourself”

This will bring us to the editor of the yml file which is the backbone of a GitHub action and will place it automatically to the needed folder structure

Our yml file should be the following:

name: run my automation # name of the workflow

on:
  workflow_dispatch: # manual trigger of the workflow (if not set you cannot trigger it manually)
  schedule:
    - cron: '0 5 * * *' # at 05:00 UTC

jobs: # the jobs that will run sequentially
  build:
    runs-on: ubuntu-latest
    steps:

      - name: checkout repo content
        uses: actions/checkout@v4 # checkout the repository content to github runner

      - name: setup python
        uses: actions/setup-python@v4
        with:
          python-version: '3.12' # install the python version needed

      - name: install python packages
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt

      - name: execute py script # run main.py
        env:
          WEATHER_API_KEY: ${{ secrets.WEATHER_API_KEY }}
          SENDER_EMAIL: ${{ secrets.SENDER_EMAIL }}
          SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
          SMTP_PORT: ${{ secrets.SMTP_PORT }}
          SMTP_SERVER: ${{ secrets.SMTP_SERVER }}
          SMTP_USER: ${{ secrets.SMTP_USER }}
          TO_EMAIL: ${{ secrets.TO_EMAIL }}
        run: python main.py

      - name: commit files
        run: |
          git config --local user.email "[email protected]"
          git config --local user.name "GitHub Action"
          git add -A
          git diff-index --quiet HEAD || (git commit -a -m "updated logs" --allow-empty)

      - name: push changes
        uses: ad-m/[email protected]
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          branch: main

I have added some comments to the file, and some are self-explanatory, however, I think that some things need further explanation

OS Variables

You must have noticed that the code reads some environment variables like in the main.py

weather_api_key = os.environ["WEATHER_API_KEY"]

and the sendemail.py

smtp_user = os.environ["SMTP_USER"]
smtp_password = os.environ["SMTP_PASSWORD"]
smtp_server = os.environ["SMTP_SERVER"]
smtp_port = int(os.environ["SMTP_PORT"])
sender_email = os.environ["SENDER_EMAIL"]
to_email = os.environ["TO_EMAIL"]

Also, you should have noticed in the yml file the existence of those variables

env:
          WEATHER_API_KEY: ${{ secrets.WEATHER_API_KEY }}
          SENDER_EMAIL: ${{ secrets.SENDER_EMAIL }}
          SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
          SMTP_PORT: ${{ secrets.SMTP_PORT }}
          SMTP_SERVER: ${{ secrets.SMTP_SERVER }}
          SMTP_USER: ${{ secrets.SMTP_USER }}
          TO_EMAIL: ${{ secrets.TO_EMAIL }}

That is the way, for you to store configurations that should not be available to anyone, usually API keys, passwords, servers, ports, etc. To store those variables you should go to the repository settings, Actions, Secrets, and select Add a new repository secret.

After setting up the variables that are needed for the script, the list should look something like this:

Note that all those variables from now on are not even accessible to you. You can update them, but you cannot see them.

GitHub Token

When GitHub runs the action, a token is created and destroyed after it finishes, and you don’t have to do anything about it.

However, in our case, you see I am trying to save some data in a CSV file of the repository. This means that the token created should have the necessary access to do that. This should be done in Settings, Actions, and General at the Workflows Permissions. You don’t have to do that if you just run some script that does not require you to write anything.

Let’s run it

Now you should be ready to execute the script. There are 2 ways. The first one is to wait for the scheduler to kick in.

Here I should say that the scheduler is not extremely precise, so do not count that the action will start at the exact second.

The second one is to run it manually.

That should be it! Now every day you should be receiving an email with the current weather and the forecast of the next 3 days for free!

You can find the repository at https://github.com/phitzi/medium_actions_weather