weathergrabber 0.0.8b7__tar.gz → 0.0.9__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.
Files changed (113) hide show
  1. {weathergrabber-0.0.8b7/weathergrabber.egg-info → weathergrabber-0.0.9}/PKG-INFO +32 -7
  2. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9}/README.md +28 -6
  3. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9}/pyproject.toml +14 -2
  4. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9}/tests/test_core.py +9 -3
  5. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9}/weathergrabber/__init__.py +1 -1
  6. weathergrabber-0.0.9/weathergrabber/adapter/repository/forecast_repository.py +135 -0
  7. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9}/weathergrabber/adapter/tty/console_tty.py +4 -4
  8. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9}/weathergrabber/adapter/tty/json_tty.py +3 -3
  9. weathergrabber-0.0.9/weathergrabber/adapter/tty/statistics_tty.py +35 -0
  10. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9}/weathergrabber/adapter/tty/waybar_tty.py +3 -3
  11. {weathergrabber-0.0.8b7/weathergrabber/service → weathergrabber-0.0.9/weathergrabber/application/services}/extract_aqi_service.py +1 -1
  12. {weathergrabber-0.0.8b7/weathergrabber/service → weathergrabber-0.0.9/weathergrabber/application/services}/extract_current_conditions_service.py +5 -5
  13. {weathergrabber-0.0.8b7/weathergrabber/service → weathergrabber-0.0.9/weathergrabber/application/services}/extract_daily_forecast_oldstyle_service.py +4 -4
  14. {weathergrabber-0.0.8b7/weathergrabber/service → weathergrabber-0.0.9/weathergrabber/application/services}/extract_daily_forecast_service.py +6 -6
  15. {weathergrabber-0.0.8b7/weathergrabber/service → weathergrabber-0.0.9/weathergrabber/application/services}/extract_health_activities_service.py +1 -1
  16. {weathergrabber-0.0.8b7/weathergrabber/service → weathergrabber-0.0.9/weathergrabber/application/services}/extract_hourly_forecast_oldstyle_service.py +3 -3
  17. {weathergrabber-0.0.8b7/weathergrabber/service → weathergrabber-0.0.9/weathergrabber/application/services}/extract_hourly_forecast_service.py +5 -5
  18. {weathergrabber-0.0.8b7/weathergrabber/service → weathergrabber-0.0.9/weathergrabber/application/services}/extract_today_details_service.py +8 -8
  19. weathergrabber-0.0.9/weathergrabber/application/services/retrieve_forecast_from_cache_service.py +27 -0
  20. weathergrabber-0.0.9/weathergrabber/application/services/retrieve_statistics_service.py +20 -0
  21. weathergrabber-0.0.9/weathergrabber/application/services/save_forecast_to_cache_service.py +21 -0
  22. weathergrabber-0.0.9/weathergrabber/application/usecases/statistics_uc.py +19 -0
  23. weathergrabber-0.0.9/weathergrabber/application/usecases/weather_forecast_uc.py +123 -0
  24. weathergrabber-0.0.9/weathergrabber/application/weathergrabber_application.py +92 -0
  25. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9}/weathergrabber/cli.py +6 -2
  26. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9}/weathergrabber/core.py +7 -3
  27. weathergrabber-0.0.9/weathergrabber/domain/adapter/mappers/air_quality_index_mapper.py +25 -0
  28. weathergrabber-0.0.9/weathergrabber/domain/adapter/mappers/city_location_mapper.py +15 -0
  29. weathergrabber-0.0.9/weathergrabber/domain/adapter/mappers/color_mapper.py +17 -0
  30. weathergrabber-0.0.9/weathergrabber/domain/adapter/mappers/current_conditions_mapper.py +32 -0
  31. weathergrabber-0.0.9/weathergrabber/domain/adapter/mappers/daily_predictions_mapper.py +30 -0
  32. weathergrabber-0.0.9/weathergrabber/domain/adapter/mappers/day_night_mapper.py +23 -0
  33. weathergrabber-0.0.9/weathergrabber/domain/adapter/mappers/forecast_mapper.py +38 -0
  34. weathergrabber-0.0.9/weathergrabber/domain/adapter/mappers/health_activities_mapper.py +15 -0
  35. weathergrabber-0.0.9/weathergrabber/domain/adapter/mappers/hourly_predictions_mapper.py +38 -0
  36. weathergrabber-0.0.9/weathergrabber/domain/adapter/mappers/label_value_mapper.py +13 -0
  37. weathergrabber-0.0.9/weathergrabber/domain/adapter/mappers/moon_phase_mapper.py +16 -0
  38. weathergrabber-0.0.9/weathergrabber/domain/adapter/mappers/precipitation_mapper.py +13 -0
  39. weathergrabber-0.0.9/weathergrabber/domain/adapter/mappers/search_mapper.py +13 -0
  40. weathergrabber-0.0.9/weathergrabber/domain/adapter/mappers/statistics_mapper.py +25 -0
  41. weathergrabber-0.0.9/weathergrabber/domain/adapter/mappers/sunrise_sunset_mapper.py +23 -0
  42. weathergrabber-0.0.9/weathergrabber/domain/adapter/mappers/temperature_high_low_mapper.py +15 -0
  43. weathergrabber-0.0.9/weathergrabber/domain/adapter/mappers/timestamp_mapper.py +15 -0
  44. weathergrabber-0.0.9/weathergrabber/domain/adapter/mappers/today_details_mapper.py +42 -0
  45. weathergrabber-0.0.9/weathergrabber/domain/adapter/mappers/uv_index_mapper.py +17 -0
  46. weathergrabber-0.0.9/weathergrabber/domain/adapter/mappers/weather_icon_enum_mapper.py +11 -0
  47. weathergrabber-0.0.9/weathergrabber/domain/adapter/mappers/wind_mapper.py +16 -0
  48. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9}/weathergrabber/domain/adapter/output_enum.py +1 -1
  49. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9}/weathergrabber/domain/adapter/params.py +25 -4
  50. {weathergrabber-0.0.8b7/weathergrabber/domain → weathergrabber-0.0.9/weathergrabber/domain/entities}/moon_phase.py +1 -1
  51. weathergrabber-0.0.9/weathergrabber/domain/entities/statistics.py +46 -0
  52. {weathergrabber-0.0.8b7/weathergrabber/domain → weathergrabber-0.0.9/weathergrabber/domain/entities}/sunrise_sunset.py +0 -1
  53. {weathergrabber-0.0.8b7/weathergrabber/domain → weathergrabber-0.0.9/weathergrabber/domain/entities}/weather_icon_enum.py +1 -0
  54. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9/weathergrabber.egg-info}/PKG-INFO +32 -7
  55. weathergrabber-0.0.9/weathergrabber.egg-info/SOURCES.txt +87 -0
  56. weathergrabber-0.0.9/weathergrabber.egg-info/requires.txt +6 -0
  57. weathergrabber-0.0.8b7/weathergrabber/domain/adapter/mapper/air_quality_index_mapper.py +0 -13
  58. weathergrabber-0.0.8b7/weathergrabber/domain/adapter/mapper/city_location_mapper.py +0 -8
  59. weathergrabber-0.0.8b7/weathergrabber/domain/adapter/mapper/color_mapper.py +0 -10
  60. weathergrabber-0.0.8b7/weathergrabber/domain/adapter/mapper/current_conditions_mapper.py +0 -16
  61. weathergrabber-0.0.8b7/weathergrabber/domain/adapter/mapper/daily_predictions_mapper.py +0 -15
  62. weathergrabber-0.0.8b7/weathergrabber/domain/adapter/mapper/day_night_mapper.py +0 -12
  63. weathergrabber-0.0.8b7/weathergrabber/domain/adapter/mapper/forecast_mapper.py +0 -20
  64. weathergrabber-0.0.8b7/weathergrabber/domain/adapter/mapper/health_activities_mapper.py +0 -8
  65. weathergrabber-0.0.8b7/weathergrabber/domain/adapter/mapper/hourly_predictions_mapper.py +0 -19
  66. weathergrabber-0.0.8b7/weathergrabber/domain/adapter/mapper/label_value_mapper.py +0 -7
  67. weathergrabber-0.0.8b7/weathergrabber/domain/adapter/mapper/moon_phase_mapper.py +0 -9
  68. weathergrabber-0.0.8b7/weathergrabber/domain/adapter/mapper/precipitation_mapper.py +0 -7
  69. weathergrabber-0.0.8b7/weathergrabber/domain/adapter/mapper/search_mapper.py +0 -7
  70. weathergrabber-0.0.8b7/weathergrabber/domain/adapter/mapper/sunrise_sunset_mapper.py +0 -13
  71. weathergrabber-0.0.8b7/weathergrabber/domain/adapter/mapper/temperature_high_low_mapper.py +0 -8
  72. weathergrabber-0.0.8b7/weathergrabber/domain/adapter/mapper/timestamp_mapper.py +0 -8
  73. weathergrabber-0.0.8b7/weathergrabber/domain/adapter/mapper/today_details_mapper.py +0 -21
  74. weathergrabber-0.0.8b7/weathergrabber/domain/adapter/mapper/uv_index_mapper.py +0 -9
  75. weathergrabber-0.0.8b7/weathergrabber/domain/adapter/mapper/weather_icon_enum_mapper.py +0 -8
  76. weathergrabber-0.0.8b7/weathergrabber/domain/adapter/mapper/wind_mapper.py +0 -7
  77. weathergrabber-0.0.8b7/weathergrabber/usecase/use_case.py +0 -87
  78. weathergrabber-0.0.8b7/weathergrabber/weathergrabber_application.py +0 -78
  79. weathergrabber-0.0.8b7/weathergrabber.egg-info/SOURCES.txt +0 -79
  80. weathergrabber-0.0.8b7/weathergrabber.egg-info/requires.txt +0 -2
  81. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9}/LICENSE +0 -0
  82. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9}/setup.cfg +0 -0
  83. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9}/tests/test_cli.py +0 -0
  84. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9}/tests/test_cli_version.py +0 -0
  85. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9}/tests/test_main.py +0 -0
  86. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9}/weathergrabber/__main__.py +0 -0
  87. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9}/weathergrabber/adapter/client/weather_api.py +0 -0
  88. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9}/weathergrabber/adapter/client/weather_search_api.py +0 -0
  89. {weathergrabber-0.0.8b7/weathergrabber/service → weathergrabber-0.0.9/weathergrabber/application/services}/extract_temperature_service.py +0 -0
  90. {weathergrabber-0.0.8b7/weathergrabber/service → weathergrabber-0.0.9/weathergrabber/application/services}/read_weather_service.py +0 -0
  91. {weathergrabber-0.0.8b7/weathergrabber/service → weathergrabber-0.0.9/weathergrabber/application/services}/search_location_service.py +0 -0
  92. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9}/weathergrabber/domain/adapter/icon_enum.py +0 -0
  93. {weathergrabber-0.0.8b7/weathergrabber/domain → weathergrabber-0.0.9/weathergrabber/domain/entities}/air_quality_index.py +0 -0
  94. {weathergrabber-0.0.8b7/weathergrabber/domain → weathergrabber-0.0.9/weathergrabber/domain/entities}/city_location.py +0 -0
  95. {weathergrabber-0.0.8b7/weathergrabber/domain → weathergrabber-0.0.9/weathergrabber/domain/entities}/color.py +0 -0
  96. {weathergrabber-0.0.8b7/weathergrabber/domain → weathergrabber-0.0.9/weathergrabber/domain/entities}/current_conditions.py +0 -0
  97. {weathergrabber-0.0.8b7/weathergrabber/domain → weathergrabber-0.0.9/weathergrabber/domain/entities}/daily_predictions.py +0 -0
  98. {weathergrabber-0.0.8b7/weathergrabber/domain → weathergrabber-0.0.9/weathergrabber/domain/entities}/day_night.py +0 -0
  99. {weathergrabber-0.0.8b7/weathergrabber/domain → weathergrabber-0.0.9/weathergrabber/domain/entities}/forecast.py +0 -0
  100. {weathergrabber-0.0.8b7/weathergrabber/domain → weathergrabber-0.0.9/weathergrabber/domain/entities}/health_activities.py +0 -0
  101. {weathergrabber-0.0.8b7/weathergrabber/domain → weathergrabber-0.0.9/weathergrabber/domain/entities}/hourly_predictions.py +0 -0
  102. {weathergrabber-0.0.8b7/weathergrabber/domain → weathergrabber-0.0.9/weathergrabber/domain/entities}/label_value.py +0 -0
  103. {weathergrabber-0.0.8b7/weathergrabber/domain → weathergrabber-0.0.9/weathergrabber/domain/entities}/moon_phase_enum.py +0 -0
  104. {weathergrabber-0.0.8b7/weathergrabber/domain → weathergrabber-0.0.9/weathergrabber/domain/entities}/precipitation.py +0 -0
  105. {weathergrabber-0.0.8b7/weathergrabber/domain → weathergrabber-0.0.9/weathergrabber/domain/entities}/search.py +0 -0
  106. {weathergrabber-0.0.8b7/weathergrabber/domain → weathergrabber-0.0.9/weathergrabber/domain/entities}/temperature_hight_low.py +0 -0
  107. {weathergrabber-0.0.8b7/weathergrabber/domain → weathergrabber-0.0.9/weathergrabber/domain/entities}/timestamp.py +0 -0
  108. {weathergrabber-0.0.8b7/weathergrabber/domain → weathergrabber-0.0.9/weathergrabber/domain/entities}/today_details.py +0 -0
  109. {weathergrabber-0.0.8b7/weathergrabber/domain → weathergrabber-0.0.9/weathergrabber/domain/entities}/uv_index.py +0 -0
  110. {weathergrabber-0.0.8b7/weathergrabber/domain → weathergrabber-0.0.9/weathergrabber/domain/entities}/wind.py +0 -0
  111. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9}/weathergrabber.egg-info/dependency_links.txt +0 -0
  112. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9}/weathergrabber.egg-info/entry_points.txt +0 -0
  113. {weathergrabber-0.0.8b7 → weathergrabber-0.0.9}/weathergrabber.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weathergrabber
