weathergrabber 0.0.8b2__tar.gz → 0.0.8b4__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 (85) hide show
  1. {weathergrabber-0.0.8b2/weathergrabber.egg-info → weathergrabber-0.0.8b4}/PKG-INFO +1 -1
  2. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/pyproject.toml +1 -1
  3. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/tests/test_cli.py +10 -1
  4. weathergrabber-0.0.8b4/tests/test_cli_version.py +13 -0
  5. weathergrabber-0.0.8b4/tests/test_main.py +11 -0
  6. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/__init__.py +4 -1
  7. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/__main__.py +1 -1
  8. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/adapter/tty/json_tty.py +1 -1
  9. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/adapter/tty/waybar_tty.py +1 -1
  10. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/cli.py +3 -5
  11. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/air_quality_index.py +17 -19
  12. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/city_location.py +5 -4
  13. weathergrabber-0.0.8b4/weathergrabber/domain/color.py +55 -0
  14. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/health_activities.py +3 -3
  15. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/moon_phase_enum.py +7 -7
  16. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/uv_index.py +4 -2
  17. weathergrabber-0.0.8b4/weathergrabber/service/extract_aqi_service.py +27 -0
  18. weathergrabber-0.0.8b4/weathergrabber/service/extract_hourly_forecast_service.py +58 -0
  19. weathergrabber-0.0.8b4/weathergrabber/service/extract_today_details_service.py +80 -0
  20. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/usecase/use_case.py +7 -7
  21. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4/weathergrabber.egg-info}/PKG-INFO +1 -1
  22. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber.egg-info/SOURCES.txt +2 -0
  23. weathergrabber-0.0.8b2/weathergrabber/domain/color.py +0 -43
  24. weathergrabber-0.0.8b2/weathergrabber/service/extract_aqi_service.py +0 -30
  25. weathergrabber-0.0.8b2/weathergrabber/service/extract_hourly_forecast_service.py +0 -64
  26. weathergrabber-0.0.8b2/weathergrabber/service/extract_today_details_service.py +0 -85
  27. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/LICENSE +0 -0
  28. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/README.md +0 -0
  29. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/setup.cfg +0 -0
  30. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/tests/test_core.py +0 -0
  31. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/adapter/client/weather_api.py +0 -0
  32. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/adapter/client/weather_search_api.py +0 -0
  33. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/adapter/tty/console_tty.py +0 -0
  34. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/core.py +0 -0
  35. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/icon_enum.py +0 -0
  36. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/air_quality_index_mapper.py +0 -0
  37. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/city_location_mapper.py +0 -0
  38. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/color_mapper.py +0 -0
  39. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/current_conditions_mapper.py +0 -0
  40. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/daily_predictions_mapper.py +0 -0
  41. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/day_night_mapper.py +0 -0
  42. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/forecast_mapper.py +0 -0
  43. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/health_activities_mapper.py +0 -0
  44. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/hourly_predictions_mapper.py +0 -0
  45. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/label_value_mapper.py +0 -0
  46. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/moon_phase_mapper.py +0 -0
  47. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/precipitation_mapper.py +0 -0
  48. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/search_mapper.py +0 -0
  49. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/sunrise_sunset_mapper.py +0 -0
  50. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/temperature_high_low_mapper.py +0 -0
  51. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/timestamp_mapper.py +0 -0
  52. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/today_details_mapper.py +0 -0
  53. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/uv_index_mapper.py +0 -0
  54. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/weather_icon_enum_mapper.py +0 -0
  55. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/mapper/wind_mapper.py +0 -0
  56. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/output_enum.py +0 -0
  57. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/adapter/params.py +0 -0
  58. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/current_conditions.py +0 -0
  59. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/daily_predictions.py +0 -0
  60. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/day_night.py +0 -0
  61. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/forecast.py +0 -0
  62. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/hourly_predictions.py +0 -0
  63. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/label_value.py +0 -0
  64. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/moon_phase.py +0 -0
  65. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/precipitation.py +0 -0
  66. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/search.py +0 -0
  67. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/sunrise_sunset.py +0 -0
  68. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/temperature_hight_low.py +0 -0
  69. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/timestamp.py +0 -0
  70. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/today_details.py +0 -0
  71. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/weather_icon_enum.py +0 -0
  72. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/domain/wind.py +0 -0
  73. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/service/extract_current_conditions_service.py +0 -0
  74. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/service/extract_daily_forecast_oldstyle_service.py +0 -0
  75. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/service/extract_daily_forecast_service.py +0 -0
  76. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/service/extract_health_activities_service.py +0 -0
  77. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/service/extract_hourly_forecast_oldstyle_service.py +0 -0
  78. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/service/extract_temperature_service.py +0 -0
  79. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/service/read_weather_service.py +0 -0
  80. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/service/search_location_service.py +0 -0
  81. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber/weathergrabber_application.py +0 -0
  82. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber.egg-info/dependency_links.txt +0 -0
  83. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber.egg-info/entry_points.txt +0 -0
  84. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/weathergrabber.egg-info/requires.txt +0 -0
  85. {weathergrabber-0.0.8b2 → weathergrabber-0.0.8b4}/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.8b2
