Skip to content

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:

  1. Reliability Through Redundancy
  2. If Open-Meteo experiences downtime → Still have OWM + Met Office
  3. If OWM rate limits exceed → Still have Open-Meteo + Met Office
  4. Single-source dependency creates fragility

  5. Accuracy Through Averaging

  6. Different weather models have different biases
  7. Averaging reduces single-source prediction errors
  8. Statistical studies show ensemble forecasts outperform single models

  9. Cross-Validation

  10. Large discrepancies between sources = high uncertainty
  11. Similar forecasts = high confidence
  12. 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

  1. Date Validation Errors
  2. Past dates → Return error response
  3. 16 days away → Return success with too_far_in_future: true

  4. API Timeout Errors

  5. Logged per API
  6. That API marked as failed
  7. Continue with remaining APIs

  8. HTTP Request Errors

  9. 401 Unauthorized (invalid API key) → Skip that API
  10. 429 Rate Limit → Skip that API (relies on cache to prevent hitting limits)
  11. 500 Server Error → Skip that API

  12. Response Parsing Errors

  13. Malformed JSON → Skip that API
  14. Missing expected fields → Skip that API

  15. Complete Failure (All APIs fail)

  16. Return `{'success': False, 'error': '...'}
  17. Template shows warning message
  18. 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 = ?;
- JSON column read: ~1-5ms - Indexed by primary key: O(log n) lookup

Update Query (cache refetch):

UPDATE project SET weather_forecast = ? WHERE id = ?;
- JSON column update: ~5-15ms - Rare operation (once per 24 hours per project)

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()

  1. API Client Methods (Mocked Responses)
  2. test_fetch_open_meteo_success()
  3. test_fetch_open_meteo_timeout()
  4. test_fetch_openweathermap_success()
  5. test_fetch_openweathermap_401_unauthorized()
  6. test_fetch_met_office_uk_location_check()

  7. Aggregation Logic

  8. test_aggregate_single_source()
  9. test_aggregate_two_sources_average()
  10. test_aggregate_three_sources_average()
  11. test_aggregate_precipitation_probability_max()
  12. test_aggregate_conditions_majority_vote()

  13. Cache Validation

  14. test_cache_valid_within_24_hours()
  15. test_cache_expired_after_24_hours()
  16. 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()

  1. Cache Validation Flow
  2. test_cache_refetch_on_project_view()
  3. test_cache_not_refetched_if_valid()

  4. Error Scenarios

  5. test_all_apis_fail_gracefully()
  6. 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

External Resources

Code Files

  • app/utils/weather_api.py - Main implementation
  • app/routes_new_project.py - Project creation routes
  • app/routes_dashboard.py - Project view route
  • app/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)

  1. app/utils/weather_api.py (~400 lines)
  2. WeatherAPI class with aggregation logic
  3. 3 API client methods
  4. Cache validation
  5. WMO code mapping

  6. migrations/versions/0951d93a4956_add_weather_forecast_json_field_to_project.py

  7. Database migration for weather_forecast column

  8. docs/api/weather-apis.md

  9. API documentation for 3 weather services

  10. docs/design/weather-forecast-design.md (this file)

  11. Design documentation with architecture diagrams

  12. Unit/Integration Test Files (to be created)

Modified Files (7)

  1. app/models.py
  2. Added: weather_forecast = db.Column(db.JSON, nullable=True) (line 180)

  3. app/routes_new_project.py

  4. Modified: new_project_details() - Fetch weather on form submit
  5. Modified: new_project_toggles() - Save weather_forecast to database

  6. app/routes_dashboard.py

  7. Modified: project() - Cache validation and refetch logic

  8. app/templates/create_project/new_project_details.html

  9. Added: Weather forecast panel in right sidebar

  10. app/templates/dashboard/project.html

  11. Added: Weather forecast card in right column

  12. .env.example

  13. Added: Weather API configuration placeholders

  14. create_env.py

  15. Added: Weather API environment variables