3
- Version: 0.0.8b7
3
+ Version: 0.0.9
4
4
  Summary: A grabber for weather.com data with various output formats.
5
5
  Author-email: Carlos Anselmo Mendes Junior <cjuniorfox@gmail.com>
6
6
  License: MIT
@@ -11,6 +11,9 @@ Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
12
12
  Requires-Dist: pyquery>=1.4.3
13
13
  Requires-Dist: requests>=2.32.4
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
16
+ Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
14
17
  Dynamic: license-file
15
18
 
16
19
  # Weather Forecast CLI Script
@@ -34,6 +37,8 @@ This script fetches and parses weather forecast data from Weather.com and format
34
37
  - **Console output**: Richly formatted weather data with icons.
35
38
  - **Waybar JSON**: For integration with Waybar.
36
39
  - Supports multiple languages for Weather.com data.
40
+ - **Offline support**: Automatically retrieves cached weather data when internet connection is unavailable.
41
+ - **Cache management**: SQLite-based caching system for storing weather forecasts and enabling offline access.
37
42
  - Includes data such as:
38
43
  - Current temperature and "feels-like" temperature.
39
44
  - Wind speed, humidity, visibility, and air quality.
@@ -163,24 +168,42 @@ weathergrabber.main_cli()
163
168
 
164
169
  ### Options
