weathergrabber 0.0.8b6__py3-none-any.whl → 0.0.9__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 (99) hide show
  1. weathergrabber/__init__.py +1 -1
  2. weathergrabber/adapter/repository/forecast_repository.py +135 -0
  3. weathergrabber/adapter/tty/console_tty.py +4 -4
  4. weathergrabber/adapter/tty/json_tty.py +3 -3
  5. weathergrabber/adapter/tty/statistics_tty.py +35 -0
  6. weathergrabber/adapter/tty/waybar_tty.py +4 -4
  7. weathergrabber/{service → application/services}/extract_aqi_service.py +1 -1
  8. weathergrabber/{service → application/services}/extract_current_conditions_service.py +5 -5
  9. weathergrabber/{service → application/services}/extract_daily_forecast_oldstyle_service.py +4 -4
  10. weathergrabber/{service → application/services}/extract_daily_forecast_service.py +6 -6
  11. weathergrabber/{service → application/services}/extract_health_activities_service.py +1 -1
  12. weathergrabber/{service → application/services}/extract_hourly_forecast_oldstyle_service.py +3 -3
  13. weathergrabber/{service → application/services}/extract_hourly_forecast_service.py +5 -5
  14. weathergrabber/{service → application/services}/extract_today_details_service.py +8 -8
  15. weathergrabber/application/services/retrieve_forecast_from_cache_service.py +27 -0
  16. weathergrabber/application/services/retrieve_statistics_service.py +20 -0
  17. weathergrabber/application/services/save_forecast_to_cache_service.py +21 -0
  18. weathergrabber/application/usecases/statistics_uc.py +19 -0
  19. weathergrabber/application/usecases/weather_forecast_uc.py +123 -0
  20. weathergrabber/application/weathergrabber_application.py +92 -0
  21. weathergrabber/cli.py +6 -2
  22. weathergrabber/core.py +7 -3
  23. weathergrabber/domain/adapter/mappers/air_quality_index_mapper.py +25 -0
  24. weathergrabber/domain/adapter/mappers/city_location_mapper.py +15 -0
  25. weathergrabber/domain/adapter/mappers/color_mapper.py +17 -0
  26. weathergrabber/domain/adapter/mappers/current_conditions_mapper.py +32 -0
  27. weathergrabber/domain/adapter/mappers/daily_predictions_mapper.py +30 -0
  28. weathergrabber/domain/adapter/mappers/day_night_mapper.py +23 -0
  29. weathergrabber/domain/adapter/mappers/forecast_mapper.py +38 -0
  30. weathergrabber/domain/adapter/mappers/health_activities_mapper.py +15 -0
  31. weathergrabber/domain/adapter/mappers/hourly_predictions_mapper.py +38 -0
  32. weathergrabber/domain/adapter/mappers/label_value_mapper.py +13 -0
  33. weathergrabber/domain/adapter/mappers/moon_phase_mapper.py +16 -0
  34. weathergrabber/domain/adapter/mappers/precipitation_mapper.py +13 -0
  35. weathergrabber/domain/adapter/mappers/search_mapper.py +13 -0
  36. weathergrabber/domain/adapter/mappers/statistics_mapper.py +25 -0
  37. weathergrabber/domain/adapter/mappers/sunrise_sunset_mapper.py +23 -0
  38. weathergrabber/domain/adapter/mappers/temperature_high_low_mapper.py +15 -0
  39. weathergrabber/domain/adapter/mappers/timestamp_mapper.py +15 -0
  40. weathergrabber/domain/adapter/mappers/today_details_mapper.py +42 -0
  41. weathergrabber/domain/adapter/mappers/uv_index_mapper.py +17 -0
  42. weathergrabber/domain/adapter/mappers/weather_icon_enum_mapper.py +11 -0
  43. weathergrabber/domain/adapter/mappers/wind_mapper.py +16 -0
  44. weathergrabber/domain/adapter/output_enum.py +1 -1
  45. weathergrabber/domain/adapter/params.py +25 -4
  46. weathergrabber/domain/{moon_phase.py → entities/moon_phase.py} +1 -1
  47. weathergrabber/domain/entities/statistics.py +46 -0
  48. weathergrabber/domain/{sunrise_sunset.py → entities/sunrise_sunset.py} +0 -1
  49. weathergrabber/domain/{weather_icon_enum.py → entities/weather_icon_enum.py} +1 -0
  50. {weathergrabber-0.0.8b6.dist-info → weathergrabber-0.0.9.dist-info}/METADATA +32 -7
  51. weathergrabber-0.0.9.dist-info/RECORD +80 -0
  52. {weathergrabber-0.0.8b6.dist-info → weathergrabber-0.0.9.dist-info}/WHEEL +1 -1
  53. weathergrabber/domain/adapter/mapper/air_quality_index_mapper.py +0 -13
  54. weathergrabber/domain/adapter/mapper/city_location_mapper.py +0 -8
  55. weathergrabber/domain/adapter/mapper/color_mapper.py +0 -10
  56. weathergrabber/domain/adapter/mapper/current_conditions_mapper.py +0 -16
  57. weathergrabber/domain/adapter/mapper/daily_predictions_mapper.py +0 -15
  58. weathergrabber/domain/adapter/mapper/day_night_mapper.py +0 -12
  59. weathergrabber/domain/adapter/mapper/forecast_mapper.py +0 -20
  60. weathergrabber/domain/adapter/mapper/health_activities_mapper.py +0 -8
  61. weathergrabber/domain/adapter/mapper/hourly_predictions_mapper.py +0 -19
  62. weathergrabber/domain/adapter/mapper/label_value_mapper.py +0 -7
  63. weathergrabber/domain/adapter/mapper/moon_phase_mapper.py +0 -9
  64. weathergrabber/domain/adapter/mapper/precipitation_mapper.py +0 -7
  65. weathergrabber/domain/adapter/mapper/search_mapper.py +0 -7
  66. weathergrabber/domain/adapter/mapper/sunrise_sunset_mapper.py +0 -13
  67. weathergrabber/domain/adapter/mapper/temperature_high_low_mapper.py +0 -8
  68. weathergrabber/domain/adapter/mapper/timestamp_mapper.py +0 -8
  69. weathergrabber/domain/adapter/mapper/today_details_mapper.py +0 -21
  70. weathergrabber/domain/adapter/mapper/uv_index_mapper.py +0 -9
  71. weathergrabber/domain/adapter/mapper/weather_icon_enum_mapper.py +0 -8
  72. weathergrabber/domain/adapter/mapper/wind_mapper.py +0 -7
  73. weathergrabber/usecase/use_case.py +0 -87
  74. weathergrabber/weathergrabber_application.py +0 -78
  75. weathergrabber-0.0.8b6.dist-info/RECORD +0 -72
  76. /weathergrabber/{service → application/services}/extract_temperature_service.py +0 -0
  77. /weathergrabber/{service → application/services}/read_weather_service.py +0 -0
  78. /weathergrabber/{service → application/services}/search_location_service.py +0 -0
  79. /weathergrabber/domain/{air_quality_index.py → entities/air_quality_index.py} +0 -0
  80. /weathergrabber/domain/{city_location.py → entities/city_location.py} +0 -0
  81. /weathergrabber/domain/{color.py → entities/color.py} +0 -0
  82. /weathergrabber/domain/{current_conditions.py → entities/current_conditions.py} +0 -0
  83. /weathergrabber/domain/{daily_predictions.py → entities/daily_predictions.py} +0 -0
  84. /weathergrabber/domain/{day_night.py → entities/day_night.py} +0 -0
  85. /weathergrabber/domain/{forecast.py → entities/forecast.py} +0 -0
  86. /weathergrabber/domain/{health_activities.py → entities/health_activities.py} +0 -0
  87. /weathergrabber/domain/{hourly_predictions.py → entities/hourly_predictions.py} +0 -0
  88. /weathergrabber/domain/{label_value.py → entities/label_value.py} +0 -0
  89. /weathergrabber/domain/{moon_phase_enum.py → entities/moon_phase_enum.py} +0 -0
  90. /weathergrabber/domain/{precipitation.py → entities/precipitation.py} +0 -0
  91. /weathergrabber/domain/{search.py → entities/search.py} +0 -0
  92. /weathergrabber/domain/{temperature_hight_low.py → entities/temperature_hight_low.py} +0 -0
  93. /weathergrabber/domain/{timestamp.py → entities/timestamp.py} +0 -0
  94. /weathergrabber/domain/{today_details.py → entities/today_details.py} +0 -0
  95. /weathergrabber/domain/{uv_index.py → entities/uv_index.py} +0 -0
  96. /weathergrabber/domain/{wind.py → entities/wind.py} +0 -0
  97. {weathergrabber-0.0.8b6.dist-info → weathergrabber-0.0.9.dist-info}/entry_points.txt +0 -0
  98. {weathergrabber-0.0.8b6.dist-info → weathergrabber-0.0.9.dist-info}/licenses/LICENSE +0 -0
  99. {weathergrabber-0.0.8b6.dist-info → weathergrabber-0.0.9.dist-info}/top_level.txt +0 -0
