I’ve always been fascinated by history and weather data.
In this project, I built an app that weaves together these data: hourly weather forecasts from Open-Meteo and “on this day” historical events from History.muffinlabs.
Each hour becomes its story card, pairing the temperature, precipitation, and wind speed I’m experiencing now (or the weather of any date I choose) with fascinating moments from the past.
At the same time, I will showcase modern Python techniques - async HTTP requests, clean data models, and lightweight templates - while delivering a playful, visually engaging experience.
By the end of this tutorial, you’ll see how I used FastAPI and Jinja2 to turn raw JSON into a dynamic timeline of weather and history that feels informative and fun.
You can download the full source code here:
This book offers an in-depth exploration of Python's magic methods, examining the mechanics and applications that make these features essential to Python's design.
Prerequisites & Setup
Before diving into the code, here’s what you’ll need to have in place on your machine—and how I got my environment ready:
Languages & Frameworks
- Python 3.10+: I used Python 3.11 for this project, but any 3.10 or newer interpreter works perfectly.
- FastAPI: powers the async web server and routing.
- aiohttp: handles all of my non-blocking HTTP requests to the weather and history APIs.
- Jinja2: renders the HTML templates
Installing Dependencies
Create a virtual environment (recommended):
python -m venv .venv
source .venv/bin/activate
requirements.txt
I keep the key packages in a simple requirements.txt
:
fastapi
uvicorn
jinja2
python-multipart
requests
python-dateutil
aiohttp
Install:
pip install -r requirements.txt
Obtaining Free Access
One of my favorite parts of this project is that no API keys or signup flows are required:
- Open-Meteo: their forecast and archive endpoints are fully open and free - just pass latitude/longitude in your query.
- History.muffinlabs.com: the “on this day” API is also public and doesn’t require authentication.
If you want to learn more about Open-Meteo, check out some of my other articles:
https://developer-service.blog/how-to-build-a-free-weather-app-with-pyside6-and-open-meteo/
Or some of my YouTube videos:
Project Structure
Here’s how I’ve organized the code for this app - each file serves a clear purpose to keep things modular and maintainable:
/app
├─ utils.py
├─ data_models.py
├─ main.py
└─ templates/
├─ base.html
└─ index.html
utils.py
Contains all of my asynchronous helper functions:
search_location()
for geocoding via Open-Meteofetch_current_weather()
andfetch_weather_data()
to pull today’s and historical forecastsfetch_historical_events()
for “on this day” datadistribute_events()
andcreate_story_cards()
to assemble the per-hour story cards
data_models.py
Defines the data structures I use throughout the app, all as @dataclass
:
WeatherData
,DailyWeatherData
for weatherHistoricalEvent
,HistoricalData
for events, births, deathsStoryCard
to bundle one hour’s weather + historical snippets
main.py
The FastAPI application entry point:
- Mounts static files and sets up Jinja2 templates
- Defines three routes:
GET /
renders the home formGET /search-location
powers the HTMX drop-down for live location suggestionsPOST /
handles form submission, orchestrates data fetching, and renders the story cards
templates/base.html
The HTML skeleton with <head>
imports (Bootstrap, HTMX) and a {% block content %}
placeholder for child pages.
templates/index.html
Extends base.html
to provide:
- A date picker and location input (with HTMX dropdown)
- A loop over the
story_cards
context variable to display each hour’s weather details alongside its historical events
This layout keeps concerns separated - data models, external-API logic, web-server routes, and presentation - so you can work on each part independently.

