From fd31299695853a69bd6dba3499abab88c6845c9b Mon Sep 17 00:00:00 2001 From: Aleksandar Date: Wed, 17 Dec 2025 23:13:30 +0200 Subject: [PATCH] Add 2 solutions to workshop --- .DS_Store | Bin 0 -> 8196 bytes workshops/.DS_Store | Bin 0 -> 6148 bytes workshops/workshop2/.DS_Store | Bin 0 -> 6148 bytes .../workshop2/solution-abstractions/README.md | 9 ++ .../workshop2/solution-abstractions/cli.py | 55 +++++++++++ .../workshop2/solution-abstractions/gui.py | 44 +++++++++ .../solution-abstractions/pyproject.toml | 30 ++++++ .../src/raincheck_abstractions/__init__.py | 1 + .../src/raincheck_abstractions/models.py | 19 ++++ .../services/__init__.py | 0 .../raincheck_abstractions/services/base.py | 11 +++ .../services/constants.py | 0 .../services/openmeteo.py | 30 ++++++ .../services/registry.py | 27 ++++++ .../raincheck_abstractions/services/wttrin.py | 49 ++++++++++ .../src/raincheck_abstractions/weather.py | 21 +++++ .../tests/test_weather.py | 65 +++++++++++++ .../tests/test_wttrin_parsing.py | 79 ++++++++++++++++ .../workshop2/solution-simpler/README.md | 5 + workshops/workshop2/solution-simpler/cli.py | 55 +++++++++++ workshops/workshop2/solution-simpler/gui.py | 42 +++++++++ .../workshop2/solution-simpler/pyproject.toml | 27 ++++++ .../src/raincheck_simpler/__init__.py | 1 + .../src/raincheck_simpler/client.py | 13 +++ .../src/raincheck_simpler/models.py | 19 ++++ .../src/raincheck_simpler/services.py | 33 +++++++ .../solution-simpler/tests/test_services.py | 89 ++++++++++++++++++ 27 files changed, 724 insertions(+) create mode 100644 .DS_Store create mode 100644 workshops/.DS_Store create mode 100644 workshops/workshop2/.DS_Store create mode 100644 workshops/workshop2/solution-abstractions/README.md create mode 100644 workshops/workshop2/solution-abstractions/cli.py create mode 100644 workshops/workshop2/solution-abstractions/gui.py create mode 100644 workshops/workshop2/solution-abstractions/pyproject.toml create mode 100644 workshops/workshop2/solution-abstractions/src/raincheck_abstractions/__init__.py create mode 100644 workshops/workshop2/solution-abstractions/src/raincheck_abstractions/models.py create mode 100644 workshops/workshop2/solution-abstractions/src/raincheck_abstractions/services/__init__.py create mode 100644 workshops/workshop2/solution-abstractions/src/raincheck_abstractions/services/base.py create mode 100644 workshops/workshop2/solution-abstractions/src/raincheck_abstractions/services/constants.py create mode 100644 workshops/workshop2/solution-abstractions/src/raincheck_abstractions/services/openmeteo.py create mode 100644 workshops/workshop2/solution-abstractions/src/raincheck_abstractions/services/registry.py create mode 100644 workshops/workshop2/solution-abstractions/src/raincheck_abstractions/services/wttrin.py create mode 100644 workshops/workshop2/solution-abstractions/src/raincheck_abstractions/weather.py create mode 100644 workshops/workshop2/solution-abstractions/tests/test_weather.py create mode 100644 workshops/workshop2/solution-abstractions/tests/test_wttrin_parsing.py create mode 100644 workshops/workshop2/solution-simpler/README.md create mode 100644 workshops/workshop2/solution-simpler/cli.py create mode 100644 workshops/workshop2/solution-simpler/gui.py create mode 100644 workshops/workshop2/solution-simpler/pyproject.toml create mode 100644 workshops/workshop2/solution-simpler/src/raincheck_simpler/__init__.py create mode 100644 workshops/workshop2/solution-simpler/src/raincheck_simpler/client.py create mode 100644 workshops/workshop2/solution-simpler/src/raincheck_simpler/models.py create mode 100644 workshops/workshop2/solution-simpler/src/raincheck_simpler/services.py create mode 100644 workshops/workshop2/solution-simpler/tests/test_services.py diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..02da40c817b0a9749a68337c4ad2fabd94878709 GIT binary patch literal 8196 zcmeHMO>fgm6unalb;tvXC{iWFlb_fYXleNpoAirEB}yw&U;!xDNhk)#4v9mks-mpn zKkygW@=N#`?BD8f?wxUKJ1rj@1Y%ryu4CVG?wvE&Ga8qO)IzT@Lo`W54oQRTfL*Dj=Rlq7>6|f3e1*`(Uf&y5x`EU+c_w`fTS_P~E zf20CDKe#yAR*W5JD^mv=*#f|h;AG!NGI8O{Cy>t(~b1YCmCrKty)xM~=d={2%yf+g)c zXLifv7HaN%MvBKb%Xbuf*9_lHAY@z%7#^)G<`gj34Q3VUhZ3LbiPiB$k0G^X46`i9 zP@idyGwvmrSlb~Dc%0Pn%s4qfrnUVqa!&I35>(Im{{`=~hF4J)4as?Ev{>sL?gVgY zRW6D$JnAkqXO3n*Dl3~qbJVm(H%!m=sX_&HbGQlM(kgraoR5g@Z&x$xo8vhHp0w8P zL05?luEo6`;EL~lWj+w?-(S?=ItM78)#?ZM^E;UJKLt%+BR9^RtG#B_eU59M zMY`Z=Q6nE!Y9pI#NoLO$Fnz@C(2Jorm+K*m7^B;?0`-^a4(62Q0zX$l)_W8=1jwbH z_mff1`jx*Ydl|*gnNcj!UF0!OOSB5RPk&`>@`&AjO3&Lv{pGjk8wkJOZJRagqu&mL zZZbXnNsSyDJ$z)$8FR|cB5Bb+d9EW~-?fLhEX1_K*u^2~*7evjD z0D?vnE{`4rQ6ugz#J#A|6>L>Ma7s?8Ha@w%JvTcu<<88_?o7Gc*Dp;^xpT8uc6Lh6 z>2nuv+^xL`TT%QL)q+DQZRwb?sp0vvv^VCd7O$$ED2yXsr&45!_z`VTfxNu8gS| gJJ1$)aQyTS0a%zMuwDOUi54yF`k$`(f6V&-1+|#7u>b%7 literal 0 HcmV?d00001 diff --git a/workshops/.DS_Store b/workshops/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..3656fec72c7b3818e5802de8b7bc2ad26b5396de GIT binary patch literal 6148 zcmeHKO-sW-5S?wSCWz33LXQEjMQasNyu_%7UXAEMr6#s$FlI~BnnNk%tUt!9zr??z zGrL={q~cAA%)soM-JO?B-iG}E0O1WgTL4u6u+RxhHWo9C`pFAcGA$KEp`MY#JxJ)m zRx2)KbHh9;Kyzn9hO^d(2lzIB@-~cRh`sb{9HMW0D8pzLquGk57_YxG!Yu8XG2X&n z9HoOs<6V@>m4(G6Ysp%+uKjzN`Ga6EOue9gNwqU6<8TrO;d#`}+SSz~nGAv`>Gl;N z>U1&X>LN-yGV|mx>7kiAd>~^&_8g1`2>P~aFF|IqK{U*jc+q>hjZLO_u9-Oux z;>ScjtFHxtf48y~gA;hcpr~H+$?qqzOzyCE!kjP|Spimn6_^JF%-Uov&%?{`JFo(* zz+5Rn>w|+%=o-v5s-*)9bp=4AW27>S>A*-F$DnI4*N8o6LQ@enRhTP=&~$8fOrC2n z*Qn_r%;iIvk%hUT2sJuB-%;rxT#a0^0<6Ha0%cvT(*A$)^ZkFC#5Goc75G;Qh{~~l z?BOSwy|wUhwAVW5H|S)Pmuvi@V4${Q%%!ckhOP|T9SRU#gSkd*LF0pfl7S0W;7=9! E1d>2#5C8xG literal 0 HcmV?d00001 diff --git a/workshops/workshop2/.DS_Store b/workshops/workshop2/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..29975e069c1d659df4d8c4320c4d888b1e3f9600 GIT binary patch literal 6148 zcmeHKL2uJA6n@?cO_`8#VA773cBG6_rcD!*cB&CO{QqJ&W zxbjQ*JL7w{huU>8!2zMoPkR2IpZz)Ym)eerNc1Mj6C$68JUCiy3h>%((FxY=A@ylt{i?G#Q!!e3YdD1OoNGfwv`Hl? z%4k9rrHFERL=nZvR#YH5`Dyk-BFDhPh?Nq5OzKEmvWjdS={0GlfUPWm>EHoiahUoI_Q~7(actrmS)l29^od0x^LGTlnE z|59(`R?gl{+9}`^_?-g0KS(%ZV6ir+w+%tqn1MFfd123@p|L(E<~O3N%z< zuNcD6k(ah#V6ir6=p^jrL)a$^dqWZW=@?(??j!<(u5=1G1(p?9H&vg{|D#{u|CdRw z<`i%W+$aS^qaXHrn36qPS0=}2t%SdWvvI%Lpr)X($FUChDBgl=L!0CP7+9=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