simple-dwd-weatherforecast 2.0.33__tar.gz → 2.1.0__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 (49) hide show
  1. {simple_dwd_weatherforecast-2.0.33/simple_dwd_weatherforecast.egg-info → simple_dwd_weatherforecast-2.1.0}/PKG-INFO +39 -2
  2. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/README.md +38 -1
  3. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/setup.py +1 -1
  4. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/simple_dwd_weatherforecast/dwdforecast.py +7 -4
  5. simple_dwd_weatherforecast-2.1.0/simple_dwd_weatherforecast/dwdmap.py +182 -0
  6. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0/simple_dwd_weatherforecast.egg-info}/PKG-INFO +39 -2
  7. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_daily_condition.py +8 -0
  8. simple_dwd_weatherforecast-2.1.0/tests/test_map.py +171 -0
  9. simple_dwd_weatherforecast-2.0.33/simple_dwd_weatherforecast/dwdmap.py +0 -85
  10. simple_dwd_weatherforecast-2.0.33/tests/test_map.py +0 -16
  11. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/LICENCE +0 -0
  12. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/setup.cfg +0 -0
  13. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/simple_dwd_weatherforecast/__init__.py +0 -0
  14. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/simple_dwd_weatherforecast/stations.json +0 -0
  15. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/simple_dwd_weatherforecast/uv_stations.json +0 -0
  16. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/simple_dwd_weatherforecast.egg-info/SOURCES.txt +0 -0
  17. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/simple_dwd_weatherforecast.egg-info/dependency_links.txt +0 -0
  18. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/simple_dwd_weatherforecast.egg-info/requires.txt +0 -0
  19. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/simple_dwd_weatherforecast.egg-info/top_level.txt +0 -0
  20. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/__init__.py +0 -0
  21. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/dummy_data.py +0 -0
  22. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/dummy_data_full.py +0 -0
  23. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/dummy_uv.py +0 -0
  24. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_daily_avg.py +0 -0
  25. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_daily_max.py +0 -0
  26. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_daily_min.py +0 -0
  27. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_daily_sum.py +0 -0
  28. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_day_values.py +0 -0
  29. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_forecast_condition.py +0 -0
  30. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_forecast_data.py +0 -0
  31. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_station_name.py +0 -0
  32. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_timeframe_avg.py +0 -0
  33. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_timeframe_condition.py +0 -0
  34. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_timeframe_max.py +0 -0
  35. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_timeframe_min.py +0 -0
  36. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_timeframe_sum.py +0 -0
  37. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_get_timeframe_values.py +0 -0
  38. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_is_in_timerange.py +0 -0
  39. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_is_valid_timeframe.py +0 -0
  40. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_location_tools.py +0 -0
  41. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_parsekml.py +0 -0
  42. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_region.py +0 -0
  43. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_reported_weather.py +0 -0
  44. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_station.py +0 -0
  45. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_stationsfile.py +0 -0
  46. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_update.py +0 -0
  47. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_update_hourly.py +0 -0
  48. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_uv_index.py +0 -0
  49. {simple_dwd_weatherforecast-2.0.33 → simple_dwd_weatherforecast-2.1.0}/tests/test_weather.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: simple_dwd_weatherforecast
3
- Version: 2.0.33
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.
@@ -181,13 +181,50 @@ class WeatherBackgroundMapType(Enum):
181
181
  GEMEINDEN = "dwd:Warngebiete_Gemeinden"
182
182
  SATELLIT = "dwd:bluemarble"
183
183
 
184
- 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
184
+ 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
185
185
 
186
186
  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
187
187
 
188
188
  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
