Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
__pycache__/
*.py[cod]
*$py.class

.idea/
# C extensions
*.so

Expand Down
8 changes: 0 additions & 8 deletions .idea/.gitignore

This file was deleted.

8 changes: 0 additions & 8 deletions .idea/Weather.iml

This file was deleted.

17 changes: 0 additions & 17 deletions .idea/inspectionProfiles/Project_Default.xml

This file was deleted.

6 changes: 0 additions & 6 deletions .idea/inspectionProfiles/profiles_settings.xml

This file was deleted.

4 changes: 0 additions & 4 deletions .idea/misc.xml

This file was deleted.

8 changes: 0 additions & 8 deletions .idea/modules.xml

This file was deleted.

6 changes: 0 additions & 6 deletions .idea/vcs.xml

This file was deleted.

4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ requests==2.32.4
python-dotenv==1.0.0

PyGithub>=2.1.0
pyyaml>=6.0
pyyaml>=6.0

textual
29 changes: 29 additions & 0 deletions weather-tui/css/weather.tcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* css/weather.tcss */

Sidebar {
width: 30%;
border-right: solid $accent;
padding: 1;
}

.main-content {
width: 70%;
padding: 1;
}

.sidebar-title {
text-style: bold;
background: $accent;
padding: 1;
margin-bottom: 1;
}

ListView {
height: 80%;
}

WeatherWidget {
margin: 1;
padding: 1;
border: solid $primary;
}
87 changes: 87 additions & 0 deletions weather-tui/weather_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from textual.app import App, ComposeResult
from textual.containers import Container, Horizontal, Vertical
from textual.widgets import Header, Footer, ListView, ListItem, Label, Static
from textual.reactive import reactive
from textual import work
from textual.message import Message
from weather_service import WeatherService

class WeatherWidget(Static):
"""A custom widget to display weather information."""

weather_data = reactive("")

def on_mount(self):
"""Start a timer to refresh weather data every 30 seconds."""
self.set_interval(30, self.refresh_weather)

@work
async def refresh_weather(self):
"""Perform the weather API call in a worker thread."""
weather_text = await self.run_worker(self.fetch_weather_data, thread=True)
self.weather_data = weather_text

def fetch_weather_data(self):
city_name = self.id if self.id else "Unknown"
return weather_service.get_full_weather(city_name)

def render(self):
return self.weather_data

class CityListView(ListView):
"""A list view that emits a custom message when an item is selected."""

class CitySelected(Message):
def __init__(self, city: str) -> None:
self.city = city
super().__init__()

def on_list_view_selected(self, event: ListView.Selected):
"""Send a CitySelected message when a city is chosen."""
# The city name is stored in the ListItem's id attribute
city_name = event.item.id
self.post_message(self.CitySelected(city_name))

class WeatherApp(App):
"""The main Textual application class."""

CSS_PATH = "css/weather.tcss"
BINDINGS = [("q", "quit", "Quit")]

def __init__(self):
super().__init__()
self.weather_service = WeatherService()
self.default_cities = ['tehran', 'london', 'new-york', 'tokyo', 'sydney',
'cape-town', 'mumbai', 'beijing', 'rio-de-janeiro', 'cairo']
self.current_city = "tehran"

def compose(self) -> ComposeResult:
"""Create the UI layout."""
yield Header()
with Container():
with Horizontal():
with Vertical(classes="sidebar"):
yield Label("Cities", classes="sidebar-title")
# Create ListItems with id set to the city name (lowercase)
city_items = [ListItem(Label(city.capitalize()), id=city) for city in self.default_cities]
yield CityListView(*city_items, id="city-list")
with Vertical(classes="main-content"):
yield WeatherWidget(id=self.current_city)
yield Footer()

def on_city_list_view_city_selected(self, message: CityListView.CitySelected):
"""Handle city selection by updating the weather widget."""
self.current_city = message.city
# Remove the old widget
old_widget = self.query_one(f"#{self.current_city}")
old_widget.remove()
# Mount a new widget for the selected city
new_widget = WeatherWidget(id=self.current_city)
self.query_one(".main-content").mount(new_widget)

if __name__ == "__main__":
# Create the global weather_service instance used by WeatherWidget
global weather_service
weather_service = WeatherService()
app = WeatherApp()
app.run()
72 changes: 72 additions & 0 deletions weather-tui/weather_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import os
import requests
from datetime import datetime, timedelta
from dotenv import load_dotenv

load_dotenv()

class WeatherService:
"""Handles fetching and caching weather data from OpenWeatherMap."""

def __init__(self):
self.api_key = os.getenv("OPENWEATHER_API_KEY")
if not self.api_key:
raise ValueError("OPENWEATHER_API_KEY not found in .env file")
self.base_url = "https://api.openweathermap.org/data/2.5/weather"
self.cache = {}
self.cache_duration = timedelta(minutes=10) # Cache for 10 minutes

def get_cached_weather(self, city: str):
"""Retrieves weather data from cache if it's still valid."""
if city in self.cache:
data, timestamp = self.cache[city]
if datetime.now() - timestamp < self.cache_duration:
return data
return None

def fetch_weather(self, city: str):
"""Fetches fresh weather data from the API for a given city."""
cached_data = self.get_cached_weather(city)
if cached_data:
return cached_data

try:
params = {
"q": city,
"appid": self.api_key,
"units": "metric" # Use metric units
}
response = requests.get(self.base_url, params=params)
response.raise_for_status()
weather_data = response.json()
self.cache[city] = (weather_data, datetime.now())
return weather_data
except requests.exceptions.RequestException as e:
print(f"Error fetching weather for {city}: {e}")
return None

def get_temperature(self, city: str):
"""Returns the temperature in Celsius for a given city."""
weather_data = self.fetch_weather(city)
if weather_data:
return weather_data['main']['temp']
return None

def get_full_weather(self, city: str):
"""Returns a formatted string with all relevant weather information."""
weather_data = self.fetch_weather(city)
if not weather_data:
return f"Could not retrieve weather for {city}."

temp = weather_data['main']['temp']
feels_like = weather_data['main']['feels_like']
humidity = weather_data['main']['humidity']
description = weather_data['weather'][0]['description']
wind_speed = weather_data['wind']['speed']

return (f"Location: {city}\n"
f"Temperature: {temp}°C\n"
f"Feels like: {feels_like}°C\n"
f"Conditions: {description}\n"
f"Humidity: {humidity}%\n"
f"Wind Speed: {wind_speed} m/s")
Loading