Building a Weather App Interface: From Basics to Advanced¶
Introduction¶
Now that you understand the basics of TF Utils interfaces, let's build something more exciting - a weather app! We'll start simple and gradually add features, explaining each new concept as we go.
Prerequisites¶
- Installation Guide completed
- Creating Your First Interface completed
- PyCharm installed (completed in the installation guide)
- Project opened in PyCharm (completed in the installation guide)
- Poetry environment set up (completed in the installation guide)
- Basic Python knowledge
- Internet connection (for API calls)
Part 1: Starting Simple¶
Let's begin with a basic version that just prints weather data for a hardcoded city:
from textual.containers import Container
from src.lib.interface import interface
from src.lib.utils import console
@interface("Basic Weather")
async def check_weather(container: Container):
"""Display weather for London (hardcoded version)"""
await console.print(container, "[blue]Weather in London:[/blue]")
await console.print(container, "Temperature: 20°C")
await console.print(container, "Conditions: Sunny")
Run this first to make sure everything works. You should see:
- A "Basic Weather" option in the menu
- Static weather information when selected
Part 2: Adding User Input¶
Now let's allow users to choose the city:
from textual.containers import Container
from src.lib.interface import interface
from src.lib.console import ask_input
from src.lib.utils import console
@interface("Weather Checker")
async def check_weather(container: Container):
"""Get weather information for any city."""
# Get city name from user
city = await ask_input(
container,
"Enter city name",
"e.g. London, Paris, Tokyo"
)
await console.print(container, f"[blue]Weather in {city}:[/blue]")
await console.print(container, "Temperature: 20°C") # Still hardcoded!
await console.print(container, "Conditions: Sunny")
This version:
- Asks for a city name
- Still uses hardcoded weather data (we'll fix that next!)
Part 3: Adding API Integration¶
Now comes the exciting part - getting real weather data! First, we need to add the required imports:
from textual.containers import Container
import aiohttp # For making API calls
from src.lib.interface import interface
from src.lib.console import ask_input
from src.lib.utils import console
Step 1: Create the Weather API Function¶
Let's create a function to fetch weather data:
async def async_get_weather(city_name: str) -> dict:
"""Fetch weather data for a given city."""
try:
async with aiohttp.ClientSession() as session:
# First, get city coordinates
geo_url = "https://geocoding-api.open-meteo.com/v1/search"
geo_params = {
"name": city_name,
"count": 1,
"language": "en",
"format": "json"
}
async with session.get(geo_url, params=geo_params) as geo_response:
geo_data = await geo_response.json()
if not geo_data.get("results"):
return {"error": "City not found"}
location = geo_data["results"][0]
lat = location["latitude"]
lon = location["longitude"]
# Then, get weather data using coordinates
weather_url = "https://api.open-meteo.com/v1/forecast"
weather_params = {
"latitude": lat,
"longitude": lon,
"current": ["temperature_2m", "relative_humidity_2m"],
"timezone": "auto"
}
async with session.get(weather_url, params=weather_params) as weather_response:
weather_data = await weather_response.json()
return {
"city": location["name"],
"country": location.get("country", "Unknown"),
"temperature": weather_data["current"]["temperature_2m"],
"humidity": weather_data["current"]["relative_humidity_2m"]
}
except Exception as e:
return {"error": str(e)}
Step 2: Update the Interface to Use Real Data¶
Now let's use our new function:
@interface("Live Weather")
async def check_weather(container: Container):
"""Get real-time weather information for any city."""
# Get city name from user
city = await ask_input(
container,
"Enter city name",
"e.g. London, Paris, Tokyo"
)
# Show loading message
await console.print(container, "[blue]Fetching weather data...[/blue]")
# Get real weather data
weather = await async_get_weather(city)
if "error" in weather:
await console.print(container, f"[red]Error: {weather['error']}[/red]")
return
# Display weather information
await console.print(container, "\n" + "─" * 40)
await console.print(container, f"""
Weather for {weather['city']}, {weather['country']}
────────────────────────
Temperature: {weather['temperature']}°C
Humidity: {weather['humidity']}%
""".strip())
await console.print(container, "─" * 40)
Part 4: Adding Weather Conditions and Emojis¶
Let's make it more visually appealing by adding weather conditions and emojis:
# In async_get_weather, update weather_params to include weather_code:
weather_params = {
"latitude": lat,
"longitude": lon,
"current": ["temperature_2m", "relative_humidity_2m", "weather_code"],
"timezone": "auto"
}
# Add weather code mapping
weather_codes = {
0: ("Clear sky", "☀️"),
1: ("Mainly clear", "🌤️"),
2: ("Partly cloudy", "⛅"),
3: ("Overcast", "☁️"),
45: ("Foggy", "🌫️"),
51: ("Light drizzle", "🌦️"),
61: ("Slight rain", "🌧️"),
71: ("Slight snow", "🌨️"),
95: ("Thunderstorm", "⛈️")
}
# Get weather info in the function
weather_info = weather_codes.get(
weather_data["current"]["weather_code"],
("Unknown", "❓")
)
Then update the return value to include the new information:
return {
"city": location["name"],
"country": location.get("country", "Unknown"),
"temperature": weather_data["current"]["temperature_2m"],
"humidity": weather_data["current"]["relative_humidity_2m"],
"conditions": weather_info[0],
"emoji": weather_info[1]
}
It should now look something like this:
async def async_get_weather(city_name: str) -> dict:
"""Fetch weather data for a given city."""
try:
async with aiohttp.ClientSession() as session:
# First, get city coordinates
geo_url = "https://geocoding-api.open-meteo.com/v1/search"
geo_params = {
"name": city_name,
"count": 1,
"language": "en",
"format": "json"
}
async with session.get(geo_url, params=geo_params) as geo_response:
geo_data = await geo_response.json()
if not geo_data.get("results"):
return {"error": "City not found"}
location = geo_data["results"][0]
lat = location["latitude"]
lon = location["longitude"]
# Then, get weather data using coordinates
weather_url = "https://api.open-meteo.com/v1/forecast"
# In async_get_weather, update weather_params to include weather_code:
weather_params = {
"latitude": lat,
"longitude": lon,
"current": ["temperature_2m", "relative_humidity_2m", "weather_code"],
"timezone": "auto"
}
# Add weather code mapping
weather_codes = {
0: ("Clear sky", "☀️"),
1: ("Mainly clear", "🌤️"),
2: ("Partly cloudy", "⛅"),
3: ("Overcast", "☁️"),
45: ("Foggy", "🌫️"),
51: ("Light drizzle", "🌦️"),
61: ("Slight rain", "🌧️"),
71: ("Slight snow", "🌨️"),
95: ("Thunderstorm", "⛈️")
}
async with session.get(weather_url, params=weather_params) as weather_response:
weather_data = await weather_response.json()
weather_info = weather_codes.get(
weather_data["current"]["weather_code"],
("Unknown", "❓")
)
return {
"city": location["name"],
"country": location.get("country", "Unknown"),
"temperature": weather_data["current"]["temperature_2m"],
"humidity": weather_data["current"]["relative_humidity_2m"],
"conditions": weather_info[0],
"emoji": weather_info[1]
}
except Exception as e:
return {"error": str(e)}
Finally, update the display:
await console.print(container, f"""
{weather['emoji']} Weather for {weather['city']}, {weather['country']}
────────────────────────
Temperature: {weather['temperature']}°C
Conditions: {weather['conditions']}
Humidity: {weather['humidity']}%
""".strip())
The Final Weather App¶
You now have a fully functional weather app that fetches real-time weather data for any city! Here's the complete code:
import aiohttp # For making API calls
from textual.containers import Container
from src.lib.console import ask_input
from src.lib.interface import interface
from src.lib.utils import console
async def async_get_weather(city_name: str) -> dict:
"""Fetch weather data for a given city."""
try:
async with aiohttp.ClientSession() as session:
# First, get city coordinates
geo_url = "https://geocoding-api.open-meteo.com/v1/search"
geo_params = {
"name": city_name,
"count": 1,
"language": "en",
"format": "json",
}
async with session.get(geo_url, params=geo_params) as geo_response:
geo_data = await geo_response.json()
if not geo_data.get("results"):
return {"error": "City not found"}
location = geo_data["results"][0]
lat = location["latitude"]
lon = location["longitude"]
# Then, get weather data using coordinates
weather_url = "https://api.open-meteo.com/v1/forecast"
# In async_get_weather, update weather_params to include weather_code:
weather_params = {
"latitude": lat,
"longitude": lon,
"current": ["temperature_2m", "relative_humidity_2m", "weather_code"],
"timezone": "auto",
}
# Add weather code mapping
weather_codes = {
0: ("Clear sky", "☀️"),
1: ("Mainly clear", "🌤️"),
2: ("Partly cloudy", "⛅"),
3: ("Overcast", "☁️"),
45: ("Foggy", "🌫️"),
51: ("Light drizzle", "🌦️"),
61: ("Slight rain", "🌧️"),
71: ("Slight snow", "🌨️"),
95: ("Thunderstorm", "⛈️"),
}
async with session.get(
weather_url, params=weather_params
) as weather_response:
weather_data = await weather_response.json()
weather_info = weather_codes.get(
weather_data["current"]["weather_code"], ("Unknown", "❓")
)
return {
"city": location["name"],
"country": location.get("country", "Unknown"),
"temperature": weather_data["current"]["temperature_2m"],
"humidity": weather_data["current"]["relative_humidity_2m"],
"conditions": weather_info[0],
"emoji": weather_info[1],
}
except Exception as e:
return {"error": str(e)}
@interface("Live Weather")
async def check_weather(container: Container):
"""Get real-time weather information for any city."""
# Get city name from user
city = await ask_input(container, "Enter city name", "e.g. London, Paris, Tokyo")
# Show loading message
await console.print(container, "[blue]Fetching weather data...[/blue]")
# Get real weather data
weather = await async_get_weather(city)
if "error" in weather:
await console.print(container, f"[red]Error: {weather['error']}[/red]")
return
# Display weather information
await console.print(container, "\n" + "─" * 40)
await console.print(
container,
f"""
{weather['emoji']} Weather for {weather['city']}, {weather['country']}
────────────────────────
Temperature: {weather['temperature']}°C
Conditions: {weather['conditions']}
Humidity: {weather['humidity']}%
""".strip(),
)
await console.print(container, "─" * 40)
Understanding the Code¶
Key Concepts Used:¶
-
Async/Await: For non-blocking API calls
async with
for managing connectionsawait
for waiting on responses
-
Error Handling:
- Try/except blocks
- Error message returns
- User input validation
-
API Integration:
- Multiple API endpoints
- Parameter handling
- Response processing
-
UI Enhancements:
- Loading messages
- Error formatting
- Pretty printing with borders
- Emoji support
Practice Exercises¶
Try these enhancements:
-
Add Wind Speed:
- Update
weather_params
to include "wind_speed_10m" - Add it to the display
- Update
-
Temperature Units:
- Add a second input for preferred unit (C/F)
- Convert temperatures accordingly
-
Location Memory:
- Store the last searched city
- Add an option to quickly check it again
Troubleshooting Tips¶
-
API Issues:
- Check internet connection
- Verify city spelling
- Handle API rate limits
-
Display Issues:
- Run in PowerShell for emoji support
- Check for proper string formatting
- Verify border characters display correctly
Next Steps¶
Now that you have a working weather app, try:
- Adding extended forecasts
- Saving favorite cities
- Adding weather alerts
- Including sunrise/sunset times
- Adding historical weather data
Remember: The Open-Meteo API has many more features you can explore. Check their documentation for more possibilities!