env-canada 0.6.1__py3-none-any.whl → 0.7.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.
- env_canada/__init__.py +5 -5
- env_canada/constants.py +1 -1
- env_canada/ec_radar.py +91 -72
- env_canada/ec_weather.py +7 -22
- {env_canada-0.6.1.dist-info → env_canada-0.7.0.dist-info}/METADATA +8 -6
- env_canada-0.7.0.dist-info/RECORD +16 -0
- {env_canada-0.6.1.dist-info → env_canada-0.7.0.dist-info}/WHEEL +1 -1
- env_canada-0.6.1.dist-info/RECORD +0 -16
- {env_canada-0.6.1.dist-info → env_canada-0.7.0.dist-info}/LICENSE +0 -0
- {env_canada-0.6.1.dist-info → env_canada-0.7.0.dist-info}/top_level.txt +0 -0
env_canada/__init__.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
from .ec_aqhi import
|
2
|
-
from .ec_historical import
|
3
|
-
from .ec_hydro import
|
4
|
-
from .ec_radar import
|
5
|
-
from .ec_weather import
|
1
|
+
from .ec_aqhi import ECAirQuality
|
2
|
+
from .ec_historical import ECHistorical, ECHistoricalRange
|
3
|
+
from .ec_hydro import ECHydro
|
4
|
+
from .ec_radar import ECRadar
|
5
|
+
from .ec_weather import ECWeather
|
env_canada/constants.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
USER_AGENT = "env_canada/0.
|
1
|
+
USER_AGENT = "env_canada/0.7.0"
|
env_canada/ec_radar.py
CHANGED
@@ -1,17 +1,18 @@
|
|
1
|
-
from aiohttp.client_exceptions import ClientConnectorError
|
2
1
|
import asyncio
|
3
2
|
import datetime
|
4
|
-
from io import BytesIO
|
5
3
|
import logging
|
6
4
|
import math
|
7
5
|
import os
|
8
|
-
from
|
6
|
+
from io import BytesIO
|
9
7
|
|
10
|
-
from .ec_cache import CacheClientSession as ClientSession
|
11
8
|
import dateutil.parser
|
12
9
|
import defusedxml.ElementTree as et
|
13
10
|
import imageio.v2 as imageio
|
14
11
|
import voluptuous as vol
|
12
|
+
from aiohttp.client_exceptions import ClientConnectorError
|
13
|
+
from PIL import Image, ImageDraw, ImageFont
|
14
|
+
|
15
|
+
from .ec_cache import CacheClientSession as ClientSession
|
15
16
|
|
16
17
|
ATTRIBUTION = {
|
17
18
|
"english": "Data provided by Environment Canada",
|
@@ -156,16 +157,12 @@ class ECRadar(object):
|
|
156
157
|
# Get overlay parameters
|
157
158
|
|
158
159
|
self.show_legend = kwargs["legend"]
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
self.legend_position = None
|
160
|
+
self.legend_layer = None
|
161
|
+
self.legend_image = None
|
162
|
+
self.legend_position = None
|
163
163
|
|
164
164
|
self.show_timestamp = kwargs["timestamp"]
|
165
|
-
|
166
|
-
self.font = ImageFont.load(
|
167
|
-
os.path.join(os.path.dirname(__file__), "10x20.pil")
|
168
|
-
)
|
165
|
+
self.font = None
|
169
166
|
|
170
167
|
@property
|
171
168
|
def precip_type(self):
|
@@ -197,22 +194,20 @@ class ECRadar(object):
|
|
197
194
|
async with ClientSession(raise_for_status=True) as session:
|
198
195
|
response = await session.get(url=basemap_url, params=basemap_params)
|
199
196
|
base_bytes = await response.read()
|
200
|
-
self.map_image = Image.open(BytesIO(base_bytes)).convert("RGBA")
|
201
197
|
|
202
198
|
except ClientConnectorError as e:
|
203
199
|
logging.warning("NRCan base map could not be retrieved: %s" % e)
|
204
|
-
|
205
200
|
try:
|
206
201
|
async with ClientSession(raise_for_status=True) as session:
|
207
202
|
response = await session.get(
|
208
203
|
url=backup_map_url, params=basemap_params
|
209
204
|
)
|
210
205
|
base_bytes = await response.read()
|
211
|
-
self.map_image = Image.open(BytesIO(base_bytes)).convert("RGBA")
|
212
206
|
except ClientConnectorError:
|
213
207
|
logging.warning("Mapbox base map could not be retrieved")
|
208
|
+
return None
|
214
209
|
|
215
|
-
return
|
210
|
+
return base_bytes
|
216
211
|
|
217
212
|
async def _get_legend(self):
|
218
213
|
"""Fetch legend image."""
|
@@ -221,13 +216,13 @@ class ECRadar(object):
|
|
221
216
|
layer=precip_layers[self.layer_key], style=legend_style[self.layer_key]
|
222
217
|
)
|
223
218
|
)
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
219
|
+
try:
|
220
|
+
async with ClientSession(raise_for_status=True) as session:
|
221
|
+
response = await session.get(url=geomet_url, params=legend_params)
|
222
|
+
return await response.read()
|
223
|
+
except ClientConnectorError:
|
224
|
+
logging.warning("Legend could not be retrieved")
|
225
|
+
return None
|
231
226
|
|
232
227
|
async def _get_dimensions(self):
|
233
228
|
"""Get time range of available data."""
|
@@ -255,54 +250,73 @@ class ECRadar(object):
|
|
255
250
|
async def _combine_layers(self, radar_bytes, frame_time):
|
256
251
|
"""Add radar overlay to base layer and add timestamp."""
|
257
252
|
|
258
|
-
|
259
|
-
|
260
|
-
# Add transparency to radar
|
261
|
-
|
262
|
-
if self.radar_opacity < 100:
|
263
|
-
alpha = round((self.radar_opacity / 100) * 255)
|
264
|
-
radar_copy = radar.copy()
|
265
|
-
radar_copy.putalpha(alpha)
|
266
|
-
radar.paste(radar_copy, radar)
|
267
|
-
|
268
|
-
# Overlay radar on basemap
|
269
|
-
|
253
|
+
base_bytes = None
|
270
254
|
if not self.map_image:
|
271
|
-
await self._get_basemap()
|
272
|
-
if self.map_image:
|
273
|
-
frame = Image.alpha_composite(self.map_image, radar)
|
274
|
-
else:
|
275
|
-
frame = radar
|
276
|
-
|
277
|
-
# Add legend
|
255
|
+
base_bytes = await self._get_basemap()
|
278
256
|
|
257
|
+
legend_bytes = None
|
279
258
|
if self.show_legend:
|
280
259
|
if not self.legend_image or self.legend_layer != self.layer_key:
|
281
|
-
await self._get_legend()
|
282
|
-
frame.paste(self.legend_image, self.legend_position)
|
260
|
+
legend_bytes = await self._get_legend()
|
283
261
|
|
284
|
-
#
|
262
|
+
# All the synchronous PIL stuff here
|
263
|
+
def _create_image():
|
264
|
+
radar = Image.open(BytesIO(radar_bytes)).convert("RGBA")
|
285
265
|
|
286
|
-
|
287
|
-
|
288
|
-
timestamp_label[self.layer_key][self.language]
|
289
|
-
+ " @ "
|
290
|
-
+ frame_time.astimezone().strftime("%H:%M")
|
291
|
-
)
|
292
|
-
text_box = Image.new("RGBA", self.font.getbbox(timestamp)[2:], "white")
|
293
|
-
box_draw = ImageDraw.Draw(text_box)
|
294
|
-
box_draw.text(xy=(0, 0), text=timestamp, fill=(0, 0, 0), font=self.font)
|
295
|
-
double_box = text_box.resize((text_box.width * 2, text_box.height * 2))
|
296
|
-
frame.paste(double_box)
|
297
|
-
frame = frame.quantize()
|
298
|
-
|
299
|
-
# Return frame as PNG bytes
|
300
|
-
|
301
|
-
img_byte_arr = BytesIO()
|
302
|
-
frame.save(img_byte_arr, format="PNG")
|
303
|
-
frame_bytes = img_byte_arr.getvalue()
|
266
|
+
if base_bytes:
|
267
|
+
self.map_image = Image.open(BytesIO(base_bytes)).convert("RGBA")
|
304
268
|
|
305
|
-
|
269
|
+
if legend_bytes:
|
270
|
+
self.legend_image = Image.open(BytesIO(legend_bytes)).convert("RGB")
|
271
|
+
legend_width = self.legend_image.size[0]
|
272
|
+
self.legend_position = (self.width - legend_width, 0)
|
273
|
+
self.legend_layer = self.layer_key
|
274
|
+
|
275
|
+
# Add transparency to radar
|
276
|
+
if self.radar_opacity < 100:
|
277
|
+
alpha = round((self.radar_opacity / 100) * 255)
|
278
|
+
radar_copy = radar.copy()
|
279
|
+
radar_copy.putalpha(alpha)
|
280
|
+
radar.paste(radar_copy, radar)
|
281
|
+
|
282
|
+
if self.show_timestamp and not self.font:
|
283
|
+
self.font = ImageFont.load(
|
284
|
+
os.path.join(os.path.dirname(__file__), "10x20.pil")
|
285
|
+
)
|
286
|
+
|
287
|
+
# Overlay radar on basemap
|
288
|
+
if self.map_image:
|
289
|
+
frame = Image.alpha_composite(self.map_image, radar)
|
290
|
+
else:
|
291
|
+
frame = radar
|
292
|
+
|
293
|
+
# Add legend
|
294
|
+
if self.show_legend and self.legend_image:
|
295
|
+
frame.paste(self.legend_image, self.legend_position)
|
296
|
+
|
297
|
+
# Add timestamp
|
298
|
+
if self.show_timestamp and self.font:
|
299
|
+
timestamp = (
|
300
|
+
timestamp_label[self.layer_key][self.language]
|
301
|
+
+ " @ "
|
302
|
+
+ frame_time.astimezone().strftime("%H:%M")
|
303
|
+
)
|
304
|
+
text_box = Image.new("RGBA", self.font.getbbox(timestamp)[2:], "white")
|
305
|
+
box_draw = ImageDraw.Draw(text_box)
|
306
|
+
box_draw.text(xy=(0, 0), text=timestamp, fill=(0, 0, 0), font=self.font)
|
307
|
+
double_box = text_box.resize((text_box.width * 2, text_box.height * 2))
|
308
|
+
frame.paste(double_box)
|
309
|
+
frame = frame.quantize()
|
310
|
+
|
311
|
+
# Return frame as PNG bytes
|
312
|
+
img_byte_arr = BytesIO()
|
313
|
+
frame.save(img_byte_arr, format="PNG")
|
314
|
+
frame_bytes = img_byte_arr.getvalue()
|
315
|
+
|
316
|
+
return frame_bytes
|
317
|
+
|
318
|
+
# Since PIL is synchronous, run it all in another thread
|
319
|
+
return await asyncio.get_event_loop().run_in_executor(None, _create_image)
|
306
320
|
|
307
321
|
async def _get_radar_image(self, session, frame_time):
|
308
322
|
params = dict(
|
@@ -331,6 +345,17 @@ class ECRadar(object):
|
|
331
345
|
async def get_loop(self, fps=5):
|
332
346
|
"""Build an animated GIF of recent radar images."""
|
333
347
|
|
348
|
+
def build_image():
|
349
|
+
gif_frames = [imageio.imread(f, mode="RGBA") for f in frames]
|
350
|
+
gif_bytes = imageio.mimwrite(
|
351
|
+
imageio.RETURN_BYTES,
|
352
|
+
gif_frames,
|
353
|
+
format="GIF",
|
354
|
+
duration=duration,
|
355
|
+
subrectangles=True,
|
356
|
+
)
|
357
|
+
return gif_bytes
|
358
|
+
|
334
359
|
"""Build list of frame timestamps."""
|
335
360
|
start, end = await self._get_dimensions()
|
336
361
|
frame_times = [start]
|
@@ -361,12 +386,6 @@ class ECRadar(object):
|
|
361
386
|
"""Assemble animated GIF."""
|
362
387
|
duration = 1000 / fps
|
363
388
|
|
364
|
-
|
365
|
-
gif_bytes =
|
366
|
-
imageio.RETURN_BYTES,
|
367
|
-
gif_frames,
|
368
|
-
format="GIF",
|
369
|
-
duration=duration,
|
370
|
-
subrectangles=True,
|
371
|
-
)
|
389
|
+
loop = asyncio.get_running_loop()
|
390
|
+
gif_bytes = await loop.run_in_executor(None, build_image)
|
372
391
|
return gif_bytes
|
env_canada/ec_weather.py
CHANGED
@@ -137,24 +137,6 @@ conditions_meta = {
|
|
137
137
|
"english": "Icon Code",
|
138
138
|
"french": "Code icône",
|
139
139
|
},
|
140
|
-
"high_temp_yesterday": {
|
141
|
-
"xpath": './yesterdayConditions/temperature[@class="high"]',
|
142
|
-
"type": "float",
|
143
|
-
"english": "High Temperature Yesterday",
|
144
|
-
"french": "Haute température d'hier",
|
145
|
-
},
|
146
|
-
"low_temp_yesterday": {
|
147
|
-
"xpath": './yesterdayConditions/temperature[@class="low"]',
|
148
|
-
"type": "float",
|
149
|
-
"english": "Low Temperature Yesterday",
|
150
|
-
"french": "Basse température d'hier",
|
151
|
-
},
|
152
|
-
"precip_yesterday": {
|
153
|
-
"xpath": "./yesterdayConditions/precip",
|
154
|
-
"type": "float",
|
155
|
-
"english": "Precipitation Yesterday",
|
156
|
-
"french": "Précipitation d'hier",
|
157
|
-
},
|
158
140
|
"normal_high": {
|
159
141
|
"xpath": './forecastGroup/regionalNormals/temperature[@class="high"]',
|
160
142
|
"type": "int",
|
@@ -183,7 +165,7 @@ conditions_meta = {
|
|
183
165
|
"xpath": "./currentConditions/dateTime/timeStamp",
|
184
166
|
"type": "timestamp",
|
185
167
|
"english": "Observation Time",
|
186
|
-
"french": "Temps d'observation"
|
168
|
+
"french": "Temps d'observation",
|
187
169
|
},
|
188
170
|
}
|
189
171
|
|
@@ -278,7 +260,6 @@ def closest_site(site_list, lat, lon):
|
|
278
260
|
|
279
261
|
|
280
262
|
class ECWeather(object):
|
281
|
-
|
282
263
|
"""Get weather data from Environment Canada."""
|
283
264
|
|
284
265
|
def __init__(self, **kwargs):
|
@@ -432,7 +413,9 @@ class ECWeather(object):
|
|
432
413
|
self.conditions[c].update(get_condition(meta))
|
433
414
|
|
434
415
|
# Update station metadata
|
435
|
-
self.metadata["station"] = weather_tree.find(
|
416
|
+
self.metadata["station"] = weather_tree.find(
|
417
|
+
"./currentConditions/station"
|
418
|
+
).text
|
436
419
|
|
437
420
|
# Update text summary
|
438
421
|
period = get_condition(summary_meta["forecast_period"])["value"]
|
@@ -494,7 +477,9 @@ class ECWeather(object):
|
|
494
477
|
"temperature": int(f.findtext("./temperature") or 0),
|
495
478
|
"icon_code": f.findtext("./iconCode"),
|
496
479
|
"precip_probability": int(f.findtext("./lop") or "0"),
|
497
|
-
"wind_speed": int(
|
480
|
+
"wind_speed": int(
|
481
|
+
wind_speed_text if wind_speed_text.isnumeric() else 0
|
482
|
+
),
|
498
483
|
"wind_direction": f.findtext("./wind/direction"),
|
499
484
|
}
|
500
485
|
)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
|
-
Name:
|
3
|
-
Version: 0.
|
2
|
+
Name: env_canada
|
3
|
+
Version: 0.7.0
|
4
4
|
Summary: A package to access meteorological data from Environment Canada
|
5
5
|
Home-page: https://github.com/michaeldavie/env_canada
|
6
6
|
Author: Michael Davie
|
@@ -25,7 +25,7 @@ Requires-Dist: voluptuous
|
|
25
25
|
# Environment Canada (env_canada)
|
26
26
|
|
27
27
|
[](https://badge.fury.io/py/env-canada)
|
28
|
-
[](https://snyk.io/vuln/pip:env-canada@0.
|
28
|
+
[](https://snyk.io/vuln/pip:env-canada@0.7.0?utm_source=badge)
|
29
29
|
|
30
30
|
This package provides access to various data sources published by [Environment and Climate Change Canada](https://www.canada.ca/en/environment-climate-change.html).
|
31
31
|
|
@@ -116,7 +116,8 @@ Once updated asynchronously, historical weather data is contained with the `stat
|
|
116
116
|
```python
|
117
117
|
import asyncio
|
118
118
|
|
119
|
-
from env_canada import ECHistorical
|
119
|
+
from env_canada import ECHistorical
|
120
|
+
from env_canada.ec_historical import get_historical_stations
|
120
121
|
|
121
122
|
# search for stations, response contains station_ids
|
122
123
|
coordinates = [53.916944, -122.749444] # [lat, long]
|
@@ -165,7 +166,8 @@ For example :
|
|
165
166
|
```python
|
166
167
|
import pandas as pd
|
167
168
|
import asyncio
|
168
|
-
from env_canada import ECHistoricalRange
|
169
|
+
from env_canada import ECHistoricalRange
|
170
|
+
from env_canada.ec_historical import get_historical_stations
|
169
171
|
from datetime import datetime
|
170
172
|
|
171
173
|
coordinates = ['48.508333', '-68.467667']
|
@@ -205,4 +207,4 @@ of `datetime(2022, 7, 1, 12, 12)`
|
|
205
207
|
|
206
208
|
# License
|
207
209
|
|
208
|
-
The code is available under terms of [MIT License](LICENSE.md)
|
210
|
+
The code is available under terms of [MIT License](https://github.com/michaeldavie/env_canada/tree/master/LICENSE.md)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
env_canada/10x20.pbm,sha256=ClKTs2WUmhUhTHAQzPuGwPTICGVBzCvos5l-vHRBE5M,2463
|
2
|
+
env_canada/10x20.pil,sha256=Oki6-TD7b0xFtfm6vxCKsmpEpsZ5Jaia_0v_aDz8bfE,5143
|
3
|
+
env_canada/__init__.py,sha256=wEx1BCwVUH__GoosSlhNMHuUKCKNZAvv5uuSa5ZWq_g,187
|
4
|
+
env_canada/constants.py,sha256=xUcfT4fpHqhAnvC1GZ_gpYYrJYozfitwFiOBKN65xXg,32
|
5
|
+
env_canada/ec_aqhi.py,sha256=kJQ8xEgFnujGMYdxRXpoEK17B5e-ya-Y7rK0vLo_-w0,7768
|
6
|
+
env_canada/ec_cache.py,sha256=qoFxmO-kOBT8jhgPeNWtVBRmguXcARIIOI54OaDh-20,1171
|
7
|
+
env_canada/ec_exc.py,sha256=SBJwzmLf94lTx7KYVLfQYrMXYNYUoIxeVXc-BLkuXoE,67
|
8
|
+
env_canada/ec_historical.py,sha256=slHaFwsoyW16uCVtE3_-IF3_BFhFD4IuWl7rpIRsCm4,15901
|
9
|
+
env_canada/ec_hydro.py,sha256=LBsWreTlaTKec6ObjI0ih8-zOKBNjD02oiXKTyUa1EQ,4898
|
10
|
+
env_canada/ec_radar.py,sha256=gcLa2z5T_CkrY-NLEJRqaLDHODJRcO5unW5MGxjKxF8,13115
|
11
|
+
env_canada/ec_weather.py,sha256=M7nPeZIKLirRIcCENB8z2B8aBDZHrjltzMYPgRz9lz0,16789
|
12
|
+
env_canada-0.7.0.dist-info/LICENSE,sha256=c037dTHQWAgRgDqZNN-5d-CZvcteSYN37u39SNklO0I,1072
|
13
|
+
env_canada-0.7.0.dist-info/METADATA,sha256=IueyWyN-i8vtUD8G4p8kMumCncfOUyPUrUS_TMEfw7w,10707
|
14
|
+
env_canada-0.7.0.dist-info/WHEEL,sha256=cpQTJ5IWu9CdaPViMhC9YzF8gZuS5-vlfoFihTBC86A,91
|
15
|
+
env_canada-0.7.0.dist-info/top_level.txt,sha256=fw7Pcl9ULBXYvqnAdyBdmwPXW8GSRFmhO0sLZWVfOCc,11
|
16
|
+
env_canada-0.7.0.dist-info/RECORD,,
|
@@ -1,16 +0,0 @@
|
|
1
|
-
env_canada/10x20.pbm,sha256=ClKTs2WUmhUhTHAQzPuGwPTICGVBzCvos5l-vHRBE5M,2463
|
2
|
-
env_canada/10x20.pil,sha256=Oki6-TD7b0xFtfm6vxCKsmpEpsZ5Jaia_0v_aDz8bfE,5143
|
3
|
-
env_canada/__init__.py,sha256=wQpbGVmwROrIR06RWinatD-qX7fbJvxBFOd6U0xoek4,126
|
4
|
-
env_canada/constants.py,sha256=IsaGrjrg-YGadCHWyT0ZkBjbTDaF14W1gBPGQylncUY,32
|
5
|
-
env_canada/ec_aqhi.py,sha256=kJQ8xEgFnujGMYdxRXpoEK17B5e-ya-Y7rK0vLo_-w0,7768
|
6
|
-
env_canada/ec_cache.py,sha256=qoFxmO-kOBT8jhgPeNWtVBRmguXcARIIOI54OaDh-20,1171
|
7
|
-
env_canada/ec_exc.py,sha256=SBJwzmLf94lTx7KYVLfQYrMXYNYUoIxeVXc-BLkuXoE,67
|
8
|
-
env_canada/ec_historical.py,sha256=slHaFwsoyW16uCVtE3_-IF3_BFhFD4IuWl7rpIRsCm4,15901
|
9
|
-
env_canada/ec_hydro.py,sha256=LBsWreTlaTKec6ObjI0ih8-zOKBNjD02oiXKTyUa1EQ,4898
|
10
|
-
env_canada/ec_radar.py,sha256=WaJHtNnuQa3XoidvymOjJxNHqRLORTcy6_mczjIwd_A,12217
|
11
|
-
env_canada/ec_weather.py,sha256=uBY6qd0-hVyZDhqPcpipfMDImXpJGiNIzMOjIzqNBfo,17358
|
12
|
-
env_canada-0.6.1.dist-info/LICENSE,sha256=c037dTHQWAgRgDqZNN-5d-CZvcteSYN37u39SNklO0I,1072
|
13
|
-
env_canada-0.6.1.dist-info/METADATA,sha256=yy2_dg0TKwYxaIwyc1UfBHiuIQpt3hyETSkzvnRPGU0,10580
|
14
|
-
env_canada-0.6.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
15
|
-
env_canada-0.6.1.dist-info/top_level.txt,sha256=fw7Pcl9ULBXYvqnAdyBdmwPXW8GSRFmhO0sLZWVfOCc,11
|
16
|
-
env_canada-0.6.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|