weathergrabber 0.0.1__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 (70) hide show
  1. weathergrabber/__init__.py +0 -0
  2. weathergrabber/adapter/client/weather_api.py +27 -0
  3. weathergrabber/adapter/client/weather_search_api.py +50 -0
  4. weathergrabber/adapter/tty/console_tty.py +102 -0
  5. weathergrabber/adapter/tty/json_tty.py +19 -0
  6. weathergrabber/adapter/tty/waybar_tty.py +110 -0
  7. weathergrabber/cli.py +37 -0
  8. weathergrabber/core.py +31 -0
  9. weathergrabber/domain/adapter/icon_enum.py +5 -0
  10. weathergrabber/domain/adapter/mapper/air_quality_index_mapper.py +13 -0
  11. weathergrabber/domain/adapter/mapper/city_location_mapper.py +8 -0
  12. weathergrabber/domain/adapter/mapper/color_mapper.py +10 -0
  13. weathergrabber/domain/adapter/mapper/current_conditions_mapper.py +16 -0
  14. weathergrabber/domain/adapter/mapper/daily_predictions_mapper.py +15 -0
  15. weathergrabber/domain/adapter/mapper/day_night_mapper.py +12 -0
  16. weathergrabber/domain/adapter/mapper/forecast_mapper.py +20 -0
  17. weathergrabber/domain/adapter/mapper/health_activities_mapper.py +8 -0
  18. weathergrabber/domain/adapter/mapper/hourly_predictions_mapper.py +19 -0
  19. weathergrabber/domain/adapter/mapper/label_value_mapper.py +7 -0
  20. weathergrabber/domain/adapter/mapper/moon_phase_mapper.py +9 -0
  21. weathergrabber/domain/adapter/mapper/precipitation_mapper.py +7 -0
  22. weathergrabber/domain/adapter/mapper/search_mapper.py +7 -0
  23. weathergrabber/domain/adapter/mapper/sunrise_sunset_mapper.py +13 -0
  24. weathergrabber/domain/adapter/mapper/temperature_high_low_mapper.py +8 -0
  25. weathergrabber/domain/adapter/mapper/timestamp_mapper.py +8 -0
  26. weathergrabber/domain/adapter/mapper/today_details_mapper.py +21 -0
  27. weathergrabber/domain/adapter/mapper/uv_index_mapper.py +9 -0
  28. weathergrabber/domain/adapter/mapper/weather_icon_enum_mapper.py +8 -0
  29. weathergrabber/domain/adapter/mapper/wind_mapper.py +7 -0
  30. weathergrabber/domain/adapter/output_enum.py +6 -0
  31. weathergrabber/domain/adapter/params.py +58 -0
  32. weathergrabber/domain/air_quality_index.py +78 -0
  33. weathergrabber/domain/city_location.py +37 -0
  34. weathergrabber/domain/color.py +43 -0
  35. weathergrabber/domain/current_conditions.py +53 -0
  36. weathergrabber/domain/daily_predictions.py +58 -0
  37. weathergrabber/domain/day_night.py +50 -0
  38. weathergrabber/domain/forecast.py +68 -0
  39. weathergrabber/domain/health_activities.py +39 -0
  40. weathergrabber/domain/hourly_predictions.py +76 -0
  41. weathergrabber/domain/label_value.py +19 -0
  42. weathergrabber/domain/moon_phase.py +22 -0
  43. weathergrabber/domain/moon_phase_enum.py +65 -0
  44. weathergrabber/domain/precipitation.py +20 -0
  45. weathergrabber/domain/search.py +15 -0
  46. weathergrabber/domain/sunrise_sunset.py +40 -0
  47. weathergrabber/domain/temperature_hight_low.py +32 -0
  48. weathergrabber/domain/timestamp.py +39 -0
  49. weathergrabber/domain/today_details.py +79 -0
  50. weathergrabber/domain/uv_index.py +43 -0
  51. weathergrabber/domain/weather_icon_enum.py +58 -0
  52. weathergrabber/domain/wind.py +28 -0
  53. weathergrabber/service/extract_aqi_service.py +30 -0
  54. weathergrabber/service/extract_current_conditions_service.py +47 -0
  55. weathergrabber/service/extract_daily_forecast_oldstyle_service.py +51 -0
  56. weathergrabber/service/extract_daily_forecast_service.py +56 -0
  57. weathergrabber/service/extract_health_activities_service.py +25 -0
  58. weathergrabber/service/extract_hourly_forecast_oldstyle_service.py +50 -0
  59. weathergrabber/service/extract_hourly_forecast_service.py +64 -0
  60. weathergrabber/service/extract_temperature_service.py +18 -0
  61. weathergrabber/service/extract_today_details_service.py +85 -0
  62. weathergrabber/service/read_weather_service.py +23 -0
  63. weathergrabber/service/search_location_service.py +35 -0
  64. weathergrabber/usecase/use_case.py +85 -0
  65. weathergrabber/weathergrabber_application.py +78 -0
  66. weathergrabber-0.0.1.dist-info/METADATA +176 -0
  67. weathergrabber-0.0.1.dist-info/RECORD +70 -0
  68. weathergrabber-0.0.1.dist-info/WHEEL +5 -0
  69. weathergrabber-0.0.1.dist-info/entry_points.txt +2 -0
  70. weathergrabber-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,64 @@
