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
Binary file added .DS_Store
Binary file not shown.
Binary file added workshops/.DS_Store
Binary file not shown.
Binary file added workshops/workshop2/.DS_Store
Binary file not shown.
9 changes: 9 additions & 0 deletions workshops/workshop2/solution-abstractions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# По-сложно решение

* с абстракции за доставчиците на метеоролигични данни
* `Weather` клас, на който може да се посочи кой доставчик да се използва - и не зависи от конкретни доставчици
* добавен е `registry` модул, който регистрира и инстанцира доставчиците
* тука services са зависими от конкретния доставчик и са посредници между API-то на доставчика и `Weather`
* предимство: много по-лесно се добавят нови доставчици и функционалности
* предимство: кодът е доста по-тестваем - дадени са примерни тестове за wttr.in парсване на данните + за `Weather` класа с mock service
* недостатък: ако залитнем изцяло в тази посока лесно да прекалим с абстракциите и да се окаже, че не всички ни трябват, жертвайки четимост на код
55 changes: 55 additions & 0 deletions workshops/workshop2/solution-abstractions/cli.py
Original file line number Diff line number Diff line change
@@ -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()
44 changes: 44 additions & 0 deletions workshops/workshop2/solution-abstractions/gui.py
Original file line number Diff line number Diff line change
@@ -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()
30 changes: 30 additions & 0 deletions workshops/workshop2/solution-abstractions/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .weather import *
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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]: ...
Original file line number Diff line number Diff line change
@@ -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,
),
]
Original file line number Diff line number Diff line change
@@ -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() # инстанцираме конкретния клас
Original file line number Diff line number Diff line change
@@ -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
]
Original file line number Diff line number Diff line change
@@ -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)
65 changes: 65 additions & 0 deletions workshops/workshop2/solution-abstractions/tests/test_weather.py
Original file line number Diff line number Diff line change
@@ -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
Loading