Mug Trust Me Prompt Engineer Sarcastic Design
A sarcastic "prompt badge" design coffee mug, featuring GPT-style neural network lines and a sunglasses emoji.
Perfect for professionals with a sense of humor, this mug adds a touch of personality to your morning routine.
Ideal for engineers, tech enthusiasts, and anyone who appreciates a good joke.
Great for gifting on birthdays, holidays, and work anniversaries.
Defining the Data Models (data_models.py
)
In data_models.py
, I lean on Python’s @dataclass
decorator to give structure and type safety to every piece of data flowing through the app.
Here’s how I break it down, first the Weather data:
@dataclass
class WeatherData:
time: datetime
temperature: float
precipitation: float
wind_speed: float
weather_code: int
@dataclass
class DailyWeatherData:
sunrise: datetime
sunset: datetime
WeatherData
captures each hour’s snapshot: the timestamp plus temperature (°C), precipitation (mm), wind speed (m/s), and the weather code for icon lookups.DailyWeatherData
holds just the sunrise and sunset times for the selected date, so I can show this info in the corresponding hour card.
Then the Historical data:
@dataclass
class HistoricalEvent:
year: int
text: str
wikipedia_url: Optional[str] = None
@dataclass
class HistoricalData:
events: List[HistoricalEvent]
births: List[HistoricalEvent]
deaths: List[HistoricalEvent]
HistoricalEvent
represents a single “on this day” entry—year, descriptive text, and an optional Wikipedia link.HistoricalData
bundles together three lists of those events (major occurrences, notable births, and deaths) for easy passing around.
Finally, I merge the two into a story card:
@dataclass
class StoryCard:
weather: WeatherData
daily: DailyWeatherData
events: List[HistoricalEvent]
births: List[HistoricalEvent]
deaths: List[HistoricalEvent]
StoryCard
ties one hour’s weather (weather
) and the day’s sunrise/sunset (daily
) to up to three randomly-distributed events, births, and deaths.
By keeping these models pure and decoupled, the utility functions simply fill in instances of these classes - and the FastAPI endpoints and templates can treat each card as a single, cohesive unit.
Utility Module Deep-Dive (utils.py
)
In utils.py
, I encapsulate all the API calls and data‐shaping logic into a set of clear, reusable functions.
Here’s how each piece works.
Location Search
class LocationData(TypedDict):
name: str
latitude: float
longitude: float
country: str
timezone: str
async def search_location(query: str) -> List[LocationData]:
"""Search for locations using Open-Meteo's Geocoding API."""
params = {
"name": query,
"count": 5,
"language": "en",
"format": "json"
}
try:
async with aiohttp.ClientSession() as session:
async with session.get(GEOCODING_API_URL, params=params) as response:
response.raise_for_status()
data = await response.json()
if data.get("results"):
return [
LocationData(
name=result["name"],
latitude=result["latitude"],
longitude=result["longitude"],
country=result["country"],
timezone=result["timezone"]
)
for result in data["results"]
]
return []
except Exception:
return []
Code description:
- It calls Open-Meteo’s geocoding endpoint with the user’s query and requests for up to five matches.
- The function returns a list of
LocationData
TypedDicts, each containingname
,latitude
,longitude
,country
, andtimezone
.
Fetching Weather
Function fetch_current_weather(...)
:
async def fetch_current_weather(latitude: float, longitude: float) -> Tuple[List[WeatherData], DailyWeatherData]:
"""Fetch current weather data from Open-Meteo API."""
today = datetime.now().date()
params = {
"latitude": latitude,
"longitude": longitude,
"hourly": "temperature_2m,precipitation,wind_speed_10m,weather_code",
"daily": "sunrise,sunset",
"timezone": "auto",
"start_date": today.strftime("%Y-%m-%d"),
"end_date": today.strftime("%Y-%m-%d")
}
async with aiohttp.ClientSession() as session:
async with session.get(OPEN_METEO_CURRENT_URL, params=params) as response:
response.raise_for_status()
data = await response.json()
# Parse hourly data
weather_data = []
for i in range(len(data["hourly"]["time"])):
weather_data.append(WeatherData(
time=datetime.fromisoformat(data["hourly"]["time"][i]),
temperature=data["hourly"]["temperature_2m"][i] or 0.0,
precipitation=data["hourly"]["precipitation"][i] or 0.0,
wind_speed=data["hourly"]["wind_speed_10m"][i] or 0.0,
weather_code=data["hourly"]["weather_code"][i] or 0
))
# Parse daily data
daily_data = DailyWeatherData(
sunrise=datetime.fromisoformat(data["daily"]["sunrise"][0]),
sunset=datetime.fromisoformat(data["daily"]["sunset"][0])
)
return weather_data, daily_data
Code description:
- Fetches today’s hourly temperature, precipitation, wind speed, and weather code.
- Parses the single‐day “daily” response into
sunrise
/sunset
times.
Function fetch_weather_data(...)
:
This article is for subscribers only
To continue reading this article, just register your email and we will send you access.
Subscribe NowAlready have an account? Sign In