Build a Cold-Chain Temperature Monitor

Build a Python script that charts hourly weather data and flags excursions.

Introduction

30 Second Summary

A page of temperature readings can hide the exact moment a shipment needs attention. Spotting that moment quickly turns a log into a decision.

In this project, you will build a Python script that transforms hourly weather data from the Visual Crossing Timeline Weather API into a cold-chain monitoring simulation. You will use a time-series chart to highlight excursions before rehearsing live edits for a customer demo.

What You'll Build

One command opens a time-series chart where red markers expose temperatures above the orange limit.

By the end of this project, you'll have:

  • An authenticated Python script you can run to retrieve hourly data. Your API key stays out of the source file.
  • A configurable excursion chart whose short settings block controls every safe live edit. The chart labels both axes. Red markers expose readings above the orange limit.
  • A demo-ready workflow that gives readable messages for common failures. A rehearsed customer story connects the weather-data simulation to real shipment sensors.
  • Secret Mission: Add a lower temperature limit so the chart marks readings outside a complete acceptable range.

Are there any prerequisites?

Basic Python syntax is helpful. The guide covers every setup task before you write the script.

Before We Start

Before any setup begins, this is your moment to commit to the monitoring prototype you are about to build. The prototype turns public hourly weather data into a chart where a customer can spot temperature excursions that deserve attention.

Set Up the API and Python Workspace

Hourly weather data only becomes useful when your script can reach it reliably. This setup protects the API key while giving the monitor a predictable Python workspace.

You'll create an account for the Visual Crossing Timeline Weather API. Then you'll prepare a virtual environment in Visual Studio Code with the key stored in an environment variable.

In this step, get ready to:
  • Create a Visual Crossing account and copy its API key safely.
  • Confirm Python 3.11 or newer is available inside an isolated project environment.
  • Install Requests 2.34.2 plus Matplotlib 3.11.2 before verifying the complete workspace.
Create the free API credential

Visual Crossing uses API-key authentication to connect requests to your account. Your script reads this credential from the terminal instead of storing it in a Python file.

The signup email can take a moment to arrive. A short wait here is normal because Visual Crossing verifies the address before revealing the key.

  • Visit the Visual Crossing signup page in your browser.
  • Complete the signup form with your email address.
  • Submit the signup form.
  • Retrieve the validation code from your email inbox.
  • Enter the validation code on the signup screen.
  • Copy the API key shown after account creation.
  • Select the Account page if the key is not shown on the confirmation screen.
  • Keep the copied key out of screenshots and source files.

Why Visual Crossing?

Visual Crossing supplies authenticated hourly weather data across an editable date range. That keeps this project focused on request handling plus monitoring logic.

The Free Plan includes up to 1,000 records per day. This project's short request stays within that daily allowance.

Check Python and create the project environment

Matplotlib 3.11.2 requires Python 3.11 or newer. Checking first prevents an older macOS installation from causing a confusing package failure later.

  • Press Cmd+Space to open Spotlight Search.
  • Type Terminal into Spotlight Search.
  • Press Enter to open Terminal.
  • Check the installed Python version by running this command:
python3 --version

What does this check do?

The version flag prints the Python release selected by your terminal. The result determines whether you can create the project environment immediately.

✔️ I see version 3.11 or higher

Your Python installation meets the project requirement. Continue below to create the project folder.

ⓧ I see an older version

macOS can include an older system-managed Python installation. The current python.org installer adds a newer installation alongside it.

  • Download the macOS installer for Python 3.14.8 from the official Python releases page.
  • Open the downloaded installer package.
  • Accept the standard installation options.
  • Approve the macOS permission request when it appears.
  • Complete the certificate installation described in the installer window.
  • Close Terminal after the installation finishes.
  • Open a fresh Terminal window through Spotlight Search.
  • Check the selected Python version again by running:
python3 --version

Why reopen Terminal?

The official installer updates your shell path so its newer Python can take priority over the macOS copy. A fresh Terminal window loads that updated path.

ⓧ Command not found

Your terminal cannot currently find a Python 3 installation. The official macOS installer adds Python plus the command-line links needed for this project.

  • Download the macOS installer for Python 3.14.8 from the official Python releases page.
  • Open the downloaded installer package.
  • Accept the standard installation options.
  • Approve the macOS permission request when it appears.
  • Complete the certificate installation described in the installer window.
  • Close Terminal after the installation finishes.
  • Open a fresh Terminal window through Spotlight Search.
  • Confirm Python is available by running:
python3 --version

What should you see?

The command prints Python 3.14.8 when the new installation is active. That version is ready for the environment setup below.

Still seeing the older Python?

Make sure you closed the original Terminal window after installation. A window that stayed open can retain the old shell path.

If the older version remains after reopening Terminal, help me select the Python 3.14 installation on macOS.

Your Python version is ready. Next, the project needs a named folder on your Desktop so every later file has one predictable home.

  • Move to your Desktop by running this command:
cd ~/Desktop

What does this command do?

The tilde represents your macOS home folder. This command places the terminal on your Desktop before the project folder is created.

  • Create the cold-chain-monitor folder by running these commands:
mkdir cold-chain-monitor
ls
cd cold-chain-monitor

What do these commands do?

  • The first command creates the cold-chain-monitor folder on your Desktop.
  • The second command lists the Desktop contents so you can confirm the folder exists.
  • The final command moves the terminal into the new folder.

You'll see cold-chain-monitor in the directory listing. That visible entry confirms the project folder exists.

Did the folder command stop?

A folder with the same name may already exist on your Desktop. Reuse that folder only if it is empty.

If you cannot locate the folder, help me create and enter the cold-chain-monitor folder on my Desktop.

  • Press Cmd+Space to open Spotlight Search.
  • Type Visual Studio Code into Spotlight Search.
  • Press Enter to open Visual Studio Code.
  • Click File in the menu bar.
  • Click Open Folder.
  • Select Desktop in the folder picker.
  • Select the cold-chain-monitor folder.
  • Click Open.

You'll see cold-chain-monitor at the top of the Explorer sidebar. This confirms VS Code is working inside the correct folder.

  • Right-click the cold-chain-monitor folder in the Explorer sidebar.
  • Select Open in Integrated Terminal.
  • Create and activate the isolated environment by running these commands:
python3 -m venv .venv
source .venv/bin/activate

Why use a virtual environment?

The .venv folder holds this project's Python packages separately from the rest of your Mac. Activating it makes later installation commands target that isolated environment.

You'll see the virtual environment name at the start of the terminal prompt. That prompt change confirms the environment is active.

Don't see the environment name?

Confirm that VS Code's terminal is inside the cold-chain-monitor folder. The activation path only works after the .venv folder has been created there.

If activation still fails, help me activate the .venv environment in the VS Code terminal on macOS.

Install and verify the dependencies and key

A requirements file records the exact package versions this project expects. Pinning both versions keeps your demo environment repeatable.

  • Click the New File control in the Explorer sidebar.
  • Name the file requirements.txt.
  • Add the pinned dependencies by pasting this content into requirements.txt:
requests==2.34.2
matplotlib==3.11.2

What does this file control?

  • The first line pins Requests to 2.34.2 for the later HTTP request.
  • The second line pins Matplotlib to 3.11.2 for the temperature chart.
  • Save requirements.txt.
  • Confirm that requirements.txt appears in the Explorer sidebar.

Does the file look different?

Check that each dependency appears on its own line. Extra spaces around either version pin can make comparison harder.

If VS Code saved the file under another name, help me create the exact requirements.txt file for this project.

✔️ Awesome, I've got everything!

Your saved dependency list is ready for installation.

ⓧ I'd like to double check the full code

requests==2.34.2
matplotlib==3.11.2
  • Install the pinned dependencies into the active environment by running:
python -m pip install -r requirements.txt

What does this installation do?

Python runs the copy of pip attached to the active virtual environment. The requirements file tells it which package versions to install.

The terminal returns to its prompt after the packages finish installing. A list of installed packages confirms the environment changed.

Did the dependency installation fail?

Confirm that the virtual environment name still appears in the prompt. Also confirm that requirements.txt is inside cold-chain-monitor.

