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.
- simple_dwd_weatherforecast/dwdforecast.py +31 -32
- simple_dwd_weatherforecast/dwdmap.py +145 -4
- {simple_dwd_weatherforecast-2.1.5.dist-info → simple_dwd_weatherforecast-2.1.7.dist-info}/METADATA +22 -7
- {simple_dwd_weatherforecast-2.1.5.dist-info → simple_dwd_weatherforecast-2.1.7.dist-info}/RECORD +8 -8
- tests/test_parsekml.py +7 -10
- {simple_dwd_weatherforecast-2.1.5.dist-info → simple_dwd_weatherforecast-2.1.7.dist-info}/LICENCE +0 -0
- {simple_dwd_weatherforecast-2.1.5.dist-info → simple_dwd_weatherforecast-2.1.7.dist-info}/WHEEL +0 -0
- {simple_dwd_weatherforecast-2.1.5.dist-info → simple_dwd_weatherforecast-2.1.7.dist-info}/top_level.txt +0 -0
@@ -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
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
963
|
-
|
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
|
-
|
982
|
-
|
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
|
-
|
986
|
-
|
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
|
-
|
993
|
-
|
994
|
-
|
995
|
-
|
996
|
-
|
997
|
-
|
998
|
-
if
|
999
|
-
|
1000
|
-
|
1001
|
-
|
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
|
-
|
1005
|
-
|
1000
|
+
placemark = chunk
|
1001
|
+
found_stationid = True
|
1006
1002
|
|
1007
|
-
if not
|
1003
|
+
if not placemark:
|
1008
1004
|
raise BufferError("Station not found")
|
1009
|
-
if
|
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
|
-
|
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(
|
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
|
-
|
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
|
{simple_dwd_weatherforecast-2.1.5.dist-info → simple_dwd_weatherforecast-2.1.7.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: simple_dwd_weatherforecast
|
3
|
-
Version: 2.1.
|
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
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
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
|
|
{simple_dwd_weatherforecast-2.1.5.dist-info → simple_dwd_weatherforecast-2.1.7.dist-info}/RECORD
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
simple_dwd_weatherforecast/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
simple_dwd_weatherforecast/dwdforecast.py,sha256=
|
3
|
-
simple_dwd_weatherforecast/dwdmap.py,sha256=
|
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=
|
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.
|
39
|
-
simple_dwd_weatherforecast-2.1.
|
40
|
-
simple_dwd_weatherforecast-2.1.
|
41
|
-
simple_dwd_weatherforecast-2.1.
|
42
|
-
simple_dwd_weatherforecast-2.1.
|
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(
|
31
|
-
content = kml.read(
|
28
|
+
with open(file, "rb") as kml:
|
29
|
+
content = kml.read()
|
32
30
|
while len(content) > 0:
|
33
|
-
|
34
|
-
content = kml.read(
|
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.
|
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(
|
{simple_dwd_weatherforecast-2.1.5.dist-info → simple_dwd_weatherforecast-2.1.7.dist-info}/LICENCE
RENAMED
File without changes
|
{simple_dwd_weatherforecast-2.1.5.dist-info → simple_dwd_weatherforecast-2.1.7.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|