simple-dwd-weatherforecast 2.1.5__py3-none-any.whl → 2.1.7__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.
@@ -954,16 +954,13 @@ class Weather:
954
954
  print(f"Error in download_latest_kml: {type(error)} args: {error.args}")
955
955
 
956
956
  def get_chunks(self, url):
957
- def zipped_chunks(url):
958
- # Iterable that yields the bytes of a zip file
959
- with httpx.stream(
960
- "GET",
961
- url,
962
- ) as r:
963
- self.etags[url] = r.headers["etag"] # type: ignore
964
- yield from r.iter_bytes(chunk_size=171072)
965
-
966
- return stream_unzip(zipped_chunks(url))
957
+ # Iterable that yields the bytes of a zip file
958
+ with httpx.stream(
959
+ "GET",
960
+ url,
961
+ ) as r:
962
+ self.etags[url] = r.headers["etag"] # type: ignore
963
+ yield from r.iter_bytes(chunk_size=65536)
967
964
 
968
965
  def download_large_kml(self, stationid):
969
966
  placemark = b""
@@ -977,40 +974,42 @@ class Weather:
977
974
  if r.status_code == 304:
978
975
  return
979
976
 
980
- for file_name, file_size, unzipped_chunks in self.get_chunks(url):
981
- chunk1 = b""
982
- chunk2 = b""
983
- first_chunk = None
977
+ for file_name, file_size, unzipped_chunks in stream_unzip(self.get_chunks(url)):
978
+ header = b""
979
+ placemark = b""
984
980
 
985
- save_next = False
986
- save_next_next = False
981
+ found_header = False
982
+ found_stationid = False
987
983
  stop = False
988
984
  # unzipped_chunks must be iterated to completion or UnfinishedIterationError will be raised
989
985
  for chunk in unzipped_chunks:
990
986
  if stop:
991
987
  continue
992
- if not first_chunk:
993
- first_chunk = chunk
994
- if save_next_next:
995
- placemark = chunk1 + chunk2 + chunk
996
- save_next_next = False
997
- stop = True
998
- if save_next:
999
- chunk2 = chunk
1000
- save_next_next = True
1001
- save_next = False
988
+
989
+ if not found_header:
990
+ header += chunk
991
+ if "<kml:Placemark>".encode() in chunk:
992
+ found_header = True
993
+
994
+ if found_stationid:
995
+ placemark += chunk
996
+ if "</kml:Placemark>\n".encode() in chunk:
997
+ stop = True
1002
998
 
1003
999
  if f"<kml:name>{stationid}</kml:name>".encode() in chunk:
1004
- chunk1 = chunk
1005
- save_next = True
1000
+ placemark = chunk
1001
+ found_stationid = True
1006
1002
 
1007
- if not chunk1:
1003
+ if not placemark:
1008
1004
  raise BufferError("Station not found")
1009
- if first_chunk:
1005
+ if header and placemark:
1010
1006
  start = placemark.find(b"<kml:Placemark>\n")