If the download or installation still fails, help me install this project's pinned Python dependencies.

Pasting a credential into a terminal can feel risky. This command keeps the key out of your source files and limits the setting to the current terminal session.

  • Replace YOUR_API_KEY in the command below with the copied Visual Crossing key.
  • Export the key into the current integrated terminal by running the completed command:
export VISUAL_CROSSING_API_KEY="YOUR_API_KEY"

What does this export do?

The export adds VISUAL_CROSSING_API_KEY to the current shell environment. Python can read the key without finding it inside a source file.

The setting disappears when this terminal session ends. You need to export the key again in a future terminal session.

Before you run the final check, do you expect the key plus both installed package versions to line up?

  • Verify the key and both pinned dependencies by running:
python -c "import os, requests, matplotlib; print(bool(os.environ.get('VISUAL_CROSSING_API_KEY')), requests.__version__, matplotlib.__version__)"

What should the result prove?

You'll see True 2.34.2 3.11.2. The first value confirms the environment variable exists in this terminal.

The two version numbers confirm the active virtual environment contains the exact dependencies required by the project.

That's the demo foundation in place. Your credential is available to Python while your two charting dependencies are pinned and isolated.

Does the verification differ?

If the first value shows that the key is absent, repeat the export inside this same integrated terminal. Avoid pasting the key into any file.

If either package version differs, confirm the environment is active before repeating the requirements installation. Help me fix my final workspace verification.

Your API access and Python workspace are ready. Next, you'll turn authenticated hourly weather data into the first visible temperature chart.

Fetch Hourly Data and Draw the First Chart

Your protected API key is ready from the last step. Your isolated Python environment is also ready.

The earliest useful monitor needs real hourly readings from the Visual Crossing Timeline Weather API. Requests sends the authenticated HTTP GET request that retrieves them.

The response stores hourly records inside daily JSON objects. You'll flatten that structure before Matplotlib can draw a readable time series.

In this step, get ready to:
  • Build an editable authenticated API request.
  • Transform nested hourly records into chart-ready lists.
  • Display a labeled baseline chart.
Create the authenticated request

API-key authentication proves that your request belongs to your account. An environment variable keeps that credential outside the source file.

A short configuration block puts the likely live-demo changes in one place. The request code uses those values to build the correct Timeline API call.

  • Select the new-file icon beside the cold-chain-monitor folder in the VS Code sidebar.
  • Enter cold_chain_monitor.py as the file name.
  • Press Return to create the file.
  • Add the imports and editable configuration by pasting this first chunk:
import os
from datetime import datetime
from urllib.parse import quote

import matplotlib.pyplot as plt
import requests

LOCATION = "London,UK"
START_DATE = "2026-09-01"
END_DATE = "2026-09-02"
UNIT_GROUP = "metric"
DATA_FIELD = "temp"
DATA_LABEL = "Temperature (°C)"
UPPER_THRESHOLD = 8.0
REQUEST_TIMEOUT_SECONDS = 20

BASE_URL = (
    "https://weather.visualcrossing.com/"
    "VisualCrossingWebServices/rest/services/timeline"
)

What does this setup control?

  • The imports provide access to the environment variable, date parsing, URL encoding, API requests, and chart drawing.
  • The configuration block keeps the location, date range, units, selected field, chart label, upper threshold, and timeout together.
  • BASE_URL stores the fixed Timeline API endpoint separately from the editable request values.
  • Save cold_chain_monitor.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Confirm the file starts with the five imports shown above.

Seeing import warnings?

  • Return to the integrated terminal from the previous step.
  • Confirm its prompt still shows the active .venv environment.
  • Select the Python interpreter inside the cold-chain-monitor/.venv folder if VS Code is using another interpreter.

Still stuck? Help me connect VS Code to the active virtual environment

  • Add the API-key lookup below the BASE_URL block by pasting:
def get_api_key():
    api_key = os.environ.get("VISUAL_CROSSING_API_KEY")
    if not api_key:
        raise RuntimeError("VISUAL_CROSSING_API_KEY is not set. Export it first.")
    return api_key

How does the key stay protected?

  • get_api_key() reads VISUAL_CROSSING_API_KEY from the current terminal environment.
  • The condition stops the workflow with a specific setup message when the key is absent.
  • The return value gives the request function access to the credential without placing it in the Python file.
  • Save cold_chain_monitor.py.
  • Confirm get_api_key() ends with return api_key at the outer function indentation level.

Does the key lookup look misaligned?

  • Indent the body of get_api_key() with four spaces.
  • Indent the raise RuntimeError(...) line four more spaces beneath the condition.
  • Keep the API key value out of this file.

Need a second pair of eyes? Help me check the indentation in get_api_key()

Query parameters tell the API which units, fields, and response format you need. URL encoding makes locations such as London,UK safe to place inside the request path.

  • Add the request function below get_api_key() by pasting:
def fetch_weather_data(api_key):
    encoded_location = quote(LOCATION, safe="")
    url = f"{BASE_URL}/{encoded_location}/{START_DATE}/{END_DATE}"
    params = {
        "key": api_key,
        "unitGroup": UNIT_GROUP,
        "include": "hours",
        "elements": "datetime,temp,feelslike,humidity",
        "contentType": "json",
    }
    response = requests.get(url, params=params, timeout=REQUEST_TIMEOUT_SECONDS)
    response.raise_for_status()
    return response.json()

What does the request function do?

  • encoded_location holds a URL-safe version of the configured location.
  • url combines the endpoint, location, start date, and end date.
  • params requests hourly JSON containing the practice fields used in this project.
  • requests.get() sends the request with a 20-second timeout.
  • response.raise_for_status() surfaces an unsuccessful HTTP response before the code tries to decode it.
  • Save cold_chain_monitor.py.
  • Confirm fetch_weather_data() finishes with return response.json().