@@ -4,7 +4,7 @@ from .core import main
4
4
  from .cli import main_cli
5
5
 
6
6
  __all__ = ["main", "main_cli"]
7
- __version__ = "0.0.8b6"
7
+ __version__ = "0.0.9"
8
8
 
9
9
  def get_version():
10
10
  return __version__
@@ -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.usecase.use_case import UseCase
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: UseCase):
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.usecase.use_case import UseCase
1
+ from weathergrabber.application.usecases.weather_forecast_uc import WeatherForecastUC
2
2
  from weathergrabber.domain.adapter.params import Params
3
- from weathergrabber.domain.adapter.mapper.forecast_mapper import forecast_to_dict
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: UseCase):
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.usecase.use_case import UseCase
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: UseCase):
10
+ def __init__(self, use_case: WeatherForecastUC):
11
11
  self.logger = logging.getLogger(__name__)
12
12
  self.use_case = use_case
13
13
  pass
@@ -97,7 +97,7 @@ class WaybarTTY:
97
97
  f"{d['title']}"
98
98
  f"{'\t\t' if len(d['title']) < 5 else '\t'}"
99
99
  f"{d['high_low']}"
100
- f"{'\t\t' if len(d['high_low']) < 32 else '\t'}"
100
+ f"{'\t\t' if len(d['high_low']) < 33 else '\t'}"
101
101
  f"{d['icon']}\t"
102
102
  f"{rain_icon} {d['precipitation']}"
103
103
  for d in daily_predictions_format
@@ -1,5 +1,5 @@
1
1
  import logging
2
- from weathergrabber.domain.air_quality_index import AirQualityIndex
2
+ from weathergrabber.domain.entities.air_quality_index import AirQualityIndex
3
3
  from pyquery import PyQuery
4
4
 
5
5
  class ExtractAQIService:
@@ -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,6 +1,6 @@
1
1
  import logging
2
2
  from pyquery import PyQuery
3
- from weathergrabber.domain.health_activities import HealthActivities
3
+ from weathergrabber.domain.entities.health_activities import HealthActivities
4
4
 
5
5
  class ExtractHealthActivitiesService:
6
6
  def __init__(self):
@@ -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(f"Creating domain objects for today details...")
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)