1011
-
1007
+ if start == -1:
1008
+ raise BufferError(
1009
+ "Error during stream parsing of station {}".format(stationid)
1010
+ )
1012
1011
  result = (
1013
- first_chunk[: first_chunk.find(b"<kml:Placemark>")]
1012
+ header[: header.find(b"<kml:Placemark>")]
1014
1013
  + placemark[
1015
1014
  start : placemark.find(b"</kml:Placemark>\n", start) + 17
1016
1015
  ]
@@ -2,7 +2,7 @@ from typing import Iterable
2
2
  import requests
3
3
  import math
4
4
  from io import BytesIO
5
- from PIL import Image, ImageFile
5
+ from PIL import Image, ImageFile, ImageDraw
6
6
  from enum import Enum
7
7
  from collections import deque
8
8
  from datetime import datetime, timedelta, timezone
@@ -35,6 +35,51 @@ class germany_boundaries:
35
35
  maxy = 55.6
36
36
 
37
37
 
38
+ class MarkerShape(Enum):
39
+ CIRCLE = "circle"
40
+ SQUARE = "square"
41
+ CROSS = "cross"
42
+
43
+
44
+ class Marker:
45
+ def __init__(
46
+ self,
47
+ latitude: float,
48
+ longitude: float,
49
+ shape: MarkerShape,
50
+ size: int,
51
+ colorRGB: tuple[int, int, int],
52
+ width: int = 0,
53
+ ):
54
+ if (
55
+ latitude is None
56
+ or longitude is None
57
+ or shape is None
58
+ or size is None
59
+ or colorRGB is None
60
+ ):
61
+ raise ValueError("All values have to be defined")
62
+ self.latitude = latitude
63
+ self.longitude = longitude
64
+ self.shape = shape
65
+ self.size = size
66
+ self.colorRGB = colorRGB
67
+ self.width = width
68
+
69
+
70
+ class ImageBoundaries:
71
+ minX: float
72
+ maxX: float
73
+ minY: float
74
+ maxY: float
75
+
76
+ def __init__(self, minX: float, maxX: float, minY: float, maxY: float) -> None:
77
+ self.minX = minX
78
+ self.maxX = maxX
79
+ self.minY = minY
80
+ self.maxY = maxY
81
+
82
+
38
83
  def get_from_location(
39
84
  longitude,
40
85
  latitude,
@@ -43,6 +88,7 @@ def get_from_location(
43
88
  background_type: WeatherBackgroundMapType = WeatherBackgroundMapType.BUNDESLAENDER,
44
89
  image_width=520,
45
90
  image_height=580,
91
+ markers: list[Marker] = [],
46
92
  ):
47
93
  if radius_km <= 0:
48
94
  raise ValueError("Radius must be greater than 0")
@@ -60,6 +106,7 @@ def get_from_location(
60
106
  background_type,
61
107
  image_width,
62
108
  image_height,
109
+ markers,
63
110
  )
64
111
 
65
112
 
@@ -68,6 +115,7 @@ def get_germany(
68
115
  background_type: WeatherBackgroundMapType = WeatherBackgroundMapType.BUNDESLAENDER,
69
116
  image_width=520,
70
117
  image_height=580,
118
+ markers: list[Marker] = [],
71
119
  ):
72
120
  return get_map(
73
121
  germany_boundaries.minx,
@@ -78,6 +126,7 @@ def get_germany(
78
126
  background_type,
79
127
  image_width,
80
128
  image_height,
129
+ markers,
81
130
  )
82
131
 
83
132
 
@@ -90,6 +139,7 @@ def get_map(
90
139
  background_type: WeatherBackgroundMapType,
91
140
  image_width=520,
92
141
  image_height=580,
142
+ markers: list[Marker] = [],
93
143
  ):
94
144
  if image_width > 1200 or image_height > 1400:
95
145
  raise ValueError(
@@ -107,6 +157,7 @@ def get_map(
107
157
  request = requests.get(url, stream=True)
108
158
  if request.status_code == 200:
109
159
  image = Image.open(BytesIO(request.content))
160
+ image = draw_marker(image, ImageBoundaries(minx, maxx, miny, maxy), markers)
110
161
  return image
111
162
 
112
163
 
@@ -134,6 +185,7 @@ class ImageLoop:
134
185
  steps: int = 6,
135
186
  image_width: int = 520,
136
187
  image_height: int = 580,
188
+ markers: list[Marker] = [],
137
189
  ):
138
190
  if image_width > 1200 or image_height > 1400:
139
191
  raise ValueError(
@@ -150,7 +202,7 @@ class ImageLoop:
150
202
  self._steps = steps
151
203
  self._image_width = image_width
152
204
  self._image_height = image_height
153
-
205
+ self.markers = markers
154
206
  self._images = deque([], steps)
155
207
 
156
208
  self._full_reload()
@@ -182,7 +234,10 @@ class ImageLoop:
182
234
  self._last_update += timedelta(minutes=5)
183
235
  self._images.append(self._get_image(self._last_update))
184
236
 
185
- def _get_image(self, date: datetime) -> ImageFile.ImageFile:
237
+ def _get_image(
238
+ self,
239
+ date: datetime,
240
+ ) -> ImageFile.ImageFile:
186
241
  if self._background_type in [
187
242
  WeatherBackgroundMapType.SATELLIT,
188
243
  WeatherBackgroundMapType.KREISE,
@@ -195,9 +250,95 @@ class ImageLoop:
195
250
  request = requests.get(url, stream=True)
196
251
  if request.status_code != 200:
197
252
  raise ConnectionError("Error during image request from DWD servers")
198
- return Image.open(BytesIO(request.content))
253
+ image = Image.open(BytesIO(request.content))
254
+ image = draw_marker(
255
+ image,
256
+ ImageBoundaries(self._minx, self._maxx, self._miny, self._maxy),
257
+ self.markers,
258
+ )
259
+ return image
199
260
 
200
261
 
201
262
  def get_time_last_5_min(date: datetime) -> datetime:
202
263
  minute = math.floor(date.minute / 5) * 5
203
264
  return date.replace(minute=minute, second=0, microsecond=0)
265
+
266
+
267
+ def draw_marker(
268
+ image: ImageFile.ImageFile,
269
+ image_bounderies: ImageBoundaries,
270
+ marker_list: list[Marker],
271
+ ):
272
+ draw = ImageDraw.ImageDraw(image)
273
+ for marker in marker_list:
274
+ if (
275
+ marker.longitude < image_bounderies.minX
276
+ or marker.longitude > image_bounderies.maxX
277
+ or marker.latitude < image_bounderies.minY
278
+ or marker.latitude > image_bounderies.maxY
279
+ ):
280
+ raise ValueError("Marker location out of boundaries")
281
+ location_relative_to_image = (
282
+ (
283
+ (marker.longitude - image_bounderies.minX)
284
+ / (image_bounderies.maxX - image_bounderies.minX)
285
+ )
286
+ * image.width,
287
+ (
288
+ (marker.latitude - image_bounderies.minY)
289
+ / (image_bounderies.maxY - image_bounderies.minY)
290
+ )
291
+ * image.height,
292
+ )
293
+ if marker.shape == MarkerShape.CIRCLE:
294
+ draw.circle(
295
+ location_relative_to_image,
296
+ round(marker.size / 2, 0),
297
+ fill=marker.colorRGB,
298
+ )
299
+ elif marker.shape == MarkerShape.CROSS:
300
+ size = round(marker.size / 2, 0)
301
+ draw.line(
302
+ [
303
+ (
304
+ location_relative_to_image[0] - size,
305
+ location_relative_to_image[1],
306
+ ),
307
+ (
308
+ location_relative_to_image[0] + size,
309
+ location_relative_to_image[1],
310
+ ),
311
+ ],
312
+ marker.colorRGB,
313
+ marker.width,
314
+ )
315
+ draw.line(
316
+ [
317
+ (
318
+ location_relative_to_image[0],
319
+ location_relative_to_image[1] - size,
320
+ ),
321
+ (
322
+ location_relative_to_image[0],
323
+ location_relative_to_image[1] + size,
324
+ ),
325
+ ],
326
+ marker.colorRGB,
327
+ marker.width,
328
+ )
329
+ elif marker.shape == MarkerShape.SQUARE:
330
+ size = round(marker.size / 2, 0)
331
+ draw.rectangle(
332
+ [
333
+ (
334
+ location_relative_to_image[0] - size,
335
+ location_relative_to_image[1] - size,
336
+ ),
337
+ (
338
+ location_relative_to_image[0] + size,
339
+ location_relative_to_image[1] + size,
340
+ ),
341
+ ],
342
+ marker.colorRGB,
343
+ )
344
+ return image
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: simple_dwd_weatherforecast
3
- Version: 2.1.5
3
+ Version: 2.1.7
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
@@ -14,6 +14,7 @@ Requires-Dist: lxml
14
14
  Requires-Dist: requests
15
15
  Requires-Dist: Pillow
16
16
  Requires-Dist: arrow
17
+ Requires-Dist: stream-inflate==0.0.14
17
18
  Requires-Dist: stream-unzip
18
19
  Requires-Dist: httpx
19
20
 
@@ -203,11 +204,25 @@ class WeatherBackgroundMapType(Enum):
203
204
  GEMEINDEN = "dwd:Warngebiete_Gemeinden"
204
205
  SATELLIT = "dwd:bluemarble"
205
206
 
206
- 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
207
-
208
- 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
209
-
210
- 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
207
+ class MarkerShape(Enum):
208
+ CIRCLE = "circle"
209
+ SQUARE = "square"
210
+ CROSS = "cross"
211
+
212
+ class Marker(
213
+ latitude: float,
214
+ longitude: float,
215
+ shape: MarkerShape,
216
+ size: int,
217
+ colorRGB: tuple[int, int, int],
218
+ width: int = 0,
219
+ )
220
+
221
+ get_from_location(longitude, latitude, radius_km, map_type: WeatherMapType, background_type: WeatherBackgroundMapType, optional integer image_width, optional integer image_height, optional markers: list[Marker]) #Returns map as pillow image with given radius from coordinates
222
+
223
+ get_germany(map_type: WeatherMapType, optional WeatherBackgroundMapType background_type, optional integer image_width, optional integer image_height, optional markers: list[Marker]) #Returns map as pillow image of whole germany
224
+
225
+ get_map(minx,miny,maxx,maxy, map_type: WeatherMapType, background_type: WeatherBackgroundMapType, optional integer image_width, optional integer image_height, optional markers: list[Marker]) #Returns map as pillow image
211
226
  ```
212
227
 
213
228
 
@@ -239,7 +254,7 @@ for image in enumerate(maploop._images):
239
254
 
240
255
  ```python
241
256
  ImageLoop(minx: float, miny: float, maxx: float, maxy: float, map_type: WeatherMapType, background_type: WeatherBackgroundMapType,
242
- steps: int = 6, image_width: int = 520,image_height: int = 580) -> ImageLoop
257
+ steps: int = 6, image_width: int = 520,image_height: int = 580, markers: list[Marker] = []) -> ImageLoop
243
258
 
244
259
  get_images() -> Iterable[ImageFile.ImageFile] # Returns the image loop
245
260
 
@@ -1,6 +1,6 @@
1
1
  simple_dwd_weatherforecast/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- simple_dwd_weatherforecast/dwdforecast.py,sha256=klhL-Bd8u3Ybsq5JQTifMwlV67dF3foRTMGMfwVj5oE,38828
3
- simple_dwd_weatherforecast/dwdmap.py,sha256=cPCcL1u5qeIEDLBcL0qOH0-7dPIi3mge4-PUqfZlRQc,6737
2
+ simple_dwd_weatherforecast/dwdforecast.py,sha256=wYg7XC9_5rb-ITdjoT-bLaBc_AGqii8d6eNEfdbfXtY,38863
3
+ simple_dwd_weatherforecast/dwdmap.py,sha256=tyFkEkCTUF3StNExiwwtEHsmXdZ3hbpKnpngskTAkyM,10932
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
@@ -26,7 +26,7 @@ tests/test_is_in_timerange.py,sha256=3y88L3N73NxSTJ-_edx6OCnxHWKJWWFma98gjZvJDGg
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
28
  tests/test_map.py,sha256=uKxNjMXLFT3pczZKLqkfPK5xaVfmql-r5L9VPgCbS3Q,5671
29
- tests/test_parsekml.py,sha256=mpje7FoMIz566BLW-Fr_69cxLyS7w4qvrSOIlxVYoUU,1625
29
+ tests/test_parsekml.py,sha256=aG98x3B409CqxKBIq50yf3_LxPROnI4CAhdKfp350uQ,1495
30
30
  tests/test_region.py,sha256=ReUB9Cy9roBemkpEkTjZZav-Mu3Ha7ADOAfa9J-gi80,877
31
31
  tests/test_reported_weather.py,sha256=ULg4ogZRxus01p2rdxiSFL75AisqtcvnLDOc7uJMBH0,767
32
32
  tests/test_station.py,sha256=Zjx-q0yxKVxVI_L1yB_bqY5pjZPoa1L94uC8Gx6shdY,1026
@@ -35,8 +35,8 @@ tests/test_update.py,sha256=AIzzHMxcjwQjeTB0l3YFgB7HkGDbuqiHofwy41mS0m4,7440
35
35
  tests/test_update_hourly.py,sha256=7Zl8ml3FTdqw3_Qwr_Tz-sWTzypvrBWmxeig2Vwp_ZQ,1781
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.1.5.dist-info/LICENCE,sha256=27UG7gteqvSWuZlsbIq2_OAbh7VyifGGl-1zpuUoBcw,1072
39
- simple_dwd_weatherforecast-2.1.5.dist-info/METADATA,sha256=YAtr_r5aouZhkOGqfPYqbILWGKQIrZRAxORt0tUOc0s,12177
40
- simple_dwd_weatherforecast-2.1.5.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
41
- simple_dwd_weatherforecast-2.1.5.dist-info/top_level.txt,sha256=iyEobUh14Tzitx39Oi8qm0NhBrnZovl_dNKtvLUkLEM,33
42
- simple_dwd_weatherforecast-2.1.5.dist-info/RECORD,,
38
+ simple_dwd_weatherforecast-2.1.7.dist-info/LICENCE,sha256=27UG7gteqvSWuZlsbIq2_OAbh7VyifGGl-1zpuUoBcw,1072
39
+ simple_dwd_weatherforecast-2.1.7.dist-info/METADATA,sha256=vLLlK6IieXvuXQM7C4tVDIunqx0GZwa-azB_7Qdh0_4,12548
40
+ simple_dwd_weatherforecast-2.1.7.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
41
+ simple_dwd_weatherforecast-2.1.7.dist-info/top_level.txt,sha256=iyEobUh14Tzitx39Oi8qm0NhBrnZovl_dNKtvLUkLEM,33
42
+ simple_dwd_weatherforecast-2.1.7.dist-info/RECORD,,
tests/test_parsekml.py CHANGED
@@ -23,27 +23,24 @@ class KMLParseTestCase(unittest.TestCase):
23
23
  )
24
24
 
25
25
 
26
- def helper():
27
- result = []
28
- read_size = 131072
26
+ def helper(file):
29
27
  # Iterable that yields the bytes of a zip file
30
- with open("development/MOSMIX_L_2023100809_stripped.kml", "rb") as kml:
31
- content = kml.read(read_size)
28
+ with open(file, "rb") as kml:
29
+ content = kml.read()
32
30
  while len(content) > 0:
33
- result.append(content)
34
- content = kml.read(read_size)
35
- return zip([0], [0], [result])
31
+ yield content
32
+ content = kml.read()
36
33
 
37
34
 
38
35
  class KMLParseFullTestCase(unittest.TestCase):
39
- FILE_NAME = "development/MOSMIX_L_2023100809_stripped.kml"
36
+ FILE_NAME = "development/MOSMIX_L_2023100809_stripped.kmz"
40
37
 
41
38
  def setUp(self):
42
39
  self.dwd_weather = dwdforecast.Weather("L511")
43
40
 
44
41
  @patch(
45
42
  "simple_dwd_weatherforecast.dwdforecast.Weather.get_chunks",
46
- return_value=helper(),
43
+ return_value=helper(FILE_NAME),
47
44
  )
48
45
  def test_parse_kml(self, _):
49
46
  self.dwd_weather.download_latest_kml(