simple-dwd-weatherforecast 2.0.34__py3-none-any.whl → 2.1.0__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.
@@ -358,11 +358,9 @@ class Weather:
358
358
  return None
359
359
 
360
360
  def get_daily_condition(self, timestamp: datetime, shouldUpdate=True):
361
- print("testieng")
362
361
  if shouldUpdate:
363
362
  self.update()
364
363
  if self.is_in_timerange_day(timestamp):
365
- print("working")
366
364
  return self.get_condition(self.get_day_values(timestamp))
367
365
  return None
368
366
 
@@ -1,8 +1,11 @@
1
+ from typing import Iterable
1
2
  import requests
2
3
  import math
3
4
  from io import BytesIO
4
- from PIL import Image
5
+ from PIL import Image, ImageFile
5
6
  from enum import Enum
7
+ from collections import deque
8
+ from datetime import datetime, timedelta, timezone
6
9
 
7
10
 
8
11
  class WeatherMapType(Enum):
@@ -25,6 +28,13 @@ class WeatherBackgroundMapType(Enum):
25
28
  GEWAESSER = "dwd:Gewaesser"
26
29
 
27
30
 
31
+ class germany_boundaries:
32
+ minx = 4.4
33
+ miny = 46.4
34
+ maxx = 16.1
35
+ maxy = 55.6
36
+
37
+
28
38
  def get_from_location(
29
39
  longitude,
30
40
  latitude,
@@ -60,7 +70,14 @@ def get_germany(
60
70
  image_height=580,
61
71
  ):
62
72
  return get_map(
63
- 4.4, 46.4, 16.1, 55.6, map_type, background_type, image_width, image_height
73
+ germany_boundaries.minx,
74
+ germany_boundaries.miny,
75
+ germany_boundaries.maxx,
76
+ germany_boundaries.maxy,
77
+ map_type,
78
+ background_type,
79
+ image_width,
80
+ image_height,
64
81
  )
65
82
 
66
83
 
@@ -83,3 +100,83 @@ def get_map(
83
100
  if request.status_code == 200:
84
101
  image = Image.open(BytesIO(request.content))
85
102
  return image
103
+
104
+
105
+ class ImageLoop:
106
+ _last_update: datetime
107
+ _images: deque[ImageFile.ImageFile]
108
+
109
+ _minx: float
110
+ _miny: float
111
+ _maxx: float
112
+ _maxy: float
113
+ _map_type: WeatherMapType
114
+ _background_type: WeatherBackgroundMapType
115
+ _image_width: int
116
+ _image_height: int
117
+
118
+ def __init__(
119
+ self,
120
+ minx: float,
121
+ miny: float,
122
+ maxx: float,
123
+ maxy: float,
124
+ map_type: WeatherMapType,
125
+ background_type: WeatherBackgroundMapType,
126
+ steps: int = 6,
127
+ image_width: int = 520,
128
+ image_height: int = 580,
129
+ ):
130
+ if image_width > 1200 or image_height > 1400:
131
+ raise ValueError(
132
+ "Width and height must not exceed 1200 and 1400 respectively. Please be kind to the DWD servers."
133
+ )
134
+ self._minx = minx
135
+ self._miny = miny
136
+ self._maxx = maxx
137
+ self._maxy = maxy
138
+ self._map_type = map_type
139
+ self._background_type = background_type
140
+ self._steps = steps
141
+ self._image_width = image_width
142
+ self._image_height = image_height
143
+
144
+ self._images = deque([], steps)
145
+
146
+ self._full_reload()
147
+
148
+ def get_images(self) -> Iterable[ImageFile.ImageFile]:
149
+ return self._images
150
+
151
+ def _full_reload(self):
152
+ self._images.clear()
153
+ now = get_time_last_5_min(datetime.now(timezone.utc))
154
+ self._last_update = now - timedelta(minutes=5) * self._steps
155
+
156
+ while now > self._last_update:
157
+ self._last_update += timedelta(minutes=5)
158
+ self._images.append(self._get_image(self._last_update))
159
+
160
+ def update(self):
161
+ now = get_time_last_5_min(datetime.now(timezone.utc))
162
+
163
+ # If last update is older than the buffer can hold, reload the whole buffer
164
+ if now - self._last_update > self._steps * timedelta(minutes=5):
165
+ self._full_reload()
166
+ # Update the buffer and fetch only the new images
167
+ else:
168
+ while now > self._last_update:
169
+ self._last_update += timedelta(minutes=5)
170
+ self._images.append(self._get_image(self._last_update))
171
+
172
+ def _get_image(self, date: datetime) -> ImageFile.ImageFile:
173
+ url = f"https://maps.dwd.de/geoserver/dwd/wms?service=WMS&version=1.1.0&request=GetMap&layers={self._map_type.value},{self._background_type.value}&bbox={self._minx},{self._miny},{self._maxx},{self._maxy}&width={self._image_width}&height={self._image_height}&srs=EPSG:4326&styles=&format=image/png&TIME={date.strftime("%Y-%m-%dT%H:%M:00.0Z")}"
174
+ request = requests.get(url, stream=True)
175
+ if request.status_code != 200:
176
+ raise ConnectionError("Error during image request from DWD servers")
177
+ return Image.open(BytesIO(request.content))
178
+
179
+
180
+ def get_time_last_5_min(date: datetime) -> datetime:
181
+ minute = math.floor(date.minute / 5) * 5
182
+ return date.replace(minute=minute, second=0, microsecond=0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: simple_dwd_weatherforecast
3
- Version: 2.0.34
3
+ Version: 2.1.0
4
4
  Summary: A simple tool to retrieve a weather forecast from DWD OpenData
5
5
  Home-page: https://github.com/FL550/simple_dwd_weatherforecast.git
6
6
  Author: Max Fermor
@@ -198,13 +198,50 @@ class WeatherBackgroundMapType(Enum):
198
198
  GEMEINDEN = "dwd:Warngebiete_Gemeinden"
199
199
  SATELLIT = "dwd:bluemarble"
200
200
 
201
- def get_from_location(longitude, latitude, radius_km, map_type: WeatherMapType, background_type: WeatherBackgroundMapType, optional integer image_width, optional integer image_height) #Returns map as pillow image with given radius from coordinates
201
+ get_from_location(longitude, latitude, radius_km, map_type: WeatherMapType, background_type: WeatherBackgroundMapType, optional integer image_width, optional integer image_height) #Returns map as pillow image with given radius from coordinates
202
202
 
203
203
  get_germany(map_type: WeatherMapType, optional WeatherBackgroundMapType background_type, optional integer image_width, optional integer image_height, optional string save_to_filename) #Returns map as pillow image of whole germany
204
204
 
205
205
  get_map(minx,miny,maxx,maxy, map_type: WeatherMapType, background_type: WeatherBackgroundMapType, optional integer image_width, optional integer image_height, optional string save_to_filename) #Returns map as pillow image
206
206
  ```
207
207
 
208
+
209
+ ### Image loop
210
+
211
+ There is also the possibility to retrieve multiple images as a loop. This can be done by the class ImageLoop.
212
+
213
+
214
+ #### Usage example
215
+ ```python
216
+ from simple_dwd_weatherforecast import dwdmap
217
+
218
+ maploop = dwdmap.ImageLoop(
219
+ dwdmap.germany_boundaries.minx,
220
+ dwdmap.germany_boundaries.miny,
221
+ dwdmap.germany_boundaries.maxx,
222
+ dwdmap.germany_boundaries.maxy,
223
+ dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR,
224
+ dwdmap.WeatherBackgroundMapType.BUNDESLAENDER,
225
+ steps=5,
226
+ )
227
+
228
+ for image in enumerate(maploop._images):
229
+ image[1].save(f"image{image[0]}.png")
230
+
231
+ ```
232
+
233
+ #### Available methods
234
+
235
+ ```python
236
+ ImageLoop(minx: float, miny: float, maxx: float, maxy: float, map_type: WeatherMapType, background_type: WeatherBackgroundMapType,
237
+ steps: int = 6, image_width: int = 520,image_height: int = 580) -> ImageLoop
238
+
239
+ get_images() -> Iterable[ImageFile.ImageFile] # Returns the image loop
240
+
241
+ update() # Updates the loop to the most recent files
242
+
243
+ ```
244
+
208
245
  ## Help and Contribution
209
246
 
210
247
  Feel free to open an issue if you find one and I will do my best to help you. If you want to contribute, your help is appreciated! If you want to add a new feature, add a pull request first so we can chat about the details.
@@ -1,6 +1,6 @@
1
1
  simple_dwd_weatherforecast/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- simple_dwd_weatherforecast/dwdforecast.py,sha256=OxVTpy6jJN8-yZMjRXH0A42GEqn7wIFy9lmGSWxLmvU,36749
3
- simple_dwd_weatherforecast/dwdmap.py,sha256=fZMi3nk4MwzZm0dJ8TUwCgOuTw6t8kZmnOkoHGKpXx4,2695
2
+ simple_dwd_weatherforecast/dwdforecast.py,sha256=ctnQAFJS8UID9KZCXK7O5FyBTlgkqvoNLVJG5E3TkCY,36694
3
+ simple_dwd_weatherforecast/dwdmap.py,sha256=OpFbBgALHUcvuH95dzOxMPwb7eFGNX7OJJkNPRr0AB4,5954
4
4
  simple_dwd_weatherforecast/stations.json,sha256=1u8qc2CT_rVy49SAlOicGixzHln6Y0FXevuFAz2maBw,838948
5
5
  simple_dwd_weatherforecast/uv_stations.json,sha256=ADenYo-aR6qbf0UFkfYr72kkFzL9HyUKe4VQ23POGF8,2292
6
6
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -25,7 +25,7 @@ tests/test_get_timeframe_values.py,sha256=z8ozpWlyilw8-zjbk2gDExhTobNbgmcVTbZ6ZH
25
25
  tests/test_is_in_timerange.py,sha256=3y88L3N73NxSTJ-_edx6OCnxHWKJWWFma98gjZvJDGg,1338
26
26
  tests/test_is_valid_timeframe.py,sha256=mXjeu3lUyixiBUEljirTf6qDM_FZFQGWa-Rk0NBMUDU,891
27
27
  tests/test_location_tools.py,sha256=wto_XzVnARJQ-Qc83YAn0ahfMBSaOHpfzqAeKRDsNm8,1208
28
- tests/test_map.py,sha256=Bnf7XSap7xAfozE5U9aB0GjjMiHmXrpyJPE79VmEvFg,830
28
+ tests/test_map.py,sha256=uKxNjMXLFT3pczZKLqkfPK5xaVfmql-r5L9VPgCbS3Q,5671
29
29
  tests/test_parsekml.py,sha256=Y0EHJi5e8lqFUS8N9yfA5ByQkKV4-iyrwUaIipQpM7w,1065
30
30
  tests/test_region.py,sha256=ReUB9Cy9roBemkpEkTjZZav-Mu3Ha7ADOAfa9J-gi80,877
31
31
  tests/test_reported_weather.py,sha256=ULg4ogZRxus01p2rdxiSFL75AisqtcvnLDOc7uJMBH0,767
@@ -35,8 +35,8 @@ tests/test_update.py,sha256=r763R-MfqFqQQy43PcmE9yWAiQU6T2rQ1u55ujlwMt8,7216
35
35
  tests/test_update_hourly.py,sha256=mUc66JHndgIMdZ0DD_KXQTbPPg1g86F6CPUwJYjBV5U,1619
36
36
  tests/test_uv_index.py,sha256=tr6wnOyHlXT1S3yp1oeHc4-Brmc-EMEdM4mtyrdpcHg,579
37
37
  tests/test_weather.py,sha256=ZyX4ldUoJpJp7YpiNQwU6Od-nYRay-3qcaDJdNq8fhY,780
38
- simple_dwd_weatherforecast-2.0.34.dist-info/LICENCE,sha256=27UG7gteqvSWuZlsbIq2_OAbh7VyifGGl-1zpuUoBcw,1072
39
- simple_dwd_weatherforecast-2.0.34.dist-info/METADATA,sha256=4oLRGZi10agl-c9y4GME2d1eDCw6Mx_NQEH6y91EPTA,10937
40
- simple_dwd_weatherforecast-2.0.34.dist-info/WHEEL,sha256=ixB2d4u7mugx_bCBycvM9OzZ5yD7NmPXFRtKlORZS2Y,91
41
- simple_dwd_weatherforecast-2.0.34.dist-info/top_level.txt,sha256=iyEobUh14Tzitx39Oi8qm0NhBrnZovl_dNKtvLUkLEM,33
42
- simple_dwd_weatherforecast-2.0.34.dist-info/RECORD,,
38
+ simple_dwd_weatherforecast-2.1.0.dist-info/LICENCE,sha256=27UG7gteqvSWuZlsbIq2_OAbh7VyifGGl-1zpuUoBcw,1072
39
+ simple_dwd_weatherforecast-2.1.0.dist-info/METADATA,sha256=Czz5cBb-mPwEPqRITu4_umcHf-Krkb0kGjlBJwlqQDw,11893
40
+ simple_dwd_weatherforecast-2.1.0.dist-info/WHEEL,sha256=uCRv0ZEik_232NlR4YDw4Pv3Ajt5bKvMH13NUU7hFuI,91
41
+ simple_dwd_weatherforecast-2.1.0.dist-info/top_level.txt,sha256=iyEobUh14Tzitx39Oi8qm0NhBrnZovl_dNKtvLUkLEM,33
42
+ simple_dwd_weatherforecast-2.1.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (74.1.0)
2
+ Generator: setuptools (74.1.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
tests/test_map.py CHANGED
@@ -1,16 +1,171 @@
1
1
  import unittest
2
+ from unittest.mock import patch
2
3
  from simple_dwd_weatherforecast import dwdmap
4
+ from datetime import datetime, timedelta, timezone
3
5
 
4
6
 
5
7
  class MapTestCase(unittest.TestCase):
6
8
  def test_prevent_too_large_height(self):
7
9
  with self.assertRaises(ValueError):
8
- dwdmap.get_map(4.4, 46.4, 16.1, 55.6, dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR, dwdmap.WeatherBackgroundMapType.BUNDESLAENDER, 1300, 700)
10
+ dwdmap.get_map(
11
+ 4.4,
12
+ 46.4,
13
+ 16.1,
14
+ 55.6,
15
+ dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR,
16
+ dwdmap.WeatherBackgroundMapType.BUNDESLAENDER,
17
+ 1300,
18
+ 700,
19
+ )
9
20
 
10
21
  def test_prevent_too_large_width(self):
11
22
  with self.assertRaises(ValueError):
12
- dwdmap.get_map(4.4, 46.4, 16.1, 55.6, dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR, dwdmap.WeatherBackgroundMapType.BUNDESLAENDER, 300, 1700)
23
+ dwdmap.get_map(
24
+ 4.4,
25
+ 46.4,
26
+ 16.1,
27
+ 55.6,
28
+ dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR,
29
+ dwdmap.WeatherBackgroundMapType.BUNDESLAENDER,
30
+ 300,
31
+ 1700,
32
+ )
13
33
 
14
34
  def test_prevent_too_large(self):
15
35
  with self.assertRaises(ValueError):
16
- dwdmap.get_map(4.4, 46.4, 16.1, 55.6, dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR, dwdmap.WeatherBackgroundMapType.BUNDESLAENDER, 1300, 1700)
36
+ dwdmap.get_map(
37
+ 4.4,
38
+ 46.4,
39
+ 16.1,
40
+ 55.6,
41
+ dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR,
42
+ dwdmap.WeatherBackgroundMapType.BUNDESLAENDER,
43
+ 1300,
44
+ 1700,
45
+ )
46
+
47
+
48
+ class ImageLoopTestCase(unittest.TestCase):
49
+ @patch("simple_dwd_weatherforecast.dwdmap.ImageLoop._get_image", return_value=None)
50
+ def test_init(self, mock_function):
51
+ maploop = dwdmap.ImageLoop(
52
+ 0.1,
53
+ 0.2,
54
+ 0.3,
55
+ 0.4,
56
+ dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR,
57
+ dwdmap.WeatherBackgroundMapType.BUNDESLAENDER,
58
+ 1,
59
+ )
60
+ self.assertEqual(
61
+ maploop._minx,
62
+ 0.1,
63
+ )
64
+ self.assertEqual(
65
+ maploop._miny,
66
+ 0.2,
67
+ )
68
+ self.assertEqual(
69
+ maploop._maxx,
70
+ 0.3,
71
+ )
72
+ self.assertEqual(
73
+ maploop._maxy,
74
+ 0.4,
75
+ )
76
+ self.assertEqual(
77
+ maploop._map_type,
78
+ dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR,
79
+ )
80
+ self.assertEqual(
81
+ maploop._background_type,
82
+ dwdmap.WeatherBackgroundMapType.BUNDESLAENDER,
83
+ )
84
+ mock_function.assert_called_once()
85
+
86
+ @patch("simple_dwd_weatherforecast.dwdmap.ImageLoop._get_image", return_value=None)
87
+ def test_steps1(self, mock_function):
88
+ _ = dwdmap.ImageLoop(
89
+ 0.1,
90
+ 0.2,
91
+ 0.3,
92
+ 0.4,
93
+ dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR,
94
+ dwdmap.WeatherBackgroundMapType.BUNDESLAENDER,
95
+ 5,
96
+ )
97
+ self.assertEqual(mock_function.call_count, 5)
98
+
99
+ @patch("simple_dwd_weatherforecast.dwdmap.ImageLoop._get_image", return_value=None)
100
+ def test_steps2(self, mock_function):
101
+ _ = dwdmap.ImageLoop(
102
+ 0.1,
103
+ 0.2,
104
+ 0.3,
105
+ 0.4,
106
+ dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR,
107
+ dwdmap.WeatherBackgroundMapType.BUNDESLAENDER,
108
+ 10,
109
+ )
110
+ self.assertEqual(mock_function.call_count, 10)
111
+
112
+ @patch("simple_dwd_weatherforecast.dwdmap.ImageLoop._get_image", return_value=None)
113
+ def test_update_full(self, mock_function):
114
+ maploop = dwdmap.ImageLoop(
115
+ 0.1,
116
+ 0.2,
117
+ 0.3,
118
+ 0.4,
119
+ dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR,
120
+ dwdmap.WeatherBackgroundMapType.BUNDESLAENDER,
121
+ 5,
122
+ )
123
+ maploop._last_update = datetime(year=2024, month=9, day=4, hour=5, minute=50)
124
+ self.assertEqual(mock_function.call_count, 5)
125
+
126
+ @patch("simple_dwd_weatherforecast.dwdmap.ImageLoop._get_image", return_value=None)
127
+ def test_update_part(self, mock_function):
128
+ maploop = dwdmap.ImageLoop(
129
+ 0.1,
130
+ 0.2,
131
+ 0.3,
132
+ 0.4,
133
+ dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR,
134
+ dwdmap.WeatherBackgroundMapType.BUNDESLAENDER,
135
+ 5,
136
+ )
137
+ now = dwdmap.get_time_last_5_min(datetime.now(timezone.utc))
138
+ maploop._last_update = now - timedelta(minutes=15)
139
+ mock_function.reset_mock()
140
+ maploop.update()
141
+ self.assertEqual(mock_function.call_count, 3)
142
+
143
+
144
+ class GeneralTestCase(unittest.TestCase):
145
+ def test_get_time_last_5_min_strip(self):
146
+ time = datetime(year=2024, month=9, day=4, hour=9, minute=50, second=52)
147
+ self.assertEqual(
148
+ dwdmap.get_time_last_5_min(time),
149
+ datetime(year=2024, month=9, day=4, hour=9, minute=50),
150
+ )
151
+
152
+ def test_get_time_last_5_min_exact(self):
153
+ time = datetime(year=2024, month=9, day=4, hour=9, minute=50, second=00)
154
+ self.assertEqual(
155
+ dwdmap.get_time_last_5_min(time),
156
+ datetime(year=2024, month=9, day=4, hour=9, minute=50),
157
+ )
158
+
159
+ def test_get_time_last_5_min_between1(self):
160
+ time = datetime(year=2024, month=9, day=4, hour=9, minute=52, second=00)
161
+ self.assertEqual(
162
+ dwdmap.get_time_last_5_min(time),
163
+ datetime(year=2024, month=9, day=4, hour=9, minute=50),
164
+ )
165
+
166
+ def test_get_time_last_5_min_between2(self):
167
+ time = datetime(year=2024, month=9, day=4, hour=9, minute=3, second=00)
168
+ self.assertEqual(
169
+ dwdmap.get_time_last_5_min(time),
170
+ datetime(year=2024, month=9, day=4, hour=9, minute=0),
171
+ )