diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..02da40c Binary files /dev/null and b/.DS_Store differ diff --git a/workshops/.DS_Store b/workshops/.DS_Store new file mode 100644 index 0000000..3656fec Binary files /dev/null and b/workshops/.DS_Store differ diff --git a/workshops/workshop2/.DS_Store b/workshops/workshop2/.DS_Store new file mode 100644 index 0000000..29975e0 Binary files /dev/null and b/workshops/workshop2/.DS_Store differ diff --git a/workshops/workshop2/solution-abstractions/README.md b/workshops/workshop2/solution-abstractions/README.md new file mode 100644 index 0000000..c5e8778 --- /dev/null +++ b/workshops/workshop2/solution-abstractions/README.md @@ -0,0 +1,9 @@ +# По-сложно решение + +* с абстракции за доставчиците на метеоролигични данни +* `Weather` клас, на който може да се посочи кой доставчик да се използва - и не зависи от конкретни доставчици +* добавен е `registry` модул, който регистрира и инстанцира доставчиците +* тука services са зависими от конкретния доставчик и са посредници между API-то на доставчика и `Weather` +* предимство: много по-лесно се добавят нови доставчици и функционалности +* предимство: кодът е доста по-тестваем - дадени са примерни тестове за wttr.in парсване на данните + за `Weather` класа с mock service +* недостатък: ако залитнем изцяло в тази посока лесно да прекалим с абстракциите и да се окаже, че не всички ни трябват, жертвайки четимост на код \ No newline at end of file diff --git a/workshops/workshop2/solution-abstractions/cli.py b/workshops/workshop2/solution-abstractions/cli.py new file mode 100644 index 0000000..1f057d8 --- /dev/null +++ b/workshops/workshop2/solution-abstractions/cli.py @@ -0,0 +1,55 @@ +from rich.console import Console +from rich.table import Table +import typer + +from raincheck_abstractions import Weather + + +app = typer.Typer() + + +@app.command() +def current(location: str, provider: str | None = None): + weather_info = Weather(provider).current(location) + + table = Table(title=f"Current Weather in {weather_info.location_name}") + table.add_column("Temperature (°C)", justify="right") + table.add_column("Humidity (%)", justify="right") + table.add_column("Wind Speed (km/h)", justify="right") + table.add_column("Precipitation (mm)", justify="right") + table.add_column("Condition", justify="left") + + table.add_row( + f"{weather_info.temperature_C}", + f"{weather_info.humidity}", + f"{weather_info.wind_speed_kph}", + f"{weather_info.precipitation_mm}", + weather_info.condition, + ) + console = Console() + console.print(table) + + +@app.command() +def daily(location: str, provider: str | None = None): + forecast_info = Weather(provider).daily_forecast(location) + + table = Table(title=f"Daily Weather Forecast for {location}") + table.add_column("Date", justify="left") + table.add_column("Max Temp (°C)", justify="right") + table.add_column("Avg Temp (°C)", justify="right") + table.add_column("Min Temp (°C)", justify="right") + + for day in forecast_info: + table.add_row( + day.date, + f"{day.max_temperature_C}", + f"{day.avg_temperature_C}", + f"{day.min_temperature_C}", + ) + console = Console() + console.print(table) + + +if __name__ == "__main__": + app() diff --git a/workshops/workshop2/solution-abstractions/gui.py b/workshops/workshop2/solution-abstractions/gui.py new file mode 100644 index 0000000..b9dd3eb --- /dev/null +++ b/workshops/workshop2/solution-abstractions/gui.py @@ -0,0 +1,44 @@ +import streamlit as st + +from raincheck_abstractions import Weather + + +weather = Weather() + + +def display_current_weather(location: str): + weather_info = weather.current(location) + st.header(f"Current Weather in {weather_info.location_name}") + st.metric(label="Temperature (°C)", value=weather_info.temperature_C) + st.metric(label="Humidity (%)", value=weather_info.humidity) + st.metric(label="Wind Speed (km/h)", value=weather_info.wind_speed_kph) + st.metric(label="Precipitation (mm)", value=weather_info.precipitation_mm) + st.write(f"Condition: {weather_info.condition}") + + +def display_daily_forecast(location: str): + forecast_info = weather.daily_forecast(location) + + st.header(f"Daily Weather Forecast for {location}") + for day in forecast_info: + st.subheader(day.date) + st.metric(label="Max Temperature (°C)", value=day.max_temperature_C) + st.metric(label="Avg Temperature (°C)", value=day.avg_temperature_C) + st.metric(label="Min Temperature (°C)", value=day.min_temperature_C) + st.markdown("---") + + +def main(): + st.title("RainCheck Weather App") + + location = st.text_input("Enter Location", "Sofia") + + if st.button("Get Current Weather"): + display_current_weather(location) + + if st.button("Get Daily Forecast"): + display_daily_forecast(location) + + +if __name__ == "__main__": + main() diff --git a/workshops/workshop2/solution-abstractions/pyproject.toml b/workshops/workshop2/solution-abstractions/pyproject.toml new file mode 100644 index 0000000..122c4b4 --- /dev/null +++ b/workshops/workshop2/solution-abstractions/pyproject.toml @@ -0,0 +1,30 @@ +[project] +name = "workshop2-more-abstract-solution" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "requests>=2.32.5", +] + +[project.optional-dependencies] +cli = [ + "rich>=14.2.0", + "typer>=0.20.0", +] +gui = [ + "streamlit>=1.52.1", +] + + +[tool.setuptools.packages.find] +where = ["src"] + +[dependency-groups] +dev = [ + "pytest>=9.0.2", +] + +[tool.pytest.ini_options] +pythonpath = ["src"] \ No newline at end of file diff --git a/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/__init__.py b/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/__init__.py new file mode 100644 index 0000000..2e7f97c --- /dev/null +++ b/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/__init__.py @@ -0,0 +1 @@ +from .weather import * diff --git a/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/models.py b/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/models.py new file mode 100644 index 0000000..9e2c502 --- /dev/null +++ b/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/models.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass + + +@dataclass +class CurrentWeatherInfo: + location_name: str + temperature_C: float + humidity: float + wind_speed_kph: float + precipitation_mm: float + condition: str + + +@dataclass +class DailyForecastInfo: + date: str + avg_temperature_C: float + max_temperature_C: float + min_temperature_C: float diff --git a/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/services/__init__.py b/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/services/base.py b/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/services/base.py new file mode 100644 index 0000000..0d8a739 --- /dev/null +++ b/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/services/base.py @@ -0,0 +1,11 @@ +from abc import ABC, abstractmethod + +from raincheck_abstractions.models import CurrentWeatherInfo, DailyForecastInfo + + +class WeatherServiceBase(ABC): + @abstractmethod + def get_current_weather_info(self, location: str) -> CurrentWeatherInfo: ... + + @abstractmethod + def get_daily_weather_forecast(self, location: str) -> list[DailyForecastInfo]: ... diff --git a/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/services/constants.py b/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/services/constants.py new file mode 100644 index 0000000..e69de29 diff --git a/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/services/openmeteo.py b/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/services/openmeteo.py new file mode 100644 index 0000000..77190e2 --- /dev/null +++ b/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/services/openmeteo.py @@ -0,0 +1,30 @@ +# тук можете да наследите WeatherServiceBase за да имплементирате OpenMeteo-специфична логика + +# import openmeteo_sdk - тежък импорт + +from raincheck_abstractions.models import CurrentWeatherInfo, DailyForecastInfo +from raincheck_abstractions.services.base import WeatherServiceBase + + +class OpenMeteoWeatherService(WeatherServiceBase): + def get_current_weather_info(self, location: str) -> CurrentWeatherInfo: + # dummy implementation + return CurrentWeatherInfo( + location_name=location, + temperature_C=0.0, + humidity=0.0, + wind_speed_kph=0.0, + precipitation_mm=0.0, + condition="Unknown", + ) + + def get_daily_weather_forecast(self, location: str) -> list[DailyForecastInfo]: + # dummy implementation + return [ + DailyForecastInfo( + date="2026-01-01", + avg_temperature_C=0.0, + max_temperature_C=0.0, + min_temperature_C=0.0, + ), + ] diff --git a/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/services/registry.py b/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/services/registry.py new file mode 100644 index 0000000..ee45421 --- /dev/null +++ b/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/services/registry.py @@ -0,0 +1,27 @@ +from enum import StrEnum + +from raincheck_abstractions.services.base import WeatherServiceBase +from raincheck_abstractions.services.wttrin import WttrinWeatherService +from raincheck_abstractions.services.openmeteo import OpenMeteoWeatherService + + +class WeatherProvider(StrEnum): + WTTRIN = "wttrin" + OPENMETEO = "openmeteo" + + +DEFAULT_PROVIDER = WeatherProvider.WTTRIN + + +_REGISTRY: dict[str, type[WeatherServiceBase]] = { + WeatherProvider.WTTRIN: WttrinWeatherService, + WeatherProvider.OPENMETEO: OpenMeteoWeatherService, +} + + +def get_weather_service(provider_name: str) -> WeatherServiceBase: + provider_class = _REGISTRY.get(provider_name) + if not provider_class: + raise ValueError(f"Unknown weather provider: {provider_name}") + + return provider_class() # инстанцираме конкретния клас diff --git a/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/services/wttrin.py b/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/services/wttrin.py new file mode 100644 index 0000000..3e4ce37 --- /dev/null +++ b/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/services/wttrin.py @@ -0,0 +1,49 @@ +import requests + +from raincheck_abstractions.models import CurrentWeatherInfo, DailyForecastInfo +from raincheck_abstractions.services.base import WeatherServiceBase + + +_WEATHER_API_URL = "https://wttr.in/{city}" + + +class WttrinWeatherService(WeatherServiceBase): + def get_current_weather_info(self, location: str) -> CurrentWeatherInfo: + weather_data = self._make_request(location) + return self._parse_current_weather(weather_data, location) + + def get_daily_weather_forecast(self, location: str) -> list[DailyForecastInfo]: + weather_data = self._make_request(location) + return self._parse_daily_forecast(weather_data) + + def _make_request(self, location: str) -> dict: + response = requests.get( + _WEATHER_API_URL.format(city=location), + params={"format": "j1"}, + timeout=15, + ) + response.raise_for_status() + return response.json() + + def _parse_current_weather(self, data: dict, location: str) -> CurrentWeatherInfo: + current_condition = data["current_condition"][0] + return CurrentWeatherInfo( + location_name=location, + temperature_C=float(current_condition["temp_C"]), + humidity=float(current_condition["humidity"]), + wind_speed_kph=float(current_condition["windspeedKmph"]), + precipitation_mm=float(current_condition.get("precipMM", 0.0)), + condition=current_condition["weatherDesc"][0]["value"], + ) + + def _parse_daily_forecast(self, data: dict) -> list[DailyForecastInfo]: + forecast_data = data["weather"] + return [ + DailyForecastInfo( + date=day["date"], + avg_temperature_C=float(day["avgtempC"]), + max_temperature_C=float(day["maxtempC"]), + min_temperature_C=float(day["mintempC"]), + ) + for day in forecast_data + ] diff --git a/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/weather.py b/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/weather.py new file mode 100644 index 0000000..ccb9a7b --- /dev/null +++ b/workshops/workshop2/solution-abstractions/src/raincheck_abstractions/weather.py @@ -0,0 +1,21 @@ +"""Описва основния клас на библиотеката.""" + +from raincheck_abstractions.models import CurrentWeatherInfo, DailyForecastInfo +from raincheck_abstractions.services.registry import ( + DEFAULT_PROVIDER, + get_weather_service, + WeatherProvider, +) + + +class Weather: + def __init__(self, provider_name: WeatherProvider | None = None): + if provider_name is None: + provider_name = DEFAULT_PROVIDER + self._service = get_weather_service(provider_name) + + def current(self, location: str) -> CurrentWeatherInfo: + return self._service.get_current_weather_info(location) + + def daily_forecast(self, location: str) -> list[DailyForecastInfo]: + return self._service.get_daily_weather_forecast(location) diff --git a/workshops/workshop2/solution-abstractions/tests/test_weather.py b/workshops/workshop2/solution-abstractions/tests/test_weather.py new file mode 100644 index 0000000..64802cb --- /dev/null +++ b/workshops/workshop2/solution-abstractions/tests/test_weather.py @@ -0,0 +1,65 @@ +import pytest + +from raincheck_abstractions import Weather +from raincheck_abstractions.models import CurrentWeatherInfo, DailyForecastInfo + + +@pytest.fixture +def mock_current_weather_info() -> CurrentWeatherInfo: + return CurrentWeatherInfo( + location_name="MockCity", + temperature_C=25.0, + humidity=50.0, + wind_speed_kph=10.0, + precipitation_mm=5.0, + condition="Sunny", + ) + + +@pytest.fixture +def mock_daily_forecast_info() -> list[DailyForecastInfo]: + return [ + DailyForecastInfo( + date="2024-01-01", + avg_temperature_C=20.0, + max_temperature_C=25.0, + min_temperature_C=15.0, + ), + DailyForecastInfo( + date="2024-01-02", + avg_temperature_C=22.0, + max_temperature_C=27.0, + min_temperature_C=17.0, + ), + ] + + +@pytest.fixture(autouse=True) +def mock_service(mock_current_weather_info, mock_daily_forecast_info): + global _REGISTRY + + from raincheck_abstractions.services.base import WeatherServiceBase + from raincheck_abstractions.services.registry import _REGISTRY + + class WttrinServiceMock(WeatherServiceBase): + def get_current_weather_info(self, location: str) -> CurrentWeatherInfo: + return mock_current_weather_info + + def get_daily_weather_forecast(self, location: str) -> list[DailyForecastInfo]: + return mock_daily_forecast_info + + _REGISTRY["MOCK"] = WttrinServiceMock + + return WttrinServiceMock() + + +def test_mock_current_weather_info(mock_current_weather_info): + weather = Weather("MOCK") + current_info = weather.current("TEST LOCATION") + assert current_info == mock_current_weather_info + + +def test_mock_daily_forecast_info(mock_daily_forecast_info): + weather = Weather("MOCK") + daily_info = weather.daily_forecast("TEST LOCATION") + assert daily_info == mock_daily_forecast_info diff --git a/workshops/workshop2/solution-abstractions/tests/test_wttrin_parsing.py b/workshops/workshop2/solution-abstractions/tests/test_wttrin_parsing.py new file mode 100644 index 0000000..9a53d20 --- /dev/null +++ b/workshops/workshop2/solution-abstractions/tests/test_wttrin_parsing.py @@ -0,0 +1,79 @@ +from raincheck_abstractions.services.wttrin import WttrinWeatherService +from raincheck_abstractions.models import CurrentWeatherInfo, DailyForecastInfo + + +def test_parse_current_weather_info(): + mock_response = { + "current_condition": [ + { + "temp_C": "22", + "humidity": "42", + "windspeedKmph": "8", + "precipMM": "0.0", + "weatherDesc": [{"value": "Partly cloudy"}], + } + ] + } + + expoected_result = CurrentWeatherInfo( + location_name="TestCity", + temperature_C=22, + humidity=42, + wind_speed_kph=8, + precipitation_mm=0.0, + condition="Partly cloudy", + ) + + result = WttrinWeatherService()._parse_current_weather(mock_response, "TestCity") + + assert result == expoected_result + + +def test_parse_daily_weather_forecast(): + mock_response = { + "weather": [ + { + "date": "2023-10-01", + "avgtempC": "20", + "maxtempC": "25", + "mintempC": "15", + }, + { + "date": "2023-10-02", + "avgtempC": "21", + "maxtempC": "26", + "mintempC": "16", + }, + { + "date": "2023-10-03", + "avgtempC": "19", + "maxtempC": "24", + "mintempC": "14", + }, + ] + } + + expected_result = [ + DailyForecastInfo( + date="2023-10-01", + avg_temperature_C=20, + max_temperature_C=25, + min_temperature_C=15, + ), + DailyForecastInfo( + date="2023-10-02", + avg_temperature_C=21, + max_temperature_C=26, + min_temperature_C=16, + ), + DailyForecastInfo( + date="2023-10-03", + avg_temperature_C=19, + max_temperature_C=24, + min_temperature_C=14, + ), + ] + + result = WttrinWeatherService()._parse_daily_forecast(mock_response) + + assert result == expected_result diff --git a/workshops/workshop2/solution-simpler/README.md b/workshops/workshop2/solution-simpler/README.md new file mode 100644 index 0000000..7216f56 --- /dev/null +++ b/workshops/workshop2/solution-simpler/README.md @@ -0,0 +1,5 @@ +# По-просто решение + +* без много абстракции +* минимално разделение, което има смисъл: services (бизнес логика), client (HTTP заявки), models (структури от данни) +* недостатъкът са тестовете, които трябва да използват `patch`, за да симулират HTTP заявки \ No newline at end of file diff --git a/workshops/workshop2/solution-simpler/cli.py b/workshops/workshop2/solution-simpler/cli.py new file mode 100644 index 0000000..bf16b87 --- /dev/null +++ b/workshops/workshop2/solution-simpler/cli.py @@ -0,0 +1,55 @@ +from rich.console import Console +from rich.table import Table +import typer + +from raincheck_simpler import get_current_weather_info, get_daily_weather_forecast + + +app = typer.Typer() + + +@app.command() +def current(location: str): + weather_info = get_current_weather_info(location) + + table = Table(title=f"Current Weather in {weather_info.location_name}") + table.add_column("Temperature (°C)", justify="right") + table.add_column("Humidity (%)", justify="right") + table.add_column("Wind Speed (km/h)", justify="right") + table.add_column("Precipitation (mm)", justify="right") + table.add_column("Condition", justify="left") + + table.add_row( + f"{weather_info.temperature_C}", + f"{weather_info.humidity}", + f"{weather_info.wind_speed_kph}", + f"{weather_info.precipitation_mm}", + weather_info.condition, + ) + console = Console() + console.print(table) + + +@app.command() +def daily(location: str): + forecast_info = get_daily_weather_forecast(location) + + table = Table(title=f"Daily Weather Forecast for {location}") + table.add_column("Date", justify="left") + table.add_column("Max Temp (°C)", justify="right") + table.add_column("Avg Temp (°C)", justify="right") + table.add_column("Min Temp (°C)", justify="right") + + for day in forecast_info: + table.add_row( + day.date, + f"{day.max_temperature_C}", + f"{day.avg_temperature_C}", + f"{day.min_temperature_C}", + ) + console = Console() + console.print(table) + + +if __name__ == "__main__": + app() diff --git a/workshops/workshop2/solution-simpler/gui.py b/workshops/workshop2/solution-simpler/gui.py new file mode 100644 index 0000000..f372cad --- /dev/null +++ b/workshops/workshop2/solution-simpler/gui.py @@ -0,0 +1,42 @@ +import streamlit as st + +from raincheck_simpler import get_current_weather_info, get_daily_weather_forecast + + +def display_current_weather(location: str): + weather_info = get_current_weather_info(location) + + st.header(f"Current Weather in {weather_info.location_name}") + st.metric(label="Temperature (°C)", value=weather_info.temperature_C) + st.metric(label="Humidity (%)", value=weather_info.humidity) + st.metric(label="Wind Speed (km/h)", value=weather_info.wind_speed_kph) + st.metric(label="Precipitation (mm)", value=weather_info.precipitation_mm) + st.write(f"Condition: {weather_info.condition}") + + +def display_daily_forecast(location: str): + forecast_info = get_daily_weather_forecast(location) + + st.header(f"Daily Weather Forecast for {location}") + for day in forecast_info: + st.subheader(day.date) + st.metric(label="Max Temperature (°C)", value=day.max_temperature_C) + st.metric(label="Avg Temperature (°C)", value=day.avg_temperature_C) + st.metric(label="Min Temperature (°C)", value=day.min_temperature_C) + st.markdown("---") + + +def main(): + st.title("RainCheck Weather App") + + location = st.text_input("Enter Location", "Sofia") + + if st.button("Get Current Weather"): + display_current_weather(location) + + if st.button("Get Daily Forecast"): + display_daily_forecast(location) + + +if __name__ == "__main__": + main() diff --git a/workshops/workshop2/solution-simpler/pyproject.toml b/workshops/workshop2/solution-simpler/pyproject.toml new file mode 100644 index 0000000..5b7cd29 --- /dev/null +++ b/workshops/workshop2/solution-simpler/pyproject.toml @@ -0,0 +1,27 @@ +[project] +name = "workshop2-simpler-solution" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "requests>=2.32.5", +] + +[project.optional-dependencies] +cli = [ + "rich>=14.2.0", + "typer>=0.20.0", +] +gui = [ + "streamlit>=1.52.1", +] + + +[tool.setuptools.packages.find] +where = ["src"] + +[dependency-groups] +dev = [ + "pytest>=9.0.2", +] diff --git a/workshops/workshop2/solution-simpler/src/raincheck_simpler/__init__.py b/workshops/workshop2/solution-simpler/src/raincheck_simpler/__init__.py new file mode 100644 index 0000000..9cf69fb --- /dev/null +++ b/workshops/workshop2/solution-simpler/src/raincheck_simpler/__init__.py @@ -0,0 +1 @@ +from .services import * diff --git a/workshops/workshop2/solution-simpler/src/raincheck_simpler/client.py b/workshops/workshop2/solution-simpler/src/raincheck_simpler/client.py new file mode 100644 index 0000000..847157f --- /dev/null +++ b/workshops/workshop2/solution-simpler/src/raincheck_simpler/client.py @@ -0,0 +1,13 @@ +import requests + + +_WEATHER_API_URL = "https://wttr.in/{city}" + +def fetch_weather(location: str) -> dict: + response = requests.get( + _WEATHER_API_URL.format(city=location), + params={"format": "j1"}, + timeout=15, + ) + response.raise_for_status() + return response.json() diff --git a/workshops/workshop2/solution-simpler/src/raincheck_simpler/models.py b/workshops/workshop2/solution-simpler/src/raincheck_simpler/models.py new file mode 100644 index 0000000..9e2c502 --- /dev/null +++ b/workshops/workshop2/solution-simpler/src/raincheck_simpler/models.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass + + +@dataclass +class CurrentWeatherInfo: + location_name: str + temperature_C: float + humidity: float + wind_speed_kph: float + precipitation_mm: float + condition: str + + +@dataclass +class DailyForecastInfo: + date: str + avg_temperature_C: float + max_temperature_C: float + min_temperature_C: float diff --git a/workshops/workshop2/solution-simpler/src/raincheck_simpler/services.py b/workshops/workshop2/solution-simpler/src/raincheck_simpler/services.py new file mode 100644 index 0000000..d0e192d --- /dev/null +++ b/workshops/workshop2/solution-simpler/src/raincheck_simpler/services.py @@ -0,0 +1,33 @@ +from raincheck_simpler.client import fetch_weather +from raincheck_simpler.models import CurrentWeatherInfo, DailyForecastInfo + + +def get_current_weather_info(location: str) -> CurrentWeatherInfo: + weather_data = fetch_weather(location) + current_condition = weather_data["current_condition"][0] + + return CurrentWeatherInfo( + location_name=location, + temperature_C=float(current_condition["temp_C"]), + humidity=float(current_condition["humidity"]), + wind_speed_kph=float(current_condition["windspeedKmph"]), + precipitation_mm=float(current_condition.get("precipMM", 0.0)), + condition=current_condition["weatherDesc"][0]["value"], + ) + + +def get_daily_weather_forecast(location: str) -> list[DailyForecastInfo]: + weather_data = fetch_weather(location) + forecast_data = weather_data["weather"] + + forecast_list = [ + DailyForecastInfo( + date=day["date"], + avg_temperature_C=float(day["avgtempC"]), + max_temperature_C=float(day["maxtempC"]), + min_temperature_C=float(day["mintempC"]), + ) + for day in forecast_data + ] + + return forecast_list diff --git a/workshops/workshop2/solution-simpler/tests/test_services.py b/workshops/workshop2/solution-simpler/tests/test_services.py new file mode 100644 index 0000000..d01ec2f --- /dev/null +++ b/workshops/workshop2/solution-simpler/tests/test_services.py @@ -0,0 +1,89 @@ +from unittest.mock import patch + +from raincheck_simpler import get_current_weather_info, get_daily_weather_forecast +from raincheck_simpler.models import CurrentWeatherInfo, DailyForecastInfo + + +def test_get_current_weather_info_ok_response(): + + mock_response = { + "current_condition": [ + { + "temp_C": "22", + "humidity": "42", + "windspeedKmph": "8", + "precipMM": "0.0", + "weatherDesc": [{"value": "Partly cloudy"}], + } + ] + } + + expoected_result = CurrentWeatherInfo( + location_name="TestCity", + temperature_C=22, + humidity=42, + wind_speed_kph=8, + precipitation_mm=0.0, + condition="Partly cloudy", + ) + + with patch( + "raincheck_simpler.services.fetch_weather", return_value=mock_response + ): + result = get_current_weather_info("TestCity") + + assert result == expoected_result + + +def test_get_daily_weather_forecast_ok_response(): + + mock_response = { + "weather": [ + { + "date": "2023-10-01", + "avgtempC": "20", + "maxtempC": "25", + "mintempC": "15", + }, + { + "date": "2023-10-02", + "avgtempC": "21", + "maxtempC": "26", + "mintempC": "16", + }, + { + "date": "2023-10-03", + "avgtempC": "19", + "maxtempC": "24", + "mintempC": "14", + }, + ] + } + + expected_result = [ + DailyForecastInfo( + date="2023-10-01", + avg_temperature_C=20, + max_temperature_C=25, + min_temperature_C=15, + ), + DailyForecastInfo( + date="2023-10-02", + avg_temperature_C=21, + max_temperature_C=26, + min_temperature_C=16, + ), + DailyForecastInfo( + date="2023-10-03", + avg_temperature_C=19, + max_temperature_C=24, + min_temperature_C=14, + ), + ] + + with patch( + "raincheck_simpler.services.fetch_weather", return_value=mock_response + ): + result = get_daily_weather_forecast("TestCity") + + assert result == expected_result