1
+ import logging
2
+ from pyquery import PyQuery
3
+ from weathergrabber.domain.hourly_predictions import HourlyPredictions
4
+ from weathergrabber.domain.weather_icon_enum import WeatherIconEnum
5
+ from weathergrabber.domain.uv_index import UVIndex
6
+ from weathergrabber.domain.precipitation import Precipitation
7
+ from weathergrabber.domain.wind import Wind
8
+ from typing import List
9
+
10
+
11
+ class ExtractHourlyForecastService:
12
+ def __init__(self):
13
+ self.logger = logging.getLogger(__name__)
14
+ pass
15
+
16
+ def execute(self, weather_data: PyQuery) -> List[HourlyPredictions]:
17
+ try:
18
+ self.logger.debug("Extracting hourly forecast...")
19
+
20
+ data = weather_data.find("section[data-testid='HourlyForecast'] div[class*='Card'] details")
21
+
22
+ if len(data) == 0:
23
+ raise ValueError("Unable to extract hourly forecast")
24
+
25
+ details = [ {
26
+ "title": PyQuery(item).find("h2").text(),
27
+ "temperature" : PyQuery(item).find("div[data-testid='detailsTemperature']").text(),
28
+ "icon" : PyQuery(item).find("svg[class*='DetailsSummary']").attr("name"),
29
+ "summary" : PyQuery(item).find("span[class*='DetailsSummary--wxPhrase']").text(),
30
+ "precip-percentage": PyQuery(item).find("div[data-testid='Precip'] span[data-testid='PercentageValue']").text(),
31
+ "wind": PyQuery(item).find("span[data-testid='WindTitle']").next().eq(0).text(),
32
+ "feels-like" : PyQuery(item).find("span[data-testid='FeelsLikeTitle']").next().text(),
33
+ "humidity" : PyQuery(item).find("span[data-testid='HumidityTitle']").next().text(),
34
+ "uv-index" : PyQuery(item).find("span[data-testid='UVIndexValue']").text(),
35
+ "cloud-cover" : PyQuery(item).find("span[data-testid='CloudCoverTitle']").next().text(),
36
+ "rain-amount" : PyQuery(item).find("span[data-testid='AccumulationTitle']").next().text()
37
+ } for item in data ]
38
+
39
+ self.logger.debug("Extracted %s register(s)...",len(details))
40
+
41
+ hourly_forecasts = [HourlyPredictions(
42
+ title=item["title"],
43
+ temperature=item["temperature"],
44
+ icon=WeatherIconEnum.from_name(item["icon"]),
45
+ summary=item["summary"],
46
+ precipitation=Precipitation(
47
+ percentage=item["precip-percentage"],
48
+ amount=item["rain-amount"]
49
+ ),
50
+ wind=Wind.from_string(item["wind"]),
51
+ feels_like=item["feels-like"],
52
+ humidity=item["humidity"],
53
+ uv_index=UVIndex.from_string(item["uv-index"]),
54
+ cloud_cover=item["cloud-cover"]
55
+ ) for item in details]
56
+
57
+ self.logger.debug("Created hourly forecast list with %s registers", len(hourly_forecasts))
58
+
59
+ return hourly_forecasts
60
+
61
+
62
+ except Exception as e:
63
+ self.logger.error(f"Error extracting hourly forecast: {e}")
64
+ raise ValueError("Could not extract hourly forecast.") from e
@@ -0,0 +1,18 @@
1
+ import logging
2
+ from pyquery import PyQuery
3
+
4
+
5
+ class ExtractTemperatureService:
6
+ def __init__(self):
7
+ self.logger = logging.getLogger(__name__)
8
+ pass
9
+
10
+ def execute(self, weather_data: PyQuery) -> str:
11
+ try:
12
+ temperature = weather_data("div[class*='CurrentConditions--tempIconContainer'] span[data-testid='TemperatureValue']").text()
13
+ self.logger.debug(f"Extracted temperature: {temperature}")
14
+ return temperature
15
+
16
+ except Exception as e:
17
+ self.logger.error(f"Error temperature: {e}")
18
+ raise ValueError("Could not extract temperature.")
@@ -0,0 +1,85 @@
1
+ import logging
2
+ from pyquery import PyQuery
3
+ from weathergrabber.domain.today_details import TodayDetails
4
+ from weathergrabber.domain.temperature_hight_low import TemperatureHighLow
5
+ from weathergrabber.domain.uv_index import UVIndex
6
+ from weathergrabber.domain.moon_phase import MoonPhase
7
+ from weathergrabber.domain.moon_phase_enum import MoonPhaseEnum
8
+ from weathergrabber.domain.label_value import LabelValue
9
+ from weathergrabber.domain.sunrise_sunset import SunriseSunset
10
+
11
+ class ExtractTodayDetailsService:
12
+ def __init__(self):
13
+ self.logger = logging.getLogger(__name__)
14
+ pass
15
+
16
+ def execute(self, weather_data: PyQuery) -> TodayDetails:
17
+ try:
18
+ self.logger.debug("Extracting today's details...")
19
+
20
+ today_details_data = weather_data.find("div#todayDetails")
21
+ feelslike = PyQuery(today_details_data).find("div[data-testid='FeelsLikeSection'] span")
22
+ sunrise_sunset = PyQuery(today_details_data).find("div[data-testid='sunriseSunsetContainer'] div p[class*='TwcSunChart']")
23
+
24
+ feelslike_label = feelslike.eq(0).text() #'Feels Like'
25
+ feelslike_value = feelslike.eq(1).text() #'60°'
26
+
27
+ sunrise = sunrise_sunset.eq(0).text() #'6:12 AM'
28
+ sunset = sunrise_sunset.eq(1).text() #'7:45 PM'
29
+
30
+ icons = today_details_data.find('svg[class*="WeatherDetailsListItem--icon"]')
31
+ labels = today_details_data.find('div[class*="WeatherDetailsListItem--label"]')
32
+ values = today_details_data.find('div[data-testid="wxData"]')
33
+
34
+ self.logger.debug(f"Parsing today details values...")
35
+ high_low_label = labels.eq(0).text() #'High / Low'
36
+ high_low_value = values.eq(0).text() #'--/54°'
37
+
38
+ wind_label = labels.eq(1).text() #'Wind'
39
+ wind_value = values.eq(1).text() #'7\xa0mph'
40
+
41
+ humidity_label = labels.eq(2).text() #'Humidity'
42
+ humidity_value = values.eq(2).text() #'100%'
43
+
44
+ dew_point_label = labels.eq(3).text() #'Dew Point'
45
+ dew_point_value = values.eq(3).text() #'60°'
46
+
47
+ pressure_label = labels.eq(4).text() #'Pressure'
48
+ pressure_value = values.eq(4).text() #'30.31\xa0in'
49
+
50
+ uv_index_label = labels.eq(5).text() #'UV Index'
51
+ uv_index_value = values.eq(5).text() #'5 of 10'
52
+
53
+ visibility_label = labels.eq(6).text() #'Visibility'
54
+ visibility_value = values.eq(6).text() #'10.0 mi'
55
+
56
+ moon_phase_label = labels.eq(7).text() #'Moon Phase'
57
+ moon_phase_icon = icons.eq(7).attr('name') #'phase-2'
58
+ moon_phase_value = values.eq(7).text() #'Waxing Crescent'
59
+
60
+ self.logger.debug(f"Creating domain objects for today details...")
61
+
62
+ sunrise_sunset = SunriseSunset(sunrise=sunrise, sunset=sunset)
63
+ high_low = TemperatureHighLow.from_string(high_low_value, label=high_low_label)
64
+ uv_index = UVIndex.from_string(uv_index_value, label=uv_index_label)
65
+ moon_phase = MoonPhase(MoonPhaseEnum.from_name(moon_phase_icon), moon_phase_value, moon_phase_label)
66
+
67
+ today_details = TodayDetails(
68
+ feelslike=LabelValue(label=feelslike_label, value=feelslike_value),
69
+ sunrise_sunset=sunrise_sunset,
70
+ high_low=high_low,
71
+ wind=LabelValue(label=wind_label, value=wind_value),
72
+ humidity=LabelValue(label=humidity_label, value=humidity_value),
73
+ dew_point=LabelValue(label=dew_point_label, value=dew_point_value),
74
+ pressure=LabelValue(label=pressure_label, value=pressure_value),
75
+ uv_index=uv_index,
76
+ visibility=LabelValue(label=visibility_label, value=visibility_value),
77
+ moon_phase=moon_phase
78
+ )
79
+
80
+ self.logger.debug(f"Extracted today's details: {today_details}")
81
+ return today_details
82
+
83
+ except Exception as e:
84
+ self.logger.error(f"Error extracting today's details: {e}")
85
+ raise ValueError("Could not extract today's details.") from e
@@ -0,0 +1,23 @@
1
+ from weathergrabber.adapter.client.weather_api import WeatherApi
2
+ import logging
3
+ from pyquery import PyQuery
4
+
5
+ class ReadWeatherService:
6
+ def __init__(
7
+ self,
8
+ weather_api: WeatherApi
9
+ ):
10
+ self.weather_api = weather_api
11
+ self.logging = logging.getLogger(__name__)
12
+
13
+ def execute(self, language: str, location: str) -> PyQuery:
14
+
15
+ self.logging.debug(f"Executing WeatherDataService with language: {language}, location: {location}")
16
+
17
+ weather_data = self.weather_api.get_weather(language, location)
18
+
19
+ self.logging.debug(f"Weather data retrieved.")
20
+
21
+ return weather_data
22
+
23
+
@@ -0,0 +1,35 @@
1
+ from weathergrabber.adapter.client.weather_search_api import WeatherSearchApi
2
+ import logging
3
+
4
+ class SearchLocationService:
5
+ def __init__(self, api: WeatherSearchApi):
6
+ self.api = api
7
+ self.logger = logging.getLogger(__name__)
8
+ pass
9
+
10
+ def execute(self, location_name: str, lang: str) -> str | None:
11
+ self.logger.debug(f"Searching for location: {location_name} with language: {lang}")
12
+
13
+ if not location_name:
14
+ self.logger.debug("No location name provided. Bypassing search.")
15
+ return None
16
+
17
+ try:
18
+ data = self.api.search(location_name, lang)
19
+ if not data:
20
+ self.logger.error(f"No data found for location: {location_name}")
21
+ raise ValueError(f"Location '{location_name}' not found.")
22
+
23
+ dal = data["dal"]["getSunV3LocationSearchUrlConfig"]
24
+
25
+ # Pick the first (arbitrary) key, then get the value
26
+ first_key = next(iter(dal))
27
+ location_id = dal[first_key]["data"]["location"]["placeId"][0]
28
+
29
+ self.logger.debug(f"Found location ID: {location_id} for location name: {location_name}")
30
+
31
+ return location_id
32
+ except Exception as e:
33
+ self.logger.error(f"Error searching for location '{location_name}': {e}")
34
+ raise ValueError(f"Could not find location '{location_name}'.")
35
+
@@ -0,0 +1,85 @@
1
+ import logging
2
+ from weathergrabber.domain.adapter.params import Params
3
+ from weathergrabber.service.search_location_service import SearchLocationService
4
+ from weathergrabber.service.read_weather_service import ReadWeatherService
5
+ from weathergrabber.service.extract_current_conditions_service import ExtractCurrentConditionsService
6
+ from weathergrabber.service.extract_today_details_service import ExtractTodayDetailsService
7
+ from weathergrabber.service.extract_aqi_service import ExtractAQIService
8
+ from weathergrabber.service.extract_health_activities_service import ExtractHealthActivitiesService
9
+ from weathergrabber.service.extract_hourly_forecast_service import ExtractHourlyForecastService
10
+ from weathergrabber.service.extract_hourly_forecast_oldstyle_service import ExtractHourlyForecastOldstyleService
11
+ from weathergrabber.service.extract_daily_forecast_service import ExtractDailyForecastService
12
+ from weathergrabber.service.extract_daily_forecast_oldstyle_service import ExtractDailyForecastOldstyleService
13
+ from weathergrabber.domain.search import Search
14
+ from weathergrabber.domain.forecast import Forecast
15
+
16
+ class UseCase:
17
+ def __init__(
18
+ self,
19
+ search_location_service: SearchLocationService,
20
+ read_weather_service: ReadWeatherService,
21
+ extract_current_conditions_service: ExtractCurrentConditionsService,
22
+ extract_today_details_service: ExtractTodayDetailsService,
23
+ extract_aqi_service: ExtractAQIService,
24
+ extract_health_activities_service: ExtractHealthActivitiesService,
25
+ extract_hourly_forecast_service: ExtractHourlyForecastService,
26
+ extract_hourly_forecast_oldstyle_service: ExtractHourlyForecastOldstyleService,
27
+ extract_daily_forecast_service: ExtractDailyForecastService,
28
+ extract_daily_forecast_oldstyle_service: ExtractDailyForecastOldstyleService,
29
+
30
+ ):
31
+ self.logger = logging.getLogger(__name__)
32
+ self.read_weather_service = read_weather_service
33
+ self.extract_current_conditions_service = extract_current_conditions_service
34
+ self.extract_today_details_service = extract_today_details_service
35
+ self.search_location_service = search_location_service
36
+ self.extract_aqi_service = extract_aqi_service
37
+ self.extract_health_activities_service = extract_health_activities_service
38
+ self.extract_hourly_forecast_service = extract_hourly_forecast_service
39
+ self.extract_hourly_forecast_oldstyle_service = extract_hourly_forecast_oldstyle_service
40
+ self.extract_daily_forecast_service = extract_daily_forecast_service
41
+ self.extract_daily_forecast_oldstyle_service = extract_daily_forecast_oldstyle_service
42
+
43
+ def execute(self, params: Params) -> Forecast:
44
+
45
+ self.logger.debug("Starting usecase")
46
+
47
+ location_id = params.location.id
48
+ search_name = params.location.search_name
49
+ if not location_id:
50
+ location_id = self.search_location_service.execute(params.location.search_name, params.language)
51
+
52
+ weather_data = self.read_weather_service.execute(params.language, location_id)
53
+
54
+ current_conditions = self.extract_current_conditions_service.execute(weather_data)
55
+ today_details = self.extract_today_details_service.execute(weather_data)
56
+ air_quality_index = self.extract_aqi_service.execute(weather_data)
57
+ health_activities = self.extract_health_activities_service.execute(weather_data)
58
+
59
+ try:
60
+ hourly_predictions = self.extract_hourly_forecast_service.execute(weather_data)
61
+ except ValueError:
62
+ hourly_predictions = self.extract_hourly_forecast_oldstyle_service.execute(weather_data)
63
+
64
+ try:
65
+ daily_predictions = self.extract_daily_forecast_service.execute(weather_data)
66
+ except ValueError:
67
+ daily_predictions = self.extract_daily_forecast_oldstyle_service.execute(weather_data)
68
+
69
+ forecast = Forecast(
70
+ search = Search(
71
+ id = location_id,
72
+ search_name = search_name
73
+ ),
74
+ current_conditions = current_conditions,
75
+ today_details = today_details,
76
+ air_quality_index = air_quality_index,
77
+ health_activities = health_activities,
78
+ hourly_predictions = hourly_predictions,
79
+ daily_predictions = daily_predictions
80
+ )
81
+
82
+ self.logger.debug("Forecast data obtained %s", forecast)
83
+
84
+ return forecast
85
+
@@ -0,0 +1,78 @@
1
+ import logging
2
+ from time import sleep
3
+ from .domain.adapter.params import Params
4
+ from .domain.adapter.output_enum import OutputEnum
5
+ from .adapter.client.weather_api import WeatherApi
6
+ from .adapter.client.weather_search_api import WeatherSearchApi
7
+ from .service.search_location_service import SearchLocationService
8
+ from .service.read_weather_service import ReadWeatherService
9
+ from .service.extract_current_conditions_service import ExtractCurrentConditionsService
10
+ from .service.extract_today_details_service import ExtractTodayDetailsService
11
+ from .service.extract_aqi_service import ExtractAQIService
12
+ from .service.extract_health_activities_service import ExtractHealthActivitiesService
13
+ from .service.extract_hourly_forecast_service import ExtractHourlyForecastService
14
+ from .service.extract_hourly_forecast_oldstyle_service import ExtractHourlyForecastOldstyleService
15
+ from .service.extract_daily_forecast_service import ExtractDailyForecastService
16
+ from .service.extract_daily_forecast_oldstyle_service import ExtractDailyForecastOldstyleService
17
+ from .usecase.use_case import UseCase
18
+
19
+
20
+ class WeatherGrabberApplication:
21
+
22
+ def _beans(self):
23
+ self.weather_search_api = WeatherSearchApi()
24
+ self.weather_api = WeatherApi()
25
+ self.search_location_service = SearchLocationService(self.weather_search_api)
26
+ self.read_weather_service = ReadWeatherService(self.weather_api)
27
+ self.extract_current_conditions_service = ExtractCurrentConditionsService()
28
+ self.extract_today_details_service = ExtractTodayDetailsService()
29
+ self.extract_aqi_service = ExtractAQIService()
30
+ self.extract_health_activities_service = ExtractHealthActivitiesService()
31
+ self.extract_hourly_forecast_service = ExtractHourlyForecastService()
32
+ self.extract_hourly_forecast_oldstyle_service = ExtractHourlyForecastOldstyleService()
33
+ self.extract_daily_forecast_service = ExtractDailyForecastService()
34
+ self.extract_daily_forecast_oldstyle_service = ExtractDailyForecastOldstyleService()
35
+ self.use_case = UseCase(
36
+ self.search_location_service,
37
+ self.read_weather_service,
38
+ self.extract_current_conditions_service,
39
+ self.extract_today_details_service,
40
+ self.extract_aqi_service,
41
+ self.extract_health_activities_service,
42
+ self.extract_hourly_forecast_service,
43
+ self.extract_hourly_forecast_oldstyle_service,
44
+ self.extract_daily_forecast_service,
45
+ self.extract_daily_forecast_oldstyle_service
46
+ )
47
+ pass
48
+
49
+ def _define_controller(self, output_format: OutputEnum):
50
+ if output_format == OutputEnum.CONSOLE:
51
+ from weathergrabber.adapter.tty.console_tty import ConsoleTTY
52
+ self.controller = ConsoleTTY(self.use_case)
53
+
54
+ elif output_format == OutputEnum.JSON:
55
+ from weathergrabber.adapter.tty.json_tty import JsonTTY
56
+ self.controller = JsonTTY(self.use_case)
57
+
58
+ elif output_format == OutputEnum.WAYBAR:
59
+ from weathergrabber.adapter.tty.waybar_tty import WaybarTTY
60
+ self.controller = WaybarTTY(self.use_case)
61
+
62
+ else:
63
+ self.logger.error(f"Unsupported output format: {output_format}")
64
+ raise ValueError(f"Unsupported output format: {output_format}")
65
+
66
+ def __init__(self, params: Params):
67
+ self.logger = logging.getLogger(__name__)
68
+ self._beans()
69
+ self._define_controller(params.output_format)
70
+ self.logger.info("Starting WeatherGrabber Application")
71
+ if params.keep_open:
72
+ self.logger.info("Keep open mode enabled, the application will refresh every 5 minutes")
73
+ while True:
74
+ self.controller.execute(params)
75
+ sleep(300) # Sleep for 5 minutes
76
+ else:
77
+ self.controller.execute(params)
78
+ self.logger.info("WeatherGrabber Application finished")
@@ -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
+ ![Weather widget on Waybar](image.png)
23
+
24
+ ### Terminal Console
25
+
26
+ ![Weather widget on terminal console](image-1.png)
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
+ ![Test Status](https://github.com/cjuniorfox/weather/actions/workflows/python-package.yml/badge.svg)
116
+ [![codecov](https://codecov.io/gh/cjuniorfox/weather/branch/main/graph/badge.svg)](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,70 @@
1
+ weathergrabber/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ weathergrabber/cli.py,sha256=7-sjeYPVTXZpwvD2VjvBSMA7R9yd6Y_z7NIO5NR62_4,1966
3
+ weathergrabber/core.py,sha256=TiZ2utmYKf9lkIXWv8YBfSdiHZXJZXuHS8B-dBDvevw,1138
4
+ weathergrabber/weathergrabber_application.py,sha256=axNwAKqBVrEZMo98JG-6sEcvf1yjhurzo1zV9WrD4ME,3989
5
+ weathergrabber/adapter/client/weather_api.py,sha256=2i7fosML6yBO4PxD3Z2t4Rv5RfJTQhHNq9Bp-llEwUw,960
6
+ weathergrabber/adapter/client/weather_search_api.py,sha256=1oy7JitHcmwkkhFlD0eIt5A7a4cGbf7LMNi26tR8z5o,1724
7
+ weathergrabber/adapter/tty/console_tty.py,sha256=B5BaAEqpP7aXIxzzBQv47qWcQFRSWo47tQ1Lxa9cbq0,4972
8
+ weathergrabber/adapter/tty/json_tty.py,sha256=ZRZSfVhFYsb8AXD1qjb1GBpaRMxHGNaMXW5VN8AvdaU,647
9
+ weathergrabber/adapter/tty/waybar_tty.py,sha256=_lyeA4bOidGzlz9xBKCkZPEssscQaO8SAPn-eryvm_I,5294
10
+ weathergrabber/domain/air_quality_index.py,sha256=eH1glUdedtbRCZMcw5_zB5ybJQUcyY-EBMxL8vVJ6zA,2791
11
+ weathergrabber/domain/city_location.py,sha256=usXijW59uI1RhgmR9QO6L9ttIkbzwqtwJAsRgaTbYaw,1209
12
+ weathergrabber/domain/color.py,sha256=dkWZnQ2BjXUV8qTfZnmNGng_cqKtKuDdekYw4R2NJ9U,1080
13
+ weathergrabber/domain/current_conditions.py,sha256=S14j2lL_gc2pds0wbl5M4_72PbVdubOuX8lnE_tZVTg,1477
14
+ weathergrabber/domain/daily_predictions.py,sha256=8pzmaU-yugS2Hx_6MBWAk24wlSISe1OQIRrCK_lfvbw,1618
15
+ weathergrabber/domain/day_night.py,sha256=7geOuVH9xDb1ecLKnZmj6eqp3H3kp9G0YY497cEyPtg,1442
16
+ weathergrabber/domain/forecast.py,sha256=CO0VTdMI0kjuwbpwwDNRCf_82oNtw5QkN-9GgobPet8,2399
17
+ weathergrabber/domain/health_activities.py,sha256=wCG82-lBfrnIwxptgnMZt-fXHJsBnIl4YLpoeFOmC48,1490
18
+ weathergrabber/domain/hourly_predictions.py,sha256=PFiGuo7mBoQHLR-bDirV--R126YAOzkwIHgaeRGFQSA,2179
19
+ weathergrabber/domain/label_value.py,sha256=LvrvZbSrcEUUpxvKAmYkFnpDFWsa6LFmGA9O8i6HB84,446
20
+ weathergrabber/domain/moon_phase.py,sha256=sEHRi8yPIHTzaEcOkz556uNYsKLq4YmYa5qDncSX44c,587
21
+ weathergrabber/domain/moon_phase_enum.py,sha256=MXxFjBjy4bq4SML4L-5DRStKJMWtvAjTtcYMO3nvAw0,2091
22
+ weathergrabber/domain/precipitation.py,sha256=eXrpwMOsEJWGqV4bEBhN9niWYXalgdZRLG4-I39JZ2A,466
23
+ weathergrabber/domain/search.py,sha256=j3BzskyPl0hDWV02XTOC4tJonV5RHxr5Rop_rYMKUtA,387
24
+ weathergrabber/domain/sunrise_sunset.py,sha256=wNTk01NIuLbQ7gN_giAFv4f3FaRx9khul-mj19g57vE,1207
25
+ weathergrabber/domain/temperature_hight_low.py,sha256=PQOJ5uDtfMRBR5yMxXA46xuorJC08jva2C0-WAV5yxs,909
26
+ weathergrabber/domain/timestamp.py,sha256=tbUdTiWUf1mKhVq9SESeFGlu_sZ2FdmQCb1gr7u9I0c,1032
27
+ weathergrabber/domain/today_details.py,sha256=EUlV7xerYw5QhEsBfvO5m6-9Ghm4nPkXJz9zCmSYTbA,2398
28
+ weathergrabber/domain/uv_index.py,sha256=lYZnHAWwnu_clOh_T5j-OKDcmLJALa24ROY9L1d8lO8,1422
29
+ weathergrabber/domain/weather_icon_enum.py,sha256=xAyZxEDjIBHzy4WIgcWEw3vhDlL8uVJIL3piG9dNcr0,2309
30
+ weathergrabber/domain/wind.py,sha256=wTDz3X1rYsnw_eNoDi1miwaomxwhiJkY_q6xrdZtLak,789
31
+ weathergrabber/domain/adapter/icon_enum.py,sha256=YxGYS5vBRV2AiAfeuPOdqaQOHixAssiMbOzQnTmdSBg,84
32
+ weathergrabber/domain/adapter/output_enum.py,sha256=61iR10ppY8DNALPKV-vLnDQni5HxEzpoRNZbdBdRygk,117
33
+ weathergrabber/domain/adapter/params.py,sha256=4ts2m0cQ0LpgWXS81G36aRDER4e5LEKXad2sIcxbuWY,1668
34
+ weathergrabber/domain/adapter/mapper/air_quality_index_mapper.py,sha256=buAiqYDzHiUVVo18tzRoRG5lFkM26c07qIKPc9RDwGQ,454
35
+ weathergrabber/domain/adapter/mapper/city_location_mapper.py,sha256=NkcsrZ9nEJhqs4y07_VsiagslfsIWnd6AtoYem9o01U,239
36
+ weathergrabber/domain/adapter/mapper/color_mapper.py,sha256=ZU945MVCxL7Z9BS_ztR5iE1O5mR4ZG5PHdfB6fxv8s4,243
37
+ weathergrabber/domain/adapter/mapper/current_conditions_mapper.py,sha256=5UOT8gChe8FmerAXuzch8qN-GNgRxpzCayrtdRKBv_Q,899
38
+ weathergrabber/domain/adapter/mapper/daily_predictions_mapper.py,sha256=mYTilW4N8nrE7CT2VNiAayPuAj0sGXr7Rj36azGQc2E,933
39
+ weathergrabber/domain/adapter/mapper/day_night_mapper.py,sha256=M1dyVTThxJLgRhfASPIVCfAQ9E60c3kqtsyeI-Bt-d4,377
40
+ weathergrabber/domain/adapter/mapper/forecast_mapper.py,sha256=dEow0pR35Tmfo0nmJBOoaK85kB2ZSzWkGWq78eupHGw,1558
41
+ weathergrabber/domain/adapter/mapper/health_activities_mapper.py,sha256=ABhprwPx9LSiwsSmSdvSdf_hXxnNq7MUTDUNCy1__8A,259
42
+ weathergrabber/domain/adapter/mapper/hourly_predictions_mapper.py,sha256=56S_D_7NZDIffMuTZzI_6Lbh1y0MNmRsCVAps_LVzTE,1013
43
+ weathergrabber/domain/adapter/mapper/label_value_mapper.py,sha256=_hmXtVEFkARUhgn-qbQVwF8asi9pQZnOfnZAIJxXQyE,180
44
+ weathergrabber/domain/adapter/mapper/moon_phase_mapper.py,sha256=aYMPKICqKAX50wKnPNZc5krSlafxwJXnAwd5Nz4mo6I,291
45
+ weathergrabber/domain/adapter/mapper/precipitation_mapper.py,sha256=qTtUkOz-TBuhLSVFtSiHpQrOMcZDSwP6wHK0ClQOctM,199
46
+ weathergrabber/domain/adapter/mapper/search_mapper.py,sha256=yadBVjhh6uUgLZwSa0aeIJ239QOr_CGkxMF36dVbTnI,180
47
+ weathergrabber/domain/adapter/mapper/sunrise_sunset_mapper.py,sha256=OaviTRaY9EhNdjVaTBxTksW8WN3KJhP3bbXETt5ytqA,568
48
+ weathergrabber/domain/adapter/mapper/temperature_high_low_mapper.py,sha256=EDJ6WgIzHbQSUY4Yn9-5bS7UcfTQY8yBMQvhlp7I6NU,240
49
+ weathergrabber/domain/adapter/mapper/timestamp_mapper.py,sha256=2g0PqENQJ1FkGapkV2ojhQu0GzRU5ThQ4tmaSbmA3bY,193
50
+ weathergrabber/domain/adapter/mapper/today_details_mapper.py,sha256=y9F5b3IQXIvbt5PEjLW8M2RGy8vW9EPUpICAp3x87D0,1413
51
+ weathergrabber/domain/adapter/mapper/uv_index_mapper.py,sha256=K3AdRnAPv1Yqudc3eKcw_EBQidNPbHbLcG4lYrQvOOw,230
52
+ weathergrabber/domain/adapter/mapper/weather_icon_enum_mapper.py,sha256=YC7juvt38Ehtb3Y-iQFM77s1EQAv4qNHd6vGOqws6HI,249
53
+ weathergrabber/domain/adapter/mapper/wind_mapper.py,sha256=nXyYwqTvLLMyKtSey27GaGvBV8xVhB_Y3HU0sbmIe_E,149
54
+ weathergrabber/service/extract_aqi_service.py,sha256=IT3S9zZmThdqPtJDBF_G8LDUNlwUYoBh49z_WUlB-tk,1285
55
+ weathergrabber/service/extract_current_conditions_service.py,sha256=5lmnOL1TFA5KH7b8uX87TysPg1i5ufsR_lE1rVlfLYU,2130
56
+ weathergrabber/service/extract_daily_forecast_oldstyle_service.py,sha256=DiKfaGbMnIIBnxI-IYG8HmfLHNAW5AGew5NjFSMqBk4,2190
57
+ weathergrabber/service/extract_daily_forecast_service.py,sha256=ZN61_neEAdn7RPedLvmmFKH7Pq1EQGhG9A9Dy8o09zM,2709
58
+ weathergrabber/service/extract_health_activities_service.py,sha256=2qJ4tEEz5uV6EwHUIFhWM8zol_BbDW3omEohf8kDg3M,1014
59
+ weathergrabber/service/extract_hourly_forecast_oldstyle_service.py,sha256=U5O2tG9YF8wrUUoTimDr7oqMIb670j9oeVjoIpCKKLs,2100
60
+ weathergrabber/service/extract_hourly_forecast_service.py,sha256=MhVweMlvJNQjPdVqw0AAvni3f8TVTq_0PD76NDUUQHM,3079
61
+ weathergrabber/service/extract_temperature_service.py,sha256=46nRO3Izj1QmG4BNTh8flsODsovHyWPzZzOnkl1Gbj4,634
62
+ weathergrabber/service/extract_today_details_service.py,sha256=QAwF7EzVaL1STGNDyxve9r7oQTjvNKhYQ53d_68lbKA,4126
63
+ weathergrabber/service/read_weather_service.py,sha256=7_B8E9IN1KCwOhpuS5PfWazI1sCrDyYrZhkV2R38bhc,649
64
+ weathergrabber/service/search_location_service.py,sha256=tZmVgO45hjwoa4cl5bKPjMBmYlGxJiH_I9Ymb5pwEwU,1422
65
+ weathergrabber/usecase/use_case.py,sha256=wmNmJSiy0t00-KaxQqSryiu0keX0kI4PcLI2hBBHT_0,4460
66
+ weathergrabber-0.0.1.dist-info/METADATA,sha256=gsF7Hub32I8AQGL1aQDbvwW7InBXrpJ91kBQklNhE4k,5375
67
+ weathergrabber-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
68
+ weathergrabber-0.0.1.dist-info/entry_points.txt,sha256=m2P9a4mrJDTzuNaiTU438NA60GxCfaw7VKvruWw43N8,63
69
+ weathergrabber-0.0.1.dist-info/top_level.txt,sha256=P3NMDJJYRIvQujf994Vb4gZrobkKWkL2gh3NF_ajQWM,15
70
+ weathergrabber-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ weathergrabber = weathergrabber.cli:main_cli
@@ -0,0 +1 @@
1
+ weathergrabber