3
+ Version: 0.0.8b4
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "weathergrabber"
3
- version = "0.0.8b2"
3
+ version = "0.0.8b4"
4
4
  description = "A grabber for weather.com data with various output formats."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -1,10 +1,11 @@
1
+ from asyncio import subprocess
1
2
  import pytest
2
3
  from unittest.mock import patch, MagicMock
3
4
  import sys
4
5
 
5
6
  @pytest.fixture
6
7
  def mock_main():
7
- with patch('weathergrabber.cli.main') as m:
8
+ with patch('weathergrabber.cli.weathergrabber.main') as m:
8
9
  yield m
9
10
 
10
11
  def test_cli_location_name(monkeypatch, mock_main):
@@ -73,3 +74,11 @@ def test_cli_location_id_env(monkeypatch, mock_main):
73
74
  main_cli()
74
75
  args = mock_main.call_args[1]
75
76
  assert args["location_id"] == "envlocationid"
77
+
78
+ def test_cli_log_level(monkeypatch, mock_main):
79
+ test_args = ["weathergrabber", "Tokyo", "--log", "debug"]
80
+ monkeypatch.setattr(sys, "argv", test_args)
81
+ from weathergrabber.cli import main_cli
82
+ main_cli()
83
+ args = mock_main.call_args[1]
84
+ assert args["log_level"] == "debug"
@@ -0,0 +1,13 @@
1
+ import subprocess
2
+ import sys
3
+ import weathergrabber
4
+
5
+
6
+ def test_cli_version_flag_prints_version():
7
+ result = subprocess.run([
8
+ sys.executable, "-m", "weathergrabber", "-v"
9
+ ], capture_output=True, text=True)
10
+ assert result.returncode in (0, 2)
11
+ output = (result.stdout or "") + (result.stderr or "")
12
+ assert str(weathergrabber.__version__) in output
13
+ assert "Weathergrabber" in output
@@ -0,0 +1,11 @@
1
+ import subprocess
2
+ import sys
3
+
4
+ def test_main_entrypoint():
5
+ result = subprocess.run(
6
+ [sys.executable, "-m", "weathergrabber", "--version"],
7
+ capture_output=True,
8
+ text=True
9
+ )
10
+ assert result.returncode == 0
11
+ assert "Weathergrabber" in result.stdout
@@ -4,4 +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.8b2"
7
+ __version__ = "0.0.8b4"
8
+
9
+ def get_version():
10
+ return __version__
@@ -1,4 +1,4 @@
1
- """weathergrabber CLI entry point."""
2
1
  from .cli import main_cli
2
+
3
3
  if __name__ == "__main__":
4
4
  main_cli()
@@ -15,5 +15,5 @@ class JsonTTY:
15
15
  self.logger.info("Executing JSON output")
16
16
  forecast = self.use_case.execute(params)
17
17
  output: dict = forecast_to_dict(forecast)
18
- output_json = json.dumps(output, indent=4)
18
+ output_json = json.dumps(output)
19
19
  print(output_json)
@@ -62,7 +62,7 @@ class WaybarTTY:
62
62
 
63
63
  #Air quality index
