weathergrabber 0.0.8b7__py3-none-any.whl → 0.0.9a0__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.
- weathergrabber/__init__.py +1 -1
- weathergrabber/adapter/repository/forecast_repository.py +135 -0
- weathergrabber/adapter/tty/console_tty.py +4 -4
- weathergrabber/adapter/tty/json_tty.py +3 -3
- weathergrabber/adapter/tty/statistics_tty.py +35 -0
- weathergrabber/adapter/tty/waybar_tty.py +3 -3
- weathergrabber/{service → application/services}/extract_aqi_service.py +1 -1
- weathergrabber/{service → application/services}/extract_current_conditions_service.py +5 -5
- weathergrabber/{service → application/services}/extract_daily_forecast_oldstyle_service.py +4 -4
- weathergrabber/{service → application/services}/extract_daily_forecast_service.py +6 -6
- weathergrabber/{service → application/services}/extract_health_activities_service.py +1 -1
- weathergrabber/{service → application/services}/extract_hourly_forecast_oldstyle_service.py +3 -3
- weathergrabber/{service → application/services}/extract_hourly_forecast_service.py +5 -5
- weathergrabber/{service → application/services}/extract_today_details_service.py +8 -8
- weathergrabber/application/services/retrieve_forecast_from_cache_service.py +27 -0
- weathergrabber/application/services/retrieve_statistics_service.py +20 -0
- weathergrabber/application/services/save_forecast_to_cache_service.py +21 -0
- weathergrabber/application/usecases/statistics_uc.py +19 -0
- weathergrabber/application/usecases/weather_forecast_uc.py +123 -0
- weathergrabber/application/weathergrabber_application.py +92 -0
- weathergrabber/cli.py +6 -2
- weathergrabber/core.py +7 -3
- weathergrabber/domain/adapter/mappers/air_quality_index_mapper.py +25 -0
- weathergrabber/domain/adapter/mappers/city_location_mapper.py +15 -0
- weathergrabber/domain/adapter/mappers/color_mapper.py +17 -0
- weathergrabber/domain/adapter/mappers/current_conditions_mapper.py +32 -0
- weathergrabber/domain/adapter/mappers/daily_predictions_mapper.py +30 -0
- weathergrabber/domain/adapter/mappers/day_night_mapper.py +23 -0
- weathergrabber/domain/adapter/mappers/forecast_mapper.py +38 -0
- weathergrabber/domain/adapter/mappers/health_activities_mapper.py +15 -0
- weathergrabber/domain/adapter/mappers/hourly_predictions_mapper.py +38 -0
- weathergrabber/domain/adapter/mappers/label_value_mapper.py +13 -0
- weathergrabber/domain/adapter/mappers/moon_phase_mapper.py +16 -0
- weathergrabber/domain/adapter/mappers/precipitation_mapper.py +13 -0
- weathergrabber/domain/adapter/mappers/search_mapper.py +13 -0
- weathergrabber/domain/adapter/mappers/statistics_mapper.py +25 -0
- weathergrabber/domain/adapter/mappers/sunrise_sunset_mapper.py +23 -0
- weathergrabber/domain/adapter/mappers/temperature_high_low_mapper.py +15 -0
- weathergrabber/domain/adapter/mappers/timestamp_mapper.py +15 -0
- weathergrabber/domain/adapter/mappers/today_details_mapper.py +42 -0
- weathergrabber/domain/adapter/mappers/uv_index_mapper.py +17 -0
- weathergrabber/domain/adapter/mappers/weather_icon_enum_mapper.py +11 -0
- weathergrabber/domain/adapter/mappers/wind_mapper.py +16 -0
- weathergrabber/domain/adapter/output_enum.py +1 -1
- weathergrabber/domain/adapter/params.py +25 -4
- weathergrabber/domain/{moon_phase.py → entities/moon_phase.py} +1 -1
- weathergrabber/domain/entities/statistics.py +46 -0
- weathergrabber/domain/{sunrise_sunset.py → entities/sunrise_sunset.py} +0 -1
- weathergrabber/domain/{weather_icon_enum.py → entities/weather_icon_enum.py} +1 -0
- {weathergrabber-0.0.8b7.dist-info → weathergrabber-0.0.9a0.dist-info}/METADATA +32 -7
- weathergrabber-0.0.9a0.dist-info/RECORD +80 -0
- {weathergrabber-0.0.8b7.dist-info → weathergrabber-0.0.9a0.dist-info}/WHEEL +1 -1
- weathergrabber/domain/adapter/mapper/air_quality_index_mapper.py +0 -13
- weathergrabber/domain/adapter/mapper/city_location_mapper.py +0 -8
- weathergrabber/domain/adapter/mapper/color_mapper.py +0 -10
- weathergrabber/domain/adapter/mapper/current_conditions_mapper.py +0 -16
- weathergrabber/domain/adapter/mapper/daily_predictions_mapper.py +0 -15
- weathergrabber/domain/adapter/mapper/day_night_mapper.py +0 -12
- weathergrabber/domain/adapter/mapper/forecast_mapper.py +0 -20
- weathergrabber/domain/adapter/mapper/health_activities_mapper.py +0 -8
- weathergrabber/domain/adapter/mapper/hourly_predictions_mapper.py +0 -19
- weathergrabber/domain/adapter/mapper/label_value_mapper.py +0 -7
- weathergrabber/domain/adapter/mapper/moon_phase_mapper.py +0 -9
- weathergrabber/domain/adapter/mapper/precipitation_mapper.py +0 -7
- weathergrabber/domain/adapter/mapper/search_mapper.py +0 -7
- weathergrabber/domain/adapter/mapper/sunrise_sunset_mapper.py +0 -13
- weathergrabber/domain/adapter/mapper/temperature_high_low_mapper.py +0 -8
- weathergrabber/domain/adapter/mapper/timestamp_mapper.py +0 -8
- weathergrabber/domain/adapter/mapper/today_details_mapper.py +0 -21
- weathergrabber/domain/adapter/mapper/uv_index_mapper.py +0 -9
- weathergrabber/domain/adapter/mapper/weather_icon_enum_mapper.py +0 -8
- weathergrabber/domain/adapter/mapper/wind_mapper.py +0 -7
- weathergrabber/usecase/use_case.py +0 -87
- weathergrabber/weathergrabber_application.py +0 -78
- weathergrabber-0.0.8b7.dist-info/RECORD +0 -72
- /weathergrabber/{service → application/services}/extract_temperature_service.py +0 -0
- /weathergrabber/{service → application/services}/read_weather_service.py +0 -0
- /weathergrabber/{service → application/services}/search_location_service.py +0 -0
- /weathergrabber/domain/{air_quality_index.py → entities/air_quality_index.py} +0 -0
- /weathergrabber/domain/{city_location.py → entities/city_location.py} +0 -0
- /weathergrabber/domain/{color.py → entities/color.py} +0 -0
- /weathergrabber/domain/{current_conditions.py → entities/current_conditions.py} +0 -0
- /weathergrabber/domain/{daily_predictions.py → entities/daily_predictions.py} +0 -0
- /weathergrabber/domain/{day_night.py → entities/day_night.py} +0 -0
- /weathergrabber/domain/{forecast.py → entities/forecast.py} +0 -0
- /weathergrabber/domain/{health_activities.py → entities/health_activities.py} +0 -0
- /weathergrabber/domain/{hourly_predictions.py → entities/hourly_predictions.py} +0 -0
- /weathergrabber/domain/{label_value.py → entities/label_value.py} +0 -0
- /weathergrabber/domain/{moon_phase_enum.py → entities/moon_phase_enum.py} +0 -0
- /weathergrabber/domain/{precipitation.py → entities/precipitation.py} +0 -0
- /weathergrabber/domain/{search.py → entities/search.py} +0 -0
- /weathergrabber/domain/{temperature_hight_low.py → entities/temperature_hight_low.py} +0 -0
- /weathergrabber/domain/{timestamp.py → entities/timestamp.py} +0 -0
- /weathergrabber/domain/{today_details.py → entities/today_details.py} +0 -0
- /weathergrabber/domain/{uv_index.py → entities/uv_index.py} +0 -0
- /weathergrabber/domain/{wind.py → entities/wind.py} +0 -0
- {weathergrabber-0.0.8b7.dist-info → weathergrabber-0.0.9a0.dist-info}/entry_points.txt +0 -0
- {weathergrabber-0.0.8b7.dist-info → weathergrabber-0.0.9a0.dist-info}/licenses/LICENSE +0 -0
- {weathergrabber-0.0.8b7.dist-info → weathergrabber-0.0.9a0.dist-info}/top_level.txt +0 -0
weathergrabber/__init__.py
CHANGED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Optional
|
|
7
|
+
import logging
|
|
8
|
+
from weathergrabber.domain.entities.forecast import Forecast
|
|
9
|
+
from weathergrabber.domain.adapter.mappers.forecast_mapper import forecast_to_dict, dict_to_forecast
|
|
10
|
+
|
|
11
|
+
class ForecastRepository:
|
|
12
|
+
def __init__(self, db_path: str = None):
|
|
13
|
+
# Use /tmp directory for ephemeral storage by default
|
|
14
|
+
if db_path is None:
|
|
15
|
+
db_path = os.path.join(tempfile.gettempdir(), "weather_forecasts.db")
|
|
16
|
+
|
|
17
|
+
self.logger = logging.getLogger(__name__)
|
|
18
|
+
self.logger.info(f"Initializing ForecastRepository with DB path: {db_path}")
|
|
19
|
+
self.db_path = db_path
|
|
20
|
+
self._initialize_database()
|
|
21
|
+
|
|
22
|
+
def _initialize_database(self):
|
|
23
|
+
self.logger.debug("Initializing database and creating tables if they do not exist.")
|
|
24
|
+
try:
|
|
25
|
+
with sqlite3.connect(self.db_path) as conn:
|
|
26
|
+
cursor = conn.cursor()
|
|
27
|
+
cursor.execute('''
|
|
28
|
+
CREATE TABLE IF NOT EXISTS forecasts (
|
|
29
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
30
|
+
location_id TEXT NOT NULL,
|
|
31
|
+
search_name TEXT,
|
|
32
|
+
forecast_data TEXT NOT NULL,
|
|
33
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
34
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
35
|
+
)
|
|
36
|
+
''')
|
|
37
|
+
# Create indexes for fast retrieval
|
|
38
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_location_id ON forecasts (location_id)")
|
|
39
|
+
conn.commit()
|
|
40
|
+
self.logger.debug(f"Database initialized successfully at {self.db_path}")
|
|
41
|
+
except sqlite3.Error as e:
|
|
42
|
+
self.logger.error(f"Error initializing database: {e}")
|
|
43
|
+
raise
|
|
44
|
+
|
|
45
|
+
def save_forecast(self, location_id: str, search_name: str, forecast_data: Forecast) -> None:
|
|
46
|
+
forecast_dict_data = forecast_to_dict(forecast_data)
|
|
47
|
+
now = datetime.now().isoformat()
|
|
48
|
+
with sqlite3.connect(self.db_path) as conn:
|
|
49
|
+
cursor = conn.cursor()
|
|
50
|
+
cursor.execute('''
|
|
51
|
+
INSERT INTO forecasts (location_id, search_name, forecast_data, created_at, updated_at)
|
|
52
|
+
VALUES (?, ?, ?, ?, ?)
|
|
53
|
+
''', (
|
|
54
|
+
location_id,
|
|
55
|
+
search_name,
|
|
56
|
+
json.dumps(forecast_dict_data),
|
|
57
|
+
now,
|
|
58
|
+
now
|
|
59
|
+
))
|
|
60
|
+
conn.commit()
|
|
61
|
+
|
|
62
|
+
def get_by_location_id(self, location_id: str) -> Optional[Forecast]:
|
|
63
|
+
with sqlite3.connect(self.db_path) as conn:
|
|
64
|
+
cursor = conn.cursor()
|
|
65
|
+
cursor.execute('''
|
|
66
|
+
SELECT forecast_data FROM forecasts
|
|
67
|
+
WHERE location_id = ? ORDER BY created_at DESC
|
|
68
|
+
LIMIT 1
|
|
69
|
+
''', (location_id,))
|
|
70
|
+
row = cursor.fetchone()
|
|
71
|
+
forecast_dict_data = json.loads(row[0]) if row else None
|
|
72
|
+
return dict_to_forecast(forecast_dict_data) if forecast_dict_data else None
|
|
73
|
+
|
|
74
|
+
def get_by_search_name(self, search_name: str) -> Optional[Forecast]:
|
|
75
|
+
with sqlite3.connect(self.db_path) as conn:
|
|
76
|
+
cursor = conn.cursor()
|
|
77
|
+
cursor.execute('''
|
|
78
|
+
SELECT forecast_data FROM forecasts
|
|
79
|
+
WHERE search_name = ? ORDER BY created_at DESC
|
|
80
|
+
LIMIT 1
|
|
81
|
+
''', (search_name,))
|
|
82
|
+
row = cursor.fetchone()
|
|
83
|
+
forecast_dict_data = json.loads(row[0]) if row else None
|
|
84
|
+
return dict_to_forecast(forecast_dict_data) if forecast_dict_data else None
|
|
85
|
+
|
|
86
|
+
def clear_cache(self) -> None:
|
|
87
|
+
"""Clear all cached forecasts from the database."""
|
|
88
|
+
try:
|
|
89
|
+
with sqlite3.connect(self.db_path) as conn:
|
|
90
|
+
cursor = conn.cursor()
|
|
91
|
+
cursor.execute("DELETE FROM forecasts")
|
|
92
|
+
conn.commit()
|
|
93
|
+
self.logger.info("Cache cleared successfully")
|
|
94
|
+
except sqlite3.Error as e:
|
|
95
|
+
self.logger.error(f"Error clearing cache: {e}")
|
|
96
|
+
|
|
97
|
+
def get_cache_stats(self) -> dict:
|
|
98
|
+
"""Get statistics about the cached forecasts."""
|
|
99
|
+
try:
|
|
100
|
+
with sqlite3.connect(self.db_path) as conn:
|
|
101
|
+
cursor = conn.cursor()
|
|
102
|
+
cursor.execute("SELECT COUNT(*) FROM forecasts")
|
|
103
|
+
total_count = cursor.fetchone()[0]
|
|
104
|
+
|
|
105
|
+
cursor.execute("""
|
|
106
|
+
SELECT COUNT(DISTINCT location_id) as unique_locations,
|
|
107
|
+
COUNT(DISTINCT search_name) as unique_search_names
|
|
108
|
+
FROM forecasts
|
|
109
|
+
""")
|
|
110
|
+
stats = cursor.fetchone()
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
'total_forecasts': total_count,
|
|
114
|
+
'unique_locations': stats[0],
|
|
115
|
+
'unique_search_names': stats[1],
|
|
116
|
+
'database_path': self.db_path
|
|
117
|
+
}
|
|
118
|
+
except sqlite3.Error as e:
|
|
119
|
+
self.logger.error(f"Error getting cache stats: {e}")
|
|
120
|
+
return {'error': str(e)}
|
|
121
|
+
|
|
122
|
+
def cleanup_old_forecasts(self, hours_old: int = 24) -> None:
|
|
123
|
+
"""Remove forecasts older than specified hours."""
|
|
124
|
+
try:
|
|
125
|
+
with sqlite3.connect(self.db_path) as conn:
|
|
126
|
+
cursor = conn.cursor()
|
|
127
|
+
cursor.execute("""
|
|
128
|
+
DELETE FROM forecasts
|
|
129
|
+
WHERE created_at < datetime('now', '-{} hours')
|
|
130
|
+
""".format(hours_old))
|
|
131
|
+
deleted_count = cursor.rowcount
|
|
132
|
+
conn.commit()
|
|
133
|
+
self.logger.info(f"Cleaned up {deleted_count} old forecasts")
|
|
134
|
+
except sqlite3.Error as e:
|
|
135
|
+
self.logger.error(f"Error cleaning up old forecasts: {e}")
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
from weathergrabber.
|
|
1
|
+
from weathergrabber.application.usecases.weather_forecast_uc import WeatherForecastUC
|
|
2
2
|
from weathergrabber.domain.adapter.params import Params
|
|
3
3
|
from weathergrabber.domain.adapter.icon_enum import IconEnum
|
|
4
|
-
from weathergrabber.domain.weather_icon_enum import WeatherIconEnum
|
|
5
|
-
from weathergrabber.weathergrabber_application import WeatherGrabberApplication
|
|
4
|
+
from weathergrabber.domain.entities.weather_icon_enum import WeatherIconEnum
|
|
5
|
+
from weathergrabber.application.weathergrabber_application import WeatherGrabberApplication
|
|
6
6
|
import logging
|
|
7
7
|
|
|
8
8
|
class ConsoleTTY:
|
|
9
9
|
|
|
10
|
-
def __init__(self, use_case:
|
|
10
|
+
def __init__(self, use_case: WeatherForecastUC):
|
|
11
11
|
self.logger = logging.getLogger(__name__)
|
|
12
12
|
self.use_case = use_case
|
|
13
13
|
pass
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
from weathergrabber.
|
|
1
|
+
from weathergrabber.application.usecases.weather_forecast_uc import WeatherForecastUC
|
|
2
2
|
from weathergrabber.domain.adapter.params import Params
|
|
3
|
-
from weathergrabber.domain.adapter.
|
|
3
|
+
from weathergrabber.domain.adapter.mappers.forecast_mapper import forecast_to_dict
|
|
4
4
|
import logging
|
|
5
5
|
import json
|
|
6
6
|
|
|
7
7
|
class JsonTTY:
|
|
8
8
|
|
|
9
|
-
def __init__(self, use_case:
|
|
9
|
+
def __init__(self, use_case: WeatherForecastUC):
|
|
10
10
|
self.logger = logging.getLogger(__name__)
|
|
11
11
|
self.use_case = use_case
|
|
12
12
|
pass
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from weathergrabber.application.usecases.statistics_uc import StatisticsUC
|
|
2
|
+
from weathergrabber.domain.adapter.params import Params
|
|
3
|
+
from weathergrabber.domain.adapter.output_enum import OutputEnum
|
|
4
|
+
from weathergrabber.domain.adapter.mappers.statistics_mapper import statistics_to_dict
|
|
5
|
+
from weathergrabber.domain.entities.statistics import Statistics
|
|
6
|
+
import logging
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
class StatisticsTTY:
|
|
10
|
+
def __init__(self, statistics_uc: StatisticsUC):
|
|
11
|
+
self.logger = logging.getLogger(__name__)
|
|
12
|
+
self.statistics_uc = statistics_uc
|
|
13
|
+
|
|
14
|
+
def execute(self, params : Params):
|
|
15
|
+
statistics = self.statistics_uc.execute(params)
|
|
16
|
+
print_value = self.json_print(statistics) if (params.output_format == OutputEnum.JSON) else self.tty_print(statistics)
|
|
17
|
+
print(print_value)
|
|
18
|
+
|
|
19
|
+
def tty_print(self, statistics: Statistics):
|
|
20
|
+
self.logger.info("Preparing TTY output for statistics")
|
|
21
|
+
lines = [
|
|
22
|
+
"",
|
|
23
|
+
"WeatherGrabber Statistics",
|
|
24
|
+
"=========================",
|
|
25
|
+
f"Total Forecasts: {statistics.total_forecasts}",
|
|
26
|
+
f"Unique Locations: {statistics.unique_locations}",
|
|
27
|
+
f"Unique Search Names: {statistics.unique_search_names}",
|
|
28
|
+
f"Database Path: {statistics.database_path}",
|
|
29
|
+
"",
|
|
30
|
+
]
|
|
31
|
+
return "\n".join(lines)
|
|
32
|
+
|
|
33
|
+
def json_print(self, statistics: Statistics):
|
|
34
|
+
self.logger.info("Preparing JSON output for statistics")
|
|
35
|
+
return json.dumps(statistics_to_dict(statistics))
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
from weathergrabber.
|
|
1
|
+
from weathergrabber.application.usecases.weather_forecast_uc import WeatherForecastUC
|
|
2
2
|
from weathergrabber.domain.adapter.params import Params
|
|
3
3
|
from weathergrabber.domain.adapter.icon_enum import IconEnum
|
|
4
|
-
from weathergrabber.domain.weather_icon_enum import WeatherIconEnum
|
|
4
|
+
from weathergrabber.domain.entities.weather_icon_enum import WeatherIconEnum
|
|
5
5
|
import logging
|
|
6
6
|
import json
|
|
7
7
|
|
|
8
8
|
class WaybarTTY:
|
|
9
9
|
|
|
10
|
-
def __init__(self, use_case:
|
|
10
|
+
def __init__(self, use_case: WeatherForecastUC):
|
|
11
11
|
self.logger = logging.getLogger(__name__)
|
|
12
12
|
self.use_case = use_case
|
|
13
13
|
pass
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import logging
|
|
2
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
|
|
3
|
+
from weathergrabber.domain.entities.weather_icon_enum import WeatherIconEnum
|
|
4
|
+
from weathergrabber.domain.entities.city_location import CityLocation
|
|
5
|
+
from weathergrabber.domain.entities.timestamp import Timestamp
|
|
6
|
+
from weathergrabber.domain.entities.day_night import DayNight
|
|
7
|
+
from weathergrabber.domain.entities.current_conditions import CurrentConditions
|
|
8
8
|
|
|
9
9
|
class ExtractCurrentConditionsService:
|
|
10
10
|
def __init__(self):
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import logging
|
|
2
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
|
|
3
|
+
from weathergrabber.domain.entities.daily_predictions import DailyPredictions
|
|
4
|
+
from weathergrabber.domain.entities.temperature_hight_low import TemperatureHighLow
|
|
5
|
+
from weathergrabber.domain.entities.weather_icon_enum import WeatherIconEnum
|
|
6
|
+
from weathergrabber.domain.entities.precipitation import Precipitation
|
|
7
7
|
from typing import List
|
|
8
8
|
|
|
9
9
|
class ExtractDailyForecastOldstyleService:
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
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
|
|
3
|
+
from weathergrabber.domain.entities.precipitation import Precipitation
|
|
4
|
+
from weathergrabber.domain.entities.weather_icon_enum import WeatherIconEnum
|
|
5
|
+
from weathergrabber.domain.entities.moon_phase_enum import MoonPhaseEnum
|
|
6
|
+
from weathergrabber.domain.entities.moon_phase import MoonPhase
|
|
7
|
+
from weathergrabber.domain.entities.temperature_hight_low import TemperatureHighLow
|
|
8
|
+
from weathergrabber.domain.entities.daily_predictions import DailyPredictions
|
|
9
9
|
|
|
10
10
|
from typing import List
|
|
11
11
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import logging
|
|
2
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
|
|
3
|
+
from weathergrabber.domain.entities.hourly_predictions import HourlyPredictions
|
|
4
|
+
from weathergrabber.domain.entities.weather_icon_enum import WeatherIconEnum
|
|
5
|
+
from weathergrabber.domain.entities.precipitation import Precipitation
|
|
6
6
|
from typing import List
|
|
7
7
|
|
|
8
8
|
class ExtractHourlyForecastOldstyleService:
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import logging
|
|
2
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
|
|
3
|
+
from weathergrabber.domain.entities.hourly_predictions import HourlyPredictions
|
|
4
|
+
from weathergrabber.domain.entities.weather_icon_enum import WeatherIconEnum
|
|
5
|
+
from weathergrabber.domain.entities.uv_index import UVIndex
|
|
6
|
+
from weathergrabber.domain.entities.precipitation import Precipitation
|
|
7
|
+
from weathergrabber.domain.entities.wind import Wind
|
|
8
8
|
from typing import List
|
|
9
9
|
|
|
10
10
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import logging
|
|
2
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
|
|
3
|
+
from weathergrabber.domain.entities.today_details import TodayDetails
|
|
4
|
+
from weathergrabber.domain.entities.temperature_hight_low import TemperatureHighLow
|
|
5
|
+
from weathergrabber.domain.entities.uv_index import UVIndex
|
|
6
|
+
from weathergrabber.domain.entities.moon_phase import MoonPhase
|
|
7
|
+
from weathergrabber.domain.entities.moon_phase_enum import MoonPhaseEnum
|
|
8
|
+
from weathergrabber.domain.entities.label_value import LabelValue
|
|
9
|
+
from weathergrabber.domain.entities.sunrise_sunset import SunriseSunset
|
|
10
10
|
|
|
11
11
|
class ExtractTodayDetailsService:
|
|
12
12
|
def __init__(self):
|
|
@@ -57,7 +57,7 @@ class ExtractTodayDetailsService:
|
|
|
57
57
|
moon_phase_icon = icons.eq(7).attr('name') #'phase-2'
|
|
58
58
|
moon_phase_value = values.eq(7).text() #'Waxing Crescent'
|
|
59
59
|
|
|
60
|
-
self.logger.debug(
|
|
60
|
+
self.logger.debug("Creating domain objects for today details...")
|
|
61
61
|
|
|
62
62
|
sunrise_sunset = SunriseSunset(sunrise=sunrise, sunset=sunset)
|
|
63
63
|
high_low = TemperatureHighLow.from_string(high_low_value, label=high_low_label)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from weathergrabber.adapter.repository.forecast_repository import ForecastRepository
|
|
2
|
+
from weathergrabber.domain.adapter.params import Params
|
|
3
|
+
from weathergrabber.domain.entities.forecast import Forecast
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
class RetrieveForecastFromCacheService:
|
|
8
|
+
def __init__(self, forecast_repository: ForecastRepository):
|
|
9
|
+
self.forecast_repository = forecast_repository
|
|
10
|
+
self.log = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
def execute(self, params: Params) -> Optional[Forecast]:
|
|
13
|
+
"""Retrieve forecast from cache based on parameters."""
|
|
14
|
+
if params.location.id:
|
|
15
|
+
forecast = self.forecast_repository.get_by_location_id(params.location.id)
|
|
16
|
+
elif params.location.search_name:
|
|
17
|
+
forecast = self.forecast_repository.get_by_search_name(params.location.search_name)
|
|
18
|
+
else:
|
|
19
|
+
self.log.debug("No location_id provided in params; cannot retrieve from cache.")
|
|
20
|
+
raise ValueError("Location ID must be provided to retrieve forecast from cache.")
|
|
21
|
+
|
|
22
|
+
if forecast:
|
|
23
|
+
self.log.debug("Forecast retrieved from cache successfully.")
|
|
24
|
+
return forecast
|
|
25
|
+
else:
|
|
26
|
+
self.log.debug("No forecast found in cache.")
|
|
27
|
+
raise ValueError("No forecast found in cache for the given location.")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from weathergrabber.adapter.repository.forecast_repository import ForecastRepository
|
|
2
|
+
from weathergrabber.domain.entities.statistics import Statistics
|
|
3
|
+
from weathergrabber.domain.adapter.mappers.statistics_mapper import dict_to_statistics
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
class RetrieveStatisticsService:
|
|
7
|
+
def __init__(self, weather_repository: ForecastRepository):
|
|
8
|
+
self.log = logging.getLogger(__name__)
|
|
9
|
+
self.weather_repository = weather_repository
|
|
10
|
+
|
|
11
|
+
def execute(self) -> Statistics:
|
|
12
|
+
"""Retrieve and process database."""
|
|
13
|
+
self.log.debug("Retrieving statistics from repository...")
|
|
14
|
+
|
|
15
|
+
result = self.weather_repository.get_cache_stats()
|
|
16
|
+
statistics = dict_to_statistics(result)
|
|
17
|
+
|
|
18
|
+
self.log.debug(f"Statistics retrieved successfully: {statistics}")
|
|
19
|
+
return statistics
|
|
20
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from weathergrabber.adapter.repository.forecast_repository import ForecastRepository
|
|
2
|
+
from weathergrabber.domain.entities.forecast import Forecast
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SaveForecastToCacheService:
|
|
7
|
+
def __init__(self, forecast_repository: ForecastRepository):
|
|
8
|
+
self.forecast_repository = forecast_repository
|
|
9
|
+
self.logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
def execute(self, forecast : Forecast) -> None:
|
|
12
|
+
|
|
13
|
+
self.logger.info("Saving forecast to cache")
|
|
14
|
+
|
|
15
|
+
self.forecast_repository.save_forecast(
|
|
16
|
+
location_id=forecast.search.id,
|
|
17
|
+
search_name=forecast.search.search_name,
|
|
18
|
+
forecast_data=forecast
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
self.logger.debug("Forecast saved to cache successfully")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from weathergrabber.application.services.retrieve_statistics_service import RetrieveStatisticsService
|
|
3
|
+
from weathergrabber.domain.entities.statistics import Statistics
|
|
4
|
+
from weathergrabber.domain.adapter.params import Params
|
|
5
|
+
|
|
6
|
+
class StatisticsUC:
|
|
7
|
+
def __init__(self, retrieve_statistics_service: RetrieveStatisticsService):
|
|
8
|
+
self.retrieve_statistics_service = retrieve_statistics_service
|
|
9
|
+
self.log = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def execute(self, params: Params) -> Statistics:
|
|
13
|
+
"""Execute the use case to retrieve weather statistics."""
|
|
14
|
+
|
|
15
|
+
self.log.info("Executing StatisticsUC to retrieve weather statistics...")
|
|
16
|
+
statistics = self.retrieve_statistics_service.execute()
|
|
17
|
+
self.log.info("Weather statistics retrieved successfully.")
|
|
18
|
+
|
|
19
|
+
return statistics
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from requests.exceptions import ConnectionError
|
|
5
|
+
from weathergrabber.domain.adapter.params import Params
|
|
6
|
+
from weathergrabber.application.services.search_location_service import SearchLocationService
|
|
7
|
+
from weathergrabber.application.services.read_weather_service import ReadWeatherService
|
|
8
|
+
from weathergrabber.application.services.extract_current_conditions_service import ExtractCurrentConditionsService
|
|
9
|
+
from weathergrabber.application.services.extract_today_details_service import ExtractTodayDetailsService
|
|
10
|
+
from weathergrabber.application.services.extract_aqi_service import ExtractAQIService
|
|
11
|
+
from weathergrabber.application.services.extract_health_activities_service import ExtractHealthActivitiesService
|
|
12
|
+
from weathergrabber.application.services.extract_hourly_forecast_service import ExtractHourlyForecastService
|
|
13
|
+
from weathergrabber.application.services.extract_hourly_forecast_oldstyle_service import ExtractHourlyForecastOldstyleService
|
|
14
|
+
from weathergrabber.application.services.extract_daily_forecast_service import ExtractDailyForecastService
|
|
15
|
+
from weathergrabber.application.services.extract_daily_forecast_oldstyle_service import ExtractDailyForecastOldstyleService
|
|
16
|
+
from weathergrabber.application.services.retrieve_forecast_from_cache_service import RetrieveForecastFromCacheService
|
|
17
|
+
from weathergrabber.domain.entities.air_quality_index import AirQualityIndex
|
|
18
|
+
from weathergrabber.domain.entities.daily_predictions import DailyPredictions
|
|
19
|
+
from weathergrabber.domain.entities.health_activities import HealthActivities
|
|
20
|
+
from weathergrabber.domain.entities.hourly_predictions import HourlyPredictions
|
|
21
|
+
from weathergrabber.domain.entities.search import Search
|
|
22
|
+
from weathergrabber.domain.entities.forecast import Forecast
|
|
23
|
+
from weathergrabber.domain.entities.today_details import TodayDetails
|
|
24
|
+
from weathergrabber.application.services.save_forecast_to_cache_service import SaveForecastToCacheService
|
|
25
|
+
|
|
26
|
+
class WeatherForecastUC:
|
|
27
|
+
"""Use case for retrieving weather forecast data."""
|
|
28
|
+
|
|
29
|
+
# Constants for warning messages
|
|
30
|
+
HOURLY_FORECAST_FALLBACK_MSG = "Falling back to new style hourly forecast extraction"
|
|
31
|
+
DAILY_FORECAST_FALLBACK_MSG = "Falling back to new style daily forecast extraction"
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
search_location_service: SearchLocationService,
|
|
36
|
+
read_weather_service: ReadWeatherService,
|
|
37
|
+
extract_current_conditions_service: ExtractCurrentConditionsService,
|
|
38
|
+
extract_today_details_service: ExtractTodayDetailsService,
|
|
39
|
+
extract_aqi_service: ExtractAQIService,
|
|
40
|
+
extract_health_activities_service: ExtractHealthActivitiesService,
|
|
41
|
+
extract_hourly_forecast_service: ExtractHourlyForecastService,
|
|
42
|
+
extract_hourly_forecast_oldstyle_service: ExtractHourlyForecastOldstyleService,
|
|
43
|
+
extract_daily_forecast_service: ExtractDailyForecastService,
|
|
44
|
+
extract_daily_forecast_oldstyle_service: ExtractDailyForecastOldstyleService,
|
|
45
|
+
retrieve_forecast_from_cache_service: RetrieveForecastFromCacheService,
|
|
46
|
+
save_forecast_to_cache_service: SaveForecastToCacheService
|
|
47
|
+
):
|
|
48
|
+
self.logger = logging.getLogger(__name__)
|
|
49
|
+
self.search_location_service = search_location_service
|
|
50
|
+
self.read_weather_service = read_weather_service
|
|
51
|
+
self.extract_current_conditions_service = extract_current_conditions_service
|
|
52
|
+
self.extract_today_details_service = extract_today_details_service
|
|
53
|
+
self.extract_aqi_service = extract_aqi_service
|
|
54
|
+
self.extract_health_activities_service = extract_health_activities_service
|
|
55
|
+
self.extract_hourly_forecast_service = extract_hourly_forecast_service
|
|
56
|
+
self.extract_hourly_forecast_oldstyle_service = extract_hourly_forecast_oldstyle_service
|
|
57
|
+
self.extract_daily_forecast_service = extract_daily_forecast_service
|
|
58
|
+
self.extract_daily_forecast_oldstyle_service = extract_daily_forecast_oldstyle_service
|
|
59
|
+
self.retrieve_forecast_from_cache_service = retrieve_forecast_from_cache_service
|
|
60
|
+
self.save_forecast_to_cache_service = save_forecast_to_cache_service
|
|
61
|
+
|
|
62
|
+
def execute(self, params: Params) -> Forecast:
|
|
63
|
+
"""Execute the weather forecast retrieval use case."""
|
|
64
|
+
self.logger.debug("Starting weather forecast use case")
|
|
65
|
+
|
|
66
|
+
if params.force_cache:
|
|
67
|
+
return self.retrieve_forecast_from_cache_service.execute(params)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
location_id = self._resolve_location_id(params)
|
|
71
|
+
except ConnectionError as e:
|
|
72
|
+
return self.retrieve_forecast_from_cache_service.execute(params)
|
|
73
|
+
|
|
74
|
+
weather_data = self.read_weather_service.execute(params.language, location_id)
|
|
75
|
+
|
|
76
|
+
current_conditions = self.extract_current_conditions_service.execute(weather_data)
|
|
77
|
+
today_details = self.extract_today_details_service.execute(weather_data)
|
|
78
|
+
air_quality_index = self.extract_aqi_service.execute(weather_data)
|
|
79
|
+
health_activities = self.extract_health_activities_service.execute(weather_data)
|
|
80
|
+
|
|
81
|
+
hourly_predictions = self._extract_hourly_predictions(weather_data)
|
|
82
|
+
daily_predictions = self._extract_daily_predictions(weather_data)
|
|
83
|
+
|
|
84
|
+
forecast = Forecast(
|
|
85
|
+
search=Search(id=location_id, search_name=params.location.search_name),
|
|
86
|
+
current_conditions=current_conditions,
|
|
87
|
+
today_details=today_details,
|
|
88
|
+
air_quality_index=air_quality_index,
|
|
89
|
+
health_activities=health_activities,
|
|
90
|
+
hourly_predictions=hourly_predictions,
|
|
91
|
+
daily_predictions=daily_predictions
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
self.save_forecast_to_cache_service.execute(forecast)
|
|
95
|
+
|
|
96
|
+
self.logger.debug("Forecast data obtained successfully")
|
|
97
|
+
return forecast
|
|
98
|
+
|
|
99
|
+
def _resolve_location_id(self, params: Params) -> str:
|
|
100
|
+
"""Resolve location ID from params, searching if necessary."""
|
|
101
|
+
location_id = params.location.id
|
|
102
|
+
if not location_id:
|
|
103
|
+
location_id = self.search_location_service.execute(
|
|
104
|
+
params.location.search_name,
|
|
105
|
+
params.language
|
|
106
|
+
)
|
|
107
|
+
return location_id
|
|
108
|
+
|
|
109
|
+
def _extract_hourly_predictions(self, weather_data) -> List[HourlyPredictions]:
|
|
110
|
+
"""Extract hourly predictions with fallback mechanism."""
|
|
111
|
+
try:
|
|
112
|
+
return self.extract_hourly_forecast_oldstyle_service.execute(weather_data)
|
|
113
|
+
except ValueError:
|
|
114
|
+
self.logger.warning(self.HOURLY_FORECAST_FALLBACK_MSG)
|
|
115
|
+
return self.extract_hourly_forecast_service.execute(weather_data)
|
|
116
|
+
|
|
117
|
+
def _extract_daily_predictions(self, weather_data) -> List[DailyPredictions]:
|
|
118
|
+
"""Extract daily predictions with fallback mechanism."""
|
|
119
|
+
try:
|
|
120
|
+
return self.extract_daily_forecast_oldstyle_service.execute(weather_data)
|
|
121
|
+
except ValueError:
|
|
122
|
+
self.logger.warning(self.DAILY_FORECAST_FALLBACK_MSG)
|
|
123
|
+
return self.extract_daily_forecast_service.execute(weather_data)
|