165
170
 
166
- - `--location-id`, `-l` : 64-character-hex code for location (from Weather.com)
167
- - `--lang`, `-L` : Language (e.g., `pt-BR`, `fr-FR`). Defaults to system locale if not set.
168
- - `--output`, `-o` : Output format. One of `console`, `json`, or `waybar`. Default: `console`.
169
- - `--keep-open`, `-k` : Keep open and refresh every 5 minutes (only makes sense for `console` output).
170
- - `--icons`, `-i` : Icon set. `fa` for Font-Awesome, `emoji` for emoji icons. Default: `emoji`.
171
- - `--log` : Set logging level. One of `debug`, `info`, `warning`, `error`, `critical`. Default: `critical`.
171
+ - `--location-id`, `-l` : 64-character-hex code for location (from Weather.com)
172
+ - `--lang`, `-L` : Language (e.g., `pt-BR`, `fr-FR`). Defaults to system locale if not set.
173
+ - `--output`, `-o` : Output format. One of `console`, `json`, `waybar`, or `statistics`. Default: `console`.
174
+ - `--keep-open`, `-k` : Keep open and refresh every 5 minutes (only makes sense for `console` output).
175
+ - `--force-cache` : Retrieve weather data from cache regardless of internet connection availability. Useful for offline mode.
176
+ - `--cache-statistics` : Display cache database statistics including total forecasts, unique locations, unique search names, and database file path.
177
+ - `--icons`, `-i` : Icon set. `fa` for Font-Awesome, `emoji` for emoji icons. Default: `emoji`.
178
+ - `--log` : Set logging level. One of `debug`, `info`, `warning`, `error`, `critical`. Default: `critical`.
172
179
 
