weathergrabber 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. weathergrabber/__init__.py +0 -0
  2. weathergrabber/adapter/client/weather_api.py +27 -0
  3. weathergrabber/adapter/client/weather_search_api.py +50 -0
  4. weathergrabber/adapter/tty/console_tty.py +102 -0
  5. weathergrabber/adapter/tty/json_tty.py +19 -0
  6. weathergrabber/adapter/tty/waybar_tty.py +110 -0
  7. weathergrabber/cli.py +37 -0
  8. weathergrabber/core.py +31 -0
  9. weathergrabber/domain/adapter/icon_enum.py +5 -0
  10. weathergrabber/domain/adapter/mapper/air_quality_index_mapper.py +13 -0
  11. weathergrabber/domain/adapter/mapper/city_location_mapper.py +8 -0
  12. weathergrabber/domain/adapter/mapper/color_mapper.py +10 -0
  13. weathergrabber/domain/adapter/mapper/current_conditions_mapper.py +16 -0
  14. weathergrabber/domain/adapter/mapper/daily_predictions_mapper.py +15 -0
  15. weathergrabber/domain/adapter/mapper/day_night_mapper.py +12 -0
  16. weathergrabber/domain/adapter/mapper/forecast_mapper.py +20 -0
  17. weathergrabber/domain/adapter/mapper/health_activities_mapper.py +8 -0
  18. weathergrabber/domain/adapter/mapper/hourly_predictions_mapper.py +19 -0
  19. weathergrabber/domain/adapter/mapper/label_value_mapper.py +7 -0
  20. weathergrabber/domain/adapter/mapper/moon_phase_mapper.py +9 -0
  21. weathergrabber/domain/adapter/mapper/precipitation_mapper.py +7 -0
  22. weathergrabber/domain/adapter/mapper/search_mapper.py +7 -0
  23. weathergrabber/domain/adapter/mapper/sunrise_sunset_mapper.py +13 -0
  24. weathergrabber/domain/adapter/mapper/temperature_high_low_mapper.py +8 -0
  25. weathergrabber/domain/adapter/mapper/timestamp_mapper.py +8 -0
  26. weathergrabber/domain/adapter/mapper/today_details_mapper.py +21 -0
  27. weathergrabber/domain/adapter/mapper/uv_index_mapper.py +9 -0
  28. weathergrabber/domain/adapter/mapper/weather_icon_enum_mapper.py +8 -0
  29. weathergrabber/domain/adapter/mapper/wind_mapper.py +7 -0
  30. weathergrabber/domain/adapter/output_enum.py +6 -0
  31. weathergrabber/domain/adapter/params.py +58 -0
  32. weathergrabber/domain/air_quality_index.py +78 -0
  33. weathergrabber/domain/city_location.py +37 -0
  34. weathergrabber/domain/color.py +43 -0
  35. weathergrabber/domain/current_conditions.py +53 -0
  36. weathergrabber/domain/daily_predictions.py +58 -0
  37. weathergrabber/domain/day_night.py +50 -0
  38. weathergrabber/domain/forecast.py +68 -0
  39. weathergrabber/domain/health_activities.py +39 -0
  40. weathergrabber/domain/hourly_predictions.py +76 -0
  41. weathergrabber/domain/label_value.py +19 -0
  42. weathergrabber/domain/moon_phase.py +22 -0
  43. weathergrabber/domain/moon_phase_enum.py +65 -0
  44. weathergrabber/domain/precipitation.py +20 -0
  45. weathergrabber/domain/search.py +15 -0
  46. weathergrabber/domain/sunrise_sunset.py +40 -0
  47. weathergrabber/domain/temperature_hight_low.py +32 -0
  48. weathergrabber/domain/timestamp.py +39 -0
  49. weathergrabber/domain/today_details.py +79 -0
  50. weathergrabber/domain/uv_index.py +43 -0
  51. weathergrabber/domain/weather_icon_enum.py +58 -0
  52. weathergrabber/domain/wind.py +28 -0
  53. weathergrabber/service/extract_aqi_service.py +30 -0
  54. weathergrabber/service/extract_current_conditions_service.py +47 -0
  55. weathergrabber/service/extract_daily_forecast_oldstyle_service.py +51 -0
  56. weathergrabber/service/extract_daily_forecast_service.py +56 -0
  57. weathergrabber/service/extract_health_activities_service.py +25 -0
  58. weathergrabber/service/extract_hourly_forecast_oldstyle_service.py +50 -0
  59. weathergrabber/service/extract_hourly_forecast_service.py +64 -0
  60. weathergrabber/service/extract_temperature_service.py +18 -0
  61. weathergrabber/service/extract_today_details_service.py +85 -0
  62. weathergrabber/service/read_weather_service.py +23 -0
  63. weathergrabber/service/search_location_service.py +35 -0
  64. weathergrabber/usecase/use_case.py +85 -0
  65. weathergrabber/weathergrabber_application.py +78 -0
  66. weathergrabber-0.0.1.dist-info/METADATA +176 -0
  67. weathergrabber-0.0.1.dist-info/RECORD +70 -0
  68. weathergrabber-0.0.1.dist-info/WHEEL +5 -0
  69. weathergrabber-0.0.1.dist-info/entry_points.txt +2 -0
  70. weathergrabber-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,20 @@
