diff --git a/.gitignore b/.gitignore index b6e4761..094812e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ __pycache__/ *.py[cod] *$py.class - +.idea/ # C extensions *.so diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/Weather.iml b/.idea/Weather.iml deleted file mode 100644 index 26cf95d..0000000 --- a/.idea/Weather.iml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index ffe444e..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index d517855..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index ea03d89..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f4db013..f105d01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,6 @@ requests==2.32.4 python-dotenv==1.0.0 PyGithub>=2.1.0 -pyyaml>=6.0 \ No newline at end of file +pyyaml>=6.0 + +textual diff --git a/weather-tui/css/weather.tcss b/weather-tui/css/weather.tcss new file mode 100644 index 0000000..f2478b8 --- /dev/null +++ b/weather-tui/css/weather.tcss @@ -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; +} diff --git a/weather-tui/weather_app.py b/weather-tui/weather_app.py new file mode 100644 index 0000000..a1bfcf5 --- /dev/null +++ b/weather-tui/weather_app.py @@ -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() diff --git a/weather-tui/weather_service.py b/weather-tui/weather_service.py new file mode 100644 index 0000000..10cd42a --- /dev/null +++ b/weather-tui/weather_service.py @@ -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")