Weather Forecast Integration - Design Document¶
Executive Summary¶
This document describes the design and implementation of automatic weather forecast integration for the drone operations management system. The feature aggregates forecast data from three external weather APIs (Open-Meteo, OpenWeatherMap, Met Office DataPoint) to provide reliable weather information during flight project creation and viewing.
Business Value¶
- Enhanced Safety: Pre-flight weather assessment for risk analysis
- Operational Efficiency: Automatic forecast lookup eliminates manual research
- Data Reliability: Multi-source aggregation reduces single-point-of-failure risk
- User Experience: Cached forecasts (24 hours) provide fast page loads
- Compliance Support: Weather documentation for post-flight reporting
Key Capabilities¶
- Multi-Source Aggregation: Combines data from 3 APIs with averaging algorithm
- Graceful Degradation: Functions with 1-2 API failures (minimum 1 source required)
- Intelligent Caching: 24-hour cache stored in database, automatic refetch on expiry
- UK-Specific Optimization: Met Office API only called for UK locations
- Date Range Validation: Warns users if flight date >16 days (beyond reliable forecast range)
Architecture Overview¶
High-Level System Diagram¶
┌─────────────────────────────────────────────────────────────────────────┐
│ User Journey │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ [Step 1: Location] ──> [Step 2: Details + Submit] ──> [Forecast Fetch] │
│ │ │ │
│ │ ▼ │
│ │ ┌──────────────────┐ │
│ │ │ WeatherAPI │ │
│ │ │ .get_forecast() │ │
│ │ └────────┬─────────┘ │
│ │ │ │
│ │ ┌──────────────────┴────────┐ │
│ │ │ Parallel API Calls │ │
│ │ │ (max 10s timeout each) │ │
│ │ └──┬────────┬──────────┬───┘ │
│ │ │ │ │ │
│ │ ▼ ▼ ▼ │
│ │ Open-Meteo OWM Met Office │
│ │ (always) (if key) (if UK+key) │
│ │ │ │ │ │
│ │ └────────┴──────────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌──────────────┐ │
│ │ │ Aggregation │ │
│ │ │ (averaging) │ │
│ │ └──────┬───────┘ │
│ │ │ │
│ ▼ ▼ │
│ [Step 3: Viability] [Session Storage] │
│ │ │ │
│ ▼ ▼ │
│ [Step 4: Toggles] ┌───────────────┐ │
│ │ │ Database Save │ │
│ │ │ (Project. │ │
│ │ │ weather_ │ │
│ │ │ forecast) │ │
│ │ └───────┬───────┘ │
│ ▼ │ │
│ [Dashboard] ──────────> [Project View] │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │Cache Valid? │ │
│ └──────┬───────┘ │
│ │ │
│ No ───────┤ │
│ │ │
│ ▼ │
│ [Refetch & Update] │
│ │
└──────────────────────────────────────────────────────────────────────────┘
Component Interaction Flow¶
┌──────────────────┐
│ routes_new_ │
│ project.py │
│ │
│ new_project_ │
│ details() │
└────────┬─────────┘
│ (on form submit)
│
▼
┌─────────────────────────────────────────────────────────────┐
│ app/utils/weather_api.py │
├─────────────────────────────────────────────────────────────┤
│ │
│ WeatherAPI.get_forecast(lat, lon, date) │
│ │ │
│ ├──> 1. Validate date (0-16 days) │
│ │ │
│ ├──> 2. Parallel fetch from APIs │
│ │ ├─> _fetch_open_meteo() [Always] │
│ │ ├─> _fetch_openweathermap() [If API key] │
│ │ └─> _fetch_met_office() [If UK + API key] │
│ │ │
│ ├──> 3. _aggregate_forecasts([...]) │
│ │ - Average numeric values │
│ │ - Max precipitation probability │
│ │ - Majority vote for conditions │
│ │ │
│ └──> 4. Return standardized response │
│ { │
│ 'success': True, │
│ 'forecast_data': {...}, │
│ 'metadata': { │
│ 'sources_used': ['open-meteo', 'owm'], │
│ 'sources_failed': ['met-office'], │
│ 'cache_valid_until': '...' │
│ } │
│ } │
│ │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────┐
│ Session Storage │
│ weather_forecast │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ routes_new_ │
│ project.py │
│ │
│ new_project_ │
│ toggles() │
│ │
│ → Database Save │
└──────────────────┘
Aggregation Strategy¶
Why Aggregate All 3 Sources?¶
The system fetches from all available APIs simultaneously (not as fallbacks) for three key reasons:
- Reliability Through Redundancy
- If Open-Meteo experiences downtime → Still have OWM + Met Office
- If OWM rate limits exceed → Still have Open-Meteo + Met Office
-
Single-source dependency creates fragility
-
Accuracy Through Averaging
- Different weather models have different biases
- Averaging reduces single-source prediction errors
-
Statistical studies show ensemble forecasts outperform single models
-
Cross-Validation
- Large discrepancies between sources = high uncertainty
- Similar forecasts = high confidence
- Future enhancement: Calculate variance to show confidence level
Aggregation Algorithm¶
def _aggregate_forecasts(forecasts: List[Dict]) -> Dict:
"""
Aggregate strategy:
- Numeric fields: Simple average
- Precipitation probability: Highest value (conservative)
- Conditions: Majority vote
"""
# Example with 2 sources:
# Source 1: temp_avg=10.0°C, wind=15 km/h, precip_prob=30%
# Source 2: temp_avg=12.0°C, wind=18 km/h, precip_prob=45%
#
# Result:
# temp_avg = (10.0 + 12.0) / 2 = 11.0°C
# wind = (15 + 18) / 2 = 16.5 km/h
# precip_prob = max(30, 45) = 45% (conservative)
Field-Specific Aggregation Rules¶
| Field | Aggregation Method | Rationale |
|---|---|---|
temperature_avg_c |
Mean | Temperature predictions have symmetric error distribution |
temperature_min_c |
Mean | Daily minimums reasonably averaged |
temperature_max_c |
Mean | Daily maximums reasonably averaged |
precipitation_mm |
Mean | Rainfall amounts averaged for best estimate |
precipitation_probability |
Maximum | Conservative approach for flight safety |
wind_speed_kmh |
Mean | Average wind speed for typical conditions |
wind_gusts_kmh |
Mean | Gust predictions averaged (conservative: max would be better) |
conditions |
Majority Vote | Most common weather description (Clear/Rain/Snow/etc.) |
Graceful Degradation Matrix¶
| Sources Responding | Behaviour | Confidence Level |
|---|---|---|
| 3 sources | Full aggregation (average all 3) | High |
| 2 sources | Average of 2 sources | Medium |
| 1 source | Use single source (no averaging) | Low |
| 0 sources | Return error, show warning | None |
Source Tracking:
- sources_used: ['open-meteo', 'openweathermap'] → User sees "2 sources"
- sources_failed: ['met-office'] → Logged for debugging
- System continues functioning as long as ≥1 source succeeds
Example Scenarios:
# Scenario 1: All 3 APIs succeed
{
'success': True,
'forecast_data': {...}, # Averaged from 3 sources
'metadata': {
'sources_used': ['open-meteo', 'openweathermap', 'met-office'],
'sources_failed': []
}
}
# Scenario 2: Met Office unavailable (common for non-UK locations)
{
'success': True,
'forecast_data': {...}, # Averaged from 2 sources
'metadata': {
'sources_used': ['open-meteo', 'openweathermap'],
'sources_failed': ['met-office']
}
}
# Scenario 3: Only Open-Meteo succeeds
{
'success': True,
'forecast_data': {...}, # Single source, no averaging
'metadata': {
'sources_used': ['open-meteo'],
'sources_failed': ['openweathermap', 'met-office']
}
}
# Scenario 4: All APIs fail
{
'success': False,
'error': 'All weather APIs failed to return data',
'metadata': {
'sources_used': [],
'sources_failed': ['open-meteo', 'openweathermap', 'met-office']
}
}
Component Details¶
WeatherAPI Class (app/utils/weather_api.py)¶
Design Pattern: Class-based with static methods (following ScottishGovAPI pattern)
class WeatherAPI:
# Constants
OPEN_METEO_URL = "..."
OPENWEATHERMAP_URL = "..."
API_TIMEOUT = 10 # seconds
MAX_FORECAST_DAYS = 16
CACHE_HOURS = 24
UK_BOUNDS = {'lat_min': 49.9, 'lat_max': 60.9, 'lon_min': -8.2, 'lon_max': 1.8}
@classmethod
def get_forecast(cls, lat, lon, date) -> Dict:
"""Main entry point - orchestrates fetch + aggregation"""
@classmethod
def _fetch_open_meteo(cls, lat, lon, date) -> Optional[Dict]:
"""Fetch from Open-Meteo (always called)"""
@classmethod
def _fetch_openweathermap(cls, lat, lon, date) -> Optional[Dict]:
"""Fetch from OpenWeatherMap (only if API key present)"""
@classmethod
def _fetch_met_office(cls, lat, lon, date) -> Optional[Dict]:
"""Fetch from Met Office (only if UK location + API key)"""
@classmethod
def _aggregate_forecasts(cls, forecasts: List[Dict]) -> Dict:
"""Combine forecasts using averaging algorithm"""
@classmethod
def is_cache_valid(cls, weather_forecast_json) -> bool:
"""Check if cached data is <24 hours old"""
@classmethod
def _map_wmo_code_to_conditions(cls, code: int) -> str:
"""Convert WMO weather code (0-99) to human-readable string"""
@classmethod
def _is_uk_location(cls, lat, lon) -> bool:
"""Check if coordinates within UK bounding box"""
Individual API Fetch Methods¶
Common Pattern: 1. Check for API key (if required) 2. Build request parameters 3. Make HTTP request with 10s timeout 4. Parse response JSON 5. Extract/transform fields to standard format 6. Return standardized dict or None
Error Handling:
- requests.exceptions.Timeout → Log, return None
- requests.exceptions.RequestException → Log HTTP error, return None
- KeyError/IndexError/ValueError → Log parsing error, return None
Standardized Output Format:
{
'temperature_avg_c': 12.5,
'temperature_min_c': 8.0,
'temperature_max_c': 17.0,
'precipitation_mm': 2.3,
'precipitation_probability': 45, # 0-100 (%)
'wind_speed_kmh': 18.5,
'wind_gusts_kmh': 32.0,
'conditions': 'Partly Cloudy' # Human-readable string
}
Aggregation Method¶
def _aggregate_forecasts(cls, forecasts: List[Dict]) -> Dict:
if len(forecasts) == 1:
return forecasts[0] # No aggregation needed
aggregated = {}
# Average temperature fields
for field in ['temperature_avg_c', 'temperature_min_c', 'temperature_max_c']:
values = [f.get(field) for f in forecasts if f.get(field) is not None]
if values:
aggregated[field] = round(mean(values), 1)
# Average wind fields
for field in ['wind_speed_kmh', 'wind_gusts_kmh']:
values = [f.get(field) for f in forecasts if f.get(field) is not None]
if values:
aggregated[field] = round(mean(values), 1)
# Average precipitation amount
precip_values = [f.get('precipitation_mm') for f in forecasts if f.get('precipitation_mm') is not None]
if precip_values:
aggregated['precipitation_mm'] = round(mean(precip_values), 1)
# Conservative: use highest precipitation probability
prob_values = [f.get('precipitation_probability') for f in forecasts if f.get('precipitation_probability') is not None]
if prob_values:
aggregated['precipitation_probability'] = max(prob_values)
# Majority vote for conditions
conditions = [f.get('conditions') for f in forecasts if f.get('conditions')]
if conditions:
from collections import Counter
aggregated['conditions'] = Counter(conditions).most_common(1)[0][0]
return aggregated
Cache Validation¶
def is_cache_valid(cls, weather_forecast_json: Optional[Dict]) -> bool:
"""
Check cache validity based on cache_valid_until timestamp
Returns:
True: Cache is valid, use cached data
False: Cache expired/missing, refetch needed
"""
if not weather_forecast_json:
return False
cache_valid_until = weather_forecast_json.get('metadata', {}).get('cache_valid_until')
if not cache_valid_until:
return False
expiry_time = datetime.fromisoformat(cache_valid_until)
now = datetime.now(timezone.utc)
return now < expiry_time # Cache valid if current time before expiry
Caching Strategy¶
Rationale for 24-Hour Cache¶
| Factor | Reasoning |
|---|---|
| Forecast Stability | Weather forecasts 3+ days out change slowly (<10% variation/24h) |
| API Cost Savings | Reduces API calls by ~96% (single fetch vs. per-view fetch) |
| Page Load Speed | Cached data loads instantly vs. 10s API roundtrip |
| Rate Limit Protection | Prevents exceeding free-tier limits during heavy usage |
Cache Storage¶
Location: Database column Project.weather_forecast (JSON type)
Schema:
{
"forecast_data": {
"temperature_avg_c": 12.5,
"temperature_min_c": 8.0,
"temperature_max_c": 17.0,
"precipitation_mm": 2.3,
"precipitation_probability": 45,
"wind_speed_kmh": 18.5,
"wind_gusts_kmh": 32.0,
"conditions": "Partly Cloudy"
},
"metadata": {
"fetched_at": "2025-01-10T14:30:00+00:00",
"sources_used": ["open-meteo", "openweathermap"],
"sources_failed": ["met-office"],
"days_until_flight": 5,
"too_far_in_future": false,
"cache_valid_until": "2025-01-11T14:30:00+00:00"
}
}
Key Metadata Fields:
- fetched_at: ISO 8601 timestamp (UTC) when forecast was fetched
- cache_valid_until: ISO 8601 timestamp = fetched_at + 24 hours
- sources_used: List of APIs that successfully returned data
- sources_failed: List of APIs that failed/skipped (for debugging)
- days_until_flight: How many days until flight (for UI messaging)
- too_far_in_future: Boolean flag if flight >16 days away
Cache Validation Algorithm¶
When: Every time project() route is accessed (project view page)
Logic:
if not WeatherAPI.is_cache_valid(project.weather_forecast):
# Cache expired/missing
weather_data = WeatherAPI.get_forecast(
project.latitude,
project.longitude,
project.dateOfFlight.strftime('%Y-%m-%d')
)
if weather_data.get('success'):
project.weather_forecast = weather_data
db.session.commit()
Edge Cases:
- Cache missing (weather_forecast = null) → Refetch
- Cache malformed (missing cache_valid_until) → Refetch
- Datetime parsing error → Treat as invalid, refetch
- Flight date in past (user viewing old project) → Skip refetch (no point)
Cache Performance Impact¶
Estimated API Call Volume:
Without caching:
100 users × 10 project views/day = 1,000 API calls/day
× 3 APIs = 3,000 total API calls/day
With 24-hour caching:
100 users × 5 project creations/week ÷ 7 days = ~71 API calls/day
× 3 APIs = 213 total API calls/day
Savings: ~92% reduction in API calls
Data Flow Diagrams¶
Project Creation Flow (Fetch)¶
┌─────────────────────────────────────────────────────────────────┐
│ User Journey: New Project Creation │
└─────────────────────────────────────────────────────────────────┘
[User fills form]
├─> Location (lat/lon) → Session: 'latitude', 'longitude'
├─> Project Type → Session: 'projectType'
└─> Date of Flight → Session: 'projectDateOfFlight'
↓
[User clicks "Next" on Details page]
↓
┌────────────────────────────────────────┐
│ routes_new_project.py │
│ new_project_details() - POST handler │
└────────────────────────────────────────┘
│
│ form.validate_on_submit() == True
│
├─> session['projectTitle'] = ...
├─> session['projectDateOfFlight'] = ...
│
├─> WeatherAPI.get_forecast(
│ session['latitude'],
│ session['longitude'],
│ session['projectDateOfFlight']
│ )
│
│ ↓ [Parallel API calls]
│
│ ┌──────────────────────────────────┐
│ │ Open-Meteo OpenWeatherMap │
│ │ ✓ ✓ Met Office (skipped: non-UK)
│ └──────────────────────────────────┘
│ ↓
│ [Aggregation]
│ ↓
│ return {
│ 'success': True,
│ 'forecast_data': {...},
│ 'metadata': {...}
│ }
│
└─> session['weather_forecast'] = weather_data
↓
[Redirect to /create_project/viability]
↓
[User completes viability study]
↓
[User clicks "Create Project" on Toggles page]
↓
┌────────────────────────────────────────┐
│ routes_new_project.py │
│ new_project_toggles() - POST handler │
└────────────────────────────────────────┘
│
│ project = Project(
│ ...,
│ weather_forecast=session.get('weather_forecast')
│ )
│
├─> db.session.add(project)
├─> db.session.commit()
│
└─> session.pop('weather_forecast', None)
↓
[Project saved to database with weather forecast]
Cache Validation Flow (Refetch)¶
┌─────────────────────────────────────────────────────────────────┐
│ User Journey: View Existing Project │
└─────────────────────────────────────────────────────────────────┘
[User clicks project from dashboard]
↓
┌────────────────────────────────────────┐
│ routes_dashboard.py │
│ project(project_id) - GET handler │
└────────────────────────────────────────┘
│
│ project = Project.query.get_or_404(project_id)
│
│ if not WeatherAPI.is_cache_valid(project.weather_forecast):
│ ↓
│ ┌────────────────────────────────────────────┐
│ │ Cache Invalid (>24 hours old OR missing) │
│ └────────────────────────────────────────────┘
│ ↓
│ weather_data = WeatherAPI.get_forecast(...)
│ ↓
│ project.weather_forecast = weather_data
│ db.session.commit()
│
│ else:
│ ┌────────────────────────────────────────────┐
│ │ Cache Valid (<24 hours old) │
│ └────────────────────────────────────────────┘
│ Use cached project.weather_forecast
│
└─> render_template(..., project=project)
↓
[Template displays weather forecast card]
API Aggregation Flow (Parallel)¶
WeatherAPI.get_forecast(lat, lon, date)
↓
┌────────────────────────────────────────────────────┐
│ Date Validation │
│ - If date < today → Error: "Flight date in past" │
│ - If date > today+16 → Set too_far_in_future=True │
└────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────┐
│ Parallel API Calls (NOT sequential fallbacks!) │
│ │
│ Thread 1: _fetch_open_meteo() │
│ Thread 2: _fetch_openweathermap() │
│ Thread 3: _fetch_met_office() │
│ │
│ Each has 10s timeout, failures return None │
└────────────────────────────────────────────────────┘
↓
↓ [Collect results]
↓
forecasts = [
{...}, # Open-Meteo data
{...}, # OpenWeatherMap data
None # Met Office failed
]
forecasts_filtered = [f for f in forecasts if f is not None]
↓
if len(forecasts_filtered) == 0:
return {'success': False, 'error': 'All weather APIs failed'}
↓
aggregated_forecast = _aggregate_forecasts(forecasts_filtered)
↓
↓ [Averaging algorithm]
↓
return {
'success': True,
'forecast_data': aggregated_forecast,
'metadata': {
'fetched_at': '2025-01-10T14:30:00Z',
'sources_used': ['open-meteo', 'openweathermap'],
'sources_failed': ['met-office'],
'cache_valid_until': '2025-01-11T14:30:00Z'
}
}
Database Design¶
Schema Changes¶
Migration: 0951d93a4956_add_weather_forecast_json_field_to_project.py
# Upgrade
with op.batch_alter_table('project', schema=None) as batch_op:
batch_op.add_column(sa.Column('weather_forecast', sa.JSON(), nullable=True))
# Downgrade
with op.batch_alter_table('project', schema=None) as batch_op:
batch_op.drop_column('weather_forecast')
Table: project
| Column | Type | Nullable | Description |
|---|---|---|---|
| id | INTEGER | No | Primary key |
| ... | ... | ... | Existing columns |
| weather_forecast | JSON | Yes | Cached weather forecast data |
JSON Structure (see "Caching Strategy" section for full schema)
Query Patterns¶
Create Project (Insert):
project = Project(
...,
weather_forecast=session.get('weather_forecast')
)
db.session.add(project)
db.session.commit()
View Project (Read + Conditional Update):
project = Project.query.get_or_404(project_id)
if not WeatherAPI.is_cache_valid(project.weather_forecast):
project.weather_forecast = WeatherAPI.get_forecast(...)
db.session.commit()
No Deletion Logic: Weather forecast never deleted, only updated or left stale
Storage Considerations¶
JSON Size Estimate: - Forecast data: ~300 bytes - Metadata: ~200 bytes - Total: ~500 bytes per project
Scale Impact: - 1,000 projects = 500 KB - 10,000 projects = 5 MB - 100,000 projects = 50 MB
Negligible storage impact - JSON column is highly efficient
Error Handling¶
Error Categories¶
- Date Validation Errors
- Past dates → Return error response
-
16 days away → Return success with
too_far_in_future: true -
API Timeout Errors
- Logged per API
- That API marked as failed
-
Continue with remaining APIs
-
HTTP Request Errors
- 401 Unauthorized (invalid API key) → Skip that API
- 429 Rate Limit → Skip that API (relies on cache to prevent hitting limits)
-
500 Server Error → Skip that API
-
Response Parsing Errors
- Malformed JSON → Skip that API
-
Missing expected fields → Skip that API
-
Complete Failure (All APIs fail)
- Return `{'success': False, 'error': '...'}
- Template shows warning message
- Project creation continues (non-blocking)
Error Handling Code Pattern¶
try:
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
# Extract fields
forecast = {...}
return forecast
except requests.exceptions.Timeout:
logger.error("API timeout")
return None
except requests.exceptions.RequestException as e:
logger.error(f"API request failed: {e}")
return None
except (KeyError, IndexError, ValueError) as e:
logger.error(f"Response parsing error: {e}")
return None
Logging Strategy¶
Levels:
- logger.info() - Successful fetches, cache updates
- logger.warning() - Individual API failures (expected in some cases)
- logger.error() - Unexpected errors, parsing failures
Examples:
logger.info("Fetching weather forecast for 2025-01-15 at (55.95, -3.19)")
logger.info("Weather forecast fetched successfully from 2 sources")
logger.warning("OpenWeatherMap: No forecast found for 2025-01-30")
logger.error("Weather cache check error: ...", exc_info=True)
Performance Considerations¶
API Call Performance¶
Parallel Execution: - All 3 APIs called simultaneously (not sequential) - Maximum wait time = slowest API timeout (10s) - NOT sum of all timeouts (30s)
Expected Response Times: - Open-Meteo: ~500-2000ms - OpenWeatherMap: ~300-1500ms - Met Office: ~500-2000ms (when implemented)
Total Request Time: Typically 1-3 seconds (with parallel execution)
Caching Impact¶
Without Caching (every page view):
User views project → 3 API calls → 1-3s load time
With 24-Hour Caching (subsequent views):
User views project → Database read → <100ms load time
Performance Gain: ~95% reduction in page load time for cached forecasts
Database Performance¶
Read Query:
SELECT weather_forecast FROM project WHERE id = ?;
Update Query (cache refetch):
UPDATE project SET weather_forecast = ? WHERE id = ?;
Expected API Call Volume¶
Daily Usage (100 active users): - New projects created: ~5-10/day - Project views: ~50-100/day - Weather fetches (initial): 5-10/day - Weather refetches (cache expired): ~10-20/day - Total API calls: 15-30/day × 3 APIs = 45-90 API calls/day
Free Tier Limits: - Open-Meteo: 10,000/day ✓ - OpenWeatherMap: 1,000,000/month (~33,000/day) ✓ - Met Office: 5,000/day ✓
Headroom: 100x safety margin
Security Considerations¶
API Key Management¶
Storage:
- API keys stored in .env file (gitignored)
- Never logged or sent to client
- Never committed to version control
Access Control: - Only server-side code can access keys - User cannot trigger arbitrary API calls - Rate limiting via caching prevents abuse
Input Validation¶
Coordinates:
# Validated by form submission
# Range: lat -90 to 90, lon -180 to 180
latitude = float(session.get('latitude'))
longitude = float(session.get('longitude'))
Date:
# Validated by WTForms DateField
# Parsed from trusted session data
flight_date = datetime.strptime(session.get('projectDateOfFlight'), '%Y-%m-%d')
Response Parsing Safety¶
JSON Parsing:
- All API responses validated with try/except
- Malformed JSON logged and skipped
- Missing fields caught by .get() with defaults
No Code Execution: - JSON data stored as-is (no eval/exec) - Jinja2 templates auto-escape output - No user-provided data in API URLs
Rate Limiting Protection¶
Built-in Protection: - 24-hour cache prevents rapid repeated calls - Failed APIs not retried within same request - User cannot manually trigger refetch
Theoretical Attack: - Malicious user creates 1,000 projects → 3,000 API calls - Mitigated by: User authentication required, admin monitoring
Testing Strategy¶
Unit Tests (tests/unit/test_weather_api.py)¶
Coverage:
1. Date Validation
- test_past_date_returns_error()
- test_too_far_future_returns_too_far_flag()
- test_valid_date_within_range()
- API Client Methods (Mocked Responses)
test_fetch_open_meteo_success()test_fetch_open_meteo_timeout()test_fetch_openweathermap_success()test_fetch_openweathermap_401_unauthorized()-
test_fetch_met_office_uk_location_check() -
Aggregation Logic
test_aggregate_single_source()test_aggregate_two_sources_average()test_aggregate_three_sources_average()test_aggregate_precipitation_probability_max()-
test_aggregate_conditions_majority_vote() -
Cache Validation
test_cache_valid_within_24_hours()test_cache_expired_after_24_hours()test_cache_missing_metadata()
Integration Tests (tests/integration/test_weather_integration.py)¶
Coverage:
1. Project Creation Flow
- test_weather_fetch_during_project_creation()
- test_weather_saved_to_database()
- Cache Validation Flow
test_cache_refetch_on_project_view()-
test_cache_not_refetched_if_valid() -
Error Scenarios
test_all_apis_fail_gracefully()test_project_creation_continues_without_weather()
Manual Testing Checklist¶
- [ ] Create project with flight date 5 days away → Verify forecast displays
- [ ] Create project with flight date 20 days away → Verify "too far" message
- [ ] Disable all API keys → Verify graceful degradation
- [ ] View project after 24 hours → Verify cache refetch
- [ ] Test responsive layout on mobile
- [ ] Test with UK location (Met Office should be called)
- [ ] Test with non-UK location (Met Office should be skipped)
Future Enhancements¶
1. Extended Forecast Range¶
Current: Single-day forecast for flight date Enhancement: 5-day forecast window (2 days before + flight day + 2 days after) Value: Better planning for weather-dependent rescheduling
2. Weather Alerts Integration¶
Current: No severe weather warnings Enhancement: Integrate NWS/Met Office alert APIs Value: Automatic notification of dangerous conditions
3. Historical Weather Comparison¶
Current: Forecast only Enhancement: Post-flight actual weather comparison Value: Forecast accuracy tracking, post-flight reporting
4. Weather-Based Flight Recommendations¶
Current: Display only Enhancement: Red/Yellow/Green flight viability based on weather Value: Automated pre-flight go/no-go assessment
5. Confidence Intervals¶
Current: Aggregated average with no uncertainty indication Enhancement: Calculate variance between sources Value: User understands forecast reliability (low variance = high confidence)
6. Hourly Forecast Breakdown¶
Current: Daily summary Enhancement: Hourly forecast table for flight time window Value: More precise planning for specific flight times
References¶
Internal Documentation¶
- Weather APIs Documentation - API specifications and endpoints
- Scottish Spatial Data API - Pattern reference for API client design
- Airspace Classification Design - Similar feature architecture
External Resources¶
Code Files¶
app/utils/weather_api.py- Main implementationapp/routes_new_project.py- Project creation routesapp/routes_dashboard.py- Project view routeapp/models.py- Project model with weather_forecast column
Version History¶
| Version | Date | Changes |
|---|---|---|
| 1.0 | 2025-12-29 | Initial implementation with Open-Meteo + OpenWeatherMap |
Appendix: File Summary¶
New Files Created (5)¶
app/utils/weather_api.py(~400 lines)- WeatherAPI class with aggregation logic
- 3 API client methods
- Cache validation
-
WMO code mapping
-
migrations/versions/0951d93a4956_add_weather_forecast_json_field_to_project.py -
Database migration for weather_forecast column
-
docs/api/weather-apis.md -
API documentation for 3 weather services
-
docs/design/weather-forecast-design.md(this file) -
Design documentation with architecture diagrams
-
Unit/Integration Test Files (to be created)
Modified Files (7)¶
app/models.py-
Added:
weather_forecast = db.Column(db.JSON, nullable=True)(line 180) -
app/routes_new_project.py - Modified:
new_project_details()- Fetch weather on form submit -
Modified:
new_project_toggles()- Save weather_forecast to database -
app/routes_dashboard.py -
Modified:
project()- Cache validation and refetch logic -
app/templates/create_project/new_project_details.html -
Added: Weather forecast panel in right sidebar
-
app/templates/dashboard/project.html -
Added: Weather forecast card in right column
-
.env.example -
Added: Weather API configuration placeholders
-
create_env.py - Added: Weather API environment variables