189
189
  ```
190
190
 
191
+
192
+ ### Image loop
193
+
194
+ There is also the possibility to retrieve multiple images as a loop. This can be done by the class ImageLoop.
195
+
196
+
197
+ #### Usage example
198
+ ```python
199
+ from simple_dwd_weatherforecast import dwdmap
200
+
201
+ maploop = dwdmap.ImageLoop(
202
+ dwdmap.germany_boundaries.minx,
203
+ dwdmap.germany_boundaries.miny,
204
+ dwdmap.germany_boundaries.maxx,
205
+ dwdmap.germany_boundaries.maxy,
206
+ dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR,
207
+ dwdmap.WeatherBackgroundMapType.BUNDESLAENDER,
208
+ steps=5,
209
+ )
210
+
211
+ for image in enumerate(maploop._images):
212
+ image[1].save(f"image{image[0]}.png")
213
+
214
+ ```
215
+
216
+ #### Available methods
217
+
218
+ ```python
219
+ ImageLoop(minx: float, miny: float, maxx: float, maxy: float, map_type: WeatherMapType, background_type: WeatherBackgroundMapType,
220
+ steps: int = 6, image_width: int = 520,image_height: int = 580) -> ImageLoop
221
+
222
+ get_images() -> Iterable[ImageFile.ImageFile] # Returns the image loop
223
+
224
+ update() # Updates the loop to the most recent files
225
+
226
+ ```
227
+
191
228
  ## Help and Contribution
192
229
 
193
230
  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.
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setuptools.setup(
7
7
  name="simple_dwd_weatherforecast",
8
- version="2.0.33",
8
+ version="2.1.0",
9
9
  author="Max Fermor",
10
10
  description="A simple tool to retrieve a weather forecast from DWD OpenData",
11
11
  long_description=long_description,
@@ -298,13 +298,15 @@ class Weather:
298
298
  return (
299
299
  self.strip_to_day(
300
300
  arrow.get(
301
- list(self.forecast_data.keys())[0], "YYYY-MM-DDTHH:mm:ss.SSSZ" # type: ignore
301
+ list(self.forecast_data.keys())[0], # type: ignore
302
+ "YYYY-MM-DDTHH:mm:ss.SSSZ", # type: ignore
302
303
  ).datetime
303
304
  )
304
305
  <= self.strip_to_day(timestamp)
305
306
  <= self.strip_to_day(
306
307
  arrow.get(
307
- list(self.forecast_data.keys())[-1], "YYYY-MM-DDTHH:mm:ss.SSSZ" # type: ignore
308
+ list(self.forecast_data.keys())[-1], # type: ignore
309
+ "YYYY-MM-DDTHH:mm:ss.SSSZ", # type: ignore
308
310
  ).datetime
309
311
  )
310
312
  )
@@ -358,7 +360,7 @@ class Weather:
358
360
  def get_daily_condition(self, timestamp: datetime, shouldUpdate=True):
359
361
  if shouldUpdate:
360
362
  self.update()
361
- if self.is_in_timerange(timestamp):
363
+ if self.is_in_timerange_day(timestamp):
362
364
  return self.get_condition(self.get_day_values(timestamp))
363
365
  return None
364
366
 
@@ -609,7 +611,8 @@ class Weather:
609
611
  "timestamp has to be checked prior to be in timerange"
610
612
  result = []
611
613
  first_entry_date = arrow.get(
612
- next(iter(self.forecast_data)), "YYYY-MM-DDTHH:mm:ss.SSSZ" # type: ignore
614
+ next(iter(self.forecast_data)), # type: ignore
615
+ "YYYY-MM-DDTHH:mm:ss.SSSZ", # type: ignore
613
616
  ).datetime # type: ignore
614
617
  if timestamp.day != first_entry_date.day:
615
618
  time_step = self.strip_to_day(timestamp)
@@ -0,0 +1,182 @@
1
+ from typing import Iterable
2
+ import requests
3
+ import math
4
+ from io import BytesIO
5
+ from PIL import Image, ImageFile
6
+ from enum import Enum
7
+ from collections import deque
8
+ from datetime import datetime, timedelta, timezone
9
+
10
+
11
+ class WeatherMapType(Enum):
12
+ NIEDERSCHLAGSRADAR = "dwd:Niederschlagsradar,dwd:NCEW_EU"
13
+ MAXTEMP = "dwd:GefuehlteTempMax"
14
+ UVINDEX = "dwd:UVI_CS"
15
+ POLLENFLUG = "dwd:Pollenflug"
16
+ SATELLITE_RGB = "dwd:Satellite_meteosat_1km_euat_rgb_day_hrv_and_night_ir108_3h"
17
+ SATELLITE_IR = "dwd:Satellite_worldmosaic_3km_world_ir108_3h"
18
+ WARNUNGEN_GEMEINDEN = "dwd:Warnungen_Gemeinden"
19
+ WARNUNGEN_KREISE = "dwd:Warnungen_Landkreise"
20
+
21
+
22
+ class WeatherBackgroundMapType(Enum):
23
+ LAENDER = "dwd:Laender"
24
+ BUNDESLAENDER = "dwd:Warngebiete_Bundeslaender"
25
+ KREISE = "dwd:Warngebiete_Kreise"
26
+ GEMEINDEN = "dwd:Warngebiete_Gemeinden"
27
+ SATELLIT = "dwd:bluemarble"
28
+ GEWAESSER = "dwd:Gewaesser"
29
+
30
+
31
+ class germany_boundaries:
32
+ minx = 4.4
33
+ miny = 46.4
34
+ maxx = 16.1
35
+ maxy = 55.6
36
+
37
+
38
+ def get_from_location(
39
+ longitude,
40
+ latitude,
41
+ radius_km,
42
+ map_type: WeatherMapType,
43
+ background_type: WeatherBackgroundMapType = WeatherBackgroundMapType.BUNDESLAENDER,
44
+ image_width=520,
45
+ image_height=580,
46
+ ):
47
+ if radius_km <= 0:
48
+ raise ValueError("Radius must be greater than 0")
49
+ if latitude < -90 or latitude > 90:
50
+ raise ValueError("Latitude must be between -90 and 90")
51
+ if longitude < -180 or longitude > 180:
52
+ raise ValueError("Longitude must be between -180 and 180")
53
+ radius = math.fabs(radius_km / (111.3 * math.cos(latitude)))
54
+ return get_map(
55
+ latitude - radius,
56
+ longitude - radius,
57
+ latitude + radius,
58
+ longitude + radius,
59
+ map_type,
60
+ background_type,
61
+ image_width,
62
+ image_height,
63
+ )
64
+
65
+
66
+ def get_germany(
67
+ map_type: WeatherMapType,
68
+ background_type: WeatherBackgroundMapType = WeatherBackgroundMapType.BUNDESLAENDER,
69
+ image_width=520,
70
+ image_height=580,
71
+ ):
72
+ return get_map(
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,
81
+ )
82
+
83
+
84
+ def get_map(
85
+ minx,
86
+ miny,
87
+ maxx,
88
+ maxy,
89
+ map_type: WeatherMapType,
90
+ background_type: WeatherBackgroundMapType,
91
+ image_width=520,
92
+ image_height=580,
93
+ ):
94
+ if image_width > 1200 or image_height > 1400:
95
+ raise ValueError(
96
+ "Width and height must not exceed 1200 and 1400 respectively. Please be kind to the DWD servers."
97
+ )
98
+ url = f"https://maps.dwd.de/geoserver/dwd/wms?service=WMS&version=1.1.0&request=GetMap&layers={map_type.value},{background_type.value}&bbox={minx},{miny},{maxx},{maxy}&width={image_width}&height={image_height}&srs=EPSG:4326&styles=&format=image/png"
99
+ request = requests.get(url, stream=True)
100
+ if request.status_code == 200:
101
+ image = Image.open(BytesIO(request.content))
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.33
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.
@@ -49,3 +49,11 @@ class Weather_get_daily_condition(unittest.TestCase):
49
49
  self.dwd_weather.get_daily_condition(test_time),
50
50
  "cloudy",
51
51
  )
52
+
53
+ @patch("simple_dwd_weatherforecast.dwdforecast.Weather.update", return_value=None)
54
+ def test_same_day(self, _):
55
+ test_time = datetime(2020, 11, 6, 0, 0)
56
+ self.assertEqual(
57
+ self.dwd_weather.get_daily_condition(test_time),
58
+ "sunny",
59
+ )
@@ -0,0 +1,171 @@
1
+ import unittest
2
+ from unittest.mock import patch
3
+ from simple_dwd_weatherforecast import dwdmap
4
+ from datetime import datetime, timedelta, timezone
5
+
6
+
7
+ class MapTestCase(unittest.TestCase):
8
+ def test_prevent_too_large_height(self):
9
+ with self.assertRaises(ValueError):
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
+ )
20
+
21
+ def test_prevent_too_large_width(self):
22
+ with self.assertRaises(ValueError):
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
+ )
33
+
34
+ def test_prevent_too_large(self):
35
+ with self.assertRaises(ValueError):
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
+ )
@@ -1,85 +0,0 @@
1
- import requests
2
- import math
3
- from io import BytesIO
4
- from PIL import Image
5
- from enum import Enum
6
-
7
-
8
- class WeatherMapType(Enum):
9
- NIEDERSCHLAGSRADAR = "dwd:Niederschlagsradar,dwd:NCEW_EU"
10
- MAXTEMP = "dwd:GefuehlteTempMax"
11
- UVINDEX = "dwd:UVI_CS"
12
- POLLENFLUG = "dwd:Pollenflug"
13
- SATELLITE_RGB = "dwd:Satellite_meteosat_1km_euat_rgb_day_hrv_and_night_ir108_3h"
14
- SATELLITE_IR = "dwd:Satellite_worldmosaic_3km_world_ir108_3h"
15
- WARNUNGEN_GEMEINDEN = "dwd:Warnungen_Gemeinden"
16
- WARNUNGEN_KREISE = "dwd:Warnungen_Landkreise"
17
-
18
-
19
- class WeatherBackgroundMapType(Enum):
20
- LAENDER = "dwd:Laender"
21
- BUNDESLAENDER = "dwd:Warngebiete_Bundeslaender"
22
- KREISE = "dwd:Warngebiete_Kreise"
23
- GEMEINDEN = "dwd:Warngebiete_Gemeinden"
24
- SATELLIT = "dwd:bluemarble"
25
- GEWAESSER = "dwd:Gewaesser"
26
-
27
-
28
- def get_from_location(
29
- longitude,
30
- latitude,
31
- radius_km,
32
- map_type: WeatherMapType,
33
- background_type: WeatherBackgroundMapType = WeatherBackgroundMapType.BUNDESLAENDER,
34
- image_width=520,
35
- image_height=580,
36
- ):
37
- if radius_km <= 0:
38
- raise ValueError("Radius must be greater than 0")
39
- if latitude < -90 or latitude > 90:
40
- raise ValueError("Latitude must be between -90 and 90")
41
- if longitude < -180 or longitude > 180:
42
- raise ValueError("Longitude must be between -180 and 180")
43
- radius = math.fabs(radius_km / (111.3 * math.cos(latitude)))
44
- return get_map(
45
- latitude - radius,
46
- longitude - radius,
47
- latitude + radius,
48
- longitude + radius,
49
- map_type,
50
- background_type,
51
- image_width,
52
- image_height,
53
- )
54
-
55
-
56
- def get_germany(
57
- map_type: WeatherMapType,
58
- background_type: WeatherBackgroundMapType = WeatherBackgroundMapType.BUNDESLAENDER,
59
- image_width=520,
60
- image_height=580,
61
- ):
62
- return get_map(
63
- 4.4, 46.4, 16.1, 55.6, map_type, background_type, image_width, image_height
64
- )
65
-
66
-
67
- def get_map(
68
- minx,
69
- miny,
70
- maxx,
71
- maxy,
72
- map_type: WeatherMapType,
73
- background_type: WeatherBackgroundMapType,
74
- image_width=520,
75
- image_height=580,
76
- ):
77
- if image_width > 1200 or image_height > 1400:
78
- raise ValueError(
79
- "Width and height must not exceed 1200 and 1400 respectively. Please be kind to the DWD servers."
80
- )
81
- url = f"https://maps.dwd.de/geoserver/dwd/wms?service=WMS&version=1.1.0&request=GetMap&layers={map_type.value},{background_type.value}&bbox={minx},{miny},{maxx},{maxy}&width={image_width}&height={image_height}&srs=EPSG:4326&styles=&format=image/png"
82
- request = requests.get(url, stream=True)
83
- if request.status_code == 200:
84
- image = Image.open(BytesIO(request.content))
85
- return image
@@ -1,16 +0,0 @@
1
- import unittest
2
- from simple_dwd_weatherforecast import dwdmap
3
-
4
-
5
- class MapTestCase(unittest.TestCase):
6
- def test_prevent_too_large_height(self):
7
- with self.assertRaises(ValueError):
8
- dwdmap.get_map(4.4, 46.4, 16.1, 55.6, dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR, dwdmap.WeatherBackgroundMapType.BUNDESLAENDER, 1300, 700)
9
-
10
- def test_prevent_too_large_width(self):
11
- with self.assertRaises(ValueError):
12
- dwdmap.get_map(4.4, 46.4, 16.1, 55.6, dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR, dwdmap.WeatherBackgroundMapType.BUNDESLAENDER, 300, 1700)
13
-
14
- def test_prevent_too_large(self):
15
- with self.assertRaises(ValueError):
16
- dwdmap.get_map(4.4, 46.4, 16.1, 55.6, dwdmap.WeatherMapType.NIEDERSCHLAGSRADAR, dwdmap.WeatherBackgroundMapType.BUNDESLAENDER, 1300, 1700)