173
180
  ### Environment Variables
174
181
 
175
182
  - `LANG` : Used as default language if `--lang` is not set.
176
183
  - `WEATHER_LOCATION_ID` : Used as default location if neither `location_name` nor `--location-id` is set.
177
184
 
185
+ ## Cache & Offline Support
186
+
187
+ The script maintains a SQLite cache database for storing weather forecasts. This enables several key features:
188
+
189
+ - **Automatic offline fallback**: When internet connection is unavailable, the script automatically retrieves the most recent cached weather data for the requested location.
190
+ - **Forced cache retrieval**: Use `--force-cache` to retrieve data exclusively from the cache, regardless of internet availability. This is useful for offline scenarios or reducing API calls.
191
+ - **Cache statistics**: Use `--cache-statistics` to display information about the cache database, including:
192
+ - Total number of cached forecasts
193
+ - Number of unique locations searched
194
+ - Number of unique search names used
195
+ - Database file path
196
+
197
+ By default, the cache database is stored in the system's temporary directory (`/tmp` on Linux/macOS).
198
+
178
199
  ### Example Usage
179
200
 
180
201
  ```sh
181
202
  weathergrabber "London" --output console --lang en-GB
182
203
  weathergrabber --location-id 1234567890abcdef... --output json
183
204
  weathergrabber "Paris" -o waybar -i fa
205
+ weathergrabber "New York" --force-cache
206
+ weathergrabber --cache-statistics
184
207
  ```
