weathergrabber 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- weathergrabber-0.0.1/PKG-INFO +176 -0
- weathergrabber-0.0.1/README.md +163 -0
- weathergrabber-0.0.1/pyproject.toml +22 -0
- weathergrabber-0.0.1/setup.cfg +4 -0
- weathergrabber-0.0.1/tests/test_cli.py +75 -0
- weathergrabber-0.0.1/tests/test_core.py +41 -0
- weathergrabber-0.0.1/weathergrabber/__init__.py +0 -0
- weathergrabber-0.0.1/weathergrabber/adapter/client/weather_api.py +27 -0
- weathergrabber-0.0.1/weathergrabber/adapter/client/weather_search_api.py +50 -0
- weathergrabber-0.0.1/weathergrabber/adapter/tty/console_tty.py +102 -0
- weathergrabber-0.0.1/weathergrabber/adapter/tty/json_tty.py +19 -0
- weathergrabber-0.0.1/weathergrabber/adapter/tty/waybar_tty.py +110 -0
- weathergrabber-0.0.1/weathergrabber/cli.py +37 -0
- weathergrabber-0.0.1/weathergrabber/core.py +31 -0
- weathergrabber-0.0.1/weathergrabber/domain/adapter/icon_enum.py +5 -0
- weathergrabber-0.0.1/weathergrabber/domain/adapter/mapper/air_quality_index_mapper.py +13 -0
- weathergrabber-0.0.1/weathergrabber/domain/adapter/mapper/city_location_mapper.py +8 -0
- weathergrabber-0.0.1/weathergrabber/domain/adapter/mapper/color_mapper.py +10 -0
- weathergrabber-0.0.1/weathergrabber/domain/adapter/mapper/current_conditions_mapper.py +16 -0
- weathergrabber-0.0.1/weathergrabber/domain/adapter/mapper/daily_predictions_mapper.py +15 -0
- weathergrabber-0.0.1/weathergrabber/domain/adapter/mapper/day_night_mapper.py +12 -0
- weathergrabber-0.0.1/weathergrabber/domain/adapter/mapper/forecast_mapper.py +20 -0
- weathergrabber-0.0.1/weathergrabber/domain/adapter/mapper/health_activities_mapper.py +8 -0
- weathergrabber-0.0.1/weathergrabber/domain/adapter/mapper/hourly_predictions_mapper.py +19 -0
- weathergrabber-0.0.1/weathergrabber/domain/adapter/mapper/label_value_mapper.py +7 -0
- weathergrabber-0.0.1/weathergrabber/domain/adapter/mapper/moon_phase_mapper.py +9 -0
- weathergrabber-0.0.1/weathergrabber/domain/adapter/mapper/precipitation_mapper.py +7 -0
- weathergrabber-0.0.1/weathergrabber/domain/adapter/mapper/search_mapper.py +7 -0
- weathergrabber-0.0.1/weathergrabber/domain/adapter/mapper/sunrise_sunset_mapper.py +13 -0
- weathergrabber-0.0.1/weathergrabber/domain/adapter/mapper/temperature_high_low_mapper.py +8 -0
- weathergrabber-0.0.1/weathergrabber/domain/adapter/mapper/timestamp_mapper.py +8 -0
- weathergrabber-0.0.1/weathergrabber/domain/adapter/mapper/today_details_mapper.py +21 -0
- weathergrabber-0.0.1/weathergrabber/domain/adapter/mapper/uv_index_mapper.py +9 -0
- weathergrabber-0.0.1/weathergrabber/domain/adapter/mapper/weather_icon_enum_mapper.py +8 -0
- weathergrabber-0.0.1/weathergrabber/domain/adapter/mapper/wind_mapper.py +7 -0
- weathergrabber-0.0.1/weathergrabber/domain/adapter/output_enum.py +6 -0
- weathergrabber-0.0.1/weathergrabber/domain/adapter/params.py +58 -0
- weathergrabber-0.0.1/weathergrabber/domain/air_quality_index.py +78 -0
- weathergrabber-0.0.1/weathergrabber/domain/city_location.py +37 -0
- weathergrabber-0.0.1/weathergrabber/domain/color.py +43 -0
- weathergrabber-0.0.1/weathergrabber/domain/current_conditions.py +53 -0
- weathergrabber-0.0.1/weathergrabber/domain/daily_predictions.py +58 -0
- weathergrabber-0.0.1/weathergrabber/domain/day_night.py +50 -0
- weathergrabber-0.0.1/weathergrabber/domain/forecast.py +68 -0
- weathergrabber-0.0.1/weathergrabber/domain/health_activities.py +39 -0
- weathergrabber-0.0.1/weathergrabber/domain/hourly_predictions.py +76 -0
- weathergrabber-0.0.1/weathergrabber/domain/label_value.py +19 -0
- weathergrabber-0.0.1/weathergrabber/domain/moon_phase.py +22 -0
- weathergrabber-0.0.1/weathergrabber/domain/moon_phase_enum.py +65 -0
- weathergrabber-0.0.1/weathergrabber/domain/precipitation.py +20 -0
- weathergrabber-0.0.1/weathergrabber/domain/search.py +15 -0
- weathergrabber-0.0.1/weathergrabber/domain/sunrise_sunset.py +40 -0
- weathergrabber-0.0.1/weathergrabber/domain/temperature_hight_low.py +32 -0
- weathergrabber-0.0.1/weathergrabber/domain/timestamp.py +39 -0
- weathergrabber-0.0.1/weathergrabber/domain/today_details.py +79 -0
- weathergrabber-0.0.1/weathergrabber/domain/uv_index.py +43 -0
- weathergrabber-0.0.1/weathergrabber/domain/weather_icon_enum.py +58 -0
- weathergrabber-0.0.1/weathergrabber/domain/wind.py +28 -0
- weathergrabber-0.0.1/weathergrabber/service/extract_aqi_service.py +30 -0
- weathergrabber-0.0.1/weathergrabber/service/extract_current_conditions_service.py +47 -0
- weathergrabber-0.0.1/weathergrabber/service/extract_daily_forecast_oldstyle_service.py +51 -0
- weathergrabber-0.0.1/weathergrabber/service/extract_daily_forecast_service.py +56 -0
- weathergrabber-0.0.1/weathergrabber/service/extract_health_activities_service.py +25 -0
- weathergrabber-0.0.1/weathergrabber/service/extract_hourly_forecast_oldstyle_service.py +50 -0
- weathergrabber-0.0.1/weathergrabber/service/extract_hourly_forecast_service.py +64 -0
- weathergrabber-0.0.1/weathergrabber/service/extract_temperature_service.py +18 -0
- weathergrabber-0.0.1/weathergrabber/service/extract_today_details_service.py +85 -0
- weathergrabber-0.0.1/weathergrabber/service/read_weather_service.py +23 -0
- weathergrabber-0.0.1/weathergrabber/service/search_location_service.py +35 -0
- weathergrabber-0.0.1/weathergrabber/usecase/use_case.py +85 -0
- weathergrabber-0.0.1/weathergrabber/weathergrabber_application.py +78 -0
- weathergrabber-0.0.1/weathergrabber.egg-info/PKG-INFO +176 -0
- weathergrabber-0.0.1/weathergrabber.egg-info/SOURCES.txt +75 -0
- weathergrabber-0.0.1/weathergrabber.egg-info/dependency_links.txt +1 -0
- weathergrabber-0.0.1/weathergrabber.egg-info/entry_points.txt +2 -0
- weathergrabber-0.0.1/weathergrabber.egg-info/requires.txt +2 -0
- weathergrabber-0.0.1/weathergrabber.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: weathergrabber
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A grabber for weather.com data with various output formats.
|
|
5
|
+
Author-email: Carlos Anselmo Mendes Junior <cjuniorfox@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: homepage, https://github.com/cjuniorfox/weather
|
|
8
|
+
Project-URL: repository, https://github.com/cjuniorfox/weather
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: pyquery>=2.0.1
|
|
12
|
+
Requires-Dist: requests>=2.32.5
|
|
13
|
+
|
|
14
|
+
# Weather Forecast CLI Script
|
|
15
|
+
|
|
16
|
+
## Overview
|
|
17
|
+
|
|
18
|
+
This script fetches and parses weather forecast data from Weather.com and formats it for display in various environments such as a terminal or Waybar, a status bar tool. It leverages `pyquery` for HTML parsing and provides detailed weather information, including hourly and daily predictions, formatted for ease of use.
|
|
19
|
+
|
|
20
|
+
### Waybar widget
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
### Terminal Console
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
- Retrieves current weather and forecasts for a specified location.
|
|
31
|
+
- Displays weather data in different formats:
|
|
32
|
+
- **Console output**: Richly formatted weather data with icons.
|
|
33
|
+
- **Waybar JSON**: For integration with Waybar.
|
|
34
|
+
- Supports multiple languages for Weather.com data.
|
|
35
|
+
- Includes data such as:
|
|
36
|
+
- Current temperature and "feels-like" temperature.
|
|
37
|
+
- Wind speed, humidity, visibility, and air quality.
|
|
38
|
+
- Hourly and daily forecasts with icons and precipitation chances.
|
|
39
|
+
|
|
40
|
+
## Requirements
|
|
41
|
+
|
|
42
|
+
Requires Python 3 or newer.
|
|
43
|
+
|
|
44
|
+
Install the package and all dependencies with:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
pip install .
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or, for development (editable install with dev dependencies):
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
pip install -e .[dev]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Output Formats
|
|
57
|
+
|
|
58
|
+
#### Console Output
|
|
59
|
+
|
|
60
|
+
The script displays a formatted weather summary, including:
|
|
61
|
+
|
|
62
|
+
- Current weather status.
|
|
63
|
+
- Temperature (current, max/min).
|
|
64
|
+
- Wind, humidity, visibility, and air quality.
|
|
65
|
+
- Hourly and daily forecasts with icons.
|
|
66
|
+
|
|
67
|
+
#### Waybar JSON
|
|
68
|
+
|
|
69
|
+
The JSON includes:
|
|
70
|
+
|
|
71
|
+
- `text`: Current weather icon and temperature.
|
|
72
|
+
- `alt`: Weather status.
|
|
73
|
+
- `tooltip`: Detailed weather information.
|
|
74
|
+
- `class`: Status code for further customization.
|
|
75
|
+
|
|
76
|
+
### JSON General output
|
|
77
|
+
|
|
78
|
+
Here the following [JSON Schema](schema.json) for this output.
|
|
79
|
+
|
|
80
|
+
The key values for this json is:
|
|
81
|
+
|
|
82
|
+
- `temperature`: An object containing the temperature information with the following fields:
|
|
83
|
+
- `current`: The current temperature.
|
|
84
|
+
- `feel`: The temperature feel
|
|
85
|
+
- `max` : The maximum temperature
|
|
86
|
+
- `min` : The minimum temperature
|
|
87
|
+
|
|
88
|
+
There's also other fields like `hourly_predictions` and `daily_predictions` containing lists of predictions informations. More defaults on [JSON Schema](schema.json).
|
|
89
|
+
|
|
90
|
+
### Integration with Waybar
|
|
91
|
+
|
|
92
|
+
To integrate the script with Waybar:
|
|
93
|
+
|
|
94
|
+
1. Add a custom script module in Waybar's configuration:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"modules-left": ["custom/weather"],
|
|
99
|
+
"custom/weather": {
|
|
100
|
+
"exec": "weathergrabber --output waybar",
|
|
101
|
+
"interval": 600
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
2. Reload Waybar to apply the changes.
|
|
107
|
+
|
|
108
|
+
## Error Handling
|
|
109
|
+
|
|
110
|
+
- Validates `weather_id` and `lang` inputs.
|
|
111
|
+
- Handles HTTP errors gracefully, including 404 errors for invalid locations.
|
|
112
|
+
|
|
113
|
+
## CI & Test Coverage
|
|
114
|
+
|
|
115
|
+

|
|
116
|
+
[](https://codecov.io/gh/cjuniorfox/weather)
|
|
117
|
+
|
|
118
|
+
The test suite is run automatically on every push and pull request using GitHub Actions. Coverage results are uploaded to Codecov and displayed above.
|
|
119
|
+
|
|
120
|
+
To run tests and check coverage locally:
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
pytest --cov=weathergrabber --cov-report=term
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
This script is open-source and available under the MIT License.
|
|
129
|
+
|
|
130
|
+
## CLI Usage
|
|
131
|
+
|
|
132
|
+
You can run the CLI as an installed command:
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
weathergrabber [location_name] [options]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Or as a Python module:
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
python -m weathergrabber.cli [location_name] [options]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Arguments
|
|
145
|
+
|
|
146
|
+
- `location_name` (positional, optional): City name, zip code, etc. If not provided, you can use `--location-id` or the `WEATHER_LOCATION_ID` environment variable.
|
|
147
|
+
|
|
148
|
+
### Options
|
|
149
|
+
|
|
150
|
+
- `--location-id`, `-l` : 64-character-hex code for location (from Weather.com)
|
|
151
|
+
- `--lang`, `-L` : Language (e.g., `pt-BR`, `fr-FR`). Defaults to system locale if not set.
|
|
152
|
+
- `--output`, `-o` : Output format. One of `console`, `json`, or `waybar`. Default: `console`.
|
|
153
|
+
- `--keep-open`, `-k` : Keep open and refresh every 5 minutes (only makes sense for `console` output).
|
|
154
|
+
- `--icons`, `-i` : Icon set. `fa` for Font-Awesome, `emoji` for emoji icons. Default: `emoji`.
|
|
155
|
+
- `--log` : Set logging level. One of `debug`, `info`, `warning`, `error`, `critical`. Default: `critical`.
|
|
156
|
+
|
|
157
|
+
### Environment Variables
|
|
158
|
+
|
|
159
|
+
- `LANG` : Used as default language if `--lang` is not set.
|
|
160
|
+
- `WEATHER_LOCATION_ID` : Used as default location if neither `location_name` nor `--location-id` is set.
|
|
161
|
+
|
|
162
|
+
### Example Usage
|
|
163
|
+
|
|
164
|
+
```sh
|
|
165
|
+
weathergrabber "London" --output console --lang en-GB
|
|
166
|
+
weathergrabber --location-id 1234567890abcdef... --output json
|
|
167
|
+
weathergrabber "Paris" -o waybar -i fa
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Or as a Python module:
|
|
171
|
+
|
|
172
|
+
```sh
|
|
173
|
+
python -m weathergrabber.cli "London" --output console --lang en-GB
|
|
174
|
+
python -m weathergrabber.cli --location-id 1234567890abcdef... --output json
|
|
175
|
+
python -m weathergrabber.cli "Paris" -o waybar -i fa
|
|
176
|
+
```
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Weather Forecast CLI Script
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This script fetches and parses weather forecast data from Weather.com and formats it for display in various environments such as a terminal or Waybar, a status bar tool. It leverages `pyquery` for HTML parsing and provides detailed weather information, including hourly and daily predictions, formatted for ease of use.
|
|
6
|
+
|
|
7
|
+
### Waybar widget
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
### Terminal Console
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- Retrieves current weather and forecasts for a specified location.
|
|
18
|
+
- Displays weather data in different formats:
|
|
19
|
+
- **Console output**: Richly formatted weather data with icons.
|
|
20
|
+
- **Waybar JSON**: For integration with Waybar.
|
|
21
|
+
- Supports multiple languages for Weather.com data.
|
|
22
|
+
- Includes data such as:
|
|
23
|
+
- Current temperature and "feels-like" temperature.
|
|
24
|
+
- Wind speed, humidity, visibility, and air quality.
|
|
25
|
+
- Hourly and daily forecasts with icons and precipitation chances.
|
|
26
|
+
|
|
27
|
+
## Requirements
|
|
28
|
+
|
|
29
|
+
Requires Python 3 or newer.
|
|
30
|
+
|
|
31
|
+
Install the package and all dependencies with:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
pip install .
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or, for development (editable install with dev dependencies):
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
pip install -e .[dev]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Output Formats
|
|
44
|
+
|
|
45
|
+
#### Console Output
|
|
46
|
+
|
|
47
|
+
The script displays a formatted weather summary, including:
|
|
48
|
+
|
|
49
|
+
- Current weather status.
|
|
50
|
+
- Temperature (current, max/min).
|
|
51
|
+
- Wind, humidity, visibility, and air quality.
|
|
52
|
+
- Hourly and daily forecasts with icons.
|
|
53
|
+
|
|
54
|
+
#### Waybar JSON
|
|
55
|
+
|
|
56
|
+
The JSON includes:
|
|
57
|
+
|
|
58
|
+
- `text`: Current weather icon and temperature.
|
|
59
|
+
- `alt`: Weather status.
|
|
60
|
+
- `tooltip`: Detailed weather information.
|
|
61
|
+
- `class`: Status code for further customization.
|
|
62
|
+
|
|
63
|
+
### JSON General output
|
|
64
|
+
|
|
65
|
+
Here the following [JSON Schema](schema.json) for this output.
|
|
66
|
+
|
|
67
|
+
The key values for this json is:
|
|
68
|
+
|
|
69
|
+
- `temperature`: An object containing the temperature information with the following fields:
|
|
70
|
+
- `current`: The current temperature.
|
|
71
|
+
- `feel`: The temperature feel
|
|
72
|
+
- `max` : The maximum temperature
|
|
73
|
+
- `min` : The minimum temperature
|
|
74
|
+
|
|
75
|
+
There's also other fields like `hourly_predictions` and `daily_predictions` containing lists of predictions informations. More defaults on [JSON Schema](schema.json).
|
|
76
|
+
|
|
77
|
+
### Integration with Waybar
|
|
78
|
+
|
|
79
|
+
To integrate the script with Waybar:
|
|
80
|
+
|
|
81
|
+
1. Add a custom script module in Waybar's configuration:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"modules-left": ["custom/weather"],
|
|
86
|
+
"custom/weather": {
|
|
87
|
+
"exec": "weathergrabber --output waybar",
|
|
88
|
+
"interval": 600
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
2. Reload Waybar to apply the changes.
|
|
94
|
+
|
|
95
|
+
## Error Handling
|
|
96
|
+
|
|
97
|
+
- Validates `weather_id` and `lang` inputs.
|
|
98
|
+
- Handles HTTP errors gracefully, including 404 errors for invalid locations.
|
|
99
|
+
|
|
100
|
+
## CI & Test Coverage
|
|
101
|
+
|
|
102
|
+

|
|
103
|
+
[](https://codecov.io/gh/cjuniorfox/weather)
|
|
104
|
+
|
|
105
|
+
The test suite is run automatically on every push and pull request using GitHub Actions. Coverage results are uploaded to Codecov and displayed above.
|
|
106
|
+
|
|
107
|
+
To run tests and check coverage locally:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
pytest --cov=weathergrabber --cov-report=term
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
This script is open-source and available under the MIT License.
|
|
116
|
+
|
|
117
|
+
## CLI Usage
|
|
118
|
+
|
|
119
|
+
You can run the CLI as an installed command:
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
weathergrabber [location_name] [options]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Or as a Python module:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
python -m weathergrabber.cli [location_name] [options]
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Arguments
|
|
132
|
+
|
|
133
|
+
- `location_name` (positional, optional): City name, zip code, etc. If not provided, you can use `--location-id` or the `WEATHER_LOCATION_ID` environment variable.
|
|
134
|
+
|
|
135
|
+
### Options
|
|
136
|
+
|
|
137
|
+
- `--location-id`, `-l` : 64-character-hex code for location (from Weather.com)
|
|
138
|
+
- `--lang`, `-L` : Language (e.g., `pt-BR`, `fr-FR`). Defaults to system locale if not set.
|
|
139
|
+
- `--output`, `-o` : Output format. One of `console`, `json`, or `waybar`. Default: `console`.
|
|
140
|
+
- `--keep-open`, `-k` : Keep open and refresh every 5 minutes (only makes sense for `console` output).
|
|
141
|
+
- `--icons`, `-i` : Icon set. `fa` for Font-Awesome, `emoji` for emoji icons. Default: `emoji`.
|
|
142
|
+
- `--log` : Set logging level. One of `debug`, `info`, `warning`, `error`, `critical`. Default: `critical`.
|
|
143
|
+
|
|
144
|
+
### Environment Variables
|
|
145
|
+
|
|
146
|
+
- `LANG` : Used as default language if `--lang` is not set.
|
|
147
|
+
- `WEATHER_LOCATION_ID` : Used as default location if neither `location_name` nor `--location-id` is set.
|
|
148
|
+
|
|
149
|
+
### Example Usage
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
weathergrabber "London" --output console --lang en-GB
|
|
153
|
+
weathergrabber --location-id 1234567890abcdef... --output json
|
|
154
|
+
weathergrabber "Paris" -o waybar -i fa
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Or as a Python module:
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
python -m weathergrabber.cli "London" --output console --lang en-GB
|
|
161
|
+
python -m weathergrabber.cli --location-id 1234567890abcdef... --output json
|
|
162
|
+
python -m weathergrabber.cli "Paris" -o waybar -i fa
|
|
163
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "weathergrabber"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "A grabber for weather.com data with various output formats."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Carlos Anselmo Mendes Junior", email = "cjuniorfox@gmail.com" }
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
dependencies = [
|
|
13
|
+
"pyquery>=2.0.1",
|
|
14
|
+
"requests>=2.32.5"
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
weathergrabber = "weathergrabber.cli:main_cli"
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
homepage = "https://github.com/cjuniorfox/weather"
|
|
22
|
+
repository = "https://github.com/cjuniorfox/weather"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import patch, MagicMock
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
@pytest.fixture
|
|
6
|
+
def mock_main():
|
|
7
|
+
with patch('weathergrabber.cli.main') as m:
|
|
8
|
+
yield m
|
|
9
|
+
|
|
10
|
+
def test_cli_location_name(monkeypatch, mock_main):
|
|
11
|
+
test_args = ["weathergrabber", "London"]
|
|
12
|
+
monkeypatch.setattr(sys, "argv", test_args)
|
|
13
|
+
from weathergrabber.cli import main_cli
|
|
14
|
+
main_cli()
|
|
15
|
+
mock_main.assert_called_once()
|
|
16
|
+
args = mock_main.call_args[1]
|
|
17
|
+
assert args["location_name"] == "London"
|
|
18
|
+
assert args["output"] == "console"
|
|
19
|
+
assert args["icons"] == "emoji"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_cli_location_id(monkeypatch, mock_main):
|
|
23
|
+
test_args = ["weathergrabber", "--location-id", "abcdef123456"]
|
|
24
|
+
monkeypatch.setattr(sys, "argv", test_args)
|
|
25
|
+
from weathergrabber.cli import main_cli
|
|
26
|
+
main_cli()
|
|
27
|
+
args = mock_main.call_args[1]
|
|
28
|
+
assert args["location_id"] == "abcdef123456"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_cli_output_json(monkeypatch, mock_main):
|
|
32
|
+
test_args = ["weathergrabber", "Paris", "--output", "json"]
|
|
33
|
+
monkeypatch.setattr(sys, "argv", test_args)
|
|
34
|
+
from weathergrabber.cli import main_cli
|
|
35
|
+
main_cli()
|
|
36
|
+
args = mock_main.call_args[1]
|
|
37
|
+
assert args["output"] == "json"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_cli_icons_fa(monkeypatch, mock_main):
|
|
41
|
+
test_args = ["weathergrabber", "Berlin", "--icons", "fa"]
|
|
42
|
+
monkeypatch.setattr(sys, "argv", test_args)
|
|
43
|
+
from weathergrabber.cli import main_cli
|
|
44
|
+
main_cli()
|
|
45
|
+
args = mock_main.call_args[1]
|
|
46
|
+
assert args["icons"] == "fa"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_cli_keep_open(monkeypatch, mock_main):
|
|
50
|
+
test_args = ["weathergrabber", "Madrid", "--keep-open"]
|
|
51
|
+
monkeypatch.setattr(sys, "argv", test_args)
|
|
52
|
+
from weathergrabber.cli import main_cli
|
|
53
|
+
main_cli()
|
|
54
|
+
args = mock_main.call_args[1]
|
|
55
|
+
assert args["keep_open"] is True
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_cli_lang_env(monkeypatch, mock_main):
|
|
59
|
+
test_args = ["weathergrabber", "Rome"]
|
|
60
|
+
monkeypatch.setattr(sys, "argv", test_args)
|
|
61
|
+
monkeypatch.setenv("LANG", "fr_FR.UTF-8")
|
|
62
|
+
from weathergrabber.cli import main_cli
|
|
63
|
+
main_cli()
|
|
64
|
+
args = mock_main.call_args[1]
|
|
65
|
+
assert args["lang"] == "fr-FR"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_cli_location_id_env(monkeypatch, mock_main):
|
|
69
|
+
test_args = ["weathergrabber"]
|
|
70
|
+
monkeypatch.setattr(sys, "argv", test_args)
|
|
71
|
+
monkeypatch.setenv("WEATHER_LOCATION_ID", "envlocationid")
|
|
72
|
+
from weathergrabber.cli import main_cli
|
|
73
|
+
main_cli()
|
|
74
|
+
args = mock_main.call_args[1]
|
|
75
|
+
assert args["location_id"] == "envlocationid"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import patch, MagicMock
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
@patch('weathergrabber.core.WeatherGrabberApplication')
|
|
6
|
+
def test_main_invokes_application(mock_app):
|
|
7
|
+
from weathergrabber.core import main
|
|
8
|
+
params = {
|
|
9
|
+
'log_level': 'info',
|
|
10
|
+
'location_name': 'London',
|
|
11
|
+
'location_id': '123',
|
|
12
|
+
'lang': 'en-US',
|
|
13
|
+
'output': 'console',
|
|
14
|
+
'keep_open': False,
|
|
15
|
+
'icons': 'emoji'
|
|
16
|
+
}
|
|
17
|
+
main(**params)
|
|
18
|
+
mock_app.assert_called_once()
|
|
19
|
+
# Check Params object
|
|
20
|
+
args, kwargs = mock_app.call_args
|
|
21
|
+
assert hasattr(args[0], 'location')
|
|
22
|
+
assert args[0].location.search_name == 'London'
|
|
23
|
+
assert args[0].location.id == '123'
|
|
24
|
+
assert args[0].language == 'en-US'
|
|
25
|
+
assert args[0].output_format.name.lower() == 'console'
|
|
26
|
+
assert args[0].keep_open is False
|
|
27
|
+
assert args[0].icons.name.lower() == 'emoji'
|
|
28
|
+
|
|
29
|
+
@patch('weathergrabber.core.WeatherGrabberApplication')
|
|
30
|
+
def test_main_sets_log_level(mock_app):
|
|
31
|
+
from weathergrabber.core import main
|
|
32
|
+
main(
|
|
33
|
+
log_level='debug',
|
|
34
|
+
location_name='Paris',
|
|
35
|
+
location_id='456',
|
|
36
|
+
lang='fr-FR',
|
|
37
|
+
output='json',
|
|
38
|
+
keep_open=True,
|
|
39
|
+
icons='fa'
|
|
40
|
+
)
|
|
41
|
+
assert logging.getLogger().level == logging.DEBUG
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from pyquery import PyQuery
|
|
2
|
+
from urllib.error import HTTPError
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
class WeatherApi:
|
|
6
|
+
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.logger = logging.getLogger(__name__)
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
def get_weather(self,language: str, location: str) -> PyQuery:
|
|
12
|
+
url = f"https://weather.com/{language}/weather/today/l/{location}"
|
|
13
|
+
|
|
14
|
+
if location == None:
|
|
15
|
+
url = f"https://weather.com/{language}/weather/today"
|
|
16
|
+
elif len(location) < 64 :
|
|
17
|
+
raise ValueError("Invalid location")
|
|
18
|
+
|
|
19
|
+
if language == None:
|
|
20
|
+
raise ValueError("language must be specified")
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
self.logger.debug(f"Fetching weather data from URL: %s.", url)
|
|
24
|
+
return PyQuery(url=url)
|
|
25
|
+
except HTTPError as e:
|
|
26
|
+
self.logger.error("HTTP '%s' error when fetching weather data from URL: '%s'.", e.code, url)
|
|
27
|
+
raise ValueError(f"HTTP error {e.code} when fetching weather data.")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import requests
|
|
3
|
+
|
|
4
|
+
class WeatherSearchApi:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.logger = logging.getLogger(__name__)
|
|
7
|
+
self.cache = {}
|
|
8
|
+
|
|
9
|
+
def search(self, location_name: str, lang: str = 'en-US'):
|
|
10
|
+
|
|
11
|
+
if not location_name or len(location_name) < 1:
|
|
12
|
+
raise ValueError("Location name must be provided and cannot be empty.")
|
|
13
|
+
if len(location_name) > 100:
|
|
14
|
+
raise ValueError("Location name is too long.")
|
|
15
|
+
|
|
16
|
+
key = (location_name, lang)
|
|
17
|
+
|
|
18
|
+
if key in self.cache:
|
|
19
|
+
self.logger.debug("Cache hit for location '%s' and language '%s'.", location_name, lang)
|
|
20
|
+
return self.cache[key]
|
|
21
|
+
|
|
22
|
+
url = "https://weather.com/api/v1/p/redux-dal"
|
|
23
|
+
headers = {"content-type": "application/json"}
|
|
24
|
+
|
|
25
|
+
payload = [
|
|
26
|
+
{
|
|
27
|
+
"name": "getSunV3LocationSearchUrlConfig",
|
|
28
|
+
"params": {
|
|
29
|
+
"query": location_name,
|
|
30
|
+
"language": lang,
|
|
31
|
+
"locationType": "locale"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
self.logger.debug("Sending request to Weather Search API '%s' for location '%s' with language '%s'...", url, location_name, lang)
|
|
37
|
+
|
|
38
|
+
resp = requests.post(url, json=payload, headers=headers)
|
|
39
|
+
|
|
40
|
+
if resp.status_code != 200:
|
|
41
|
+
self.logger.error("HTTP '%s' error when searching for location '%s' with language '%s'.", resp.status_code, location_name, lang)
|
|
42
|
+
raise ValueError(f"HTTP error {resp.status_code} when searching for location.")
|
|
43
|
+
|
|
44
|
+
self.logger.debug("Received successful response from Weather Search API.")
|
|
45
|
+
|
|
46
|
+
data = resp.json()
|
|
47
|
+
|
|
48
|
+
self.cache[key] = data
|
|
49
|
+
|
|
50
|
+
return data
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from weathergrabber.usecase.use_case import UseCase
|
|
2
|
+
from weathergrabber.domain.adapter.params import Params
|
|
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
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
class ConsoleTTY:
|
|
9
|
+
|
|
10
|
+
def __init__(self, use_case: UseCase):
|
|
11
|
+
self.logger = logging.getLogger(__name__)
|
|
12
|
+
self.use_case = use_case
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
def execute(self, params: Params) -> None:
|
|
16
|
+
self.logger.info("Executing Console output")
|
|
17
|
+
|
|
18
|
+
is_fa = params.icons == IconEnum.FA
|
|
19
|
+
|
|
20
|
+
forecast = self.use_case.execute(params)
|
|
21
|
+
|
|
22
|
+
rain_icon = WeatherIconEnum.RAIN.fa_icon if is_fa else WeatherIconEnum.RAIN.emoji_icon
|
|
23
|
+
|
|
24
|
+
city = forecast.current_conditions.location.city
|
|
25
|
+
state_province = forecast.current_conditions.location.state_province
|
|
26
|
+
icon = forecast.current_conditions.icon.fa_icon if is_fa else forecast.current_conditions.icon.emoji_icon
|
|
27
|
+
temperature = forecast.current_conditions.temperature
|
|
28
|
+
|
|
29
|
+
day_temp_label = WeatherIconEnum.DAY.fa_icon if is_fa else WeatherIconEnum.DAY.emoji_icon
|
|
30
|
+
day_temp_value = forecast.current_conditions.day_night.day.value
|
|
31
|
+
night_temp_label = WeatherIconEnum.NIGHT.fa_icon if is_fa else WeatherIconEnum.NIGHT.emoji_icon
|
|
32
|
+
night_temp_value = forecast.current_conditions.day_night.night.value
|
|
33
|
+
|
|
34
|
+
moon_icon = forecast.today_details.moon_phase.icon.fa_icon if is_fa else forecast.today_details.moon_phase.icon.emoji_icon
|
|
35
|
+
moon_phase = forecast.today_details.moon_phase.phase
|
|
36
|
+
summary = forecast.current_conditions.summary
|
|
37
|
+
|
|
38
|
+
feelslike_icon = WeatherIconEnum.FEEL.fa_icon if is_fa else WeatherIconEnum.FEEL.emoji_icon
|
|
39
|
+
feelslike = forecast.today_details.feelslike.value
|
|
40
|
+
|
|
41
|
+
sunrise_icon = forecast.today_details.sunrise_sunset.sunrise.icon.fa_icon if is_fa else forecast.today_details.sunrise_sunset.sunrise.icon.emoji_icon
|
|
42
|
+
sunset_icon = forecast.today_details.sunrise_sunset.sunset.icon.fa_icon if is_fa else forecast.today_details.sunrise_sunset.sunset.icon.emoji_icon
|
|
43
|
+
|
|
44
|
+
sunrise_value = forecast.today_details.sunrise_sunset.sunrise.value
|
|
45
|
+
sunset_value = forecast.today_details.sunrise_sunset.sunset.value
|
|
46
|
+
|
|
47
|
+
wind_icon = WeatherIconEnum.WIND.fa_icon if is_fa else WeatherIconEnum.WIND.emoji_icon
|
|
48
|
+
wind = forecast.today_details.wind.value
|
|
49
|
+
|
|
50
|
+
humidity_icon = WeatherIconEnum.HUMIDITY.fa_icon if is_fa else WeatherIconEnum.HUMIDITY.emoji_icon
|
|
51
|
+
humidity = forecast.today_details.humidity.value
|
|
52
|
+
|
|
53
|
+
pressure = forecast.today_details.pressure
|
|
54
|
+
|
|
55
|
+
uv_index = forecast.today_details.uv_index
|
|
56
|
+
|
|
57
|
+
visibility_icon = WeatherIconEnum.VISIBILITY.fa_icon if is_fa else WeatherIconEnum.VISIBILITY.emoji_icon
|
|
58
|
+
visibility = forecast.today_details.visibility.value
|
|
59
|
+
|
|
60
|
+
r, g, b = forecast.air_quality_index.color.red, forecast.air_quality_index.color.green, forecast.air_quality_index.color.blue
|
|
61
|
+
aqi_category = f"\033[38;2;{r};{g};{b}m{forecast.air_quality_index.category}\033[0m"
|
|
62
|
+
aqi_acronym = forecast.air_quality_index.acronym
|
|
63
|
+
aqi_value = forecast.air_quality_index.value
|
|
64
|
+
|
|
65
|
+
hourly_predictions = [
|
|
66
|
+
f"{h.title}\t{h.temperature}\t{h.icon.fa_icon if is_fa else h.icon.emoji_icon}\t{rain_icon if h.precipitation.percentage else ''} {h.precipitation.percentage}"
|
|
67
|
+
for h in forecast.hourly_predictions
|
|
68
|
+
]
|
|
69
|
+
daily_predictions = [
|
|
70
|
+
f"{d.title}\t{d.high_low}\t{d.icon.fa_icon if is_fa else d.icon.emoji_icon}\t{rain_icon} {d.precipitation.percentage}"
|
|
71
|
+
for d in forecast.daily_predictions
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
print_value = (
|
|
75
|
+
"\n"
|
|
76
|
+
f"{city}, {state_province}\n"
|
|
77
|
+
"\n"
|
|
78
|
+
f"{icon} {temperature}\n"
|
|
79
|
+
"\n"
|
|
80
|
+
f"{summary}\n"
|
|
81
|
+
f"{day_temp_label} {day_temp_value}/{night_temp_label} {night_temp_value}\t{feelslike_icon} {feelslike}\n"
|
|
82
|
+
"\n"
|
|
83
|
+
f"{sunrise_icon} {sunrise_value} • {sunset_icon} {sunset_value}\n"
|
|
84
|
+
"\n"
|
|
85
|
+
f"{moon_icon} {moon_phase}\n"
|
|
86
|
+
"\n"
|
|
87
|
+
f"{wind_icon} {wind}\t {uv_index}\n"
|
|
88
|
+
f"{humidity_icon} {humidity}\t\t {pressure}\n"
|
|
89
|
+
f"{visibility_icon} {visibility}\t {aqi_acronym} {aqi_category} {aqi_value}\n"
|
|
90
|
+
"\n"
|
|
91
|
+
f"{'\n'.join(hourly_predictions)}\n"
|
|
92
|
+
"\n"
|
|
93
|
+
f"{'\n'.join(daily_predictions)}\n"
|
|
94
|
+
"\n"
|
|
95
|
+
f"{forecast.current_conditions.timestamp}"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
print(print_value)
|
|
99
|
+
if(params.keep_open):
|
|
100
|
+
lines_count = print_value.count("\n") + 1
|
|
101
|
+
ret_prev_line = f"\033[{lines_count}A"
|
|
102
|
+
print(ret_prev_line, end='') # Move cursor back to the beginning for overwriting, the application is responsable for executing again
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from weathergrabber.usecase.use_case import UseCase
|
|
2
|
+
from weathergrabber.domain.adapter.params import Params
|
|
3
|
+
from weathergrabber.domain.adapter.mapper.forecast_mapper import forecast_to_dict
|
|
4
|
+
import logging
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
class JsonTTY:
|
|
8
|
+
|
|
9
|
+
def __init__(self, use_case: UseCase):
|
|
10
|
+
self.logger = logging.getLogger(__name__)
|
|
11
|
+
self.use_case = use_case
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
def execute(self, params: Params) -> None:
|
|
15
|
+
self.logger.info("Executing JSON output")
|
|
16
|
+
forecast = self.use_case.execute(params)
|
|
17
|
+
output: dict = forecast_to_dict(forecast)
|
|
18
|
+
output_json = json.dumps(output, indent=4)
|
|
19
|
+
print(output_json)
|