1
+ class Precipitation:
2
+
3
+ def __init__(
4
+ self,
5
+ percentage: str,
6
+ amount: str = None
7
+ ):
8
+ self._percentage = percentage
9
+ self._amount = amount
10
+
11
+ @property
12
+ def percentage(self) -> str:
13
+ return self._percentage
14
+
15
+ @property
16
+ def amount(self) -> str:
17
+ return self._amount
18
+
19
+ def __repr__(self):
20
+ return f"Precipitation(percentage='{self.percentage}', amount='{self.amount}')"
@@ -0,0 +1,15 @@
1
+ class Search:
2
+ def __init__(self, id: str, search_name: str | None = None):
3
+ self._id = id
4
+ self._search_name = search_name
5
+
6
+ @property
7
+ def id(self) -> str:
8
+ return self._id
9
+
10
+ @property
11
+ def search_name(self) -> str:
12
+ return self._search_name
13
+
14
+ def __repr__(self):
15
+ return f"Search(id={self.id!r}, search_name={self.search_name!r})"
@@ -0,0 +1,40 @@
1
+ from .weather_icon_enum import WeatherIconEnum
2
+
3
+ class SunriseSunset:
4
+ class IconValue:
5
+ def __init__(self, icon: WeatherIconEnum, value: str):
6
+ self._icon = icon
7
+ self._value = value
8
+
9
+ @property
10
+ def icon(self) -> WeatherIconEnum:
11
+ return self._icon
12
+
13
+ @property
14
+ def value(self) -> str:
15
+ return self._value
16
+
17
+ def __str__(self):
18
+ return f"{self._icon.emoji_icon} {self._value}"
19
+
20
+ def __repr__(self):
21
+ return f"IconValue(icon={self._icon}, value='{self._value}')"
22
+
23
+
24
+ def __init__(self, sunrise: str, sunset: str):
25
+ self._sunrise = SunriseSunset.IconValue(WeatherIconEnum.SUNRISE, sunrise)
26
+ self._sunset = SunriseSunset.IconValue(WeatherIconEnum.SUNSET, sunset)
27
+
28
+ @property
29
+ def sunrise(self) -> "SunriseSunset.IconValue":
30
+ return self._sunrise
31
+
32
+ @property
33
+ def sunset(self) -> "SunriseSunset.IconValue":
34
+ return self._sunset
35
+
36
+ def __str__(self):
37
+ return f"Sunrise: {self._sunrise}, Sunset: {self._sunset}"
38
+
39
+ def __repr__(self):
40
+ return f"SunriseSunset(sunrise={self._sunrise}, sunset={self._sunset})"
@@ -0,0 +1,32 @@
1
+ class TemperatureHighLow:
2
+ def __init__(self, high: str, low: str, label:str = None):
3
+ self._high = high
4
+ self._low = low
5
+ self._label = label
6
+
7
+ @classmethod
8
+ def from_string(cls, data: str, label:str = None) -> 'TemperatureHighLow':
9
+ parts = data.split('/')
10
+ if len(parts) == 2:
11
+ high, low = parts
12
+ return cls(high=high.strip(), low=low.strip(), label=label)
13
+ else:
14
+ raise ValueError("Invalid temperature high/low string format")
15
+
16
+ @property
17
+ def high(self) -> str:
18
+ return self._high
19
+
20
+ @property
21
+ def low(self) -> str:
22
+ return self._low
23
+
24
+ @property
25
+ def label(self) -> str:
26
+ return self._label
27
+
28
+ def __repr__(self):
29
+ return f"TemperatureHighLow(high={self.high!r}, low={self.low!r}, label={self.label!r})"
30
+
31
+ def __str__(self):
32
+ return f"{self.high}/{self.low}"
@@ -0,0 +1,39 @@
1
+ import re
2
+
3
+ class Timestamp:
4
+
5
+ def __init__(self, time: str, gmt: str, text: str = None):
6
+ self._time = time
7
+ self._gmt = gmt
8
+ self._text = text
9
+
10
+ @property
11
+ def time(self) -> str:
12
+ return self._time
13
+
14
+ @property
15
+ def gmt(self) -> str:
16
+ return self._gmt
17
+
18
+ @property
19
+ def text(self) -> str:
20
+ return self._text
21
+
22
+ def __repr__(self):
23
+ return f"Timestamp(time='{self.time}', gmt='{self.gmt}', text='{self.text}')"
24
+
25
+ def __str__(self):
26
+ if self.text != None:
27
+ return self.text
28
+ return f"As of {self.time} {self.gmt}"
29
+
30
+ @classmethod
31
+ def from_string(cls, text) -> "Timestamp":
32
+ # "As of 4:23 pm GMT-03:00",
33
+ # "As of 16:37 GMT-03:00",
34
+ # "Até 16:38 GMT-03:00"
35
+ pattern = re.compile(r'(\d{1,2}:\d{2}(?: ?[ap]m)?)\s+(GMT[+-]\d{2}:\d{2})')
36
+ match = pattern.search(text)
37
+ if match:
38
+ time, gmt = match.groups()
39
+ return cls(time=time, gmt=gmt, text=text)
@@ -0,0 +1,79 @@
1
+ from .temperature_hight_low import TemperatureHighLow
2
+ from .label_value import LabelValue
3
+ from typing import Optional
4
+ from .uv_index import UVIndex
5
+ from .moon_phase import MoonPhase
6
+ from .sunrise_sunset import SunriseSunset
7
+
8
+ class TodayDetails:
9
+
10
+ def __init__(
11
+ self,
12
+ feelslike: LabelValue,
13
+ sunrise_sunset: Optional["SunriseSunset"],
14
+ high_low: Optional[TemperatureHighLow],
15
+ wind: LabelValue,
16
+ humidity: LabelValue,
17
+ dew_point: LabelValue,
18
+ pressure: LabelValue,
19
+ uv_index: Optional[UVIndex],
20
+ visibility: LabelValue,
21
+ moon_phase: Optional[MoonPhase]
22
+ ):
23
+ self._feelslike = feelslike
24
+ self._sunrise_sunset = sunrise_sunset
25
+ self._high_low = high_low
26
+ self._wind = wind
27
+ self._humidity = humidity
28
+ self._dew_point = dew_point
29
+ self._pressure = pressure
30
+ self._uv_index = uv_index
31
+ self._visibility = visibility
32
+ self._moon_phase = moon_phase
33
+
34
+ @property
35
+ def feelslike(self) -> LabelValue:
36
+ return self._feelslike
37
+
38
+ @property
39
+ def sunrise_sunset(self) -> Optional[SunriseSunset]:
40
+ return self._sunrise_sunset
41
+
42
+ @property
43
+ def high_low(self) -> Optional[TemperatureHighLow]:
44
+ return self._high_low
45
+
46
+ @property
47
+ def wind(self) -> LabelValue:
48
+ return self._wind
49
+
50
+ @property
51
+ def humidity(self) -> LabelValue:
52
+ return self._humidity
53
+
54
+ @property
55
+ def dew_point(self) -> LabelValue:
56
+ return self._dew_point
57
+
58
+ @property
59
+ def pressure(self) -> LabelValue:
60
+ return self._pressure
61
+
62
+ @property
63
+ def uv_index(self) -> Optional[UVIndex]:
64
+ return self._uv_index
65
+
66
+ @property
67
+ def visibility(self) -> LabelValue:
68
+ return self._visibility
69
+
70
+ @property
71
+ def moon_phase(self) -> Optional[MoonPhase]:
72
+ return self._moon_phase
73
+
74
+ def __repr__(self):
75
+ return (f"TodayDetails(feelslike={self.feelslike!r}, sunrise_sunset={self.sunrise_sunset!r},"
76
+ f"high_low={self.high_low!r}, wind={self.wind!r}, "
77
+ f"humidity={self.humidity!r}, dew_point={self.dew_point!r}, "
78
+ f"pressure={self.pressure!r}, uv_index={self.uv_index!r}, "
79
+ f"visibility={self.visibility!r}, moon_phase={self.moon_phase!r})")
@@ -0,0 +1,43 @@
1
+ class UVIndex:
2
+
3
+ def __init__(self, string_value: str, index: str, of: str = None, label:str = None):
4
+ self._string_value = string_value
5
+ self._index = index
6
+ self._of = of
7
+ self._label = label
8
+
9
+ @property
10
+ def string_value(self) -> str:
11
+ return self._string_value
12
+
13
+ @property
14
+ def index(self) -> str:
15
+ return self._index
16
+
17
+ @property
18
+ def of(self) -> str:
19
+ return self._of
20
+
21
+ @property
22
+ def label(self) -> str:
23
+ return self._label
24
+
25
+ @classmethod
26
+ def from_string(cls, data: str, label: str = None) -> 'UVIndex':
27
+ parts = data.split(' ')
28
+ if len(parts) == 1:
29
+ return cls(string_value = data, index= parts[0].strip(), of="", label=label)
30
+ elif len(parts) == 3:
31
+ index, of, some = parts
32
+ return cls(string_value = data, index=index.strip(), of=some.strip(), label=label)
33
+ else:
34
+ raise ValueError(f"Cannot parse UV Index from string: {data}")
35
+
36
+ def __repr__(self) -> str:
37
+ return f"UVIndex(string_value={self.string_value!r}, index={self.index!r}, of={self.of!r}, label={self.label!r})"
38
+
39
+ def __str__(self) -> str:
40
+ if self.string_value:
41
+ return f"{self.label}: {self.string_value}"
42
+ elif self.index and self.of:
43
+ return f"{self.label}: {self.index} {self.of}" if self.label else f"{self.index} {self.of}"
@@ -0,0 +1,58 @@
1
+ from enum import Enum
2
+
3
+ class WeatherIconEnum(Enum):
4
+ CLEAR = ('clear', chr(0xF0599), '☀️')
5
+ CLEAR_NIGHT = ('clear-night', chr(0xF0594), '🌙')
6
+ CLOUDY = ('cloudy', '\uf0c2', '☁️')
7
+ CLOUDY_FOGGY_DAY = ('cloudy-foggy-day', '\u200B', '\u200B')
8
+ CLOUDY_FOGGY_NIGHT = ('cloudy-foggy-night', '\u200B', '\u200B')
9
+ DAY = ('day', '\uf185', '🌞')
10
+ FEEL = ('feel', '\uf2c9', '🥵')
11
+ HUMIDITY = ('humidity', '\uf043', '💧')
12
+ MOSTLY_CLEAR_DAY = ('mostly-clear-day', chr(0xF0599), '☀️')
13
+ MOSTLY_CLEAR_NIGHT = ('mostly-clear-night', chr(0xF0594), '🌙')
14
+ MOSTLY_CLOUDY_DAY = ('mostly-cloudy-day', chr(0xf013), '☁️')
15
+ MOSTLY_CLOUDY_NIGHT = ('mostly-cloudy-night', chr(0xf013), '☁️')
16
+ NIGHT = ('night', '\uf186', '🌜')
17
+ PARTLY_CLOUDY_DAY = ('partly-cloudy-day', chr(0xF0595), '⛅')
18
+ PARTLY_CLOUDY_NIGHT = ('partly-cloudy-night', chr(0xF0F31), '☁️')
19
+ RAIN = ('rain', '\uf0e9', '🌧️')
20
+ RAINY_DAY = ('rainy-day', chr(0x1F326), '🌧️')
21
+ RAINY_NIGHT = ('rainy-night', chr(0x1F326), '🌧️')
22
+ SCATTERED_SHOWERS_DAY = ('scattered-showers-day', chr(0x1F326), '🌦️')
23
+ SCATTERED_SHOWERS_NIGHT = ('scattered-showers-night', chr(0x1F326), '🌦️')
24
+ SEVERE = ('severe', '\ue317', '🌩️')
25
+ SHOWERS = ('showers', '\u26c6', '🌧️')
26
+ SNOW = ('snow', '\uf2dc', '❄️')
27
+ SNOWY_ICY_DAY = ('snowy-icy-day', '\uf2dc', '❄️')
28
+ SNOWY_ICY_NIGHT = ('snowy-icy-night', '\uf2dc', '❄️')
29
+ SUNNY = ('sunny', chr(0xF0599), '☀️')
30
+ SUNRISE = ('sunrise', '\ue34c', '🌅')
31
+ SUNSET = ('sunset', '\ue34d', '🌇')
32
+ THUNDERSTORM = ('thunderstorm', '\uf0e7', '⛈️')
33
+ VISIBILITY = ('visibility', '\uf06e', '👁️')
34
+ WIND = ('wind', chr(0xf059d), '🌪️')
35
+
36
+ def __init__(self, name: str, fa_icon: str, emoji_icon: str):
37
+ self._name = name
38
+ self._fa_icon = fa_icon
39
+ self._emoji_icon = emoji_icon
40
+
41
+ @property
42
+ def name(self):
43
+ return self._name
44
+
45
+ @property
46
+ def fa_icon(self):
47
+ return self._fa_icon
48
+
49
+ @property
50
+ def emoji_icon(self):
51
+ return self._emoji_icon
52
+
53
+ @staticmethod
54
+ def from_name(name: str):
55
+ for item in WeatherIconEnum:
56
+ if item._name == name:
57
+ return item
58
+ return None
@@ -0,0 +1,28 @@
1
+ class Wind:
2
+
3
+ def __init__(self,direction: str,speed: str):
4
+ self._direction = direction
5
+ self._speed = speed
6
+
7
+ @property
8
+ def direction(self) -> str:
9
+ return self._direction
10
+
11
+ @property
12
+ def speed(self) -> str:
13
+ return self._speed
14
+
15
+ def __str__(self):
16
+ return f"Wind Speed: {self.direction} {self.speed}"
17
+
18
+ def __repr__(self):
19
+ return f"Wind(direction:'{self.direction}', speed: '{self.speed}')"
20
+
21
+ @classmethod
22
+ def from_string(cls, data: str) -> 'Wind':
23
+ parts = data.split(" ")
24
+ if len(parts) == 2:
25
+ direction, speed = parts
26
+ return cls(direction=direction.strip(), speed=speed.strip())
27
+ else:
28
+ raise ValueError("Invalid Wind Speed string format")
@@ -0,0 +1,30 @@
1
+ import logging
2
+ from weathergrabber.domain.air_quality_index import AirQualityIndex
3
+ from pyquery import PyQuery
4
+
5
+ class ExtractAQIService:
6
+
7
+ def __init__(self):
8
+ self.logger = logging.getLogger(__name__)
9
+ pass
10
+
11
+ def execute(self, weather_data: PyQuery) -> AirQualityIndex | None:
12
+
13
+ self.logger.debug("Extracting Air Quality Index (AQI)...")
14
+
15
+ try:
16
+ # 'Air Quality Index\n27\nGood\nAir quality is considered satisfactory, and air pollution poses little or no risk.\nSee Details\nInfo'
17
+ aqi_data = weather_data("section[data-testid='AirQualityModule']").text()
18
+
19
+ # 'stroke-width:5;stroke-dasharray:10.021680564951442 172.78759594743863;stroke:#00E838'
20
+ color_data = weather_data("section[data-testid='AirQualityModule'] svg[data-testid='DonutChart'] circle:nth-of-type(2)").attr("style")
21
+
22
+ air_quality_index = AirQualityIndex.aqi_color_from_string(aqi_data,color_data)
23
+
24
+ self.logger.debug(f"Extracted AQI data: {air_quality_index}")
25
+
26
+ return air_quality_index
27
+ except Exception as e:
28
+ self.logger.error(f"Error extracting AQI data: {e}")
29
+ raise ValueError("Could not extract AQI data") from e
30
+
@@ -0,0 +1,47 @@
1
+ import logging
2
+ from pyquery import PyQuery
3
+ from weathergrabber.domain.weather_icon_enum import WeatherIconEnum
4
+ from weathergrabber.domain.city_location import CityLocation
5
+ from weathergrabber.domain.timestamp import Timestamp
6
+ from weathergrabber.domain.day_night import DayNight
7
+ from weathergrabber.domain.current_conditions import CurrentConditions
8
+
9
+ class ExtractCurrentConditionsService:
10
+ def __init__(self):
11
+ self.logger = logging.getLogger(__name__)
12
+ pass
13
+
14
+ def execute(self, weather_data: PyQuery) -> dict:
15
+ try:
16
+ self.logger.debug("Extracting Current Conditions")
17
+
18
+ data = PyQuery(weather_data('div[data-testid="CurrentConditionsContainer"]'))
19
+
20
+ city_location_data = data.find('h1').text() #'Nova Friburgo, Rio de Janeiro, Brazil'
21
+ timestamp_data = data.find('span[class*="timestamp"]').text() # 'As of 8:01 pm GMT-03:00'
22
+ icon_data = data.find('svg[class*="wxIcon"]').attr('name') # 'partly-cloudy-night'
23
+ temp_day_night = data.find('div[class*="tempHiLoValue"]').text() #'Day\xa063°\xa0•\xa0Night\xa046°'
24
+
25
+ city_location = CityLocation.from_string(city_location_data)
26
+ timestamp = Timestamp.from_string(timestamp_data)
27
+ temperature = data.find('span[class*="tempValue"]').text() # '48°'
28
+ icon = WeatherIconEnum.from_name(icon_data)
29
+ phrase = data.find('div[data-testid*="wxPhrase"]').text() # 'Partly Cloudy'
30
+ day_night = DayNight.from_string(temp_day_night)
31
+
32
+ current_conditions = CurrentConditions(
33
+ location=city_location,
34
+ timestamp=timestamp,
35
+ temperature=temperature,
36
+ icon=icon,
37
+ summary=phrase,
38
+ day_night=day_night
39
+ )
40
+
41
+ self.logger.debug(f"Extracted current conditions: {current_conditions}")
42
+
43
+ return current_conditions
44
+
45
+ except Exception as e:
46
+ self.logger.error(f"Error extracting current conditions: {e}")
47
+ raise ValueError("Could not extract current conditions") from e
@@ -0,0 +1,51 @@
1
+ import logging
2
+ from pyquery import PyQuery
3
+ from weathergrabber.domain.daily_predictions import DailyPredictions
4
+ from weathergrabber.domain.temperature_hight_low import TemperatureHighLow
5
+ from weathergrabber.domain.weather_icon_enum import WeatherIconEnum
6
+ from weathergrabber.domain.precipitation import Precipitation
7
+ from typing import List
8
+
9
+ class ExtractDailyForecastOldstyleService:
10
+ def __init__(self):
11
+ self.logger = logging.getLogger(__name__)
12
+ pass
13
+
14
+ def execute(self, weather_data: PyQuery) -> List[DailyPredictions]:
15
+ self.logger.debug("Extracting daily forecast, oldstyle...")
16
+
17
+ try:
18
+
19
+ data = weather_data.find("div[class*='DailyWeatherCard'] > ul li")
20
+
21
+ if len(data) == 0:
22
+ raise ValueError("Unable to extract hourly forecast using old style")
23
+
24
+ details = [ {
25
+ "title": PyQuery(item).find("h3 > span").text(),
26
+ "high-low" : PyQuery(item).find("div[data-testid='SegmentHighTemp']").text(),
27
+ "icon" : PyQuery(item).find("svg").attr("name"),
28
+ "summary" : PyQuery(item).find("span[class*='Column--iconPhrase']").text(),
29
+ "precip-percentage" : PyQuery(item).find("div[data-testid='SegmentPrecipPercentage'] span[class*='Column--precip']").contents().eq(1).text()
30
+ } for item in data ]
31
+
32
+ self.logger.debug("Extracted %s register(s)...",len(details))
33
+
34
+ daily_predictions = [DailyPredictions(
35
+ title=item["title"],
36
+ high_low=TemperatureHighLow.from_string(item["high-low"]),
37
+ icon=WeatherIconEnum.from_name(item["icon"]),
38
+ summary=item["summary"],
39
+ precipitation=Precipitation(
40
+ percentage=item["precip-percentage"]
41
+ )
42
+ ) for item in details]
43
+
44
+ self.logger.debug("Created daily forecast list with %s registers", len(daily_predictions))
45
+
46
+ return daily_predictions
47
+
48
+ except Exception as e:
49
+
50
+ self.logger.error(f"Error extracting daily forecast: {e}")
51
+ raise ValueError("Could not extract daily forecast.") from e
@@ -0,0 +1,56 @@
1
+ import logging
2
+ from pyquery import PyQuery
3
+ from weathergrabber.domain.precipitation import Precipitation
4
+ from weathergrabber.domain.weather_icon_enum import WeatherIconEnum
5
+ from weathergrabber.domain.moon_phase_enum import MoonPhaseEnum
6
+ from weathergrabber.domain.moon_phase import MoonPhase
7
+ from weathergrabber.domain.temperature_hight_low import TemperatureHighLow
8
+ from weathergrabber.domain.daily_predictions import DailyPredictions
9
+
10
+ from typing import List
11
+
12
+
13
+ class ExtractDailyForecastService:
14
+ def __init__(self):
15
+ self.logger = logging.getLogger(__name__)
16
+ pass
17
+
18
+ def execute(self, weather_data: PyQuery) -> List[DailyPredictions]:
19
+ try:
20
+ self.logger.debug("Extracting hourly forecast...")
21
+
22
+ data = weather_data.find("section[data-testid='DailyForecast'] div[class*='Card'] details")
23
+
24
+ if len(data) == 0:
25
+ raise ValueError("Unable to extract daily forecast")
26
+
27
+ details = [ {
28
+ "title": PyQuery(item).find("h2[data-testid='daypartName']").text(),
29
+ "high-low" : PyQuery(item).find("div[data-testid='detailsTemperature']").text(),
30
+ "icon" : PyQuery(item).find("svg[class*='DetailsSummary']").attr("name"),
31
+ "summary" : PyQuery(item).find("span[class*='DetailsSummary--wxPhrase']").text(),
32
+ "precip-percentage": PyQuery(item).find("div[data-testid='Precip'] span[data-testid='PercentageValue']").text(),
33
+ "moon-phase-icon": PyQuery(item).find("li[data-testid='MoonphaseSection'] svg[class*='DetailsTable']").attr('name'),
34
+ "moon-phase-value": PyQuery(item).find("li[data-testid='MoonphaseSection'] span[data-testid='moonPhase']").text(),
35
+ } for item in data ]
36
+
37
+ self.logger.debug("Extracted %s register(s)...",len(details))
38
+
39
+ daily_predictions = [
40
+ DailyPredictions(
41
+ title=item["title"],
42
+ high_low = TemperatureHighLow.from_string(item["high-low"]),
43
+ icon = WeatherIconEnum.from_name(item["icon"]),
44
+ summary = item["summary"],
45
+ precipitation = Precipitation(percentage=item["precip-percentage"]),
46
+ moon_phase = MoonPhase(MoonPhaseEnum.from_name(item["moon-phase-icon"]),item["moon-phase-value"])
47
+ ) for item in details ]
48
+
49
+ self.logger.debug("Created list of daily predictions with %s items", len(daily_predictions))
50
+
51
+ return daily_predictions
52
+
53
+
54
+ except Exception as e:
55
+ self.logger.error(f"Error extracting daily forecast: {e}")
56
+ raise ValueError("Could not extract daily forecast.") from e
@@ -0,0 +1,25 @@
1
+ import logging
2
+ from pyquery import PyQuery
3
+ from weathergrabber.domain.health_activities import HealthActivities
4
+
5
+ class ExtractHealthActivitiesService:
6
+ def __init__(self):
7
+ self.logger = logging.getLogger(__name__)
8
+
9
+ def execute(self, weather_data: PyQuery) -> HealthActivities | None:
10
+
11
+ self.logger.debug("Extracting Health & Activities...")
12
+
13
+ try:
14
+ # 'Health & Activities\nGrass\nSeasonal Allergies and Pollen Count Forecast\nGrass pollen is low in your area'
15
+ data = weather_data("section[data-testid='HealthAndActivitiesModule']").text()
16
+
17
+ health_activities = HealthActivities.from_text(data) if data else None
18
+
19
+ self.logger.debug(f"Extracted Health & Activities data: {health_activities}")
20
+
21
+ return health_activities
22
+
23
+ except Exception as e:
24
+ self.logger.error(f"Error extracting Health & Activities data: {e}")
25
+ raise ValueError("Could not extract Health & Activities data") from e
@@ -0,0 +1,50 @@
1
+ import logging
2
+ from pyquery import PyQuery
3
+ from weathergrabber.domain.hourly_predictions import HourlyPredictions
4
+ from weathergrabber.domain.weather_icon_enum import WeatherIconEnum
5
+ from weathergrabber.domain.precipitation import Precipitation
6
+ from typing import List
7
+
8
+ class ExtractHourlyForecastOldstyleService:
9
+ def __init__(self):
10
+ self.logger = logging.getLogger(__name__)
11
+ pass
12
+
13
+ def execute(self, weather_data: PyQuery) -> List[HourlyPredictions]:
14
+ self.logger.debug("Extracting hourly forecast, oldstyle...")
15
+
16
+ try:
17
+
18
+ data = weather_data.find("div[class*='TodayWeatherCard'] > ul li")
19
+
20
+ if len(data) == 0:
21
+ raise ValueError("Unable to extract hourly forecast using old style")
22
+
23
+ details = [ {
24
+ "title": PyQuery(item).find("h3 > span").text(),
25
+ "temperature" : PyQuery(item).find("span[data-testid='TemperatureValue']").text(),
26
+ "icon" : PyQuery(item).find("svg").attr("name"),
27
+ "summary" : PyQuery(item).find("span[class*='Column--iconPhrase']").text(),
28
+ "precip-percentage" : PyQuery(item).find("div[data-testid='SegmentPrecipPercentage'] span[class*='Column--precip']").contents().eq(1).text()
29
+ } for item in data ]
30
+
31
+ self.logger.debug("Extracted %s register(s)...",len(details))
32
+
33
+ hourly_forecasts = [HourlyPredictions(
34
+ title=item["title"],
35
+ temperature=item["temperature"],
36
+ icon=WeatherIconEnum.from_name(item["icon"]),
37
+ summary=item["summary"],
38
+ precipitation=Precipitation(
39
+ percentage=item["precip-percentage"]
40
+ )
41
+ ) for item in details]
42
+
43
+ self.logger.debug("Created hourly forecast list with %s registers", len(hourly_forecasts))
44
+
45
+ return hourly_forecasts
46
+
47
+ except Exception as e:
48
+
49
+ self.logger.error(f"Error extracting hourly forecast: {e}")
50
+ raise ValueError("Could not extract hourly forecast.") from e