185
208
 
186
209
  Or as a Python module:
@@ -189,4 +212,6 @@ Or as a Python module:
189
212
  python -m weathergrabber "London" --output console --lang en-GB
190
213
  python -m weathergrabber --location-id 1234567890abcdef... --output json
191
214
  python -m weathergrabber "Paris" -o waybar -i fa
215
+ python -m weathergrabber "Toronto" --force-cache
216
+ python -m weathergrabber --cache-statistics
192
217
  ```
@@ -19,6 +19,8 @@ This script fetches and parses weather forecast data from Weather.com and format
19
19
  - **Console output**: Richly formatted weather data with icons.
20
20
  - **Waybar JSON**: For integration with Waybar.
21
21
  - Supports multiple languages for Weather.com data.
22
+ - **Offline support**: Automatically retrieves cached weather data when internet connection is unavailable.
23
+ - **Cache management**: SQLite-based caching system for storing weather forecasts and enabling offline access.
22
24
  - Includes data such as:
23
25
  - Current temperature and "feels-like" temperature.
24
26
  - Wind speed, humidity, visibility, and air quality.
@@ -148,24 +150,42 @@ weathergrabber.main_cli()
148
150
 
149
151
  ### Options
150
152
 
151
- - `--location-id`, `-l` : 64-character-hex code for location (from Weather.com)
152
- - `--lang`, `-L` : Language (e.g., `pt-BR`, `fr-FR`). Defaults to system locale if not set.
153
- - `--output`, `-o` : Output format. One of `console`, `json`, or `waybar`. Default: `console`.
154
- - `--keep-open`, `-k` : Keep open and refresh every 5 minutes (only makes sense for `console` output).
155
- - `--icons`, `-i` : Icon set. `fa` for Font-Awesome, `emoji` for emoji icons. Default: `emoji`.
156
- - `--log` : Set logging level. One of `debug`, `info`, `warning`, `error`, `critical`. Default: `critical`.
153
+ - `--location-id`, `-l` : 64-character-hex code for location (from Weather.com)
154
+ - `--lang`, `-L` : Language (e.g., `pt-BR`, `fr-FR`). Defaults to system locale if not set.
155
+ - `--output`, `-o` : Output format. One of `console`, `json`, `waybar`, or `statistics`. Default: `console`.
156
+ - `--keep-open`, `-k` : Keep open and refresh every 5 minutes (only makes sense for `console` output).
157
+ - `--force-cache` : Retrieve weather data from cache regardless of internet connection availability. Useful for offline mode.
158
+ - `--cache-statistics` : Display cache database statistics including total forecasts, unique locations, unique search names, and database file path.
159
+ - `--icons`, `-i` : Icon set. `fa` for Font-Awesome, `emoji` for emoji icons. Default: `emoji`.
160
+ - `--log` : Set logging level. One of `debug`, `info`, `warning`, `error`, `critical`. Default: `critical`.
157
161
 
158
162
  ### Environment Variables
159
163
 
160
164
  - `LANG` : Used as default language if `--lang` is not set.
161
165
  - `WEATHER_LOCATION_ID` : Used as default location if neither `location_name` nor `--location-id` is set.
162
166
 
167
+ ## Cache & Offline Support
168
+
169
+ The script maintains a SQLite cache database for storing weather forecasts. This enables several key features:
170
+
171
+ - **Automatic offline fallback**: When internet connection is unavailable, the script automatically retrieves the most recent cached weather data for the requested location.
172
+ - **Forced cache retrieval**: Use `--force-cache` to retrieve data exclusively from the cache, regardless of internet availability. This is useful for offline scenarios or reducing API calls.
173
+ - **Cache statistics**: Use `--cache-statistics` to display information about the cache database, including:
174
+ - Total number of cached forecasts
175
+ - Number of unique locations searched
176
+ - Number of unique search names used
177
+ - Database file path
178
+
179
+ By default, the cache database is stored in the system's temporary directory (`/tmp` on Linux/macOS).
180
+
163
181
  ### Example Usage
164
182
 
165
183
  ```sh
