
Generate Report
- 1 installs
- 2 repo stars
- Updated March 15, 2026
- aiagentwithdhruv/automation
generate-report is a Claude Code skill that generates weekly Canada weather PDF reports using the free Open-Meteo API.
About
generate-report is a Claude Code skill that builds a weekly Canada weather PDF report. It fetches forecast data from the free Open-Meteo API and generates a styled multi-section PDF via bundled Python scripts. A developer uses it to produce a recurring, formatted weather summary document.
- Fetches Canada weather from the free Open-Meteo API (no API key)
- Generates a styled 12-section PDF weather report
- Covers 12 default Canadian cities across 5 regions
Generate Report by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,980 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
generate-report capabilities & compatibility
Free; Open-Meteo requires no API key
- Capabilities
- report generation · pdf generation · weather data fetch
- Works with
- weather
- Use cases
- documentation · data analysis
- Pricing
- Free
What generate-report says it does
Generate weekly weather reports for Canada using Open-Meteo API (free, no API key required) and PDF generation.
The report uses the "Orange and Black Modern Annual Report" template style.
**No API key required!** Open-Meteo is free and open-source.
npx skills add https://github.com/aiagentwithdhruv/automation --skill generate-reportAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 2 |
| Last updated | March 15, 2026 |
| Repository | aiagentwithdhruv/automation ↗ |
What it does
Fetch Canada weather data from Open-Meteo and generate a styled weekly PDF report.
Who is it for?
Producing a recurring, formatted weather report PDF from live data
When should I use this skill?
Asked to create a weather report or build a weekly Canada weather PDF
What you get
A styled multi-section PDF weather report for Canadian cities
- PDF weather report
By the numbers
- 12 default Canadian cities
- 12-section report structure
- up to 16 forecast days
Files
Canada Weekly Weather Report Generator
Goal
Generate a professional PDF weather report for Canada using real-time data from Open-Meteo API (free, no API key required). The report uses the "Orange and Black Modern Annual Report" template style.
Inputs
- Week Start Date (optional): Start date for the report period (defaults to current week)
- Cities (optional): List of Canadian cities to include (defaults to major cities)
Default Canadian Cities
The report covers these major cities by default:
- West Coast: Vancouver, Victoria
- Prairies: Calgary, Edmonton, Winnipeg
- Central: Toronto, Ottawa, Montreal
- Atlantic: Halifax, St. John's
- North: Whitehorse, Yellowknife
Scripts
All scripts are in ./scripts/:
fetch_weather.py- Fetches weather data from Open-Meteo API (no API key needed)generate_report_pdf.py- Generates the styled PDF report
Process
1. Fetch Weather Data
python3 ./scripts/fetch_weather.py --output .tmp/canada_weather.jsonOptional parameters:
--cities "Vancouver,Toronto,Montreal"- Custom city list--days 7- Number of forecast days (default: 7, max: 16)
2. Generate PDF Report
python3 ./scripts/generate_report_pdf.py \
--input .tmp/canada_weather.json \
--output .tmp/canada_weekly_weather_report.pdf \
--template ".tmp/Orange and Black Modern Annual Report.pdf"3. Review and Deliver
- Open
.tmp/canada_weekly_weather_report.pdfto verify - Upload to Google Drive or send via email if requested
Report Structure (Matching Template)
1. Cover Page: "Canada Weekly Weather Report" with date range 2. Table of Contents: Regional sections listed 3. National Overview: Summary of weather patterns across Canada 4. Regional Highlights: Key metrics (avg temp, precipitation, extremes) 5. West Coast Weather: Vancouver, Victoria details 6. Prairies Weather: Calgary, Edmonton, Winnipeg details 7. Central Canada Weather: Toronto, Ottawa, Montreal details 8. Atlantic Weather: Halifax, St. John's details 9. Northern Territories: Whitehorse, Yellowknife details 10. 7-Day Outlook: Forecast summary with trends 11. Weather Alerts: Any active warnings/advisories 12. Data Sources: Open-Meteo attribution
Output
Primary deliverable: PDF report at .tmp/canada_weekly_weather_report.pdf
The report includes:
- Current conditions for each city
- 7-day forecast with highs/lows
- Precipitation amounts
- Regional comparisons
Error Handling
- City not found: Skip city, log warning, continue
- Network error: Retry up to 3 times with backoff
- Missing data: Use "N/A" placeholders
Environment
No API key required! Open-Meteo is free and open-source.
Data source: https://open-meteo.com/
---
Schema
Inputs
| Name | Type | Required | Description |
|---|---|---|---|
week_start_date | string | No | Start date for report period (defaults to current week) |
cities | string | No | Comma-separated list of Canadian cities |
Outputs
| Name | Type | Description |
|---|---|---|
pdf_path | file_path | PDF report at .tmp/canada_weekly_weather_report.pdf |
Cost
Free (Open-Meteo API)
#!/usr/bin/env python3
"""
Fetch weather data for Canadian cities from Open-Meteo API.
No API key required - completely free and open source.
https://open-meteo.com/
"""
import argparse
import json
import sys
import time
from datetime import datetime, timedelta
from pathlib import Path
import requests
# Default Canadian cities with coordinates
CANADA_CITIES = {
# West Coast
"Vancouver": {"lat": 49.2827, "lon": -123.1207, "region": "West Coast"},
"Victoria": {"lat": 48.4284, "lon": -123.3656, "region": "West Coast"},
# Prairies
"Calgary": {"lat": 51.0447, "lon": -114.0719, "region": "Prairies"},
"Edmonton": {"lat": 53.5461, "lon": -113.4938, "region": "Prairies"},
"Winnipeg": {"lat": 49.8951, "lon": -97.1384, "region": "Prairies"},
# Central
"Toronto": {"lat": 43.6532, "lon": -79.3832, "region": "Central"},
"Ottawa": {"lat": 45.4215, "lon": -75.6972, "region": "Central"},
"Montreal": {"lat": 45.5017, "lon": -73.5673, "region": "Central"},
# Atlantic
"Halifax": {"lat": 44.6488, "lon": -63.5752, "region": "Atlantic"},
"St. John's": {"lat": 47.5615, "lon": -52.7126, "region": "Atlantic"},
# North
"Whitehorse": {"lat": 60.7212, "lon": -135.0568, "region": "North"},
"Yellowknife": {"lat": 62.4540, "lon": -114.3718, "region": "North"},
}
BASE_URL = "https://api.open-meteo.com/v1/forecast"
def fetch_weather(lat: float, lon: float, forecast_days: int = 7) -> dict:
"""Fetch weather data from Open-Meteo API."""
params = {
"latitude": lat,
"longitude": lon,
"hourly": ",".join([
"temperature_2m",
"relative_humidity_2m",
"apparent_temperature",
"precipitation_probability",
"precipitation",
"weather_code",
"wind_speed_10m",
"wind_direction_10m",
]),
"daily": ",".join([
"weather_code",
"temperature_2m_max",
"temperature_2m_min",
"apparent_temperature_max",
"apparent_temperature_min",
"precipitation_sum",
"precipitation_probability_max",
"wind_speed_10m_max",
]),
"current": ",".join([
"temperature_2m",
"relative_humidity_2m",
"apparent_temperature",
"weather_code",
"wind_speed_10m",
"wind_direction_10m",
"precipitation",
]),
"timezone": "auto",
"forecast_days": min(forecast_days, 16), # Max 16 days
}
for attempt in range(3):
try:
response = requests.get(BASE_URL, params=params, timeout=30)
response.raise_for_status()
return response.json()
except requests.RequestException as e:
if attempt < 2:
print(f" Retry {attempt + 1}/3 after error: {e}")
time.sleep(2 ** attempt) # Exponential backoff
else:
raise
return {}
def weather_code_to_description(code: int) -> str:
"""Convert WMO weather code to human-readable description."""
codes = {
0: "Clear sky",
1: "Mainly clear",
2: "Partly cloudy",
3: "Overcast",
45: "Foggy",
48: "Depositing rime fog",
51: "Light drizzle",
53: "Moderate drizzle",
55: "Dense drizzle",
56: "Light freezing drizzle",
57: "Dense freezing drizzle",
61: "Slight rain",
63: "Moderate rain",
65: "Heavy rain",
66: "Light freezing rain",
67: "Heavy freezing rain",
71: "Slight snow",
73: "Moderate snow",
75: "Heavy snow",
77: "Snow grains",
80: "Slight rain showers",
81: "Moderate rain showers",
82: "Violent rain showers",
85: "Slight snow showers",
86: "Heavy snow showers",
95: "Thunderstorm",
96: "Thunderstorm with slight hail",
99: "Thunderstorm with heavy hail",
}
return codes.get(code, "Unknown")
def process_daily_forecast(data: dict) -> list:
"""Process daily forecast data into structured format."""
daily = data.get("daily", {})
dates = daily.get("time", [])
forecasts = []
for i, date in enumerate(dates):
forecasts.append({
"date": date,
"high": daily.get("temperature_2m_max", [None])[i],
"low": daily.get("temperature_2m_min", [None])[i],
"feels_like_high": daily.get("apparent_temperature_max", [None])[i],
"feels_like_low": daily.get("apparent_temperature_min", [None])[i],
"condition": weather_code_to_description(
daily.get("weather_code", [0])[i] or 0
),
"precip_sum": daily.get("precipitation_sum", [0])[i] or 0,
"precip_chance": daily.get("precipitation_probability_max", [0])[i] or 0,
"wind_max": daily.get("wind_speed_10m_max", [0])[i] or 0,
})
return forecasts
def fetch_all_cities(cities: dict, forecast_days: int = 7) -> dict:
"""Fetch weather data for all specified cities."""
results = {
"generated_at": datetime.now().isoformat(),
"week_start": datetime.now().strftime("%Y-%m-%d"),
"week_end": (datetime.now() + timedelta(days=forecast_days - 1)).strftime("%Y-%m-%d"),
"data_source": "Open-Meteo (https://open-meteo.com/)",
"regions": {},
"cities": {},
"national_summary": {},
}
all_temps = []
for city_name, city_info in cities.items():
print(f"Fetching weather for {city_name}...")
try:
data = fetch_weather(city_info["lat"], city_info["lon"], forecast_days)
current = data.get("current", {})
city_data = {
"name": city_name,
"region": city_info["region"],
"coordinates": {"lat": city_info["lat"], "lon": city_info["lon"]},
"timezone": data.get("timezone", "Unknown"),
"current": {
"temp": current.get("temperature_2m"),
"feels_like": current.get("apparent_temperature"),
"humidity": current.get("relative_humidity_2m"),
"wind_speed": current.get("wind_speed_10m"),
"wind_direction": current.get("wind_direction_10m"),
"precipitation": current.get("precipitation"),
"condition": weather_code_to_description(
current.get("weather_code", 0) or 0
),
},
"forecast": process_daily_forecast(data),
}
results["cities"][city_name] = city_data
# Add to regional grouping
region = city_info["region"]
if region not in results["regions"]:
results["regions"][region] = []
results["regions"][region].append(city_name)
# Collect for national summary
if current.get("temperature_2m") is not None:
all_temps.append(current["temperature_2m"])
# Small delay to be respectful to the API
time.sleep(0.3)
except Exception as e:
print(f" Warning: Failed to fetch data for {city_name}: {e}")
continue
# Calculate national summary
if all_temps:
warmest_city = max(
results["cities"].items(),
key=lambda x: x[1]["current"]["temp"] or -999
)[0]
coldest_city = min(
results["cities"].items(),
key=lambda x: x[1]["current"]["temp"] if x[1]["current"]["temp"] is not None else 999
)[0]
results["national_summary"] = {
"avg_temp": round(sum(all_temps) / len(all_temps), 1),
"max_temp": round(max(all_temps), 1),
"min_temp": round(min(all_temps), 1),
"warmest_city": warmest_city,
"coldest_city": coldest_city,
"cities_covered": len(results["cities"]),
}
return results
def main():
parser = argparse.ArgumentParser(
description="Fetch Canadian weather data from Open-Meteo (free, no API key)"
)
parser.add_argument("--output", "-o", default=".tmp/canada_weather.json",
help="Output JSON file path")
parser.add_argument("--cities", "-c", type=str, default=None,
help="Comma-separated list of cities (default: all major cities)")
parser.add_argument("--days", "-d", type=int, default=7,
help="Number of forecast days (default: 7, max: 16)")
args = parser.parse_args()
# Filter cities if specified
if args.cities:
city_list = [c.strip() for c in args.cities.split(",")]
cities = {k: v for k, v in CANADA_CITIES.items() if k in city_list}
if not cities:
print(f"Error: No valid cities found in: {args.cities}")
print(f"Available cities: {', '.join(CANADA_CITIES.keys())}")
sys.exit(1)
else:
cities = CANADA_CITIES
print(f"Fetching weather data for {len(cities)} Canadian cities...")
print(f"Data source: Open-Meteo (free, no API key required)\n")
# Fetch all weather data
weather_data = fetch_all_cities(cities, args.days)
# Ensure output directory exists
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
# Save to JSON
with open(output_path, "w") as f:
json.dump(weather_data, f, indent=2)
print(f"\nWeather data saved to: {output_path}")
print(f"Cities covered: {weather_data['national_summary'].get('cities_covered', 0)}")
print(f"Week: {weather_data['week_start']} to {weather_data['week_end']}")
if weather_data["national_summary"]:
print(f"\nNational Summary:")
print(f" Average temp: {weather_data['national_summary']['avg_temp']}C")
print(f" Warmest: {weather_data['national_summary']['warmest_city']}")
print(f" Coldest: {weather_data['national_summary']['coldest_city']}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Generate a styled PDF weather report for Canada.
Uses the Orange and Black Modern Annual Report template style.
"""
import argparse
import json
from datetime import datetime
from pathlib import Path
from reportlab.lib import colors
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import inch
from reportlab.pdfgen import canvas
from reportlab.platypus import (
BaseDocTemplate,
Frame,
PageTemplate,
Paragraph,
Spacer,
Table,
TableStyle,
)
# Template colors (matching Orange and Black Modern theme)
ORANGE = colors.Color(0.85, 0.35, 0.15) # #D95926
BLACK = colors.Color(0.1, 0.1, 0.1) # #1A1A1A
CREAM = colors.Color(0.95, 0.93, 0.90) # #F2EDE6
WHITE = colors.white
def get_weather_icon(condition: str) -> str:
"""Map weather condition to text symbol."""
condition_lower = condition.lower()
if "clear" in condition_lower or "sun" in condition_lower:
return "Sun"
elif "cloud" in condition_lower:
return "Cloud"
elif "rain" in condition_lower:
return "Rain"
elif "snow" in condition_lower:
return "Snow"
elif "thunder" in condition_lower:
return "Storm"
elif "fog" in condition_lower or "mist" in condition_lower:
return "Fog"
else:
return "Mix"
class WeatherReportPDF:
"""Generate a styled weather report PDF."""
def __init__(self, output_path: str):
self.output_path = output_path
self.width, self.height = letter
self.c = canvas.Canvas(output_path, pagesize=letter)
self.page_num = 0
def draw_header(self, title: str, subtitle: str = ""):
"""Draw page header with orange bar."""
# Orange header bar
self.c.setFillColor(ORANGE)
self.c.rect(0, self.height - 80, self.width, 80, fill=1, stroke=0)
# Title text
self.c.setFillColor(WHITE)
self.c.setFont("Helvetica-Bold", 24)
self.c.drawString(0.75 * inch, self.height - 50, title)
if subtitle:
self.c.setFont("Helvetica", 12)
self.c.drawString(0.75 * inch, self.height - 70, subtitle)
def draw_footer(self, date_str: str):
"""Draw page footer."""
self.c.setFillColor(BLACK)
self.c.rect(0, 0, self.width, 40, fill=1, stroke=0)
self.c.setFillColor(WHITE)
self.c.setFont("Helvetica", 9)
self.c.drawString(0.75 * inch, 15, f"Canada Weekly Weather Report | {date_str}")
self.c.drawRightString(self.width - 0.75 * inch, 15, f"Page {self.page_num}")
def create_cover_page(self, data: dict):
"""Create the cover page."""
self.page_num = 1
# Black background
self.c.setFillColor(BLACK)
self.c.rect(0, 0, self.width, self.height, fill=1, stroke=0)
# Orange section at top
self.c.setFillColor(ORANGE)
self.c.rect(0, self.height - 400, self.width, 400, fill=1, stroke=0)
# Title
self.c.setFillColor(CREAM)
self.c.setFont("Helvetica-Bold", 48)
self.c.drawString(0.75 * inch, self.height - 180, "Canada")
self.c.drawString(0.75 * inch, self.height - 240, "Weekly")
self.c.drawString(0.75 * inch, self.height - 300, "Weather")
# Date range
self.c.setFont("Helvetica", 14)
self.c.drawString(0.75 * inch, self.height - 380,
f"{data['week_start']} to {data['week_end']}")
# Big year
self.c.setFillColor(CREAM)
self.c.setFont("Helvetica-Bold", 72)
year = datetime.now().strftime("%Y")
self.c.drawCentredString(self.width / 2, 150, year)
# Prepared info
self.c.setFont("Helvetica", 11)
self.c.setFillColor(CREAM)
self.c.drawString(0.75 * inch, 80, "Generated by:")
self.c.setFont("Helvetica-Bold", 14)
self.c.drawString(0.75 * inch, 60, "Weather Report Skill")
self.c.drawString(4 * inch, 80, "Data source:")
self.c.setFont("Helvetica-Bold", 14)
self.c.drawString(4 * inch, 60, "OpenWeatherMap")
self.c.showPage()
def create_toc_page(self, regions: list):
"""Create table of contents page."""
self.page_num = 2
# Black background
self.c.setFillColor(BLACK)
self.c.rect(0, 0, self.width, self.height, fill=1, stroke=0)
# Title
self.c.setFillColor(CREAM)
self.c.setFont("Helvetica-Bold", 36)
self.c.drawString(0.75 * inch, self.height - 100, "Table of Contents")
# Orange underline
self.c.setStrokeColor(ORANGE)
self.c.setLineWidth(3)
self.c.line(0.75 * inch, self.height - 120, 4 * inch, self.height - 120)
# TOC items
items = [
("01", "National Overview"),
("02", "Weather Highlights"),
("03", "West Coast"),
("04", "Prairies"),
("05", "Central Canada"),
("06", "Atlantic Canada"),
("07", "Northern Territories"),
("08", "7-Day Outlook"),
]
y_pos = self.height - 200
for i, (num, title) in enumerate(items):
col = 0 if i < 4 else 1
row = i % 4
x_base = 0.75 * inch if col == 0 else 4.5 * inch
y = y_pos - (row * 100)
# Number in orange circle
self.c.setFillColor(ORANGE)
self.c.circle(x_base + 15, y + 5, 15, fill=1, stroke=0)
self.c.setFillColor(WHITE)
self.c.setFont("Helvetica-Bold", 12)
self.c.drawCentredString(x_base + 15, y, num)
# Title
self.c.setFillColor(CREAM)
self.c.setFont("Helvetica", 16)
self.c.drawString(x_base + 45, y, title)
self.c.showPage()
def create_overview_page(self, data: dict):
"""Create national overview page."""
self.page_num = 3
# Cream background
self.c.setFillColor(CREAM)
self.c.rect(0, 0, self.width, self.height, fill=1, stroke=0)
self.draw_header("National Overview", data['week_start'])
summary = data.get("national_summary", {})
# Main stats section - black box
self.c.setFillColor(BLACK)
self.c.rect(0, self.height - 350, self.width, 200, fill=1, stroke=0)
# Stats
stats = [
(f"{summary.get('avg_temp', 'N/A')}C", "National Avg Temp"),
(f"{summary.get('max_temp', 'N/A')}C", "Highest Temp"),
(f"{summary.get('min_temp', 'N/A')}C", "Lowest Temp"),
]
x_positions = [1.5 * inch, 4 * inch, 6.5 * inch]
for i, (value, label) in enumerate(stats):
self.c.setFillColor(CREAM)
self.c.setFont("Helvetica-Bold", 36)
self.c.drawCentredString(x_positions[i], self.height - 220, str(value))
self.c.setFont("Helvetica", 11)
self.c.drawCentredString(x_positions[i], self.height - 250, label)
# Orange accent line
self.c.setFillColor(ORANGE)
self.c.rect(0, self.height - 360, self.width, 10, fill=1, stroke=0)
# City highlights
y_pos = self.height - 420
self.c.setFillColor(BLACK)
self.c.setFont("Helvetica-Bold", 18)
self.c.drawString(0.75 * inch, y_pos, "City Highlights")
warmest = summary.get("warmest_city", "N/A")
coldest = summary.get("coldest_city", "N/A")
y_pos -= 40
self.c.setFont("Helvetica", 12)
self.c.drawString(0.75 * inch, y_pos, f"Warmest City: {warmest}")
y_pos -= 25
self.c.drawString(0.75 * inch, y_pos, f"Coldest City: {coldest}")
y_pos -= 25
self.c.drawString(0.75 * inch, y_pos,
f"Cities Covered: {summary.get('cities_covered', 0)}")
self.draw_footer(data['week_start'])
self.c.showPage()
def create_region_page(self, data: dict, region: str, cities: list):
"""Create a regional weather page."""
self.page_num += 1
# Cream top half
self.c.setFillColor(CREAM)
self.c.rect(0, self.height / 2, self.width, self.height / 2, fill=1, stroke=0)
# Black bottom half
self.c.setFillColor(BLACK)
self.c.rect(0, 0, self.width, self.height / 2, fill=1, stroke=0)
self.draw_header(f"{region} Weather", data['week_start'])
# City data
y_pos = self.height - 150
for city_name in cities:
city_data = data["cities"].get(city_name, {})
if not city_data:
continue
current = city_data.get("current", {})
# City name
self.c.setFillColor(BLACK)
self.c.setFont("Helvetica-Bold", 16)
self.c.drawString(0.75 * inch, y_pos, city_name)
# Current temp
self.c.setFillColor(ORANGE)
self.c.setFont("Helvetica-Bold", 28)
self.c.drawString(4 * inch, y_pos, f"{current.get('temp', 'N/A')}C")
# Condition
self.c.setFillColor(BLACK)
self.c.setFont("Helvetica", 11)
self.c.drawString(0.75 * inch, y_pos - 20,
current.get("condition", "Unknown").capitalize())
# Additional stats
self.c.drawString(4 * inch, y_pos - 20,
f"Feels like: {current.get('feels_like', 'N/A')}C")
self.c.drawString(5.5 * inch, y_pos - 20,
f"Humidity: {current.get('humidity', 'N/A')}%")
y_pos -= 70
# Forecast section in black area
y_pos = self.height / 2 - 50
self.c.setFillColor(CREAM)
self.c.setFont("Helvetica-Bold", 14)
self.c.drawString(0.75 * inch, y_pos, "7-Day Forecast")
y_pos -= 30
# Show forecast for first city in region
if cities and cities[0] in data["cities"]:
forecast = data["cities"][cities[0]].get("forecast", [])[:5]
for day in forecast:
self.c.setFillColor(CREAM)
self.c.setFont("Helvetica", 10)
date_str = day.get("date", "")[-5:] # MM-DD
self.c.drawString(0.75 * inch, y_pos, date_str)
self.c.setFont("Helvetica-Bold", 12)
self.c.drawString(1.5 * inch, y_pos,
f"{day.get('high', 'N/A')}C / {day.get('low', 'N/A')}C")
self.c.setFont("Helvetica", 10)
self.c.drawString(3.5 * inch, y_pos,
day.get("condition", "Unknown")[:20])
self.c.drawString(5.5 * inch, y_pos,
f"Precip: {day.get('precip_chance', 0)}%")
y_pos -= 25
self.draw_footer(data['week_start'])
self.c.showPage()
def create_outlook_page(self, data: dict):
"""Create 7-day outlook summary page."""
self.page_num += 1
# Black background
self.c.setFillColor(BLACK)
self.c.rect(0, 0, self.width, self.height, fill=1, stroke=0)
self.draw_header("7-Day Outlook", data['week_start'])
# Orange section
self.c.setFillColor(ORANGE)
self.c.rect(self.width / 2, self.height - 400, self.width / 2, 250, fill=1, stroke=0)
# Left side - forecast summary
y_pos = self.height - 150
self.c.setFillColor(CREAM)
self.c.setFont("Helvetica-Bold", 18)
self.c.drawString(0.75 * inch, y_pos, "Forecast Summary")
y_pos -= 40
self.c.setFont("Helvetica", 12)
# Aggregate forecast data
all_temps = []
all_precip = []
for city_name, city_data in data.get("cities", {}).items():
forecast = city_data.get("forecast", [])
for day in forecast:
all_temps.extend([day.get("high", 0), day.get("low", 0)])
all_precip.append(day.get("precip_chance", 0))
if all_temps:
avg_temp = sum(all_temps) / len(all_temps)
max_temp = max(all_temps)
min_temp = min(all_temps)
avg_precip = sum(all_precip) / len(all_precip) if all_precip else 0
self.c.drawString(0.75 * inch, y_pos, f"Expected High: {max_temp:.1f}C")
y_pos -= 25
self.c.drawString(0.75 * inch, y_pos, f"Expected Low: {min_temp:.1f}C")
y_pos -= 25
self.c.drawString(0.75 * inch, y_pos, f"Average Temp: {avg_temp:.1f}C")
y_pos -= 25
self.c.drawString(0.75 * inch, y_pos, f"Avg Precip Chance: {avg_precip:.0f}%")
# Right side (orange section)
self.c.setFillColor(BLACK)
self.c.setFont("Helvetica-Bold", 16)
self.c.drawString(4.5 * inch, self.height - 200, "Week Ahead")
self.c.setFont("Helvetica", 11)
self.c.drawString(4.5 * inch, self.height - 230,
"Conditions vary across")
self.c.drawString(4.5 * inch, self.height - 250,
"Canada's diverse regions.")
self.c.drawString(4.5 * inch, self.height - 280,
"Check local forecasts")
self.c.drawString(4.5 * inch, self.height - 300,
"for detailed updates.")
self.draw_footer(data['week_start'])
self.c.showPage()
def create_contact_page(self):
"""Create final contact/attribution page."""
self.page_num += 1
# Black background
self.c.setFillColor(BLACK)
self.c.rect(0, 0, self.width, self.height, fill=1, stroke=0)
# Title
self.c.setFillColor(CREAM)
self.c.setFont("Helvetica-Bold", 48)
self.c.drawString(0.75 * inch, self.height - 200, "Data")
self.c.drawString(0.75 * inch, self.height - 260, "Sources")
# Orange underline
self.c.setFillColor(ORANGE)
self.c.rect(0.75 * inch, self.height - 280, 3 * inch, 4, fill=1, stroke=0)
# Attribution
y_pos = self.height - 350
self.c.setFillColor(ORANGE)
self.c.setFont("Helvetica-Bold", 14)
self.c.drawString(0.75 * inch, y_pos, "Weather Data")
self.c.setFillColor(CREAM)
self.c.setFont("Helvetica", 12)
y_pos -= 25
self.c.drawString(0.75 * inch, y_pos, "OpenWeatherMap API")
y_pos -= 20
self.c.drawString(0.75 * inch, y_pos, "openweathermap.org")
y_pos -= 50
self.c.setFillColor(ORANGE)
self.c.setFont("Helvetica-Bold", 14)
self.c.drawString(0.75 * inch, y_pos, "Report Generated By")
self.c.setFillColor(CREAM)
self.c.setFont("Helvetica", 12)
y_pos -= 25
self.c.drawString(0.75 * inch, y_pos, "Claude Skills - generate-report")
y_pos -= 20
self.c.drawString(0.75 * inch, y_pos,
f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
self.c.showPage()
def generate(self, data: dict):
"""Generate the complete PDF report."""
# Cover page
self.create_cover_page(data)
# Table of contents
self.create_toc_page(list(data.get("regions", {}).keys()))
# National overview
self.create_overview_page(data)
# Regional pages
region_order = ["West Coast", "Prairies", "Central", "Atlantic", "North"]
for region in region_order:
cities = data.get("regions", {}).get(region, [])
if cities:
self.create_region_page(data, region, cities)
# 7-day outlook
self.create_outlook_page(data)
# Contact/attribution page
self.create_contact_page()
# Save
self.c.save()
print(f"PDF report saved to: {self.output_path}")
def main():
parser = argparse.ArgumentParser(description="Generate Canada weather PDF report")
parser.add_argument("--input", "-i", default=".tmp/canada_weather.json",
help="Input JSON weather data file")
parser.add_argument("--output", "-o", default=".tmp/canada_weekly_weather_report.pdf",
help="Output PDF file path")
parser.add_argument("--template", "-t",
default=".tmp/Orange and Black Modern Annual Report.pdf",
help="Template PDF for style reference (not used directly)")
args = parser.parse_args()
# Load weather data
input_path = Path(args.input)
if not input_path.exists():
print(f"Error: Input file not found: {args.input}")
print("Run fetch_weather.py first to get weather data.")
return 1
with open(input_path) as f:
weather_data = json.load(f)
print(f"Loaded weather data for {len(weather_data.get('cities', {}))} cities")
print(f"Week: {weather_data.get('week_start')} to {weather_data.get('week_end')}")
# Ensure output directory exists
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
# Generate PDF
pdf = WeatherReportPDF(str(output_path))
pdf.generate(weather_data)
print(f"\nReport generated successfully!")
print(f"Output: {output_path}")
return 0
if __name__ == "__main__":
exit(main())