#!/usr/bin/env python3 """ Advanced Weather App using Python and PyQt5 Production-ready weather application with caching, error handling, and beautiful UI Uses Open-Meteo API (completely free, no API key required) """ import sys import json import pickle import logging from datetime import datetime, timedelta from pathlib import Path from typing import Optional, Tuple, Dict, Any import requests from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QMessageBox, QFrame, QScrollArea, QGridLayout, QProgressBar, QSystemTrayIcon, QMenu ) from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer from PyQt5.QtGui import QFont, QPixmap, QPalette, QColor, QIcon # Setup logging logging.basicConfig( filename='weather_app.log', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) class WeatherAPI: """Handle all weather API interactions with Open-Meteo""" BASE_URL = "https://api.open-meteo.com/v1/forecast" GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search" @staticmethod def get_coordinates(city_name: str) -> Optional[Tuple[float, float, str, str]]: """Get coordinates for a city name using Open-Meteo geocoding""" try: params = { "name": city_name, "count": 1, "language": "en", "format": "json" } response = requests.get(WeatherAPI.GEOCODING_URL, params=params, timeout=10) response.raise_for_status() data = response.json() results = data.get("results", []) if results: location = results[0] return ( location['latitude'], location['longitude'], location['name'], location.get('country', 'Unknown') ) return None except Exception as e: logging.error(f"Geocoding error for {city_name}: {e}") raise Exception(f"Could not find location: {city_name}") @staticmethod def get_weather_data(lat: float, lon: float) -> Dict[str, Any]: """Get current weather and 7-day forecast""" try: params = { "latitude": lat, "longitude": lon, "current_weather": "true", "daily": "temperature_2m_max,temperature_2m_min,weathercode,precipitation_sum,windspeed_10m_max", "timezone": "auto", "forecast_days": 7 } response = requests.get(WeatherAPI.BASE_URL, params=params, timeout=15) response.raise_for_status() return response.json() except Exception as e: logging.error(f"Weather API error for {lat}, {lon}: {e}") raise Exception("Failed to fetch weather data") class WeatherWorker(QThread): """Background thread for API calls""" data_ready = pyqtSignal(dict, str, str) error_occurred = pyqtSignal(str) def __init__(self, city_name: str): super().__init__() self.city_name = city_name def run(self): try: # Get coordinates coords = WeatherAPI.get_coordinates(self.city_name) if not coords: self.error_occurred.emit(f"Location '{self.city_name}' not found") return lat, lon, city, country = coords # Get weather data weather_data = WeatherAPI.get_weather_data(lat, lon) self.data_ready.emit(weather_data, city, country) except Exception as e: self.error_occurred.emit(str(e)) class WeatherCache: """Handle caching of weather data""" def __init__(self, cache_dir: str = "weather_cache"): self.cache_dir = Path(cache_dir) self.cache_dir.mkdir(exist_ok=True) self.cache_duration = timedelta(minutes=30) # Cache for 30 minutes def get_cache_key(self, city: str) -> str: return f"{city.lower().replace(' ', '_')}.pkl" def save_data(self, city: str, data: Dict[str, Any]): """Save weather data to cache""" try: cache_data = { 'timestamp': datetime.now(), 'data': data } cache_file = self.cache_dir / self.get_cache_key(city) with open(cache_file, 'wb') as f: pickle.dump(cache_data, f) except Exception as e: logging.error(f"Cache save error: {e}") def load_data(self, city: str) -> Optional[Dict[str, Any]]: """Load weather data from cache if recent enough""" try: cache_file = self.cache_dir / self.get_cache_key(city) if not cache_file.exists(): return None with open(cache_file, 'rb') as f: cache_data = pickle.load(f) # Check if cache is still valid if datetime.now() - cache_data['timestamp'] < self.cache_duration: return cache_data['data'] else: cache_file.unlink() # Remove expired cache return None except Exception as e: logging.error(f"Cache load error: {e}") return None class WeatherCard(QFrame): """Widget for displaying daily weather forecast""" def __init__(self, date: str, temp_max: float, temp_min: float, weather_code: int): super().__init__() self.setFrameStyle(QFrame.StyledPanel) self.setStyleSheet(""" QFrame { background-color: rgba(255, 255, 255, 0.1); border-radius: 10px; padding: 10px; margin: 5px; } """) layout = QVBoxLayout() # Date date_label = QLabel(date) date_label.setAlignment(Qt.AlignCenter) date_label.setFont(QFont("Segoe UI", 10, QFont.Bold)) # Weather icon (text representation) icon_label = QLabel(self.get_weather_icon(weather_code)) icon_label.setAlignment(Qt.AlignCenter) icon_label.setFont(QFont("Segoe UI Emoji", 24)) # Temperature temp_label = QLabel(f"{int(temp_max)}° / {int(temp_min)}°") temp_label.setAlignment(Qt.AlignCenter) temp_label.setFont(QFont("Segoe UI", 11)) layout.addWidget(date_label) layout.addWidget(icon_label) layout.addWidget(temp_label) self.setLayout(layout) self.setFixedSize(120, 150) @staticmethod def get_weather_icon(weather_code: int) -> str: """Map weather codes to emoji icons""" icons = { 0: "☀️", # Clear sky 1: "🌤️", # Mainly clear 2: "⛅", # Partly cloudy 3: "☁️", # Overcast 45: "🌫️", # Fog 48: "🌫️", # Depositing rime fog 51: "🌦️", # Light drizzle 53: "🌦️", # Moderate drizzle 55: "🌧️", # Dense drizzle 61: "🌧️", # Slight rain 63: "🌧️", # Moderate rain 65: "⛈️", # Heavy rain 71: "🌨️", # Slight snow 73: "❄️", # Moderate snow 75: "❄️", # Heavy snow 95: "⛈️", # Thunderstorm 96: "⛈️", # Thunderstorm with hail 99: "⛈️" # Heavy thunderstorm with hail } return icons.get(weather_code, "🌡️") class WeatherApp(QMainWindow): """Main weather application window""" def __init__(self): super().__init__() self.cache = WeatherCache() self.current_city = None self.setup_ui() self.setup_tray() self.setup_auto_refresh() def setup_ui(self): """Setup the main user interface""" self.setWindowTitle("Advanced Weather App") self.setGeometry(100, 100, 800, 600) self.setStyleSheet(""" QMainWindow { background-color: #1e1e2f; } QLabel { color: #f0f0f0; font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif; } QLineEdit { padding: 10px; border-radius: 8px; border: 1px solid #555; background-color: #2c2c3c; color: #f0f0f0; font-size: 14px; } QPushButton { padding: 10px 20px; border-radius: 8px; border: none; background-color: #5e60ce; color: white; font-weight: 600; font-size: 14px; } QPushButton:hover { background-color: #4ea8de; } QPushButton:pressed { background-color: #3a86ff; } QProgressBar { border-radius: 5px; height: 12px; background-color: #2e2e38; } QProgressBar::chunk { background-color: #5e60ce; border-radius: 5px; } QScrollArea { border: none; } """) central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout() # Search section search_layout = QHBoxLayout() self.city_input = QLineEdit() self.city_input.setPlaceholderText("Enter city name...") self.city_input.returnPressed.connect(self.search_weather) self.search_button = QPushButton("Search") self.search_button.clicked.connect(self.search_weather) search_layout.addWidget(self.city_input) search_layout.addWidget(self.search_button) # Progress bar self.progress_bar = QProgressBar() self.progress_bar.setVisible(False) # Current weather section self.current_weather_frame = QFrame() self.current_weather_frame.setStyleSheet(""" QFrame { background-color: #2e2e3e; border-radius: 12px; padding: 15px; color: #f0f0f0; } """) current_layout = QVBoxLayout() self.location_label = QLabel("Search for a city to see weather") self.location_label.setAlignment(Qt.AlignCenter) self.location_label.setFont(QFont("Arial", 18, QFont.Bold)) self.temperature_label = QLabel("") self.temperature_label.setAlignment(Qt.AlignCenter) self.temperature_label.setFont(QFont("Arial", 48, QFont.Bold)) self.weather_icon_label = QLabel("") self.weather_icon_label.setAlignment(Qt.AlignCenter) self.weather_icon_label.setFont(QFont("Arial", 60)) self.description_label = QLabel("") self.description_label.setAlignment(Qt.AlignCenter) self.description_label.setFont(QFont("Arial", 16)) current_layout.addWidget(self.location_label) current_layout.addWidget(self.weather_icon_label) current_layout.addWidget(self.temperature_label) current_layout.addWidget(self.description_label) self.current_weather_frame.setLayout(current_layout) # Forecast section forecast_label = QLabel("7-Day Forecast") forecast_label.setAlignment(Qt.AlignCenter) forecast_label.setFont(QFont("Arial", 16, QFont.Bold)) # Scrollable forecast area scroll_area = QScrollArea() scroll_widget = QWidget() self.forecast_layout = QHBoxLayout() scroll_widget.setLayout(self.forecast_layout) scroll_area.setWidget(scroll_widget) scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) scroll_area.setFixedHeight(200) # Add all widgets to main layout main_layout.addLayout(search_layout) main_layout.addWidget(self.progress_bar) main_layout.addWidget(self.current_weather_frame) main_layout.addWidget(forecast_label) main_layout.addWidget(scroll_area) central_widget.setLayout(main_layout) # Status bar self.statusBar().showMessage("Ready") def setup_tray(self): """Setup system tray icon""" if QSystemTrayIcon.isSystemTrayAvailable(): self.tray_icon = QSystemTrayIcon(self) self.tray_icon.setIcon(self.style().standardIcon(self.style().SP_ComputerIcon)) tray_menu = QMenu(self) show_action = tray_menu.addAction("Show") show_action.triggered.connect(self.show) quit_action = tray_menu.addAction("Quit") quit_action.triggered.connect(self.close) self.tray_icon.setContextMenu(tray_menu) self.tray_icon.show() def setup_auto_refresh(self): """Setup automatic weather refresh every 30 minutes""" self.refresh_timer = QTimer() self.refresh_timer.timeout.connect(self.auto_refresh_weather) self.refresh_timer.start(30 * 60 * 1000) # 30 minutes def auto_refresh_weather(self): """Automatically refresh weather for current city""" if self.current_city: self.search_weather() def search_weather(self): """Search for weather data""" city = self.city_input.text().strip() if not city: QMessageBox.warning(self, "Warning", "Please enter a city name") return # Check cache first cached_data = self.cache.load_data(city) if cached_data: self.display_weather(cached_data, city, "") self.statusBar().showMessage(f"Showing cached data for {city}") return # Show loading state self.progress_bar.setVisible(True) self.search_button.setEnabled(False) self.statusBar().showMessage(f"Searching weather for {city}...") # Start background API call self.worker = WeatherWorker(city) self.worker.data_ready.connect(self.on_weather_data_ready) self.worker.error_occurred.connect(self.on_weather_error) self.worker.start() def on_weather_data_ready(self, data: Dict[str, Any], city: str, country: str): """Handle successful weather data retrieval""" self.progress_bar.setVisible(False) self.search_button.setEnabled(True) # Cache the data self.cache.save_data(city, data) # Display the weather self.display_weather(data, city, country) self.current_city = city self.statusBar().showMessage(f"Weather updated for {city}") logging.info(f"Weather data retrieved for {city}") def on_weather_error(self, error_message: str): """Handle weather data retrieval errors""" self.progress_bar.setVisible(False) self.search_button.setEnabled(True) QMessageBox.critical(self, "Error", error_message) self.statusBar().showMessage("Error occurred") logging.error(f"Weather error: {error_message}") def display_weather(self, data: Dict[str, Any], city: str, country: str): """Display weather data in the UI""" try: current = data['current_weather'] daily = data['daily'] # Update current weather location_text = f"{city}" if country: location_text += f", {country}" self.location_label.setText(location_text) temp = int(current['temperature']) self.temperature_label.setText(f"{temp}°C") weather_code = current['weathercode'] icon = WeatherCard.get_weather_icon(weather_code) self.weather_icon_label.setText(icon) # Weather description descriptions = { 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", 61: "Light rain", 63: "Moderate rain", 65: "Heavy rain", 71: "Light snow", 73: "Moderate snow", 75: "Heavy snow", 95: "Thunderstorm", 96: "Thunderstorm with hail", 99: "Heavy thunderstorm" } description = descriptions.get(weather_code, "Unknown") wind_speed = current.get('windspeed', 0) self.description_label.setText(f"{description} • Wind: {int(wind_speed)} km/h") # Clear existing forecast for i in reversed(range(self.forecast_layout.count())): child = self.forecast_layout.itemAt(i).widget() if child: child.setParent(None) # Add forecast cards dates = daily['time'] temp_max = daily['temperature_2m_max'] temp_min = daily['temperature_2m_min'] weather_codes = daily['weathercode'] for i in range(len(dates)): date_str = datetime.fromisoformat(dates[i]).strftime("%a\n%m/%d") card = WeatherCard(date_str, temp_max[i], temp_min[i], weather_codes[i]) self.forecast_layout.addWidget(card) # Update tray tooltip if available if hasattr(self, 'tray_icon'): self.tray_icon.setToolTip(f"{city}: {temp}°C - {description}") except Exception as e: logging.error(f"Display error: {e}") QMessageBox.critical(self, "Error", "Failed to display weather data") def main(): """Main application entry point""" app = QApplication(sys.argv) app.setApplicationName("Advanced Weather App") app.setQuitOnLastWindowClosed(False) # Keep running in system tray # Set application icon app.setWindowIcon(app.style().standardIcon(app.style().SP_ComputerIcon)) window = WeatherApp() window.show() sys.exit(app.exec_()) if __name__ == "__main__": app = QApplication(sys.argv) app.setStyle("Fusion") # Optional dark Fusion palette palette = QPalette() palette.setColor(QPalette.Window, QColor(44, 47, 59)) palette.setColor(QPalette.WindowText, Qt.white) palette.setColor(QPalette.Base, QColor(25, 25, 25)) palette.setColor(QPalette.AlternateBase, QColor(44, 47, 59)) palette.setColor(QPalette.ToolTipBase, Qt.white) palette.setColor(QPalette.ToolTipText, Qt.white) palette.setColor(QPalette.Text, Qt.white) palette.setColor(QPalette.Button, QColor(70, 70, 90)) palette.setColor(QPalette.ButtonText, Qt.white) palette.setColor(QPalette.BrightText, Qt.red) palette.setColor(QPalette.Link, QColor(42, 130, 218)) palette.setColor(QPalette.Highlight, QColor(42, 130, 218)) palette.setColor(QPalette.HighlightedText, Qt.black) app.setPalette(palette) window = WeatherApp() window.show() sys.exit(app.exec_())