7. Presentation layer: user-friendly display
Three workflow outcomes deserve three different displays. The presentation layer reads the WorkflowResult.status from §6 and renders the right shape: full forecast on success, found-locations-but-no-weather on partial success, three-part guidance on complete failure. The display module is the only place this branching lives; no other layer branches on status.
In this section you build display.py, with one public function (display_weather_result), three private status-dispatch helpers (one per outcome), and two small formatting helpers (compass directions and quality labels). Keeping display logic out of the orchestrator pays off the moment a second output format shows up. Swap display.py for a Flask template, a JSON serialiser, or a TUI; the orchestrator doesn't change.
Display strategy by workflow outcome
The mapping from status to layout follows directly from what the user can do next:
| Status | What to Display | User Guidance |
|---|---|---|
| SUCCESS | Full weather data, location details, quality scores | Alternative locations if multiple matches found |
| PARTIAL_SUCCESS | Location data found, weather unavailable message | Suggestions to retry, alternative locations shown |
| FAILURE | Clear error message and what-to-do bullets | Three-part message pattern from Chapter 9 |
The partial-success row matters most. A simple two-state model would force the user to retry blind; here they see "We found Paris, France, but couldn't fetch current conditions" and know the geocode worked, the city was correct, and the retry should target the weather call rather than the spelling of the input.
display.py: one dispatch, three displays
All three display branches live in one module. Save it at the project root as display.py:
"""
Presentation layer for weather dashboard.
Transforms WorkflowResult into user-friendly displays.
Handles SUCCESS, PARTIAL_SUCCESS, and FAILURE states.
"""
from models import WorkflowResult, WorkflowStatus
def display_weather_result(result: WorkflowResult) -> None:
"""
Print the weather dashboard result, dispatching by status.
Args:
result: WorkflowResult from orchestrator
"""
print("\n" + "="*70)
if result.status == WorkflowStatus.SUCCESS:
_display_success(result)
elif result.status == WorkflowStatus.PARTIAL_SUCCESS:
_display_partial_success(result)
else: # FAILURE
_display_failure(result)
print("="*70 + "\n")
def _display_success(result):
"""Display successful weather data retrieval."""
weather = result.weather_data
location = weather.location
current = weather.current
# Header with location
location_name = f"{location.name}"
if location.state:
location_name += f", {location.state}"
location_name += f", {location.country}"
print(f" WEATHER FOR {location_name.upper()}")
print("="*70)
# Location confidence (if not perfect)
if location.confidence_score < 1.0:
print(f"\n Location confidence: {location.confidence_score:.0%}")
# Current conditions
print(f"\n CURRENT CONDITIONS")
print(f" Temperature: {current.temperature:.1f}°C")
# Show feels-like if significantly different
temp_diff = abs(current.feels_like - current.temperature)
if temp_diff > 2:
print(f" Feels like: {current.feels_like:.1f}°C")
print(f" Description: {current.description}")
print(f" Humidity: {current.humidity}%")
print(f" Pressure: {current.pressure:.0f} hPa")
# Optional fields (only if available)
if current.wind_speed:
wind_str = f" Wind: {current.wind_speed:.1f} km/h"
if current.wind_direction:
compass = _degrees_to_compass(current.wind_direction)
wind_str += f" from {compass}"
print(wind_str)
if current.visibility:
print(f" Visibility: {current.visibility:.1f} km")
# Data quality indicator
print(f"\n DATA QUALITY")
print(f" Updated: {weather.timestamp.strftime('%Y-%m-%d %H:%M:%S')}")
print(f" UTC offset: {_format_utc_offset(weather.timezone_offset_seconds)}")
quality_description = _quality_score_description(weather.data_quality_score)
print(f" Quality: {weather.data_quality_score:.0%} ({quality_description})")
# Show validation warnings if any
if weather.validation_warnings:
print(f"\n[WARN] DATA WARNINGS:")
for warning in weather.validation_warnings:
print(f" • {warning}")
# Show alternative locations if multiple were found
if len(result.locations) > 1:
print(f"\n OTHER LOCATIONS MATCHING YOUR SEARCH:")
for i, alt_location in enumerate(result.locations[1:4], 1): # Show top 3 alternatives
alt_name = f"{alt_location.name}"
if alt_location.state:
alt_name += f", {alt_location.state}"
alt_name += f", {alt_location.country}"
print(f" {i}. {alt_name} (confidence: {alt_location.confidence_score:.0%})")
def _display_partial_success(result):
"""Display partial success (location found, weather failed)."""
print("[WARN] PARTIAL SUCCESS")
print("="*70)
print(f"\n[OK] Location(s) found:")
for i, location in enumerate(result.locations[:3], 1):
location_name = f"{location.name}"
if location.state:
location_name += f", {location.state}"
location_name += f", {location.country}"
print(f" {i}. {location_name}")
print(f" Coordinates: ({location.latitude:.4f}, {location.longitude:.4f})")
print(f" Confidence: {location.confidence_score:.0%}")
# Show why weather failed
print(f"\n[FAIL] Weather data unavailable")
print(f" Reason: {result.error_message}")
# Render the orchestrator's per-failure suggestions verbatim
if result.suggestions:
print(f"\n WHAT TO DO:")
for suggestion in result.suggestions:
print(f" • {suggestion}")
# Show warnings if any
if result.warnings:
print(f"\n[WARN] WARNINGS:")
for warning in result.warnings:
print(f" • {warning}")
def _display_failure(result):
"""Display complete failure with Chapter 9's three-part guidance."""
print("[FAIL] UNABLE TO GET WEATHER DATA")
print("="*70)
# Three-part error message pattern from Chapter 9
print(f"\n{result.error_message}")
# Render the orchestrator's per-failure suggestions verbatim
if result.suggestions:
print(f"\n WHAT TO DO:")
for suggestion in result.suggestions:
print(f" • {suggestion}")
# Show warnings if any
if result.warnings:
print(f"\n[WARN] WARNINGS:")
for warning in result.warnings:
print(f" • {warning}")
def _degrees_to_compass(degrees):
"""Convert wind direction degrees to compass direction."""
directions = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE',
'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']
index = round(degrees / 22.5) % 16
return directions[index]
def _quality_score_description(score):
"""Convert quality score to human-readable description."""
if score >= 0.9:
return "Excellent"
elif score >= 0.8:
return "Good"
elif score >= 0.7:
return "Fair"
else:
return "Poor"
def _format_utc_offset(offset_seconds):
"""Format OpenWeather's timezone offset seconds as UTC±HH:MM."""
sign = "+" if offset_seconds >= 0 else "-"
offset_seconds = abs(offset_seconds)
hours = offset_seconds // 3600
minutes = (offset_seconds % 3600) // 60
return f"UTC{sign}{hours:02d}:{minutes:02d}"
Five small decisions add up to the user experience. Feels-like only renders when it's noticeably different from the actual temperature. The data-quality score appears with a descriptive label (Excellent, Good, Fair, or Poor) on successful results, so a 0.92 score reads as "Excellent" rather than as a bare number. Optional weather fields appear conditionally instead of as None. The partial-success view lists the alternative location matches the geocoder returned. The failure view borrows Chapter 9's three-part error template (what happened, what to do, an example) so the user has a concrete next step.
Verifying the presentation layer
Three test scenarios cover the three outcome types. Save this at the project root as test_presentation.py and run it with python test_presentation.py:
# Test the presentation layer with mock results
from models import WorkflowResult, WorkflowStatus
from models import WeatherData, Location, CurrentWeather
from display import display_weather_result
from datetime import datetime
print("=== Testing Presentation Layer ===\n")
# Test 1: Success display
print("Test 1: SUCCESS display")
mock_location = Location(
name="London",
country="GB",
state="England",
latitude=51.5074,
longitude=-0.1278,
confidence_score=0.95,
raw_data={}
)
mock_weather = CurrentWeather(
temperature=15.3,
feels_like=12.5,
humidity=72,
pressure=1013.2,
description="Light Rain",
icon="10d",
wind_speed=12.5,
wind_direction=225,
visibility=8.5
)
mock_weather_data = WeatherData(
location=mock_location,
current=mock_weather,
timestamp=datetime.now(),
timezone_offset_seconds=3600,
data_quality_score=0.92,
validation_warnings=[],
raw_data={}
)
success_result = WorkflowResult(
status=WorkflowStatus.SUCCESS,
weather_data=mock_weather_data,
locations=[mock_location],
error_message=None,
error_category=None,
warnings=[],
suggestions=[],
processing_metadata={'total_duration': 1.23}
)
display_weather_result(success_result)
# Test 2: Partial success display
print("\nTest 2: PARTIAL_SUCCESS display")
partial_result = WorkflowResult(
status=WorkflowStatus.PARTIAL_SUCCESS,
weather_data=None,
locations=[mock_location],
error_message="Weather API temporarily unavailable",
error_category='transient',
warnings=[],
suggestions=[
"Weather service is temporarily unavailable",
"Location data is still available above",
"Try again in a few moments"
],
processing_metadata={'total_duration': 0.85}
)
display_weather_result(partial_result)
# Test 3: Failure display
print("\nTest 3: FAILURE display")
failure_result = WorkflowResult(
status=WorkflowStatus.FAILURE,
weather_data=None,
locations=[],
error_message="Could not process location data for 'Xqzthlptv99999'",
error_category='not_found',
warnings=[],
suggestions=[
"Check the spelling of the city name",
"Try a more specific name (include country or state)",
"Examples: 'London, UK' or 'Paris, France'"
],
processing_metadata={'total_duration': 0.45}
)
display_weather_result(failure_result)
======================================================================
WEATHER FOR LONDON, ENGLAND, GB
======================================================================
Location confidence: 95%
CURRENT CONDITIONS
Temperature: 15.3°C
Feels like: 12.5°C
Description: Light Rain
Humidity: 72%
Pressure: 1013 hPa
Wind: 12.5 km/h from SW
Visibility: 8.5 km
DATA QUALITY
Updated: 2026-05-01 14:30:00
UTC offset: UTC+01:00
Quality: 92% (Excellent)
======================================================================
======================================================================
[WARN] PARTIAL SUCCESS
======================================================================
[OK] Location(s) found:
1. London, England, GB
Coordinates: (51.5074, -0.1278)
Confidence: 95%
[FAIL] Weather data unavailable
Reason: Weather API temporarily unavailable
WHAT TO DO:
• Weather service is temporarily unavailable
• Location data is still available above
• Try again in a few moments
======================================================================
======================================================================
[FAIL] UNABLE TO GET WEATHER DATA
======================================================================
Could not process location data for 'Xqzthlptv99999'
WHAT TO DO:
• Check the spelling of the city name
• Try a more specific name (include country or state)
• Examples: 'London, UK' or 'Paris, France'
======================================================================
The output above shows the three statuses producing three different displays. The dispatch in display_weather_result() routes each WorkflowResult to the helper that matches its status, so the calling code only ever invokes one function regardless of outcome.
If your output diverges, the most likely culprit is the import path. If ImportError fires, display.py or §5's models.py isn't where Python expects them; both should sit at the project root.
The lead's promise has been delivered: branching lives in display.py alone. Every other layer builds a WorkflowResult and hands it back; only display.py knows how to render each shape.
_display_partial_success and _display_failure render result.suggestions verbatim. The orchestrator hand-tunes the suggestions per failure path (step-3 ValidationError, step-5 weather timeout, step-2 not_found, etc.), so the display layer doesn't need to peek at the category to know what to show. That's the architectural payoff §6's lead promised: outcome judgement in the orchestrator, rendering in the display, with WorkflowResult as the contract between them.
Section 8 assembles the complete application: configuration, the orchestrator, and display.py wired together into an interactive weather dashboard the user can run from the command line.