64
64
  color = forecast.air_quality_index.color.hex
65
- aqi_category = f" <span color=\"{color}\">{forecast.air_quality_index.category}</span>"
65
+ aqi_category = f" <span color=\"#{color}\">{forecast.air_quality_index.category}</span>"
66
66
  aqi_acronym = forecast.air_quality_index.acronym
67
67
  aqi_value = forecast.air_quality_index.value
68
68
 
@@ -1,6 +1,6 @@
1
1
  import argparse
2
2
  import os
3
- from weathergrabber import main
3
+ import weathergrabber
4
4
 
5
5
  def main_cli():
6
6
  ## Get current locale, or use the default one
@@ -11,6 +11,7 @@ def main_cli():
11
11
  parser.add_argument("--output", "-o", type=str, choices=['console','json','waybar'], default='console', help="Output format. console, json or waybar")
12
12
  parser.add_argument("--keep-open", "-k",action='store_true', default=False, help="Keep open and refreshing every 5 minutes instead of exiting after execution. Does only makes sense for --output=console")
13
13
  parser.add_argument("--icons", "-i", type=str, choices=['fa','emoji'], default='emoji', help="Icon set. 'fa' for Font-Awesome, or 'emoji'")
14
+ parser.add_argument("--version", "-v", action='version', version=f'Weathergrabber {weathergrabber.get_version()}', help="Show version and exit")
14
15
  parser.add_argument(
15
16
  "--log",
16
17
  default="critical",
@@ -25,7 +26,7 @@ def main_cli():
25
26
  if not args.location_id and not args.location_name:
26
27
  location_id = os.getenv('WEATHER_LOCATION_ID')
27
28
 
28
- main(
29
+ weathergrabber.main(
29
30
  log_level=args.log,
30
31
  location_name = args.location_name,
31
32
  location_id = location_id,
@@ -34,6 +35,3 @@ def main_cli():
34
35
  keep_open=args.keep_open,
35
36
  icons=args.icons
36
37
  )
37
-
38
- if __name__ == "__main__":
39
- main_cli()
@@ -45,34 +45,32 @@ class AirQualityIndex:
45
45
  return f"Title: {self.title}. AQI: {self.value}, Category: {self.category}, Description: {self.description}, Acronym: {self.acronym}, Color: {self.color}"
46
46
 
47
47
  def __repr__(self) -> str:
48
- return f"AirQualityIndex(title={self.title}, value={self.value}, category={self.category}, description={self.description}, acronym:{self.acronym}, color={self.color})"
48
+ return f"AirQualityIndex(title='{self.title}', value={self.value}, category='{self.category}', description='{self.description}', acronym='{self.acronym}', color='{self.color}')"
49
49
 
50
50
  @staticmethod
51
51
  def _extract_aqi(data: str):
52
- parts = data.split('\n')
53
- title = parts[0].strip()
54
- aqi = int(parts[1].strip())
55
- category = parts[2].strip() if len(parts) > 2 else None
56
- description = parts[3].strip() if len(parts) > 3 else None
57
- acronym = ''.join(word[0].strip().upper() for word in title.split())
52
+ try:
53
+ parts = data.split('\n')
54
+ title = parts[0].strip()
55
+ aqi = int(parts[1].strip())
56
+ category = parts[2].strip() if len(parts) > 2 else None
57
+ description = parts[3].strip() if len(parts) > 3 else None
58
+ acronym = ''.join(word[0].strip().upper() for word in title.split())
58
59
 
59
- return title, aqi, category, description, acronym
60
+ return title, aqi, category, description, acronym
61
+ except (ValueError, IndexError) as e:
62
+ raise ValueError("Invalid AQI data format") from e
60
63
 
61
64
  # 'Air Quality Index\n26\nGood\nAir quality is considered satisfactory, and air pollution poses little or no risk.'
62
65
  @classmethod
63
66
  def from_string(cls, data: str) -> 'AirQualityIndex':
64
- try:
65
- title, aqi, category, description, acronym = AirQualityIndex._extract_aqi(data)
66
- return cls(title, aqi, category, description, acronym)
67
- except (ValueError, IndexError) as e:
68
- raise ValueError("Invalid AQI data format") from e
67
+ title, aqi, category, description, acronym = AirQualityIndex._extract_aqi(data)
68
+ return cls(title, aqi, category, description, acronym)
69
69
 
70
70
  @classmethod
71
71
  def aqi_color_from_string(cls, aqi_data: str, color_data: str):
72
- try:
73
- title, aqi, category, description, acronym = AirQualityIndex._extract_aqi(aqi_data)
74
- color = Color.from_string(color_data)
75
- return cls(title, aqi, category, description, acronym, color)
76
- except(ValueError, IndexError) as e:
77
- raise ValueError("Invalid AQI data format or color data format") from e
72
+ title, aqi, category, description, acronym = AirQualityIndex._extract_aqi(aqi_data)
73
+ color = Color.from_string(color_data)
74
+ return cls(title, aqi, category, description, acronym, color)
75
+
78
76
 
@@ -46,9 +46,12 @@ class CityLocation:
46
46
  country, state_province, city, location = None, None, None, None
47
47
  parts = data.split(", ")
48
48
 
49
+ if data.strip() == "":
50
+ raise ValueError("City location string cannot be empty")
51
+
49
52
  if len(parts) > 2:
50
53
  i = len(parts) - 1
51
- while i > 0:
54
+ while i >= 0:
52
55
  if not country:
53
56
  country = parts[i]
54
57
  elif not state_province:
@@ -66,6 +69,4 @@ class CityLocation:
66
69
  return cls(city=city, state_province=state_province)
67
70
  elif len(parts) == 1:
68
71
  city = parts[0]
69
- return cls(city=city)
70
- else:
71
- raise ValueError("Invalid city location string format")
72
+ return cls(city=city)
@@ -0,0 +1,55 @@
1
+ import re
2
+
3
+ class Color:
4
+
5
+ def __init__(self, red: str | int, green: str | int, blue: str | int):
6
+ self._red = self._int_or_hex(red)
7
+ self._green = self._int_or_hex(green)
8
+ self._blue = self._int_or_hex(blue)
9
+
10
+ def _int_or_hex(self, value: str | int) -> int:
11
+ if type(value) == int:
12
+ if not (0 <= value <= 255):
13
+ raise ValueError("RGB integer values must be between 0 and 255")
14
+ return value
15
+ return int(value, 16)
16
+
17
+ @property
18
+ def red(self) -> int:
19
+ return self._red
20
+
21
+ @property
22
+ def green(self) -> int:
23
+ return self._green
24
+
25
+ @property
26
+ def blue(self) -> int:
27
+ return self._blue
28
+
29
+ @classmethod
30
+ def from_string(cls,string_value: str) -> "Color":
31
+
32
+ color_pattern = r"#([0-9A-Fa-f]{6})"
33
+
34
+ try:
35
+ match = re.search(color_pattern, string_value)
36
+ color = f"#{match.group(1)}"
37
+ hex_color = color.lstrip('#')
38
+ r, g, b = hex_color[:2], hex_color[2:4], hex_color[4:]
39
+ return cls(r, g, b)
40
+ except (AttributeError, ValueError):
41
+ raise ValueError(f"Invalid color string: {string_value}")
42
+
43
+ @property
44
+ def hex(self) -> str:
45
+ return f"{self.red:02x}{self.green:02x}{self.blue:02x}".upper()
46
+
47
+ @property
48
+ def rgb(self) -> str:
49
+ return f"rgb({self.red}, {self.green}, {self.blue})"
50
+
51
+ def __str__(self):
52
+ return f"{self.hex}"
53
+
54
+ def __repr__(self):
55
+ return f"Color(red='{self.red}', green='{self.green}', blue='{self.blue}')"
@@ -23,8 +23,8 @@ class HealthActivities:
23
23
  return f"HealthActivities(category_name={self._category_name!r}, title={self._title!r}, description={self._description!r})"
24
24
 
25
25
  # 'Health & Activities\nGrass\nSeasonal Allergies and Pollen Count Forecast\nGrass pollen is low in your area'
26
- @staticmethod
27
- def from_text(text: str):
26
+ @classmethod
27
+ def from_text(cls, text: str):
28
28
  try:
29
29
  lines = text.split('\n')
30
30
  if len(lines) >= 4:
@@ -32,7 +32,7 @@ class HealthActivities:
32
32
  #Ignore the "grass" line
33
33
  title = lines[2].strip()
34
34
  description = ' '.join(line.strip() for line in lines[3:]).strip()
35
- return HealthActivities(category_name, title, description)
35
+ return cls(category_name, title, description)
36
36
  else:
37
37
  raise ValueError("Insufficient data to parse HealthActivities")
38
38
  except Exception as e:
@@ -8,9 +8,9 @@ class MoonPhaseEnum(Enum):
8
8
  PHASE_2 = ("phase-2", "\uf186", "🌒")
9
9
  PHASE_3 = ("phase-3", "\uf186", "🌒")
10
10
  PHASE_4 = ("phase-4", "\uf186", "🌒")
11
- PHASE_5 = ("phase-5", "\uf186", "🌒")
12
- PHASE_6 = ("phase-6", "\uf186", "🌒")
13
11
  # First Quarter
12
+ PHASE_5 = ("phase-5", "\uf186", "🌓")
13
+ PHASE_6 = ("phase-6", "\uf186", "🌓")
14
14
  PHASE_7 = ("phase-7", "\uf186", "🌓")
15
15
  # Waxing Gibbous
16
16
  PHASE_8 = ("phase-8", "\uf186", "🌔")
@@ -21,8 +21,8 @@ class MoonPhaseEnum(Enum):
21
21
  PHASE_13 = ("phase-13", "\uf186", "🌕")
22
22
  # Full Moon
23
23
  PHASE_14 = ("phase-14", "\uf186", "🌕")
24
- # Waning Gibbous
25
24
  PHASE_15 = ("phase-15", "\uf186", "🌕")
25
+ # Waning Gibbous
26
26
  PHASE_16 = ("phase-16", "\uf186", "🌖")
27
27
  PHASE_17 = ("phase-17", "\uf186", "🌖")
28
28
  PHASE_18 = ("phase-18", "\uf186", "🌖")
@@ -30,10 +30,10 @@ class MoonPhaseEnum(Enum):
30
30
  PHASE_20 = ("phase-20", "\uf186", "🌖")
31
31
  # Last Quarter
32
32
  PHASE_21 = ("phase-21", "\uf186", "🌗")
33
- # Waning Crescent
34
- PHASE_22 = ("phase-22", "\uf186", "🌘")
35
- PHASE_23 = ("phase-23", "\uf186", "🌘")
36
- PHASE_24 = ("phase-24", "\uf186", "🌘")
33
+ PHASE_22 = ("phase-22", "\uf186", "🌗")
34
+ PHASE_23 = ("phase-23", "\uf186", "🌗")
35
+ PHASE_24 = ("phase-24", "\uf186", "🌗")
36
+
37
37
  PHASE_25 = ("phase-25", "\uf186", "🌘")
38
38
  PHASE_26 = ("phase-26", "\uf186", "🌘")
39
39
  PHASE_27 = ("phase-27", "\uf186", "🌘")
@@ -24,6 +24,8 @@ class UVIndex:
24
24
 
25
25
  @classmethod
26
26
  def from_string(cls, data: str, label: str = None) -> 'UVIndex':
27
+ if not data:
28
+ raise ValueError("UV Index string cannot be empty")
27
29
  parts = data.split(' ')
28
30
  if len(parts) == 1:
29
31
  return cls(string_value = data, index= parts[0].strip(), of="", label=label)
@@ -31,7 +33,7 @@ class UVIndex:
31
33
  index, of, some = parts
32
34
  return cls(string_value = data, index=index.strip(), of=some.strip(), label=label)
33
35
  else:
34
- raise ValueError(f"Cannot parse UV Index from string: {data}")
36
+ return cls(string_value = data, index="", of="", label=label)
35
37
 
36
38
  def __repr__(self) -> str:
37
39
  return f"UVIndex(string_value={self.string_value!r}, index={self.index!r}, of={self.of!r}, label={self.label!r})"
@@ -39,5 +41,5 @@ class UVIndex:
39
41
  def __str__(self) -> str:
40
42
  if self.string_value:
41
43
  return f"{self.label} {self.string_value}"
42
- elif self.index and self.of:
44
+ else:
43
45
  return f"{self.label} {self.index} {self.of}" if self.label else f"{self.index} {self.of}"
@@ -0,0 +1,27 @@
1
+ import logging
2
+ from weathergrabber.domain.air_quality_index import AirQualityIndex
3
+ from pyquery import PyQuery
4
+
5
+ class ExtractAQIService:
6
+
7
+ def __init__(self):
8
+ self.logger = logging.getLogger(__name__)
9
+ pass
10
+
11
+ def execute(self, weather_data: PyQuery) -> AirQualityIndex | None:
12
+
13
+ self.logger.debug("Extracting Air Quality Index (AQI)...")
14
+
15
+ # 'Air Quality Index\n27\nGood\nAir quality is considered satisfactory, and air pollution poses little or no risk.\nSee Details\nInfo'
16
+ aqi_data = weather_data("section[data-testid='AirQualityModule']").text()
17
+
18
+ # 'stroke-width:5;stroke-dasharray:10.021680564951442 172.78759594743863;stroke:#00E838'
19
+ color_data = weather_data("section[data-testid='AirQualityModule'] svg[data-testid='DonutChart'] circle:nth-of-type(2)").attr("style")
20
+
21
+ air_quality_index = AirQualityIndex.aqi_color_from_string(aqi_data,color_data)
22
+
23
+ self.logger.debug(f"Extracted AQI data: {air_quality_index}")
24
+
25
+ return air_quality_index
26
+
27
+
@@ -0,0 +1,58 @@
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
+ self.logger.debug("Extracting hourly forecast...")
18
+
19
+ data = weather_data.find("section[data-testid='HourlyForecast'] div[class*='Card'] details")
20
+
21
+ if len(data) == 0:
22
+ raise ValueError("There's no hourly forecast data available.")
23
+
24
+ details = [ {
25
+ "title": PyQuery(item).find("h2").text(),
26
+ "temperature" : PyQuery(item).find("div[data-testid='detailsTemperature']").text(),
27
+ "icon" : PyQuery(item).find("svg[class*='DetailsSummary']").attr("name"),
28
+ "summary" : PyQuery(item).find("span[class*='DetailsSummary--wxPhrase']").text(),
29
+ "precip-percentage": PyQuery(item).find("div[data-testid='Precip'] span[data-testid='PercentageValue']").text(),
30
+ "wind": PyQuery(item).find("span[data-testid='WindTitle']").next().eq(0).text(),
31
+ "feels-like" : PyQuery(item).find("span[data-testid='FeelsLikeTitle']").next().text(),
32
+ "humidity" : PyQuery(item).find("span[data-testid='HumidityTitle']").next().text(),
33
+ "uv-index" : PyQuery(item).find("span[data-testid='UVIndexValue']").text(),
34
+ "cloud-cover" : PyQuery(item).find("span[data-testid='CloudCoverTitle']").next().text(),
35
+ "rain-amount" : PyQuery(item).find("span[data-testid='AccumulationTitle']").next().text()
36
+ } for item in data ]
37
+
38
+ self.logger.debug("Extracted %s register(s)...",len(details))
39
+
40
+ hourly_forecasts = [HourlyPredictions(
41
+ title=item["title"],
42
+ temperature=item["temperature"],
43
+ icon=WeatherIconEnum.from_name(item["icon"]),
44
+ summary=item["summary"],
45
+ precipitation=Precipitation(
46
+ percentage=item["precip-percentage"],
47
+ amount=item["rain-amount"]
48
+ ),
49
+ wind=Wind.from_string(item["wind"]),
50
+ feels_like=item["feels-like"],
51
+ humidity=item["humidity"],
52
+ uv_index=UVIndex.from_string(item["uv-index"]),
53
+ cloud_cover=item["cloud-cover"]
54
+ ) for item in details]
55
+
56
+ self.logger.debug("Created hourly forecast list with %s registers", len(hourly_forecasts))
57
+
58
+ return hourly_forecasts
@@ -0,0 +1,80 @@
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
+ self.logger.debug("Extracting today's details...")
18
+
19
+ today_details_data = weather_data.find("div#todayDetails")
20
+ feelslike = PyQuery(today_details_data).find("div[data-testid='FeelsLikeSection'] span")
21
+ sunrise_sunset = PyQuery(today_details_data).find("div[data-testid='sunriseSunsetContainer'] div p[class*='TwcSunChart']")
22
+
23
+ feelslike_label = feelslike.eq(0).text() #'Feels Like'
24
+ feelslike_value = feelslike.eq(1).text() #'60°'
25
+
26
+ sunrise = sunrise_sunset.eq(0).text() #'6:12 AM'
27
+ sunset = sunrise_sunset.eq(1).text() #'7:45 PM'
28
+
29
+ icons = today_details_data.find('svg[class*="WeatherDetailsListItem--icon"]')
30
+ labels = today_details_data.find('div[class*="WeatherDetailsListItem--label"]')
31
+ values = today_details_data.find('div[data-testid="wxData"]')
32
+
33
+ self.logger.debug(f"Parsing today details values...")
34
+ high_low_label = labels.eq(0).text() #'High / Low'
35
+ high_low_value = values.eq(0).text() #'--/54°'
36
+
37
+ wind_label = labels.eq(1).text() #'Wind'
38
+ wind_value = values.eq(1).text() #'7\xa0mph'
39
+
40
+ humidity_label = labels.eq(2).text() #'Humidity'
41
+ humidity_value = values.eq(2).text() #'100%'
42
+
43
+ dew_point_label = labels.eq(3).text() #'Dew Point'
44
+ dew_point_value = values.eq(3).text() #'60°'
45
+
46
+ pressure_label = labels.eq(4).text() #'Pressure'
47
+ pressure_value = values.eq(4).text() #'30.31\xa0in'
48
+
49
+ uv_index_label = labels.eq(5).text() #'UV Index'
50
+ uv_index_value = values.eq(5).text() #'5 of 10'
51
+
52
+ visibility_label = labels.eq(6).text() #'Visibility'
53
+ visibility_value = values.eq(6).text() #'10.0 mi'
54
+
55
+ moon_phase_label = labels.eq(7).text() #'Moon Phase'
56
+ moon_phase_icon = icons.eq(7).attr('name') #'phase-2'
57
+ moon_phase_value = values.eq(7).text() #'Waxing Crescent'
58
+
59
+ self.logger.debug(f"Creating domain objects for today details...")
60
+
61
+ sunrise_sunset = SunriseSunset(sunrise=sunrise, sunset=sunset)
62
+ high_low = TemperatureHighLow.from_string(high_low_value, label=high_low_label)
63
+ uv_index = UVIndex.from_string(uv_index_value, label=uv_index_label)
64
+ moon_phase = MoonPhase(MoonPhaseEnum.from_name(moon_phase_icon), moon_phase_value, moon_phase_label)
65
+
66
+ today_details = TodayDetails(
67
+ feelslike=LabelValue(label=feelslike_label, value=feelslike_value),
68
+ sunrise_sunset=sunrise_sunset,
69
+ high_low=high_low,
70
+ wind=LabelValue(label=wind_label, value=wind_value),
71
+ humidity=LabelValue(label=humidity_label, value=humidity_value),
72
+ dew_point=LabelValue(label=dew_point_label, value=dew_point_value),
73
+ pressure=LabelValue(label=pressure_label, value=pressure_value),
74
+ uv_index=uv_index,
75
+ visibility=LabelValue(label=visibility_label, value=visibility_value),
76
+ moon_phase=moon_phase
77
+ )
78
+
79
+ self.logger.debug(f"Extracted today's details: {today_details}")
80
+ return today_details
@@ -57,16 +57,16 @@ class UseCase:
57
57
  health_activities = self.extract_health_activities_service.execute(weather_data)
58
58
 
59
59
  try:
60
- hourly_predictions = self.extract_hourly_forecast_service.execute(weather_data)
61
- except ValueError:
62
- self.logger.warning("Falling back to old style hourly forecast extraction")
63
60
  hourly_predictions = self.extract_hourly_forecast_oldstyle_service.execute(weather_data)
64
-
65
- try:
66
- daily_predictions = self.extract_daily_forecast_service.execute(weather_data)
67
61
  except ValueError:
68
- self.logger.warning("Falling back to old style daily forecast extraction")
62
+ self.logger.warning("Falling back to new style hourly forecast extraction")
63
+ hourly_predictions = self.extract_hourly_forecast_service.execute(weather_data)
64
+
65
+ try:
69
66
  daily_predictions = self.extract_daily_forecast_oldstyle_service.execute(weather_data)
67
+ except ValueError:
68
+ self.logger.warning("Falling back to new style daily forecast extraction")
69
+ daily_predictions = self.extract_daily_forecast_service.execute(weather_data)
70
70
 
71
71
  forecast = Forecast(
72
72
  search = Search(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weathergrabber
3
- Version: 0.0.8b2
3
+ Version: 0.0.8b4
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
@@ -2,7 +2,9 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  tests/test_cli.py
5
+ tests/test_cli_version.py
5
6
  tests/test_core.py
7
+ tests/test_main.py
6
8
  weathergrabber/__init__.py
7
9
  weathergrabber/__main__.py
8
10
  weathergrabber/cli.py
@@ -1,43 +0,0 @@
1
- import re
2
-
3
- class Color:
4
-
5
- def __init__(self, red: str, green: str, blue: str):
6
- self._red = red
7
- self._green = green
8
- self._blue = blue
9
-
10
- @property
11
- def red(self):
12
- return self._red
13
-
14
- @property
15
- def green(self):
16
- return self._green
17
-
18
- @property
19
- def blue(self):
20
- return self._blue
21
-
22
- @classmethod
23
- def from_string(cls,string_value: str) -> "Color":
24
-
25
- color_pattern = r"#([0-9A-Fa-f]{6})"
26
-
27
- match = re.search(color_pattern, string_value)
28
- color = f"#{match.group(1)}"
29
- hex_color = color.lstrip('#')
30
- r, g, b = int(hex_color[:2], 16), int(hex_color[2:4], 16), int(hex_color[4:], 16)
31
-
32
- return cls(r, g, b)
33
-
34
- @property
35
- def hex(self) -> str:
36
- return f"#{self.red:02x}{self.green:02x}{self.blue:02x}"
37
-
38
- @property
39
- def rgb(self) -> str:
40
- return f"rgb({self.red}, {self.green}, {self.blue})"
41
-
42
- def __repr__(self):
43
- return f"Color(red='{self.red}', green='{self.green}', blue='{self.blue}')"
@@ -1,30 +0,0 @@
1
- import logging
2
- from weathergrabber.domain.air_quality_index import AirQualityIndex
3
- from pyquery import PyQuery
4
-
5
- class ExtractAQIService:
6
-
7
- def __init__(self):
8
- self.logger = logging.getLogger(__name__)
9
- pass
10
-
11
- def execute(self, weather_data: PyQuery) -> AirQualityIndex | None:
12
-
13
- self.logger.debug("Extracting Air Quality Index (AQI)...")
14
-
15
- try:
16
- # 'Air Quality Index\n27\nGood\nAir quality is considered satisfactory, and air pollution poses little or no risk.\nSee Details\nInfo'
17
- aqi_data = weather_data("section[data-testid='AirQualityModule']").text()
18
-
19
- # 'stroke-width:5;stroke-dasharray:10.021680564951442 172.78759594743863;stroke:#00E838'
20
- color_data = weather_data("section[data-testid='AirQualityModule'] svg[data-testid='DonutChart'] circle:nth-of-type(2)").attr("style")
21
-
22
- air_quality_index = AirQualityIndex.aqi_color_from_string(aqi_data,color_data)
23
-
24
- self.logger.debug(f"Extracted AQI data: {air_quality_index}")
25
-
26
- return air_quality_index
27
- except Exception as e:
28
- self.logger.error(f"Error extracting AQI data: {e}")
29
- raise ValueError("Could not extract AQI data") from e
30
-
@@ -1,64 +0,0 @@
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
@@ -1,85 +0,0 @@
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