166
184
  weathergrabber "London" --output console --lang en-GB
167
185
  weathergrabber --location-id 1234567890abcdef... --output json
168
186
  weathergrabber "Paris" -o waybar -i fa
187
+ weathergrabber "New York" --force-cache
188
+ weathergrabber --cache-statistics
169
189
  ```
170
190
 
171
191
  Or as a Python module:
@@ -174,4 +194,6 @@ Or as a Python module:
174
194
  python -m weathergrabber "London" --output console --lang en-GB
175
195
  python -m weathergrabber --location-id 1234567890abcdef... --output json
176
196
  python -m weathergrabber "Paris" -o waybar -i fa
197
+ python -m weathergrabber "Toronto" --force-cache
198
+ python -m weathergrabber --cache-statistics
177
199
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "weathergrabber"
3
- version = "0.0.8b7"
3
+ version = "0.0.9"
4
4
  description = "A grabber for weather.com data with various output formats."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -14,6 +14,12 @@ dependencies = [
14
14
  "requests>=2.32.4"
15
15
  ]
16
16
 
17
+ [project.optional-dependencies]
18
+ dev = [
19
+ "pytest>=8.0.0",
20
+ "pytest-cov>=4.1.0",
21
+ ]
22
+
17
23
  [build-system]
18
24
  requires = ["setuptools>=61", "wheel"]
19
25
  build-backend = "setuptools.build_meta"
@@ -23,4 +29,10 @@ weathergrabber = "weathergrabber.cli:main_cli"
23
29
 
24
30
  [project.urls]
25
31
  homepage = "https://github.com/cjuniorfox/weather"
26
- repository = "https://github.com/cjuniorfox/weather"
32
+ repository = "https://github.com/cjuniorfox/weather"
33
+
34
+ [tool.pytest.ini_options]
35
+ filterwarnings = [
36
+ "ignore:unclosed database:ResourceWarning",
37
+ "ignore::DeprecationWarning:sqlite3",
38
+ ]
@@ -12,7 +12,9 @@ def test_main_invokes_application(mock_app):
12
12
  'lang': 'en-US',
13
13
  'output': 'console',
14
14
  'keep_open': False,
15
- 'icons': 'emoji'
15
+ 'icons': 'emoji',
16
+ 'force_cache': False,
17
+ 'cache_statistics': False,
16
18
  }
17
19
  main(**params)
18
20
  mock_app.assert_called_once()
@@ -25,7 +27,9 @@ def test_main_invokes_application(mock_app):
25
27
  assert args[0].output_format.name.lower() == 'console'
26
28
  assert args[0].keep_open is False
27
29
  assert args[0].icons.name.lower() == 'emoji'
28
-
30
+ assert args[0].force_cache is False
31
+ assert args[0].cache_statistics is False
32
+
29
33
  @patch('weathergrabber.core.WeatherGrabberApplication')
30
34
  def test_main_sets_log_level(mock_app):
31
35
  from weathergrabber.core import main
@@ -36,6 +40,8 @@ def test_main_sets_log_level(mock_app):
36
40
  lang='fr-FR',
37
41
  output='json',
38
42
  keep_open=True,
39
- icons='fa'
43
+ icons='fa',
44
+ force_cache=False,
45
+ cache_statistics=False,
40
46
  )
41
47
  assert logging.getLogger().level == logging.DEBUG
@@ -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.8b7"
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
@@ -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")