weathergrabber 0.0.8b2__tar.gz → 0.0.8b4__tar.gz
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.
- {weathergrabber-0.0.8b2/weathergrabber.egg-info → weathergrabber-0.0.8b4}/PKG-INFO +1 -1
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/pyproject.toml +1 -1
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/tests/test_cli.py +10 -1
- weathergrabber-0.0.8b4/tests/test_cli_version.py +13 -0
- weathergrabber-0.0.8b4/tests/test_main.py +11 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/__init__.py +4 -1
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/__main__.py +1 -1
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/adapter/tty/json_tty.py +1 -1
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/adapter/tty/waybar_tty.py +1 -1
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/cli.py +3 -5
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/air_quality_index.py +17 -19
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/city_location.py +5 -4
- weathergrabber-0.0.8b4/weathergrabber/domain/color.py +55 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/health_activities.py +3 -3
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/moon_phase_enum.py +7 -7
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/uv_index.py +4 -2
- weathergrabber-0.0.8b4/weathergrabber/service/extract_aqi_service.py +27 -0
- weathergrabber-0.0.8b4/weathergrabber/service/extract_hourly_forecast_service.py +58 -0
- weathergrabber-0.0.8b4/weathergrabber/service/extract_today_details_service.py +80 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/usecase/use_case.py +7 -7
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4/weathergrabber.egg-info}/PKG-INFO +1 -1
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber.egg-info/SOURCES.txt +2 -0
- weathergrabber-0.0.8b2/weathergrabber/domain/color.py +0 -43
- weathergrabber-0.0.8b2/weathergrabber/service/extract_aqi_service.py +0 -30
- weathergrabber-0.0.8b2/weathergrabber/service/extract_hourly_forecast_service.py +0 -64
- weathergrabber-0.0.8b2/weathergrabber/service/extract_today_details_service.py +0 -85
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/LICENSE +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/README.md +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/setup.cfg +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/tests/test_core.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/adapter/client/weather_api.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/adapter/client/weather_search_api.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/adapter/tty/console_tty.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/core.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/icon_enum.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/air_quality_index_mapper.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/city_location_mapper.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/color_mapper.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/current_conditions_mapper.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/daily_predictions_mapper.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/day_night_mapper.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/forecast_mapper.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/health_activities_mapper.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/hourly_predictions_mapper.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/label_value_mapper.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/moon_phase_mapper.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/precipitation_mapper.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/search_mapper.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/sunrise_sunset_mapper.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/temperature_high_low_mapper.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/timestamp_mapper.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/today_details_mapper.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/uv_index_mapper.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/weather_icon_enum_mapper.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/wind_mapper.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/output_enum.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/params.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/current_conditions.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/daily_predictions.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/day_night.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/forecast.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/hourly_predictions.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/label_value.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/moon_phase.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/precipitation.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/search.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/sunrise_sunset.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/temperature_hight_low.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/timestamp.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/today_details.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/weather_icon_enum.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/wind.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/service/extract_current_conditions_service.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/service/extract_daily_forecast_oldstyle_service.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/service/extract_daily_forecast_service.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/service/extract_health_activities_service.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/service/extract_hourly_forecast_oldstyle_service.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/service/extract_temperature_service.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/service/read_weather_service.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/service/search_location_service.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/weathergrabber_application.py +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber.egg-info/dependency_links.txt +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber.egg-info/entry_points.txt +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber.egg-info/requires.txt +0 -0
- {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber.egg-info/top_level.txt +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
from asyncio import subprocess
|
|
1
2
|
import pytest
|
|
2
3
|
from unittest.mock import patch, MagicMock
|
|
3
4
|
import sys
|
|
4
5
|
|
|
5
6
|
@pytest.fixture
|
|
6
7
|
def mock_main():
|
|
7
|
-
with patch('weathergrabber.cli.main') as m:
|
|
8
|
+
with patch('weathergrabber.cli.weathergrabber.main') as m:
|
|
8
9
|
yield m
|
|
9
10
|
|
|
10
11
|
def test_cli_location_name(monkeypatch, mock_main):
|
|
@@ -73,3 +74,11 @@ def test_cli_location_id_env(monkeypatch, mock_main):
|
|
|
73
74
|
main_cli()
|
|
74
75
|
args = mock_main.call_args[1]
|
|
75
76
|
assert args["location_id"] == "envlocationid"
|
|
77
|
+
|
|
78
|
+
def test_cli_log_level(monkeypatch, mock_main):
|
|
79
|
+
test_args = ["weathergrabber", "Tokyo", "--log", "debug"]
|
|
80
|
+
monkeypatch.setattr(sys, "argv", test_args)
|
|
81
|
+
from weathergrabber.cli import main_cli
|
|
82
|
+
main_cli()
|
|
83
|
+
args = mock_main.call_args[1]
|
|
84
|
+
assert args["log_level"] == "debug"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
import weathergrabber
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_cli_version_flag_prints_version():
|
|
7
|
+
result = subprocess.run([
|
|
8
|
+
sys.executable, "-m", "weathergrabber", "-v"
|
|
9
|
+
], capture_output=True, text=True)
|
|
10
|
+
assert result.returncode in (0, 2)
|
|
11
|
+
output = (result.stdout or "") + (result.stderr or "")
|
|
12
|
+
assert str(weathergrabber.__version__) in output
|
|
13
|
+
assert "Weathergrabber" in output
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
def test_main_entrypoint():
|
|
5
|
+
result = subprocess.run(
|
|
6
|
+
[sys.executable, "-m", "weathergrabber", "--version"],
|
|
7
|
+
capture_output=True,
|
|
8
|
+
text=True
|
|
9
|
+
)
|
|
10
|
+
assert result.returncode == 0
|
|
11
|
+
assert "Weathergrabber" in result.stdout
|
|
@@ -62,7 +62,7 @@ class WaybarTTY:
|
|
|
62
62
|
|
|
63
63
|
#Air quality index
|
|
64
64
|
color = forecast.air_quality_index.color.hex
|
|
65
|
-
aqi_category = f" <span color=\"{color}\">{forecast.air_quality_index.category}</span>"
|
|
65
|
+
aqi_category = f" <span color=\"#{color}\">{forecast.air_quality_index.category}</span>"
|
|
66
66
|
aqi_acronym = forecast.air_quality_index.acronym
|
|
67
67
|
aqi_value = forecast.air_quality_index.value
|
|
68
68
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import os
|
|
3
|
-
|
|
3
|
+
import weathergrabber
|
|
4
4
|
|
|
5
5
|
def main_cli():
|
|
6
6
|
## Get current locale, or use the default one
|
|
@@ -11,6 +11,7 @@ def main_cli():
|
|
|
11
11
|
parser.add_argument("--output", "-o", type=str, choices=['console','json','waybar'], default='console', help="Output format. console, json or waybar")
|
|
12
12
|
parser.add_argument("--keep-open", "-k",action='store_true', default=False, help="Keep open and refreshing every 5 minutes instead of exiting after execution. Does only makes sense for --output=console")
|
|
13
13
|
parser.add_argument("--icons", "-i", type=str, choices=['fa','emoji'], default='emoji', help="Icon set. 'fa' for Font-Awesome, or 'emoji'")
|
|
14
|
+
parser.add_argument("--version", "-v", action='version', version=f'Weathergrabber {weathergrabber.get_version()}', help="Show version and exit")
|
|
14
15
|
parser.add_argument(
|
|
15
16
|
"--log",
|
|
16
17
|
default="critical",
|
|
@@ -25,7 +26,7 @@ def main_cli():
|
|
|
25
26
|
if not args.location_id and not args.location_name:
|
|
26
27
|
location_id = os.getenv('WEATHER_LOCATION_ID')
|
|
27
28
|
|
|
28
|
-
main(
|
|
29
|
+
weathergrabber.main(
|
|
29
30
|
log_level=args.log,
|
|
30
31
|
location_name = args.location_name,
|
|
31
32
|
location_id = location_id,
|
|
@@ -34,6 +35,3 @@ def main_cli():
|
|
|
34
35
|
keep_open=args.keep_open,
|
|
35
36
|
icons=args.icons
|
|
36
37
|
)
|
|
37
|
-
|
|
38
|
-
if __name__ == "__main__":
|
|
39
|
-
main_cli()
|
{weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/air_quality_index.py
RENAMED
|
@@ -45,34 +45,32 @@ class AirQualityIndex:
|
|
|
45
45
|
return f"Title: {self.title}. AQI: {self.value}, Category: {self.category}, Description: {self.description}, Acronym: {self.acronym}, Color: {self.color}"
|
|
46
46
|
|
|
47
47
|
def __repr__(self) -> str:
|
|
48
|
-
return f"AirQualityIndex(title={self.title}, value={self.value}, category={self.category}, description={self.description}, acronym
|
|
48
|
+
return f"AirQualityIndex(title='{self.title}', value={self.value}, category='{self.category}', description='{self.description}', acronym='{self.acronym}', color='{self.color}')"
|
|
49
49
|
|
|
50
50
|
@staticmethod
|
|
51
51
|
def _extract_aqi(data: str):
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
try:
|
|
53
|
+
parts = data.split('\n')
|
|
54
|
+
title = parts[0].strip()
|
|
55
|
+
aqi = int(parts[1].strip())
|
|
56
|
+
category = parts[2].strip() if len(parts) > 2 else None
|
|
57
|
+
description = parts[3].strip() if len(parts) > 3 else None
|
|
58
|
+
acronym = ''.join(word[0].strip().upper() for word in title.split())
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
return title, aqi, category, description, acronym
|
|
61
|
+
except (ValueError, IndexError) as e:
|
|
62
|
+
raise ValueError("Invalid AQI data format") from e
|
|
60
63
|
|
|
61
64
|
# 'Air Quality Index\n26\nGood\nAir quality is considered satisfactory, and air pollution poses little or no risk.'
|
|
62
65
|
@classmethod
|
|
63
66
|
def from_string(cls, data: str) -> 'AirQualityIndex':
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return cls(title, aqi, category, description, acronym)
|
|
67
|
-
except (ValueError, IndexError) as e:
|
|
68
|
-
raise ValueError("Invalid AQI data format") from e
|
|
67
|
+
title, aqi, category, description, acronym = AirQualityIndex._extract_aqi(data)
|
|
68
|
+
return cls(title, aqi, category, description, acronym)
|
|
69
69
|
|
|
70
70
|
@classmethod
|
|
71
71
|
def aqi_color_from_string(cls, aqi_data: str, color_data: str):
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
except(ValueError, IndexError) as e:
|
|
77
|
-
raise ValueError("Invalid AQI data format or color data format") from e
|
|
72
|
+
title, aqi, category, description, acronym = AirQualityIndex._extract_aqi(aqi_data)
|
|
73
|
+
color = Color.from_string(color_data)
|
|
74
|
+
return cls(title, aqi, category, description, acronym, color)
|
|
75
|
+
|
|
78
76
|
|
|
@@ -46,9 +46,12 @@ class CityLocation:
|
|
|
46
46
|
country, state_province, city, location = None, None, None, None
|
|
47
47
|
parts = data.split(", ")
|
|
48
48
|
|
|
49
|
+
if data.strip() == "":
|
|
50
|
+
raise ValueError("City location string cannot be empty")
|
|
51
|
+
|
|
49
52
|
if len(parts) > 2:
|
|
50
53
|
i = len(parts) - 1
|
|
51
|
-
while i
|
|
54
|
+
while i >= 0:
|
|
52
55
|
if not country:
|
|
53
56
|
country = parts[i]
|
|
54
57
|
elif not state_province:
|
|
@@ -66,6 +69,4 @@ class CityLocation:
|
|
|
66
69
|
return cls(city=city, state_province=state_province)
|
|
67
70
|
elif len(parts) == 1:
|
|
68
71
|
city = parts[0]
|
|
69
|
-
return cls(city=city)
|
|
70
|
-
else:
|
|
71
|
-
raise ValueError("Invalid city location string format")
|
|
72
|
+
return cls(city=city)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
class Color:
|
|
4
|
+
|
|
5
|
+
def __init__(self, red: str | int, green: str | int, blue: str | int):
|
|
6
|
+
self._red = self._int_or_hex(red)
|
|
7
|
+
self._green = self._int_or_hex(green)
|
|
8
|
+
self._blue = self._int_or_hex(blue)
|
|
9
|
+
|
|
10
|
+
def _int_or_hex(self, value: str | int) -> int:
|
|
11
|
+
if type(value) == int:
|
|
12
|
+
if not (0 <= value <= 255):
|
|
13
|
+
raise ValueError("RGB integer values must be between 0 and 255")
|
|
14
|
+
return value
|
|
15
|
+
return int(value, 16)
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def red(self) -> int:
|
|
19
|
+
return self._red
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def green(self) -> int:
|
|
23
|
+
return self._green
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def blue(self) -> int:
|
|
27
|
+
return self._blue
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_string(cls,string_value: str) -> "Color":
|
|
31
|
+
|
|
32
|
+
color_pattern = r"#([0-9A-Fa-f]{6})"
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
match = re.search(color_pattern, string_value)
|
|
36
|
+
color = f"#{match.group(1)}"
|
|
37
|
+
hex_color = color.lstrip('#')
|
|
38
|
+
r, g, b = hex_color[:2], hex_color[2:4], hex_color[4:]
|
|
39
|
+
return cls(r, g, b)
|
|
40
|
+
except (AttributeError, ValueError):
|
|
41
|
+
raise ValueError(f"Invalid color string: {string_value}")
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def hex(self) -> str:
|
|
45
|
+
return f"{self.red:02x}{self.green:02x}{self.blue:02x}".upper()
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def rgb(self) -> str:
|
|
49
|
+
return f"rgb({self.red}, {self.green}, {self.blue})"
|
|
50
|
+
|
|
51
|
+
def __str__(self):
|
|
52
|
+
return f"{self.hex}"
|
|
53
|
+
|
|
54
|
+
def __repr__(self):
|
|
55
|
+
return f"Color(red='{self.red}', green='{self.green}', blue='{self.blue}')"
|
{weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/health_activities.py
RENAMED
|
@@ -23,8 +23,8 @@ class HealthActivities:
|
|
|
23
23
|
return f"HealthActivities(category_name={self._category_name!r}, title={self._title!r}, description={self._description!r})"
|
|
24
24
|
|
|
25
25
|
# 'Health & Activities\nGrass\nSeasonal Allergies and Pollen Count Forecast\nGrass pollen is low in your area'
|
|
26
|
-
@
|
|
27
|
-
def from_text(text: str):
|
|
26
|
+
@classmethod
|
|
27
|
+
def from_text(cls, text: str):
|
|
28
28
|
try:
|
|
29
29
|
lines = text.split('\n')
|
|
30
30
|
if len(lines) >= 4:
|
|
@@ -32,7 +32,7 @@ class HealthActivities:
|
|
|
32
32
|
#Ignore the "grass" line
|
|
33
33
|
title = lines[2].strip()
|
|
34
34
|
description = ' '.join(line.strip() for line in lines[3:]).strip()
|
|
35
|
-
return
|
|
35
|
+
return cls(category_name, title, description)
|
|
36
36
|
else:
|
|
37
37
|
raise ValueError("Insufficient data to parse HealthActivities")
|
|
38
38
|
except Exception as e:
|
|
@@ -8,9 +8,9 @@ class MoonPhaseEnum(Enum):
|
|
|
8
8
|
PHASE_2 = ("phase-2", "\uf186", "🌒")
|
|
9
9
|
PHASE_3 = ("phase-3", "\uf186", "🌒")
|
|
10
10
|
PHASE_4 = ("phase-4", "\uf186", "🌒")
|
|
11
|
-
PHASE_5 = ("phase-5", "\uf186", "🌒")
|
|
12
|
-
PHASE_6 = ("phase-6", "\uf186", "🌒")
|
|
13
11
|
# First Quarter
|
|
12
|
+
PHASE_5 = ("phase-5", "\uf186", "🌓")
|
|
13
|
+
PHASE_6 = ("phase-6", "\uf186", "🌓")
|
|
14
14
|
PHASE_7 = ("phase-7", "\uf186", "🌓")
|
|
15
15
|
# Waxing Gibbous
|
|
16
16
|
PHASE_8 = ("phase-8", "\uf186", "🌔")
|
|
@@ -21,8 +21,8 @@ class MoonPhaseEnum(Enum):
|
|
|
21
21
|
PHASE_13 = ("phase-13", "\uf186", "🌕")
|
|
22
22
|
# Full Moon
|
|
23
23
|
PHASE_14 = ("phase-14", "\uf186", "🌕")
|
|
24
|
-
# Waning Gibbous
|
|
25
24
|
PHASE_15 = ("phase-15", "\uf186", "🌕")
|
|
25
|
+
# Waning Gibbous
|
|
26
26
|
PHASE_16 = ("phase-16", "\uf186", "🌖")
|
|
27
27
|
PHASE_17 = ("phase-17", "\uf186", "🌖")
|
|
28
28
|
PHASE_18 = ("phase-18", "\uf186", "🌖")
|
|
@@ -30,10 +30,10 @@ class MoonPhaseEnum(Enum):
|
|
|
30
30
|
PHASE_20 = ("phase-20", "\uf186", "🌖")
|
|
31
31
|
# Last Quarter
|
|
32
32
|
PHASE_21 = ("phase-21", "\uf186", "🌗")
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
PHASE_22 = ("phase-22", "\uf186", "🌗")
|
|
34
|
+
PHASE_23 = ("phase-23", "\uf186", "🌗")
|
|
35
|
+
PHASE_24 = ("phase-24", "\uf186", "🌗")
|
|
36
|
+
|
|
37
37
|
PHASE_25 = ("phase-25", "\uf186", "🌘")
|
|
38
38
|
PHASE_26 = ("phase-26", "\uf186", "🌘")
|
|
39
39
|
PHASE_27 = ("phase-27", "\uf186", "🌘")
|
|
@@ -24,6 +24,8 @@ class UVIndex:
|
|
|
24
24
|
|
|
25
25
|
@classmethod
|
|
26
26
|
def from_string(cls, data: str, label: str = None) -> 'UVIndex':
|
|
27
|
+
if not data:
|
|
28
|
+
raise ValueError("UV Index string cannot be empty")
|
|
27
29
|
parts = data.split(' ')
|
|
28
30
|
if len(parts) == 1:
|
|
29
31
|
return cls(string_value = data, index= parts[0].strip(), of="", label=label)
|
|
@@ -31,7 +33,7 @@ class UVIndex:
|
|
|
31
33
|
index, of, some = parts
|
|
32
34
|
return cls(string_value = data, index=index.strip(), of=some.strip(), label=label)
|
|
33
35
|
else:
|
|
34
|
-
|
|
36
|
+
return cls(string_value = data, index="", of="", label=label)
|
|
35
37
|
|
|
36
38
|
def __repr__(self) -> str:
|
|
37
39
|
return f"UVIndex(string_value={self.string_value!r}, index={self.index!r}, of={self.of!r}, label={self.label!r})"
|
|
@@ -39,5 +41,5 @@ class UVIndex:
|
|
|
39
41
|
def __str__(self) -> str:
|
|
40
42
|
if self.string_value:
|
|
41
43
|
return f"{self.label} {self.string_value}"
|
|
42
|
-
|
|
44
|
+
else:
|
|
43
45
|
return f"{self.label} {self.index} {self.of}" if self.label else f"{self.index} {self.of}"
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
# 'Air Quality Index\n27\nGood\nAir quality is considered satisfactory, and air pollution poses little or no risk.\nSee Details\nInfo'
|
|
16
|
+
aqi_data = weather_data("section[data-testid='AirQualityModule']").text()
|
|
17
|
+
|
|
18
|
+
# 'stroke-width:5;stroke-dasharray:10.021680564951442 172.78759594743863;stroke:#00E838'
|
|
19
|
+
color_data = weather_data("section[data-testid='AirQualityModule'] svg[data-testid='DonutChart'] circle:nth-of-type(2)").attr("style")
|
|
20
|
+
|
|
21
|
+
air_quality_index = AirQualityIndex.aqi_color_from_string(aqi_data,color_data)
|
|
22
|
+
|
|
23
|
+
self.logger.debug(f"Extracted AQI data: {air_quality_index}")
|
|
24
|
+
|
|
25
|
+
return air_quality_index
|
|
26
|
+
|
|
27
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
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.uv_index import UVIndex
|
|
6
|
+
from weathergrabber.domain.precipitation import Precipitation
|
|
7
|
+
from weathergrabber.domain.wind import Wind
|
|
8
|
+
from typing import List
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ExtractHourlyForecastService:
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.logger = logging.getLogger(__name__)
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
def execute(self, weather_data: PyQuery) -> List[HourlyPredictions]:
|
|
17
|
+
self.logger.debug("Extracting hourly forecast...")
|
|
18
|
+
|
|
19
|
+
data = weather_data.find("section[data-testid='HourlyForecast'] div[class*='Card'] details")
|
|
20
|
+
|
|
21
|
+
if len(data) == 0:
|
|
22
|
+
raise ValueError("There's no hourly forecast data available.")
|
|
23
|
+
|
|
24
|
+
details = [ {
|
|
25
|
+
"title": PyQuery(item).find("h2").text(),
|
|
26
|
+
"temperature" : PyQuery(item).find("div[data-testid='detailsTemperature']").text(),
|
|
27
|
+
"icon" : PyQuery(item).find("svg[class*='DetailsSummary']").attr("name"),
|
|
28
|
+
"summary" : PyQuery(item).find("span[class*='DetailsSummary--wxPhrase']").text(),
|
|
29
|
+
"precip-percentage": PyQuery(item).find("div[data-testid='Precip'] span[data-testid='PercentageValue']").text(),
|
|
30
|
+
"wind": PyQuery(item).find("span[data-testid='WindTitle']").next().eq(0).text(),
|
|
31
|
+
"feels-like" : PyQuery(item).find("span[data-testid='FeelsLikeTitle']").next().text(),
|
|
32
|
+
"humidity" : PyQuery(item).find("span[data-testid='HumidityTitle']").next().text(),
|
|
33
|
+
"uv-index" : PyQuery(item).find("span[data-testid='UVIndexValue']").text(),
|
|
34
|
+
"cloud-cover" : PyQuery(item).find("span[data-testid='CloudCoverTitle']").next().text(),
|
|
35
|
+
"rain-amount" : PyQuery(item).find("span[data-testid='AccumulationTitle']").next().text()
|
|
36
|
+
} for item in data ]
|
|
37
|
+
|
|
38
|
+
self.logger.debug("Extracted %s register(s)...",len(details))
|
|
39
|
+
|
|
40
|
+
hourly_forecasts = [HourlyPredictions(
|
|
41
|
+
title=item["title"],
|
|
42
|
+
temperature=item["temperature"],
|
|
43
|
+
icon=WeatherIconEnum.from_name(item["icon"]),
|
|
44
|
+
summary=item["summary"],
|
|
45
|
+
precipitation=Precipitation(
|
|
46
|
+
percentage=item["precip-percentage"],
|
|
47
|
+
amount=item["rain-amount"]
|
|
48
|
+
),
|
|
49
|
+
wind=Wind.from_string(item["wind"]),
|
|
50
|
+
feels_like=item["feels-like"],
|
|
51
|
+
humidity=item["humidity"],
|
|
52
|
+
uv_index=UVIndex.from_string(item["uv-index"]),
|
|
53
|
+
cloud_cover=item["cloud-cover"]
|
|
54
|
+
) for item in details]
|
|
55
|
+
|
|
56
|
+
self.logger.debug("Created hourly forecast list with %s registers", len(hourly_forecasts))
|
|
57
|
+
|
|
58
|
+
return hourly_forecasts
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pyquery import PyQuery
|
|
3
|
+
from weathergrabber.domain.today_details import TodayDetails
|
|
4
|
+
from weathergrabber.domain.temperature_hight_low import TemperatureHighLow
|
|
5
|
+
from weathergrabber.domain.uv_index import UVIndex
|
|
6
|
+
from weathergrabber.domain.moon_phase import MoonPhase
|
|
7
|
+
from weathergrabber.domain.moon_phase_enum import MoonPhaseEnum
|
|
8
|
+
from weathergrabber.domain.label_value import LabelValue
|
|
9
|
+
from weathergrabber.domain.sunrise_sunset import SunriseSunset
|
|
10
|
+
|
|
11
|
+
class ExtractTodayDetailsService:
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.logger = logging.getLogger(__name__)
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
def execute(self, weather_data: PyQuery) -> TodayDetails:
|
|
17
|
+
self.logger.debug("Extracting today's details...")
|
|
18
|
+
|
|
19
|
+
today_details_data = weather_data.find("div#todayDetails")
|
|
20
|
+
feelslike = PyQuery(today_details_data).find("div[data-testid='FeelsLikeSection'] span")
|
|
21
|
+
sunrise_sunset = PyQuery(today_details_data).find("div[data-testid='sunriseSunsetContainer'] div p[class*='TwcSunChart']")
|
|
22
|
+
|
|
23
|
+
feelslike_label = feelslike.eq(0).text() #'Feels Like'
|
|
24
|
+
feelslike_value = feelslike.eq(1).text() #'60°'
|
|
25
|
+
|
|
26
|
+
sunrise = sunrise_sunset.eq(0).text() #'6:12 AM'
|
|
27
|
+
sunset = sunrise_sunset.eq(1).text() #'7:45 PM'
|
|
28
|
+
|
|
29
|
+
icons = today_details_data.find('svg[class*="WeatherDetailsListItem--icon"]')
|
|
30
|
+
labels = today_details_data.find('div[class*="WeatherDetailsListItem--label"]')
|
|
31
|
+
values = today_details_data.find('div[data-testid="wxData"]')
|
|
32
|
+
|
|
33
|
+
self.logger.debug(f"Parsing today details values...")
|
|
34
|
+
high_low_label = labels.eq(0).text() #'High / Low'
|
|
35
|
+
high_low_value = values.eq(0).text() #'--/54°'
|
|
36
|
+
|
|
37
|
+
wind_label = labels.eq(1).text() #'Wind'
|
|
38
|
+
wind_value = values.eq(1).text() #'7\xa0mph'
|
|
39
|
+
|
|
40
|
+
humidity_label = labels.eq(2).text() #'Humidity'
|
|
41
|
+
humidity_value = values.eq(2).text() #'100%'
|
|
42
|
+
|
|
43
|
+
dew_point_label = labels.eq(3).text() #'Dew Point'
|
|
44
|
+
dew_point_value = values.eq(3).text() #'60°'
|
|
45
|
+
|
|
46
|
+
pressure_label = labels.eq(4).text() #'Pressure'
|
|
47
|
+
pressure_value = values.eq(4).text() #'30.31\xa0in'
|
|
48
|
+
|
|
49
|
+
uv_index_label = labels.eq(5).text() #'UV Index'
|
|
50
|
+
uv_index_value = values.eq(5).text() #'5 of 10'
|
|
51
|
+
|
|
52
|
+
visibility_label = labels.eq(6).text() #'Visibility'
|
|
53
|
+
visibility_value = values.eq(6).text() #'10.0 mi'
|
|
54
|
+
|
|
55
|
+
moon_phase_label = labels.eq(7).text() #'Moon Phase'
|
|
56
|
+
moon_phase_icon = icons.eq(7).attr('name') #'phase-2'
|
|
57
|
+
moon_phase_value = values.eq(7).text() #'Waxing Crescent'
|
|
58
|
+
|
|
59
|
+
self.logger.debug(f"Creating domain objects for today details...")
|
|
60
|
+
|
|
61
|
+
sunrise_sunset = SunriseSunset(sunrise=sunrise, sunset=sunset)
|
|
62
|
+
high_low = TemperatureHighLow.from_string(high_low_value, label=high_low_label)
|
|
63
|
+
uv_index = UVIndex.from_string(uv_index_value, label=uv_index_label)
|
|
64
|
+
moon_phase = MoonPhase(MoonPhaseEnum.from_name(moon_phase_icon), moon_phase_value, moon_phase_label)
|
|
65
|
+
|
|
66
|
+
today_details = TodayDetails(
|
|
67
|
+
feelslike=LabelValue(label=feelslike_label, value=feelslike_value),
|
|
68
|
+
sunrise_sunset=sunrise_sunset,
|
|
69
|
+
high_low=high_low,
|
|
70
|
+
wind=LabelValue(label=wind_label, value=wind_value),
|
|
71
|
+
humidity=LabelValue(label=humidity_label, value=humidity_value),
|
|
72
|
+
dew_point=LabelValue(label=dew_point_label, value=dew_point_value),
|
|
73
|
+
pressure=LabelValue(label=pressure_label, value=pressure_value),
|
|
74
|
+
uv_index=uv_index,
|
|
75
|
+
visibility=LabelValue(label=visibility_label, value=visibility_value),
|
|
76
|
+
moon_phase=moon_phase
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
self.logger.debug(f"Extracted today's details: {today_details}")
|
|
80
|
+
return today_details
|
|
@@ -57,16 +57,16 @@ class UseCase:
|
|
|
57
57
|
health_activities = self.extract_health_activities_service.execute(weather_data)
|
|
58
58
|
|
|
59
59
|
try:
|
|
60
|
-
hourly_predictions = self.extract_hourly_forecast_service.execute(weather_data)
|
|
61
|
-
except ValueError:
|
|
62
|
-
self.logger.warning("Falling back to old style hourly forecast extraction")
|
|
63
60
|
hourly_predictions = self.extract_hourly_forecast_oldstyle_service.execute(weather_data)
|
|
64
|
-
|
|
65
|
-
try:
|
|
66
|
-
daily_predictions = self.extract_daily_forecast_service.execute(weather_data)
|
|
67
61
|
except ValueError:
|
|
68
|
-
self.logger.warning("Falling back to
|
|
62
|
+
self.logger.warning("Falling back to new style hourly forecast extraction")
|
|
63
|
+
hourly_predictions = self.extract_hourly_forecast_service.execute(weather_data)
|
|
64
|
+
|
|
65
|
+
try:
|
|
69
66
|
daily_predictions = self.extract_daily_forecast_oldstyle_service.execute(weather_data)
|
|
67
|
+
except ValueError:
|
|
68
|
+
self.logger.warning("Falling back to new style daily forecast extraction")
|
|
69
|
+
daily_predictions = self.extract_daily_forecast_service.execute(weather_data)
|
|
70
70
|
|
|
71
71
|
forecast = Forecast(
|
|
72
72
|
search = Search(
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
|
|
3
|
-
class Color:
|
|
4
|
-
|
|
5
|
-
def __init__(self, red: str, green: str, blue: str):
|
|
6
|
-
self._red = red
|
|
7
|
-
self._green = green
|
|
8
|
-
self._blue = blue
|
|
9
|
-
|
|
10
|
-
@property
|
|
11
|
-
def red(self):
|
|
12
|
-
return self._red
|
|
13
|
-
|
|
14
|
-
@property
|
|
15
|
-
def green(self):
|
|
16
|
-
return self._green
|
|
17
|
-
|
|
18
|
-
@property
|
|
19
|
-
def blue(self):
|
|
20
|
-
return self._blue
|
|
21
|
-
|
|
22
|
-
@classmethod
|
|
23
|
-
def from_string(cls,string_value: str) -> "Color":
|
|
24
|
-
|
|
25
|
-
color_pattern = r"#([0-9A-Fa-f]{6})"
|
|
26
|
-
|
|
27
|
-
match = re.search(color_pattern, string_value)
|
|
28
|
-
color = f"#{match.group(1)}"
|
|
29
|
-
hex_color = color.lstrip('#')
|
|
30
|
-
r, g, b = int(hex_color[:2], 16), int(hex_color[2:4], 16), int(hex_color[4:], 16)
|
|
31
|
-
|
|
32
|
-
return cls(r, g, b)
|
|
33
|
-
|
|
34
|
-
@property
|
|
35
|
-
def hex(self) -> str:
|
|
36
|
-
return f"#{self.red:02x}{self.green:02x}{self.blue:02x}"
|
|
37
|
-
|
|
38
|
-
@property
|
|
39
|
-
def rgb(self) -> str:
|
|
40
|
-
return f"rgb({self.red}, {self.green}, {self.blue})"
|
|
41
|
-
|
|
42
|
-
def __repr__(self):
|
|
43
|
-
return f"Color(red='{self.red}', green='{self.green}', blue='{self.blue}')"
|
|
@@ -1,30 +0,0 @@
|
|
|
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
|
-
|
|
@@ -1,64 +0,0 @@
|
|
|
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.uv_index import UVIndex
|
|
6
|
-
from weathergrabber.domain.precipitation import Precipitation
|
|
7
|
-
from weathergrabber.domain.wind import Wind
|
|
8
|
-
from typing import List
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class ExtractHourlyForecastService:
|
|
12
|
-
def __init__(self):
|
|
13
|
-
self.logger = logging.getLogger(__name__)
|
|
14
|
-
pass
|
|
15
|
-
|
|
16
|
-
def execute(self, weather_data: PyQuery) -> List[HourlyPredictions]:
|
|
17
|
-
try:
|
|
18
|
-
self.logger.debug("Extracting hourly forecast...")
|
|
19
|
-
|
|
20
|
-
data = weather_data.find("section[data-testid='HourlyForecast'] div[class*='Card'] details")
|
|
21
|
-
|
|
22
|
-
if len(data) == 0:
|
|
23
|
-
raise ValueError("Unable to extract hourly forecast")
|
|
24
|
-
|
|
25
|
-
details = [ {
|
|
26
|
-
"title": PyQuery(item).find("h2").text(),
|
|
27
|
-
"temperature" : PyQuery(item).find("div[data-testid='detailsTemperature']").text(),
|
|
28
|
-
"icon" : PyQuery(item).find("svg[class*='DetailsSummary']").attr("name"),
|
|
29
|
-
"summary" : PyQuery(item).find("span[class*='DetailsSummary--wxPhrase']").text(),
|
|
30
|
-
"precip-percentage": PyQuery(item).find("div[data-testid='Precip'] span[data-testid='PercentageValue']").text(),
|
|
31
|
-
"wind": PyQuery(item).find("span[data-testid='WindTitle']").next().eq(0).text(),
|
|
32
|
-
"feels-like" : PyQuery(item).find("span[data-testid='FeelsLikeTitle']").next().text(),
|
|
33
|
-
"humidity" : PyQuery(item).find("span[data-testid='HumidityTitle']").next().text(),
|
|
34
|
-
"uv-index" : PyQuery(item).find("span[data-testid='UVIndexValue']").text(),
|
|
35
|
-
"cloud-cover" : PyQuery(item).find("span[data-testid='CloudCoverTitle']").next().text(),
|
|
36
|
-
"rain-amount" : PyQuery(item).find("span[data-testid='AccumulationTitle']").next().text()
|
|
37
|
-
} for item in data ]
|
|
38
|
-
|
|
39
|
-
self.logger.debug("Extracted %s register(s)...",len(details))
|
|
40
|
-
|
|
41
|
-
hourly_forecasts = [HourlyPredictions(
|
|
42
|
-
title=item["title"],
|
|
43
|
-
temperature=item["temperature"],
|
|
44
|
-
icon=WeatherIconEnum.from_name(item["icon"]),
|
|
45
|
-
summary=item["summary"],
|
|
46
|
-
precipitation=Precipitation(
|
|
47
|
-
percentage=item["precip-percentage"],
|
|
48
|
-
amount=item["rain-amount"]
|
|
49
|
-
),
|
|
50
|
-
wind=Wind.from_string(item["wind"]),
|
|
51
|
-
feels_like=item["feels-like"],
|
|
52
|
-
humidity=item["humidity"],
|
|
53
|
-
uv_index=UVIndex.from_string(item["uv-index"]),
|
|
54
|
-
cloud_cover=item["cloud-cover"]
|
|
55
|
-
) for item in details]
|
|
56
|
-
|
|
57
|
-
self.logger.debug("Created hourly forecast list with %s registers", len(hourly_forecasts))
|
|
58
|
-
|
|
59
|
-
return hourly_forecasts
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
except Exception as e:
|
|
63
|
-
self.logger.error(f"Error extracting hourly forecast: {e}")
|
|
64
|
-
raise ValueError("Could not extract hourly forecast.") from e
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from pyquery import PyQuery
|
|
3
|
-
from weathergrabber.domain.today_details import TodayDetails
|
|
4
|
-
from weathergrabber.domain.temperature_hight_low import TemperatureHighLow
|
|
5
|
-
from weathergrabber.domain.uv_index import UVIndex
|
|
6
|
-
from weathergrabber.domain.moon_phase import MoonPhase
|
|
7
|
-
from weathergrabber.domain.moon_phase_enum import MoonPhaseEnum
|
|
8
|
-
from weathergrabber.domain.label_value import LabelValue
|
|
9
|
-
from weathergrabber.domain.sunrise_sunset import SunriseSunset
|
|
10
|
-
|
|
11
|
-
class ExtractTodayDetailsService:
|
|
12
|
-
def __init__(self):
|
|
13
|
-
self.logger = logging.getLogger(__name__)
|
|
14
|
-
pass
|
|
15
|
-
|
|
16
|
-
def execute(self, weather_data: PyQuery) -> TodayDetails:
|
|
17
|
-
try:
|
|
18
|
-
self.logger.debug("Extracting today's details...")
|
|
19
|
-
|
|
20
|
-
today_details_data = weather_data.find("div#todayDetails")
|
|
21
|
-
feelslike = PyQuery(today_details_data).find("div[data-testid='FeelsLikeSection'] span")
|
|
22
|
-
sunrise_sunset = PyQuery(today_details_data).find("div[data-testid='sunriseSunsetContainer'] div p[class*='TwcSunChart']")
|
|
23
|
-
|
|
24
|
-
feelslike_label = feelslike.eq(0).text() #'Feels Like'
|
|
25
|
-
feelslike_value = feelslike.eq(1).text() #'60°'
|
|
26
|
-
|
|
27
|
-
sunrise = sunrise_sunset.eq(0).text() #'6:12 AM'
|
|
28
|
-
sunset = sunrise_sunset.eq(1).text() #'7:45 PM'
|
|
29
|
-
|
|
30
|
-
icons = today_details_data.find('svg[class*="WeatherDetailsListItem--icon"]')
|
|
31
|
-
labels = today_details_data.find('div[class*="WeatherDetailsListItem--label"]')
|
|
32
|
-
values = today_details_data.find('div[data-testid="wxData"]')
|
|
33
|
-
|
|
34
|
-
self.logger.debug(f"Parsing today details values...")
|
|
35
|
-
high_low_label = labels.eq(0).text() #'High / Low'
|
|
36
|
-
high_low_value = values.eq(0).text() #'--/54°'
|
|
37
|
-
|
|
38
|
-
wind_label = labels.eq(1).text() #'Wind'
|
|
39
|
-
wind_value = values.eq(1).text() #'7\xa0mph'
|
|
40
|
-
|
|
41
|
-
humidity_label = labels.eq(2).text() #'Humidity'
|
|
42
|
-
humidity_value = values.eq(2).text() #'100%'
|
|
43
|
-
|
|
44
|
-
dew_point_label = labels.eq(3).text() #'Dew Point'
|
|
45
|
-
dew_point_value = values.eq(3).text() #'60°'
|
|
46
|
-
|
|
47
|
-
pressure_label = labels.eq(4).text() #'Pressure'
|
|
48
|
-
pressure_value = values.eq(4).text() #'30.31\xa0in'
|
|
49
|
-
|
|
50
|
-
uv_index_label = labels.eq(5).text() #'UV Index'
|
|
51
|
-
uv_index_value = values.eq(5).text() #'5 of 10'
|
|
52
|
-
|
|
53
|
-
visibility_label = labels.eq(6).text() #'Visibility'
|
|
54
|
-
visibility_value = values.eq(6).text() #'10.0 mi'
|
|
55
|
-
|
|
56
|
-
moon_phase_label = labels.eq(7).text() #'Moon Phase'
|
|
57
|
-
moon_phase_icon = icons.eq(7).attr('name') #'phase-2'
|
|
58
|
-
moon_phase_value = values.eq(7).text() #'Waxing Crescent'
|
|
59
|
-
|
|
60
|
-
self.logger.debug(f"Creating domain objects for today details...")
|
|
61
|
-
|
|
62
|
-
sunrise_sunset = SunriseSunset(sunrise=sunrise, sunset=sunset)
|
|
63
|
-
high_low = TemperatureHighLow.from_string(high_low_value, label=high_low_label)
|
|
64
|
-
uv_index = UVIndex.from_string(uv_index_value, label=uv_index_label)
|
|
65
|
-
moon_phase = MoonPhase(MoonPhaseEnum.from_name(moon_phase_icon), moon_phase_value, moon_phase_label)
|
|
66
|
-
|
|
67
|
-
today_details = TodayDetails(
|
|
68
|
-
feelslike=LabelValue(label=feelslike_label, value=feelslike_value),
|
|
69
|
-
sunrise_sunset=sunrise_sunset,
|
|
70
|
-
high_low=high_low,
|
|
71
|
-
wind=LabelValue(label=wind_label, value=wind_value),
|
|
72
|
-
humidity=LabelValue(label=humidity_label, value=humidity_value),
|
|
73
|
-
dew_point=LabelValue(label=dew_point_label, value=dew_point_value),
|
|
74
|
-
pressure=LabelValue(label=pressure_label, value=pressure_value),
|
|
75
|
-
uv_index=uv_index,
|
|
76
|
-
visibility=LabelValue(label=visibility_label, value=visibility_value),
|
|
77
|
-
moon_phase=moon_phase
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
self.logger.debug(f"Extracted today's details: {today_details}")
|
|
81
|
-
return today_details
|
|
82
|
-
|
|
83
|
-
except Exception as e:
|
|
84
|
-
self.logger.error(f"Error extracting today's details: {e}")
|
|
85
|
-
raise ValueError("Could not extract today's details.") from e
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/adapter/client/weather_api.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/icon_enum.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/output_enum.py
RENAMED
|
File without changes
|
|
File without changes
|
{weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/current_conditions.py
RENAMED
|
File without changes
|
{weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/daily_predictions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/hourly_predictions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/temperature_hight_low.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/weather_icon_enum.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/service/read_weather_service.py
RENAMED
|
File without changes
|
{weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/service/search_location_service.py
RENAMED
|
File without changes
|
{weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/weathergrabber_application.py
RENAMED
|
File without changes
|
{weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|