Does the request function show an editor error?

  • Check that every key inside params has a matching value.
  • Check that the closing brace aligns with params = {.
  • Check that requests.get() remains inside fetch_weather_data().

Still seeing a problem? Help me check my Visual Crossing request function

Parse the nested hourly response

Each response day contains its own hourly collection. Datetime parsing combines the day date with each hour time so every value lands at the correct point on the chart.

Some API records can omit a field. The parser skips one incomplete record so the remaining readings can still become a useful series.

  • Add the parser below fetch_weather_data() by pasting:
def parse_hourly_readings(weather_data):
    timestamps = []
    values = []
    for day in weather_data.get("days", []):
        day_date = day.get("datetime")
        for hour in day.get("hours", []):
            hour_time = hour.get("datetime")
            value = hour.get(DATA_FIELD)
            if not day_date or not hour_time or value is None:
                continue
            timestamps.append(datetime.fromisoformat(f"{day_date}T{hour_time}"))
            values.append(value)
    return timestamps, values

How does the nested JSON become a chart series?

  • timestamps stores the combined local date and time for each usable record.
  • values stores the matching value selected by DATA_FIELD.
  • The outer loop visits each day in the response.
  • The inner loop visits every hourly record inside that day.
  • continue skips a record when its timestamp or selected value is absent.
  • Save cold_chain_monitor.py.
  • Confirm the parser returns timestamps, values after both loops finish.

Does the parser show indentation problems?

  • Place the hour loop inside the day loop.
  • Place both append operations inside the hour loop.
  • Keep the final return statement aligned with the two empty-list declarations.

Need help tracing the loops? Help me check my nested JSON parser

Draw and run the baseline chart

A time-series chart places each hourly value against its local timestamp. Labels and rotated date ticks make the result understandable during a live demonstration.

  • Add the plotting function below parse_hourly_readings() by pasting:
def plot_readings(timestamps, values, resolved_location):
    fig, ax = plt.subplots(figsize=(10, 5))
    ax.plot(timestamps, values, color="blue", marker="o", markersize=4, label=DATA_FIELD)
    ax.set_xlabel("Local date and time")
    ax.set_ylabel(DATA_LABEL)
    ax.set_title(f"Temperature Monitor: {resolved_location}")
    ax.grid(True, alpha=0.3)
    ax.legend()
    fig.autofmt_xdate()
    plt.show()

What does the plotting function add?

  • plt.subplots() creates a figure with one plotting area.
  • ax.plot() draws the hourly values as a blue line with visible point markers.
  • The axis labels identify local time and the selected measurement.
  • The title includes the location resolved by the API.
  • fig.autofmt_xdate() rotates the date labels so they remain readable.
  • Save cold_chain_monitor.py.
  • Confirm plot_readings() finishes with plt.show().

Does the plotting code show a syntax problem?

  • Check that every chart label remains inside matching quotation marks.
  • Check that the title string starts with the letter f.
  • Keep every chart method indented inside plot_readings().

Still stuck? Help me check my Matplotlib plotting function

  • Connect the fetch, parse, and plot functions by adding this final chunk below plot_readings():
def main():
    api_key = get_api_key()
    weather_data = fetch_weather_data(api_key)
    timestamps, values = parse_hourly_readings(weather_data)
    resolved_location = weather_data.get("resolvedAddress", LOCATION)
    plot_readings(timestamps, values, resolved_location)


if __name__ == "__main__":
    main()

How does the workflow run?

  • main() reads the protected API key first.
  • The request result flows into parse_hourly_readings().
  • The resolved API location becomes part of the chart title.
  • The entry-point condition runs main() when you execute this file as a script.
  • Save cold_chain_monitor.py.
  • Confirm the final line contains main() with four spaces of indentation.

Does the workflow look disconnected?

  • Check that each function name matches its earlier definition exactly.
  • Check that main() sits beneath the entry-point condition.
  • Keep the entry-point condition outside every function.

Need help checking the call order? Help me trace the main workflow

✔️ Awesome, I've got everything!

Great. Double-check that you've saved cold_chain_monitor.py before running it.

ⓧ I'd like to double check the full code

import os
from datetime import datetime
from urllib.parse import quote

import matplotlib.pyplot as plt
import requests

LOCATION = "London,UK"
START_DATE = "2026-09-01"
END_DATE = "2026-09-02"
UNIT_GROUP = "metric"
DATA_FIELD = "temp"
DATA_LABEL = "Temperature (°C)"
UPPER_THRESHOLD = 8.0
REQUEST_TIMEOUT_SECONDS = 20

BASE_URL = (
    "https://weather.visualcrossing.com/"
    "VisualCrossingWebServices/rest/services/timeline"
)


def get_api_key():
    api_key = os.environ.get("VISUAL_CROSSING_API_KEY")
    if not api_key:
        raise RuntimeError("VISUAL_CROSSING_API_KEY is not set. Export it first.")
    return api_key


def fetch_weather_data(api_key):
    encoded_location = quote(LOCATION, safe="")
    url = f"{BASE_URL}/{encoded_location}/{START_DATE}/{END_DATE}"
    params = {
        "key": api_key,
        "unitGroup": UNIT_GROUP,
        "include": "hours",
        "elements": "datetime,temp,feelslike,humidity",
        "contentType": "json",
    }
    response = requests.get(url, params=params, timeout=REQUEST_TIMEOUT_SECONDS)
    response.raise_for_status()
    return response.json()


def parse_hourly_readings(weather_data):
    timestamps = []
    values = []
    for day in weather_data.get("days", []):
        day_date = day.get("datetime")
        for hour in day.get("hours", []):
            hour_time = hour.get("datetime")
            value = hour.get(DATA_FIELD)
            if not day_date or not hour_time or value is None:
                continue
            timestamps.append(datetime.fromisoformat(f"{day_date}T{hour_time}"))
            values.append(value)
    return timestamps, values


def plot_readings(timestamps, values, resolved_location):
    fig, ax = plt.subplots(figsize=(10, 5))
    ax.plot(timestamps, values, color="blue", marker="o", markersize=4, label=DATA_FIELD)
    ax.set_xlabel("Local date and time")
    ax.set_ylabel(DATA_LABEL)
    ax.set_title(f"Temperature Monitor: {resolved_location}")
    ax.grid(True, alpha=0.3)
    ax.legend()
    fig.autofmt_xdate()
    plt.show()


def main():
    api_key = get_api_key()
    weather_data = fetch_weather_data(api_key)
    timestamps, values = parse_hourly_readings(weather_data)
    resolved_location = weather_data.get("resolvedAddress", LOCATION)
    plot_readings(timestamps, values, resolved_location)


if __name__ == "__main__":
    main()

Keep the chart window open

The terminal remains occupied while the chart window is open. Closing the chart returns control to the terminal prompt.

Before you run it, do you expect the configured date range to produce a single point or an hourly sequence?

  • Run the baseline monitor in the integrated terminal by using:
python cold_chain_monitor.py

What happens when the script runs?

  • The script reads the API key from the active terminal session.
  • The Timeline API returns hourly JSON for the configured location and inclusive date range.
  • The parser creates matching timestamp and temperature lists.
  • Matplotlib displays those lists in a separate chart window.

You'll see a window with hourly temperature plotted against local date and time. The title includes the resolved location.

Does the chart fail to open?

  • Confirm the integrated terminal prompt still shows the active .venv environment.
  • Confirm the current terminal session still contains VISUAL_CROSSING_API_KEY.
  • Compare each function against the full-code reference above if Python reports a syntax or name problem.

Need help interpreting the result? Help me troubleshoot why cold_chain_monitor.py does not open its chart

That is your first live result. Your monitor now turns authenticated hourly weather data into a labeled visual timeline.

What does the baseline reveal?

The blue line makes the temperature pattern visible across the configured range. Each circular marker represents one usable hourly reading.

The chart gives the viewer no direct cue about which readings exceed the configured upper threshold. That deliberate gap exposes the business signal your next step needs to add.

Your authenticated data pipeline is working from request to chart. Next, you'll turn the configured limit into a visible excursion signal.

Make Temperature Excursions Visible

Your baseline chart already turns hourly readings into a line you can inspect. A viewer still has to examine every point manually.

This step uses threshold detection to compare each value with the configured upper limit. Matplotlib then turns the comparison into an immediate visual signal.

In this step, get ready to:
  • Calculate which readings exceed the configured upper limit.
  • Add the visual upper limit with red markers for highlighted readings.
  • Connect each highlighted point to a reading that deserves attention.
Calculate excursions

An excursion is a reading that falls outside an acceptable limit. For this simulation, any value above UPPER_THRESHOLD qualifies.

  • In cold_chain_monitor.py, locate the plot_readings function.
  • Insert the detector immediately above plot_readings by adding this code:
def find_excursions(timestamps, values):
    excursion_times = []
    excursion_values = []
    for timestamp, value in zip(timestamps, values):
        if value > UPPER_THRESHOLD:
            excursion_times.append(timestamp)
            excursion_values.append(value)
    return excursion_times, excursion_values

How the detector works

  • The zip(timestamps, values) call pairs each reading with its time.
  • The value > UPPER_THRESHOLD comparison selects readings above the configured limit.
  • The two matching lists let the chart place each marker at the correct time.
  • Save cold_chain_monitor.py.
  • Confirm find_excursions now sits between parse_hourly_readings and plot_readings in the editor.

Does the detector look misplaced?

  • Place find_excursions after the final line of parse_hourly_readings.
  • Align each function definition with the left edge of the file.

Still stuck? Help me place and indent find_excursions in cold_chain_monitor.py.

Add the visual threshold

The detector identifies the important readings. The chart now needs distinct layers for the full series, the upper limit, and the selected points.

  • In cold_chain_monitor.py, replace the entire plot_readings function with this updated version:
def plot_readings(timestamps, values, resolved_location):
    excursion_times, excursion_values = find_excursions(timestamps, values)
    fig, ax = plt.subplots(figsize=(10, 5))
    ax.plot(timestamps, values, color="blue", marker="o", markersize=4, label=DATA_FIELD)
    ax.axhline(y=UPPER_THRESHOLD, color="orange", linestyle="--", linewidth=2, label=f"Upper limit: {UPPER_THRESHOLD}")
    if excursion_times:
        ax.scatter(excursion_times, excursion_values, color="red", s=60, zorder=3, label="Excursion")
    ax.set_xlabel("Local date and time")
    ax.set_ylabel(DATA_LABEL)
    ax.set_title(f"Temperature Excursion Monitor: {resolved_location}")
    ax.grid(True, alpha=0.3)
    ax.legend()
    fig.autofmt_xdate()
    print(f"Loaded {len(values)} readings.")
    print(f"Found {len(excursion_values)} readings above {UPPER_THRESHOLD}.")
    plt.show()

What does this code add?

  • The blue plot preserves the complete hourly temperature series.
  • The orange horizontal line makes UPPER_THRESHOLD visible across the chart.
  • The red scatter points highlight every reading selected by find_excursions.
  • The two printed counts summarize the dataset before the chart window opens.
  • Save cold_chain_monitor.py.
  • Confirm plot_readings contains the blue series, the orange threshold line, and the red scatter points.

Does the chart code look misaligned?

  • Keep every chart statement indented beneath def plot_readings.
  • Keep ax.scatter inside if excursion_times.

Need another pair of eyes? Help me check the indentation in my updated plot_readings function.

✔️ Awesome, I've got everything!

Great. Double-check that you saved cold_chain_monitor.py before testing it.

ⓧ I'd like to double check the full code

Compare your file with this complete version.

import os
from datetime import datetime
from urllib.parse import quote

import matplotlib.pyplot as plt
import requests

LOCATION = "London,UK"
START_DATE = "2026-09-01"
END_DATE = "2026-09-02"
UNIT_GROUP = "metric"
DATA_FIELD = "temp"
DATA_LABEL = "Temperature (°C)"
UPPER_THRESHOLD = 8.0
REQUEST_TIMEOUT_SECONDS = 20

BASE_URL = (
    "https://weather.visualcrossing.com/"
    "VisualCrossingWebServices/rest/services/timeline"
)


def get_api_key():
    api_key = os.environ.get("VISUAL_CROSSING_API_KEY")
    if not api_key:
        raise RuntimeError("VISUAL_CROSSING_API_KEY is not set. Export it first.")
    return api_key


def fetch_weather_data(api_key):
    encoded_location = quote(LOCATION, safe="")
    url = f"{BASE_URL}/{encoded_location}/{START_DATE}/{END_DATE}"
    params = {
        "key": api_key,
        "unitGroup": UNIT_GROUP,
        "include": "hours",
        "elements": "datetime,temp,feelslike,humidity",
        "contentType": "json",
    }
    response = requests.get(url, params=params, timeout=REQUEST_TIMEOUT_SECONDS)
    response.raise_for_status()
    return response.json()


def parse_hourly_readings(weather_data):
    timestamps = []
    values = []
    for day in weather_data.get("days", []):
        day_date = day.get("datetime")
        for hour in day.get("hours", []):
            hour_time = hour.get("datetime")
            value = hour.get(DATA_FIELD)
            if not day_date or not hour_time or value is None:
                continue
            timestamps.append(datetime.fromisoformat(f"{day_date}T{hour_time}"))
            values.append(value)
    return timestamps, values


def find_excursions(timestamps, values):
    excursion_times = []
    excursion_values = []
    for timestamp, value in zip(timestamps, values):
        if value > UPPER_THRESHOLD:
            excursion_times.append(timestamp)
            excursion_values.append(value)
    return excursion_times, excursion_values


def plot_readings(timestamps, values, resolved_location):
    excursion_times, excursion_values = find_excursions(timestamps, values)
    fig, ax = plt.subplots(figsize=(10, 5))
    ax.plot(timestamps, values, color="blue", marker="o", markersize=4, label=DATA_FIELD)
    ax.axhline(y=UPPER_THRESHOLD, color="orange", linestyle="--", linewidth=2, label=f"Upper limit: {UPPER_THRESHOLD}")
    if excursion_times:
        ax.scatter(excursion_times, excursion_values, color="red", s=60, zorder=3, label="Excursion")
    ax.set_xlabel("Local date and time")
    ax.set_ylabel(DATA_LABEL)
    ax.set_title(f"Temperature Excursion Monitor: {resolved_location}")
    ax.grid(True, alpha=0.3)
    ax.legend()
    fig.autofmt_xdate()
    print(f"Loaded {len(values)} readings.")
    print(f"Found {len(excursion_values)} readings above {UPPER_THRESHOLD}.")
    plt.show()


def main():
    api_key = get_api_key()
    weather_data = fetch_weather_data(api_key)
    timestamps, values = parse_hourly_readings(weather_data)
    resolved_location = weather_data.get("resolvedAddress", LOCATION)
    plot_readings(timestamps, values, resolved_location)


if __name__ == "__main__":
    main()

This reference combines the detector with the updated chart and console summary.

Confirm the business signal

The finished chart should make the important readings obvious at a glance. Each red point represents a measurement that deserves attention.

Before you run the updated script, consider whether each red marker should sit above or below the orange line.

  • Run the updated monitor in the activated integrated terminal from earlier by using this command:
python cold_chain_monitor.py

What should you see?

You'll see the blue hourly series with an orange upper-limit line. You'll also see red markers on every reading above that line.

The terminal prints the total number of loaded readings. It also prints the number above UPPER_THRESHOLD.

Missing the threshold or markers?

  • Confirm the .venv environment remains active in the integrated terminal.
  • Confirm VISUAL_CROSSING_API_KEY remains exported in that terminal session.
  • Check that plot_readings calls find_excursions before drawing the chart.

Still stuck? Help me diagnose why my threshold line or excursion markers are missing.

  • Point to one red marker during your practice explanation.
  • Explain that the marker identifies a reading above the configured upper limit.

That's the signal in place. Your chart now turns a raw measurement into an item that deserves attention.

Your monitor now highlights temperature excursions immediately. Next, you'll give the script clear messages for credential, data, and network failures.

Make the Script Safe to Demo

Your monitor now turns hourly weather readings into a visible business signal. The chart highlights every temperature above the configured limit.

A live demonstration can still hit credential problems or incomplete data. Network failures can also interrupt the request. This step gives each failure a short message that you can understand under pressure.

In this step, get ready to:
  • Translate common request failures into readable messages.
  • Stop empty datasets from reaching the chart.
  • Rehearse missing-field and bad-key failures safely.
Protect the request path

The request path combines API-key authentication with an HTTP call. Clear sections make that path easier to scan during a live demonstration.

  • In the cold_chain_monitor.py editor tab from earlier, replace everything above get_api_key() with this documented setup block:
# Standard-library imports handle secrets, dates, and URL-safe locations.
import os
from datetime import datetime
from urllib.parse import quote

# Third-party imports handle the HTTP request and the chart.
import matplotlib.pyplot as plt
import requests


# Configuration: keep likely live-demo changes together in one place.
LOCATION = "London,UK"
START_DATE = "2026-09-01"
END_DATE = "2026-09-02"
UNIT_GROUP = "metric"
DATA_FIELD = "temp"
DATA_LABEL = "Temperature (°C)"
UPPER_THRESHOLD = 8.0
REQUEST_TIMEOUT_SECONDS = 20

# API setup: this endpoint supports a location followed by start and end dates.
BASE_URL = (
    "https://weather.visualcrossing.com/"
    "VisualCrossingWebServices/rest/services/timeline"
)

Why organize the setup block?

  • The import comments separate standard-library tools from third-party packages.
  • The configuration comment marks the values you are likely to change during a demonstration.
  • The API comment identifies the purpose of BASE_URL without exposing the credential.
  • Save cold_chain_monitor.py.
  • Confirm the documented setup still reaches the chart by running this command in the integrated terminal:
python cold_chain_monitor.py

What should you see?

You should see the existing temperature chart with its orange limit and red excursion markers. This confirms that the documentation changes preserved the working baseline.

Does the chart fail to open?

  • Check that every import remains above the configuration block.
  • Check that both URL strings remain inside the BASE_URL parentheses.

Still stuck? Help me compare the documented setup block with my working Python file

The API key comes from the current terminal's environment variable. Keeping that check separate tells you whether a failure happened before the request began.

  • Replace the existing get_api_key() function with this version:
def get_api_key():
    """Read the secret from the environment so it never lives in this file."""
    api_key = os.environ.get("VISUAL_CROSSING_API_KEY")
    if not api_key:
        raise RuntimeError(
            "VISUAL_CROSSING_API_KEY is not set. Export it in this terminal first."
        )
    return api_key

What does this code do?

  • The function reads VISUAL_CROSSING_API_KEY from the active terminal environment.
  • The missing-key check raises a message that points directly to the setup problem.
  • The function returns the key to the request code without storing it in cold_chain_monitor.py.

The request handler is split across two paste chunks to keep each section readable. Add both chunks before saving the file.

  • Begin replacing the existing fetch_weather_data() function with this first chunk:
def fetch_weather_data(api_key):
    """Call the Timeline API and translate common request failures."""
    encoded_location = quote(LOCATION, safe="")
    url = f"{BASE_URL}/{encoded_location}/{START_DATE}/{END_DATE}"

    # Request only hourly fields needed by the baseline and live-change drills.
    params = {
        "key": api_key,
        "unitGroup": UNIT_GROUP,
        "include": "hours",
        "elements": "datetime,temp,feelslike,humidity",
        "contentType": "json",
    }

    try:
        response = requests.get(
            url,
            params=params,
            timeout=REQUEST_TIMEOUT_SECONDS,
        )

        # A 401 response specifically points to the API key or account access.
        if response.status_code == 401:
            raise PermissionError(
                "API authentication failed. Check VISUAL_CROSSING_API_KEY."
            )

        response.raise_for_status()
        return response.json()

How does the protected request begin?

  • The query parameters request hourly JSON using the configured fields and units.
  • The timeout prevents the demonstration from waiting indefinitely for a response.
  • The status check identifies an authentication problem before the general HTTP check runs.
  • Continue the same function immediately below return response.json() with these exception handlers:
    except requests.exceptions.Timeout as error:
        raise RuntimeError(
            "The API request timed out. Check the network and try again."
        ) from error
    except requests.exceptions.ConnectionError as error:
        raise RuntimeError(
            "The API could not be reached. Check the network and try again."
        ) from error
    except requests.exceptions.JSONDecodeError as error:
        raise ValueError("The API returned a response that was not valid JSON.") from error
    except requests.exceptions.RequestException as error:
        raise RuntimeError(f"The API request failed: {error}") from error

How are request failures translated?

  • A timeout reports that the API took too long to respond.
  • A connection failure directs attention to the network.
  • A decoding failure explains that the response was not valid JSON.
  • The final handler catches other request failures without hiding the original detail.
  • Save cold_chain_monitor.py.
  • Confirm the protected request still succeeds by running this command:
python cold_chain_monitor.py

What should you see now?

You should see the chart again. The terminal should also report the loaded-reading count and above-threshold count.

Seeing a request error?

  • Check that every except line aligns with the try line.
  • Check that the response processing remains indented inside the try block.
  • Confirm that the real API key remains exported in this integrated terminal.

Still stuck? Help me debug the request exception handling in my weather script

That is the request safety net working. A healthy API response still reaches the same chart while common failures now have specific explanations.

Protect the parsing path

A successful API call can still contain missing arrays or incomplete hourly objects. The parser skips individual gaps and raises one clear error when no usable readings remain.

  • Replace the existing parse_hourly_readings() function with this protected version:
def parse_hourly_readings(weather_data):
    """Flatten nested day and hour objects into chart-ready Python lists."""
    timestamps = []
    values = []

    for day in weather_data.get("days", []):
        day_date = day.get("datetime")

        for hour in day.get("hours", []):
            hour_time = hour.get("datetime")
            value = hour.get(DATA_FIELD)

            # Skip one incomplete reading without discarding the whole response.
            if not day_date or not hour_time or value is None:
                continue

            timestamp = datetime.fromisoformat(f"{day_date}T{hour_time}")
            timestamps.append(timestamp)
            values.append(value)

    if not timestamps:
        raise ValueError(
            f"No usable '{DATA_FIELD}' readings were returned for this request."
        )

    return timestamps, values

What does this parser protect?

  • Each .get() call supplies a safe fallback for an optional array or field.
  • The continue statement skips one incomplete reading while preserving the rest of the response.
  • The final check stops an empty result before Matplotlib receives it.
  • Replace the existing main() function and entry point with this protected workflow:
def main():
    """Run the fetch, parse, and plot workflow with learner-friendly errors."""
    try:
        api_key = get_api_key()
        weather_data = fetch_weather_data(api_key)
        timestamps, values = parse_hourly_readings(weather_data)
        resolved_location = weather_data.get("resolvedAddress", LOCATION)
        plot_readings(timestamps, values, resolved_location)
    except (PermissionError, RuntimeError, ValueError) as error:
        print(f"Error: {error}")


# Entry point: run the workflow only when this file is executed as a script.
if __name__ == "__main__":
    main()

Why catch errors in main?

  • The workflow keeps the key lookup separate from the API call.
  • The parser runs only after the request returns data.
  • The shared handler prints the friendly error without an unexplained traceback.
  • Save cold_chain_monitor.py.

Before you run the protected workflow, do you expect valid data to reach the chart or the error handler?

  • Test the complete protected workflow by running this command:
python cold_chain_monitor.py

What should the protected workflow do?

Your valid key and requested field should still produce the chart. This proves the safety checks preserve the successful path.

Seeing a syntax error?

  • Check that the except line in main() aligns with its try line.
  • Check that the entry-point condition remains outside main().

Still stuck? Help me fix the protected main workflow in my Python script

Rehearse controlled failures

A controlled failure lets you practice the exact recovery before a customer is watching. You will first request a field that is absent from every hourly object.

  • In cold_chain_monitor.py, locate the DATA_FIELD configuration line.
  • Replace the value temp with missing_field.
  • Save cold_chain_monitor.py.

Before you run the script, do you expect the request itself to fail or the parser to reject the empty result?

  • Run the missing-field test with this command:
python cold_chain_monitor.py

What does the missing-field result prove?

You should see a friendly no-data message in the terminal. The message names missing_field without opening an empty chart.

Did an empty chart open?

  • Check that the if not timestamps: block appears before the parser's return statement.
  • Check that main() catches ValueError.

Still stuck? Help me make the missing-field test print a friendly no-data message

  • Restore the DATA_FIELD value to temp.
  • Save cold_chain_monitor.py.

Before you rerun the script, do you expect the chart to return now that the requested field exists?

  • Confirm the restored field works by running this command:
python cold_chain_monitor.py

What should return?

You should see the temperature chart return with its threshold line and excursion markers. The terminal should show both summary counts.

The API-key rehearsal temporarily replaces the current terminal's credential. This affects only the current terminal session. Keep the real key out of screenshots and source files.

  • Switch back to the integrated terminal from earlier.
  • Set the documented placeholder as a deliberately incorrect key by running this command exactly:
export VISUAL_CROSSING_API_KEY="YOUR_API_KEY"

What does this command change?

The command replaces the current terminal's exported value with an invalid placeholder. It does not edit cold_chain_monitor.py.

Before you run the script, which protected branch do you expect an invalid key to reach?

  • Run the authentication-failure rehearsal with this command:
python cold_chain_monitor.py

What should the bad-key test show?

You should see the authentication message from the 401 check. The script should finish without displaying a traceback.

Seeing a general request failure?

  • Check that the response.status_code == 401 condition appears before response.raise_for_status().
  • Check that main() catches PermissionError.

Still stuck? Help me make the bad-key test print the authentication message

Restoring the real credential is the important final move. The command keeps the key in the terminal environment instead of the Python file.

  • Restore the real key by running this command after replacing only YOUR_API_KEY with your copied credential:
export VISUAL_CROSSING_API_KEY="YOUR_API_KEY"

Why restore the key here?

The new export replaces the deliberate bad value in this terminal session. Your next run can authenticate without putting the secret in the script.

Use the reference below to make every line in cold_chain_monitor.py match the demo-ready version. This includes the documentation around the existing excursion and chart helpers.

✔️ Awesome, I've got everything!

Great. Double-check that you have saved cold_chain_monitor.py.

ⓧ I'd like to double check the full code

# Standard-library imports handle secrets, dates, and URL-safe locations.
import os
from datetime import datetime
from urllib.parse import quote

# Third-party imports handle the HTTP request and the chart.
import matplotlib.pyplot as plt
import requests


# Configuration: keep likely live-demo changes together in one place.
LOCATION = "London,UK"
START_DATE = "2026-09-01"
END_DATE = "2026-09-02"
UNIT_GROUP = "metric"
DATA_FIELD = "temp"
DATA_LABEL = "Temperature (°C)"
UPPER_THRESHOLD = 8.0
REQUEST_TIMEOUT_SECONDS = 20

# API setup: this endpoint supports a location followed by start and end dates.
BASE_URL = (
    "https://weather.visualcrossing.com/"
    "VisualCrossingWebServices/rest/services/timeline"
)


def get_api_key():
    """Read the secret from the environment so it never lives in this file."""
    api_key = os.environ.get("VISUAL_CROSSING_API_KEY")
    if not api_key:
        raise RuntimeError(
            "VISUAL_CROSSING_API_KEY is not set. Export it in this terminal first."
        )
    return api_key


def fetch_weather_data(api_key):
    """Call the Timeline API and translate common request failures."""
    encoded_location = quote(LOCATION, safe="")
    url = f"{BASE_URL}/{encoded_location}/{START_DATE}/{END_DATE}"

    # Request only hourly fields needed by the baseline and live-change drills.
    params = {
        "key": api_key,
        "unitGroup": UNIT_GROUP,
        "include": "hours",
        "elements": "datetime,temp,feelslike,humidity",
        "contentType": "json",
    }

    try:
        response = requests.get(
            url,
            params=params,
            timeout=REQUEST_TIMEOUT_SECONDS,
        )

        # A 401 response specifically points to the API key or account access.
        if response.status_code == 401:
            raise PermissionError(
                "API authentication failed. Check VISUAL_CROSSING_API_KEY."
            )

        response.raise_for_status()
        return response.json()

    except requests.exceptions.Timeout as error:
        raise RuntimeError(
            "The API request timed out. Check the network and try again."
        ) from error
    except requests.exceptions.ConnectionError as error:
        raise RuntimeError(
            "The API could not be reached. Check the network and try again."
        ) from error
    except requests.exceptions.JSONDecodeError as error:
        raise ValueError("The API returned a response that was not valid JSON.") from error
    except requests.exceptions.RequestException as error:
        raise RuntimeError(f"The API request failed: {error}") from error


def parse_hourly_readings(weather_data):
    """Flatten nested day and hour objects into chart-ready Python lists."""
    timestamps = []
    values = []

    for day in weather_data.get("days", []):
        day_date = day.get("datetime")

        for hour in day.get("hours", []):
            hour_time = hour.get("datetime")
            value = hour.get(DATA_FIELD)

            # Skip one incomplete reading without discarding the whole response.
            if not day_date or not hour_time or value is None:
                continue

            timestamp = datetime.fromisoformat(f"{day_date}T{hour_time}")
            timestamps.append(timestamp)
            values.append(value)

    if not timestamps:
        raise ValueError(
            f"No usable '{DATA_FIELD}' readings were returned for this request."
        )

    return timestamps, values


def find_excursions(timestamps, values):
    """Return only readings above the configured upper threshold."""
    excursion_times = []
    excursion_values = []

    for timestamp, value in zip(timestamps, values):
        if value > UPPER_THRESHOLD:
            excursion_times.append(timestamp)
            excursion_values.append(value)

    return excursion_times, excursion_values


def plot_readings(timestamps, values, resolved_location):
    """Draw the time series, upper limit, and highlighted excursions."""
    excursion_times, excursion_values = find_excursions(timestamps, values)

    fig, ax = plt.subplots(figsize=(10, 5))
    ax.plot(
        timestamps,
        values,
        color="blue",
        marker="o",
        markersize=4,
        label=DATA_FIELD,
    )
    ax.axhline(
        y=UPPER_THRESHOLD,
        color="orange",
        linestyle="--",
        linewidth=2,
        label=f"Upper limit: {UPPER_THRESHOLD}",
    )

    if excursion_times:
        ax.scatter(
            excursion_times,
            excursion_values,
            color="red",
            s=60,
            zorder=3,
            label="Excursion",
        )

    # Labels make the chart understandable without reading the source code.
    ax.set_xlabel("Local date and time")
    ax.set_ylabel(DATA_LABEL)
    ax.set_title(f"Temperature Excursion Monitor: {resolved_location}")
    ax.grid(True, alpha=0.3)
    ax.legend()
    fig.autofmt_xdate()

    print(f"Loaded {len(values)} readings.")
    print(f"Found {len(excursion_values)} readings above {UPPER_THRESHOLD}.")
    plt.show()


def main():
    """Run the fetch, parse, and plot workflow with learner-friendly errors."""
    try:
        api_key = get_api_key()
        weather_data = fetch_weather_data(api_key)
        timestamps, values = parse_hourly_readings(weather_data)
        resolved_location = weather_data.get("resolvedAddress", LOCATION)
        plot_readings(timestamps, values, resolved_location)
    except (PermissionError, RuntimeError, ValueError) as error:
        print(f"Error: {error}")


# Entry point: run the workflow only when this file is executed as a script.
if __name__ == "__main__":
    main()
  • Save cold_chain_monitor.py after completing the comparison.

Before the final run, do you expect the restored key and restored field to reach the chart successfully?

  • Complete the end-of-step verification by running this command:
python cold_chain_monitor.py

What confirms the recovery?

You should see the chart return with the blue series, orange upper limit, and red excursion markers. The successful run confirms that both controlled failures have been reversed.

Still seeing an authentication message?

  • Run the export command again with the real key in place of the placeholder.
  • Confirm that you are using the same integrated terminal where the virtual environment remains active.
  • Keep the real key out of screenshots when checking the command.

Still stuck? Help me restore my API key safely after the bad-key rehearsal

You have the demo safety net in place. The monitor now distinguishes setup failures, request failures, and empty data while preserving the working chart.

Your monitor can now fail clearly and recover cleanly. Next up, you will rehearse fast configuration changes and turn the chart into a concise customer story.

Rehearse Live Changes and the Customer Story

Your monitor now turns authenticated weather data into a chart while reporting common failures clearly.

A live demo adds pressure because slow edits can break the flow. You'll rehearse safe changes in Visual Studio Code.

The customer also needs to understand why the result matters. You'll connect each red marker to a business decision without hiding the limits of the prototype.

In this step, get ready to:
  • Rehearse six timed configuration changes.
  • Restore the complete upper-threshold baseline.
  • Deliver a customer explanation in no more than three minutes.
Run six timed modification challenges

The configuration block keeps your safest live changes near the top of cold_chain_monitor.py. Each drill follows the same loop: edit, predict, run, observe, then restore.

Keep the Baseline Close

Use Cmd+Z to undo temporary edits after each drill. Check the restored values before moving to the next challenge.

Challenge 1: Date range.

  • Start a stopwatch for the date-range drill.
  • Set both START_DATE and END_DATE to 2026-09-03 as one temporary date-window edit.
  • Save cold_chain_monitor.py.

Before you run this, predict how many hourly readings the one-day window should remove.

  • Test the shorter date range by running:
python cold_chain_monitor.py

The command fetches the temporary one-day range. You'll see the chart cover one local date while the console reports fewer loaded readings.

  • Stop the stopwatch when the changed chart appears.
  • Close the chart window to return control to the terminal.
  • Restore START_DATE to 2026-09-01.
  • Restore END_DATE to 2026-09-02.
  • Save cold_chain_monitor.py.
  • Verify the restored date range by running:
python cold_chain_monitor.py

The restored command fetches the original inclusive range. You'll see both configured dates on the chart again.

  • Close the restored chart window.

Does the Date Drill Fail?

  • Check that the start date comes before or matches the end date.
  • Check that each date uses the same year-month-day pattern as the baseline.

Still stuck? Help me diagnose my temporary date-range edit

Challenge 2: Requested field and label.

  • Start a stopwatch for the field drill.
  • Replace the adjacent DATA_FIELD and DATA_LABEL settings with feelslike and Feels-like temperature (°C) as one coordinated edit.
  • Save cold_chain_monitor.py.
  • Test the new field by running:
python cold_chain_monitor.py

The command now plots the requested feels-like readings. You'll see the updated Celsius label on the vertical axis.

  • Stop the stopwatch when the updated chart appears.
  • Close the chart window.
  • Restore the adjacent settings to DATA_FIELD = "temp" and DATA_LABEL = "Temperature (°C)" as one baseline edit.
  • Save cold_chain_monitor.py.
  • Verify the restored field by running:
python cold_chain_monitor.py

The command plots temperature readings again. You'll see Temperature (°C) on the vertical axis.

  • Close the restored chart window.

Does the Label Look Wrong?

  • Check that DATA_FIELD contains feelslike during the drill.
  • Check that DATA_LABEL describes the same measurement shown by the field.

Still stuck? Help me align the selected field with its chart label

Challenge 3: Plot type.

A line chart emphasizes movement between hourly readings. A bar chart gives each reading more visual weight, so this drill helps you choose the clearer story for your audience.

  • Start a stopwatch for the plot-type drill.
  • Convert the Matplotlib call from ax.plot(...) to ax.bar(...).
  • Remove the marker="o" and markersize=4 arguments from the temporary bar call as one cleanup edit.
  • Save cold_chain_monitor.py.

Before you run this, predict which chart shape will make the hourly pattern easier to scan.

  • Test the bar chart by running:
python cold_chain_monitor.py

The command displays one bar for each hourly value. The orange threshold line and red excursion markers remain available for comparison.

  • Stop the stopwatch when the bar chart appears.
  • Close the chart window.
  • Undo the plot-type edits until the original ax.plot(...) call returns.
  • Save cold_chain_monitor.py.
  • Verify the restored line chart by running:
python cold_chain_monitor.py

The command displays the blue time-series line again. You'll see circular markers connecting the hourly readings.

  • Close the restored chart window.

Does the Bar Chart Raise an Error?

  • Remove the line-specific marker arguments from the temporary bar call.
  • Keep timestamps as the horizontal values.
  • Keep values as the bar heights.

Still stuck? Help me convert this plot call into a temporary bar chart

Challenge 4: Upper threshold.

  • Start a stopwatch for the threshold drill.
  • Increase UPPER_THRESHOLD from 8.0 to 10.0.
  • Save cold_chain_monitor.py.

Before you run this, predict whether the excursion count should rise or fall.

  • Test the higher threshold by running:
python cold_chain_monitor.py

The orange line moves higher on the chart. The reported excursion count should stay the same or fall because fewer readings can exceed the new limit.

  • Stop the stopwatch when the changed count appears.
  • Close the chart window.
  • Restore UPPER_THRESHOLD to 8.0.
  • Save cold_chain_monitor.py.
  • Verify the restored threshold by running:
python cold_chain_monitor.py

The command returns the upper-limit line to 8.0. The console summary again uses the baseline limit.

  • Close the restored chart window.

Does the Excursion Count Look Unchanged?

The count can remain unchanged when no readings fall between the old limit and the temporary limit. Confirm the orange line moved before deciding the edit failed.

Still unsure? Help me interpret my threshold drill

Challenge 5: Unit system.

  • Start a stopwatch for the unit-system drill.
  • Set UNIT_GROUP to us.
  • Set DATA_LABEL to Temperature (°F).
  • Set UPPER_THRESHOLD to 46.4.
  • Save cold_chain_monitor.py.
  • Test the coordinated unit changes by running:
python cold_chain_monitor.py

The request now returns temperatures in the US unit group. You'll see the Fahrenheit label and an upper-limit line at 46.4.

  • Stop the stopwatch when the Fahrenheit chart appears.
  • Close the chart window.
  • Restore UNIT_GROUP to metric.
  • Restore DATA_LABEL to Temperature (°C).
  • Restore UPPER_THRESHOLD to 8.0.
  • Save cold_chain_monitor.py.
  • Verify the restored metric configuration by running:
python cold_chain_monitor.py

The command returns the chart to Celsius. You'll see the upper-limit line at 8.0.

  • Close the restored chart window.

These Are Simulation Limits

The temperature limits in this project are demonstration settings. A production cold-chain system must use the limits defined for its specific product and operating process.

Do the Units Look Mismatched?

  • Check that the unit group matches the unit named by the vertical-axis label.
  • Check that the threshold uses the same unit as the returned values.

Still stuck? Help me align the unit group, label, and threshold

Challenge 6: Unrequested field.

  • Start a stopwatch for the missing-field drill.
  • Set DATA_FIELD to windspeed without changing the requested elements.
  • Save cold_chain_monitor.py.

Before you run this, predict whether the script should open a chart or print its friendly no-data message.

  • Test the unrequested field by running:
python cold_chain_monitor.py

The command reaches the protected parsing path. You'll see Error: No usable 'windspeed' readings were returned for this request. instead of a traceback.

  • Stop the stopwatch when the friendly message appears.
  • Restore DATA_FIELD to temp.
  • Save cold_chain_monitor.py.

Before the final baseline check, predict whether the chart or the no-data message should return.

  • Confirm the complete baseline works by running:
python cold_chain_monitor.py

The command fetches the original metric temperature series. You'll see the blue line, the orange 8.0 limit, and red excursion markers.

Does the Final Chart Stay Missing?

  • Check that DATA_FIELD is restored to temp.
  • Check that the requested elements string still contains temp.
  • Check that the real API key remains exported in the current terminal.

Still stuck? Help me restore the final temperature chart

That's your demo baseline back in place. The same script now feels familiar enough to edit under time pressure.

✔️ Awesome, I've got everything!

Your temporary edits are restored. requirements.txt still pins Requests 2.34.2 and Matplotlib 3.11.2.

ⓧ I'd like to double check the full code

Compare your restored cold_chain_monitor.py with this baseline.

# Standard-library imports handle secrets, dates, and URL-safe locations.
import os
from datetime import datetime
from urllib.parse import quote

# Third-party imports handle the HTTP request and the chart.
import matplotlib.pyplot as plt
import requests


# Configuration: keep likely live-demo changes together in one place.
LOCATION = "London,UK"
START_DATE = "2026-09-01"
END_DATE = "2026-09-02"
UNIT_GROUP = "metric"
DATA_FIELD = "temp"
DATA_LABEL = "Temperature (°C)"
UPPER_THRESHOLD = 8.0
REQUEST_TIMEOUT_SECONDS = 20

# API setup: this endpoint supports a location followed by start and end dates.
BASE_URL = (
    "https://weather.visualcrossing.com/"
    "VisualCrossingWebServices/rest/services/timeline"
)


def get_api_key():
    """Read the secret from the environment so it never lives in this file."""
    api_key = os.environ.get("VISUAL_CROSSING_API_KEY")
    if not api_key:
        raise RuntimeError(
            "VISUAL_CROSSING_API_KEY is not set. Export it in this terminal first."
        )
    return api_key


def fetch_weather_data(api_key):
    """Call the Timeline API and translate common request failures."""
    encoded_location = quote(LOCATION, safe="")
    url = f"{BASE_URL}/{encoded_location}/{START_DATE}/{END_DATE}"

    # Request only hourly fields needed by the baseline and live-change drills.
    params = {
        "key": api_key,
        "unitGroup": UNIT_GROUP,
        "include": "hours",
        "elements": "datetime,temp,feelslike,humidity",
        "contentType": "json",
    }

    try:
        response = requests.get(
            url,
            params=params,
            timeout=REQUEST_TIMEOUT_SECONDS,
        )

        # A 401 response specifically points to the API key or account access.
        if response.status_code == 401:
            raise PermissionError(
                "API authentication failed. Check VISUAL_CROSSING_API_KEY."
            )

        response.raise_for_status()
        return response.json()

    except requests.exceptions.Timeout as error:
        raise RuntimeError(
            "The API request timed out. Check the network and try again."
        ) from error
    except requests.exceptions.ConnectionError as error:
        raise RuntimeError(
            "The API could not be reached. Check the network and try again."
        ) from error
    except requests.exceptions.JSONDecodeError as error:
        raise ValueError("The API returned a response that was not valid JSON.") from error
    except requests.exceptions.RequestException as error:
        raise RuntimeError(f"The API request failed: {error}") from error


def parse_hourly_readings(weather_data):
    """Flatten nested day and hour objects into chart-ready Python lists."""
    timestamps = []
    values = []

    for day in weather_data.get("days", []):
        day_date = day.get("datetime")

        for hour in day.get("hours", []):
            hour_time = hour.get("datetime")
            value = hour.get(DATA_FIELD)

            # Skip one incomplete reading without discarding the whole response.
            if not day_date or not hour_time or value is None:
                continue

            timestamp = datetime.fromisoformat(f"{day_date}T{hour_time}")
            timestamps.append(timestamp)
            values.append(value)

    if not timestamps:
        raise ValueError(
            f"No usable '{DATA_FIELD}' readings were returned for this request."
        )

    return timestamps, values


def find_excursions(timestamps, values):
    """Return only readings above the configured upper threshold."""
    excursion_times = []
    excursion_values = []

    for timestamp, value in zip(timestamps, values):
        if value > UPPER_THRESHOLD:
            excursion_times.append(timestamp)
            excursion_values.append(value)

    return excursion_times, excursion_values


def plot_readings(timestamps, values, resolved_location):
    """Draw the time series, upper limit, and highlighted excursions."""
    excursion_times, excursion_values = find_excursions(timestamps, values)

    fig, ax = plt.subplots(figsize=(10, 5))
    ax.plot(
        timestamps,
        values,
        color="blue",
        marker="o",
        markersize=4,
        label=DATA_FIELD,
    )
    ax.axhline(
        y=UPPER_THRESHOLD,
        color="orange",
        linestyle="--",
        linewidth=2,
        label=f"Upper limit: {UPPER_THRESHOLD}",
    )

    if excursion_times:
        ax.scatter(
            excursion_times,
            excursion_values,
            color="red",
            s=60,
            zorder=3,
            label="Excursion",
        )

    # Labels make the chart understandable without reading the source code.
    ax.set_xlabel("Local date and time")
    ax.set_ylabel(DATA_LABEL)
    ax.set_title(f"Temperature Excursion Monitor: {resolved_location}")
    ax.grid(True, alpha=0.3)
    ax.legend()
    fig.autofmt_xdate()

    print(f"Loaded {len(values)} readings.")
    print(f"Found {len(excursion_values)} readings above {UPPER_THRESHOLD}.")
    plt.show()


def main():
    """Run the fetch, parse, and plot workflow with learner-friendly errors."""
    try:
        api_key = get_api_key()
        weather_data = fetch_weather_data(api_key)
        timestamps, values = parse_hourly_readings(weather_data)
        resolved_location = weather_data.get("resolvedAddress", LOCATION)
        plot_readings(timestamps, values, resolved_location)
    except (PermissionError, RuntimeError, ValueError) as error:
        print(f"Error: {error}")


# Entry point: run the workflow only when this file is executed as a script.
if __name__ == "__main__":
    main()

This reference preserves the resilient upper-threshold implementation. Make any corrections before rehearsing the customer story.

Deliver the customer story

A customer-facing explanation leads with the operational problem. The code supports that story by showing where a reading deserves attention.

Aim for at least two minutes. Finish before a three-minute timer expires.

  • Start a three-minute timer on your phone.
  • Describe the business problem of spotting temperature excursions across many readings.
  • Explain that the script retrieves hourly public weather data for an editable location and date range.
  • Point to one red marker as a reading above the configured upper limit.
  • Describe how a customer could investigate the affected period after seeing that signal.
  • State that this prototype demonstrates monitoring logic with public weather data.
  • Close with authenticated shipment or device sensors as the production data source.

What Makes the Story Credible?

The weather-data limitation keeps your claim accurate. The prototype proves the authentication, transformation, detection, visualization, and communication workflow.

A production version connects that workflow to real shipment sensors. This distinction shows the customer what already works and what must change next.

You now have more than a working monitor. You can change it quickly, restore it confidently, and explain its value within a customer conversation.

Secret mission

Add a Two-Sided Safe Temperature Range

Your current monitor catches temperatures above one limit. Extend it with a lower limit so the chart shows a complete safe band and highlights readings outside either boundary.

Clean Up Your Resources

Clean Up Your Resources

Everything you built stays free because the files are local and the guided requests fit within the Visual Crossing Timeline Weather API Free Plan allowance. Decide whether to keep the monitor ready, pause your terminal session, or remove the project entirely.

Clear the session credential

Your API key remains exported in the current VS Code terminal session. Keep that terminal private while it remains open.

Closing the terminal clears the session-only export. Your account and local source files remain available.

Resources you used:

  • Local project folder cold-chain-monitor containing .venv, requirements.txt, and cold_chain_monitor.py.
  • Visual Crossing account containing your API key credential.

Keep everything running

No action needed. Choose this if you want to keep rehearsing the monitor or extending its safe-range logic.

  • Keep the cold-chain-monitor folder so the script and virtual environment remain ready for future practice.
  • Keep your Visual Crossing account so you can continue retrieving hourly weather data.
  • Leave the current VS Code integrated terminal open only while you are actively using its exported API key.
  • Close the terminal when you finish so the session-only credential disappears.

Pause - I'll come back to this later

Stop the active Python environment to free the terminal session while keeping your files and account for later.

  • Exit the active virtual environment by running this command:
deactivate

What does this command do?

The deactivate command exits the active .venv environment. It leaves every project file installed on your computer.

  • Close the current VS Code integrated terminal to clear VISUAL_CROSSING_API_KEY from that session.

Your cold-chain-monitor folder and Visual Crossing account stay available. A future terminal session needs the virtual environment activated and the API key exported again.

Delete - I don't want to use this again

Deleting cold-chain-monitor is permanent. Your Python script and virtual environment disappear with the folder.

  • Remove the active environment and local project folder by running these commands in the current VS Code integrated terminal:
deactivate
cd ..
rm -rf cold-chain-monitor

What do these commands remove?

  • The deactivate command exits .venv before the folder is removed.
  • The cd .. command moves the terminal to the directory containing cold-chain-monitor.
  • The rm -rf cold-chain-monitor command permanently removes .venv, requirements.txt, cold_chain_monitor.py, and the project folder.
  • Check the parent folder in Finder to confirm that cold-chain-monitor is no longer listed.

Still see the project folder?

Confirm that the terminal moved to the directory containing cold-chain-monitor before the removal command ran. A different starting directory can target the wrong location.

Help me safely remove my cold-chain-monitor folder.

  • Sign in to Visual Crossing.
  • Select the Account page.
  • Use the account deletion option on that page if you no longer need the account.
  • Complete the confirmation shown by the page.
  • Close the VS Code integrated terminal to clear the exported API key from the session.

Nice Work!

Nice Work!

You made it! Your Python monitor now turns authenticated public weather data into a two-sided temperature excursion chart.

You've learned how to:

  • Keep your credential out of cold_chain_monitor.py by loading it from an environment variable. Use API-key authentication to retrieve hourly data from the Visual Crossing Timeline Weather API. Transform nested JSON into a chart-ready time series.
  • Plot a labeled time series with Matplotlib. Turn readings outside the safe range into red visual signals. Report bad credentials clearly. Report empty datasets clearly. Catch invalid JSON. Translate network failures into useful messages.
  • Complete six timed live changes without losing the working baseline. Deliver a customer story in no more than three minutes. Explain that public weather data demonstrates the monitoring workflow. Position authenticated shipment sensors as the production next step.
  • Secret Mission: Extend the monitor with a two-sided safe range. Show both boundaries as orange lines. Mark readings below 2.0 or above 8.0 in red.

Ready to quiz yourself?