env-canada 0.7.0__py3-none-any.whl → 0.7.2__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/constants.py +1 -1
- env_canada/ec_aqhi.py +5 -6
- env_canada/ec_cache.py +16 -29
- env_canada/ec_radar.py +156 -170
- env_canada/ec_weather.py +4 -0
- {env_canada-0.7.0.dist-info → env_canada-0.7.2.dist-info}/METADATA +2 -2
- env_canada-0.7.2.dist-info/RECORD +16 -0
- {env_canada-0.7.0.dist-info → env_canada-0.7.2.dist-info}/WHEEL +1 -1
- env_canada-0.7.0.dist-info/RECORD +0 -16
- {env_canada-0.7.0.dist-info → env_canada-0.7.2.dist-info}/LICENSE +0 -0
- {env_canada-0.7.0.dist-info → env_canada-0.7.2.dist-info}/top_level.txt +0 -0
env_canada/constants.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
USER_AGENT = "env_canada/0.7.
|
1
|
+
USER_AGENT = "env_canada/0.7.2"
|
env_canada/ec_aqhi.py
CHANGED
@@ -84,7 +84,6 @@ async def find_closest_region(language, lat, lon):
|
|
84
84
|
|
85
85
|
|
86
86
|
class ECAirQuality(object):
|
87
|
-
|
88
87
|
"""Get air quality data from Environment Canada."""
|
89
88
|
|
90
89
|
def __init__(self, **kwargs):
|
@@ -172,7 +171,7 @@ class ECAirQuality(object):
|
|
172
171
|
# Fetch current measurement
|
173
172
|
aqhi_current = await self.get_aqhi_data(url=AQHI_OBSERVATION_URL)
|
174
173
|
|
175
|
-
if aqhi_current:
|
174
|
+
if aqhi_current is not None:
|
176
175
|
# Update region name
|
177
176
|
element = aqhi_current.find("region")
|
178
177
|
self.region_name = element.attrib[
|
@@ -202,7 +201,7 @@ class ECAirQuality(object):
|
|
202
201
|
# Update AQHI forecasts
|
203
202
|
aqhi_forecast = await self.get_aqhi_data(url=AQHI_FORECAST_URL)
|
204
203
|
|
205
|
-
if aqhi_forecast:
|
204
|
+
if aqhi_forecast is not None:
|
206
205
|
# Update AQHI daily forecasts
|
207
206
|
for f in aqhi_forecast.findall("./forecastGroup/forecast"):
|
208
207
|
for p in f.findall("./period"):
|
@@ -214,6 +213,6 @@ class ECAirQuality(object):
|
|
214
213
|
|
215
214
|
# Update AQHI hourly forecasts
|
216
215
|
for f in aqhi_forecast.findall("./hourlyForecastGroup/hourlyForecast"):
|
217
|
-
self.forecasts["hourly"][
|
218
|
-
|
219
|
-
|
216
|
+
self.forecasts["hourly"][timestamp_to_datetime(f.attrib["UTCTime"])] = (
|
217
|
+
int(f.text or 0)
|
218
|
+
)
|
env_canada/ec_cache.py
CHANGED
@@ -1,38 +1,25 @@
|
|
1
|
-
from
|
2
|
-
from datetime import datetime, timedelta
|
1
|
+
from datetime import datetime
|
3
2
|
|
4
|
-
from .constants import USER_AGENT
|
5
|
-
|
6
|
-
CACHE_EXPIRE_TIME = timedelta(minutes=200) # Time is tuned for 3h radar image
|
7
3
|
|
4
|
+
class Cache:
|
5
|
+
_cache = {}
|
8
6
|
|
9
|
-
|
10
|
-
|
7
|
+
@classmethod
|
8
|
+
def add(cls, cache_key, item, cache_time):
|
9
|
+
"""Add an entry to the cache."""
|
11
10
|
|
12
|
-
|
11
|
+
cls._cache[cache_key] = (datetime.now() + cache_time, item)
|
12
|
+
return item # Returning item useful for chaining calls
|
13
13
|
|
14
|
-
|
15
|
-
|
14
|
+
@classmethod
|
15
|
+
def get(cls, cache_key):
|
16
|
+
"""Get an entry from the cache."""
|
16
17
|
|
18
|
+
# Delete expired entries at start so we don't use expired entries
|
17
19
|
now = datetime.now()
|
18
|
-
expired = [key for key, value in
|
20
|
+
expired = [key for key, value in cls._cache.items() if value[0] < now]
|
19
21
|
for key in expired:
|
20
|
-
del
|
21
|
-
|
22
|
-
async def get(self, url, params, cache_time=CACHE_EXPIRE_TIME):
|
23
|
-
"""Thin wrapper around ClientSession.get to cache responses."""
|
24
|
-
|
25
|
-
self._flush_cache() # Flush at start so we don't use expired entries
|
26
|
-
|
27
|
-
cache_key = (url, tuple(sorted(params.items())))
|
28
|
-
result = self._cache.get(cache_key)
|
29
|
-
if not result:
|
30
|
-
result = (
|
31
|
-
datetime.now() + cache_time,
|
32
|
-
await super().get(
|
33
|
-
url=url, params=params, headers={"User-Agent": USER_AGENT}
|
34
|
-
),
|
35
|
-
)
|
36
|
-
self._cache[cache_key] = result
|
22
|
+
del cls._cache[key]
|
37
23
|
|
38
|
-
|
24
|
+
result = cls._cache.get(cache_key)
|
25
|
+
return result[1] if result else None
|
env_canada/ec_radar.py
CHANGED
@@ -1,18 +1,20 @@
|
|
1
1
|
import asyncio
|
2
|
-
import
|
2
|
+
from datetime import date, timedelta
|
3
3
|
import logging
|
4
4
|
import math
|
5
5
|
import os
|
6
6
|
from io import BytesIO
|
7
|
+
from typing import cast
|
7
8
|
|
8
9
|
import dateutil.parser
|
9
10
|
import defusedxml.ElementTree as et
|
10
|
-
import imageio.v2 as imageio
|
11
11
|
import voluptuous as vol
|
12
|
+
from aiohttp import ClientSession
|
12
13
|
from aiohttp.client_exceptions import ClientConnectorError
|
13
14
|
from PIL import Image, ImageDraw, ImageFont
|
14
15
|
|
15
|
-
from .
|
16
|
+
from .constants import USER_AGENT
|
17
|
+
from .ec_cache import Cache
|
16
18
|
|
17
19
|
ATTRIBUTION = {
|
18
20
|
"english": "Data provided by Environment Canada",
|
@@ -69,7 +71,7 @@ legend_params = {
|
|
69
71
|
"sld_version": "1.1.0",
|
70
72
|
"format": "image/png",
|
71
73
|
}
|
72
|
-
radar_interval = 6
|
74
|
+
radar_interval = timedelta(minutes=6)
|
73
75
|
|
74
76
|
timestamp_label = {
|
75
77
|
"rain": {"english": "Rain", "french": "Pluie"},
|
@@ -77,7 +79,7 @@ timestamp_label = {
|
|
77
79
|
}
|
78
80
|
|
79
81
|
|
80
|
-
def
|
82
|
+
def _compute_bounding_box(distance, latittude, longitude):
|
81
83
|
"""
|
82
84
|
Modified from https://gist.github.com/alexcpn/f95ae83a7ee0293a5225
|
83
85
|
"""
|
@@ -102,6 +104,16 @@ def compute_bounding_box(distance, latittude, longitude):
|
|
102
104
|
return lat_min, lon_min, lat_max, lon_max
|
103
105
|
|
104
106
|
|
107
|
+
async def _get_resource(url, params, bytes=True):
|
108
|
+
async with ClientSession(raise_for_status=True) as session:
|
109
|
+
response = await session.get(
|
110
|
+
url=url, params=params, headers={"User-Agent": USER_AGENT}
|
111
|
+
)
|
112
|
+
if bytes:
|
113
|
+
return await response.read()
|
114
|
+
return await response.text()
|
115
|
+
|
116
|
+
|
105
117
|
class ECRadar(object):
|
106
118
|
def __init__(self, **kwargs):
|
107
119
|
"""Initialize the radar object."""
|
@@ -133,259 +145,233 @@ class ECRadar(object):
|
|
133
145
|
self.language = kwargs["language"]
|
134
146
|
self.metadata = {"attribution": ATTRIBUTION[self.language]}
|
135
147
|
|
136
|
-
|
137
|
-
|
138
|
-
if "precip_type" in kwargs and kwargs["precip_type"] is not None:
|
139
|
-
self.precip_type = kwargs["precip_type"]
|
140
|
-
else:
|
141
|
-
self.precip_type = "auto"
|
148
|
+
self._precip_type_setting = kwargs.get("precip_type")
|
149
|
+
self._precip_type_actual = self.precip_type[1]
|
142
150
|
|
143
151
|
# Get map parameters
|
144
|
-
|
145
152
|
self.image = None
|
146
153
|
self.width = kwargs["width"]
|
147
154
|
self.height = kwargs["height"]
|
148
|
-
self.bbox =
|
155
|
+
self.bbox = _compute_bounding_box(kwargs["radius"], *kwargs["coordinates"])
|
149
156
|
self.map_params = {
|
150
157
|
"bbox": ",".join([str(coord) for coord in self.bbox]),
|
151
158
|
"width": self.width,
|
152
159
|
"height": self.height,
|
153
160
|
}
|
154
|
-
self.map_image = None
|
155
161
|
self.radar_opacity = kwargs["radar_opacity"]
|
156
162
|
|
157
163
|
# Get overlay parameters
|
158
|
-
|
159
164
|
self.show_legend = kwargs["legend"]
|
160
|
-
self.legend_layer = None
|
161
|
-
self.legend_image = None
|
162
|
-
self.legend_position = None
|
163
|
-
|
164
165
|
self.show_timestamp = kwargs["timestamp"]
|
165
|
-
|
166
|
+
|
167
|
+
self._font = None
|
166
168
|
|
167
169
|
@property
|
168
170
|
def precip_type(self):
|
169
|
-
|
171
|
+
# NOTE: this is a breaking change for this lib; HA doesn't use this so not breaking for that
|
172
|
+
if self._precip_type_setting in ["rain", "snow"]:
|
173
|
+
return (self._precip_type_setting, self._precip_type_setting)
|
174
|
+
self._precip_type_actual = (
|
175
|
+
"rain" if date.today().month in range(4, 11) else "snow"
|
176
|
+
)
|
177
|
+
return ("auto", self._precip_type_actual)
|
170
178
|
|
171
179
|
@precip_type.setter
|
172
180
|
def precip_type(self, user_input):
|
173
181
|
if user_input not in ["rain", "snow", "auto"]:
|
174
182
|
raise ValueError("precip_type must be 'rain', 'snow', or 'auto'")
|
175
|
-
|
176
|
-
self.
|
177
|
-
|
178
|
-
if self._precip_setting in ["rain", "snow"]:
|
179
|
-
self.layer_key = self._precip_setting
|
180
|
-
else:
|
181
|
-
self._auto_precip_type()
|
182
|
-
|
183
|
-
def _auto_precip_type(self):
|
184
|
-
if datetime.date.today().month in range(4, 11):
|
185
|
-
self.layer_key = "rain"
|
186
|
-
else:
|
187
|
-
self.layer_key = "snow"
|
183
|
+
self._precip_type_setting = user_input
|
184
|
+
self._precip_type_actual = self.precip_type[1]
|
188
185
|
|
189
186
|
async def _get_basemap(self):
|
190
187
|
"""Fetch the background map image."""
|
191
|
-
|
192
|
-
|
193
|
-
try:
|
194
|
-
async with ClientSession(raise_for_status=True) as session:
|
195
|
-
response = await session.get(url=basemap_url, params=basemap_params)
|
196
|
-
base_bytes = await response.read()
|
188
|
+
if base_bytes := Cache.get("basemap"):
|
189
|
+
return base_bytes
|
197
190
|
|
198
|
-
|
199
|
-
|
191
|
+
basemap_params.update(self.map_params)
|
192
|
+
for map_url in [basemap_url, backup_map_url]:
|
200
193
|
try:
|
201
|
-
|
202
|
-
|
203
|
-
url=backup_map_url, params=basemap_params
|
204
|
-
)
|
205
|
-
base_bytes = await response.read()
|
206
|
-
except ClientConnectorError:
|
207
|
-
logging.warning("Mapbox base map could not be retrieved")
|
208
|
-
return None
|
194
|
+
base_bytes = await _get_resource(map_url, basemap_params)
|
195
|
+
return Cache.add("basemap", base_bytes, timedelta(days=7))
|
209
196
|
|
210
|
-
|
197
|
+
except ClientConnectorError as e:
|
198
|
+
logging.warning("Map from %s could not be retrieved: %s" % map_url, e)
|
211
199
|
|
212
200
|
async def _get_legend(self):
|
213
201
|
"""Fetch legend image."""
|
202
|
+
|
203
|
+
legend_cache_key = f"legend-{self._precip_type_actual}"
|
204
|
+
if legend := Cache.get(legend_cache_key):
|
205
|
+
return legend
|
206
|
+
|
214
207
|
legend_params.update(
|
215
208
|
dict(
|
216
|
-
layer=precip_layers[self.
|
209
|
+
layer=precip_layers[self._precip_type_actual],
|
210
|
+
style=legend_style[self._precip_type_actual],
|
217
211
|
)
|
218
212
|
)
|
219
213
|
try:
|
220
|
-
|
221
|
-
|
222
|
-
|
214
|
+
legend = await _get_resource(geomet_url, legend_params)
|
215
|
+
return Cache.add(legend_cache_key, legend, timedelta(days=7))
|
216
|
+
|
223
217
|
except ClientConnectorError:
|
224
218
|
logging.warning("Legend could not be retrieved")
|
225
219
|
return None
|
226
220
|
|
227
221
|
async def _get_dimensions(self):
|
228
|
-
"""Get time range of available
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
222
|
+
"""Get time range of available radar images."""
|
223
|
+
|
224
|
+
capabilities_cache_key = f"capabilities-{self._precip_type_actual}"
|
225
|
+
|
226
|
+
if not (capabilities_xml := Cache.get(capabilities_cache_key)):
|
227
|
+
capabilities_params["layer"] = precip_layers[self._precip_type_actual]
|
228
|
+
capabilities_xml = await _get_resource(
|
229
|
+
geomet_url, capabilities_params, bytes=False
|
236
230
|
)
|
237
|
-
capabilities_xml =
|
231
|
+
Cache.add(capabilities_cache_key, capabilities_xml, timedelta(minutes=5))
|
238
232
|
|
239
|
-
|
240
|
-
|
241
|
-
dimension_xpath.format(layer=precip_layers[self.layer_key]),
|
233
|
+
dimension_string = et.fromstring(capabilities_xml).find(
|
234
|
+
dimension_xpath.format(layer=precip_layers[self._precip_type_actual]),
|
242
235
|
namespaces=wms_namespace,
|
243
|
-
)
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
if not self.map_image:
|
255
|
-
base_bytes = await self._get_basemap()
|
256
|
-
|
257
|
-
legend_bytes = None
|
258
|
-
if self.show_legend:
|
259
|
-
if not self.legend_image or self.legend_layer != self.layer_key:
|
260
|
-
legend_bytes = await self._get_legend()
|
261
|
-
|
262
|
-
# All the synchronous PIL stuff here
|
236
|
+
)
|
237
|
+
if dimension_string is not None:
|
238
|
+
if dimension_string := dimension_string.text:
|
239
|
+
start, end = [
|
240
|
+
dateutil.parser.isoparse(t) for t in dimension_string.split("/")[:2]
|
241
|
+
]
|
242
|
+
self.timestamp = end.isoformat()
|
243
|
+
return (start, end)
|
244
|
+
return None
|
245
|
+
|
246
|
+
async def _get_radar_image(self, frame_time):
|
263
247
|
def _create_image():
|
264
|
-
|
248
|
+
"""Contains all the PIL calls; run in another thread."""
|
265
249
|
|
250
|
+
radar_image = Image.open(BytesIO(cast(bytes, radar_bytes))).convert("RGBA")
|
251
|
+
|
252
|
+
map_image = None
|
266
253
|
if base_bytes:
|
267
|
-
|
254
|
+
map_image = Image.open(BytesIO(base_bytes)).convert("RGBA")
|
268
255
|
|
269
256
|
if legend_bytes:
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
257
|
+
legend_image = Image.open(BytesIO(legend_bytes)).convert("RGB")
|
258
|
+
legend_position = (self.width - legend_image.size[0], 0)
|
259
|
+
else:
|
260
|
+
legend_image = None
|
261
|
+
legend_position = None
|
274
262
|
|
275
263
|
# Add transparency to radar
|
276
264
|
if self.radar_opacity < 100:
|
277
265
|
alpha = round((self.radar_opacity / 100) * 255)
|
278
|
-
radar_copy =
|
266
|
+
radar_copy = radar_image.copy()
|
279
267
|
radar_copy.putalpha(alpha)
|
280
|
-
|
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
|
-
)
|
268
|
+
radar_image.paste(radar_copy, radar_image)
|
286
269
|
|
287
270
|
# Overlay radar on basemap
|
288
|
-
if
|
289
|
-
frame = Image.alpha_composite(
|
271
|
+
if map_image:
|
272
|
+
frame = Image.alpha_composite(map_image, radar_image)
|
290
273
|
else:
|
291
|
-
frame =
|
274
|
+
frame = radar_image
|
292
275
|
|
293
276
|
# Add legend
|
294
|
-
if
|
295
|
-
frame.paste(
|
277
|
+
if legend_image:
|
278
|
+
frame.paste(legend_image, legend_position)
|
296
279
|
|
297
280
|
# Add timestamp
|
298
|
-
if self.show_timestamp
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
281
|
+
if self.show_timestamp:
|
282
|
+
if not self._font:
|
283
|
+
self._font = ImageFont.load(
|
284
|
+
os.path.join(os.path.dirname(__file__), "10x20.pil")
|
285
|
+
)
|
286
|
+
|
287
|
+
if self._font:
|
288
|
+
label = timestamp_label[self._precip_type_actual][self.language]
|
289
|
+
timestamp = f"{label} @ {frame_time.astimezone().strftime('%H:%M')}"
|
290
|
+
text_box = Image.new(
|
291
|
+
"RGBA", self._font.getbbox(timestamp)[2:], "white"
|
292
|
+
)
|
293
|
+
box_draw = ImageDraw.Draw(text_box)
|
294
|
+
box_draw.text(
|
295
|
+
xy=(0, 0), text=timestamp, fill=(0, 0, 0), font=self._font
|
296
|
+
)
|
297
|
+
double_box = text_box.resize(
|
298
|
+
(text_box.width * 2, text_box.height * 2)
|
299
|
+
)
|
300
|
+
frame.paste(double_box)
|
301
|
+
frame = frame.quantize()
|
302
|
+
|
303
|
+
# Convert frame to PNG for return
|
312
304
|
img_byte_arr = BytesIO()
|
313
305
|
frame.save(img_byte_arr, format="PNG")
|
314
|
-
frame_bytes = img_byte_arr.getvalue()
|
315
306
|
|
316
|
-
|
307
|
+
# Time is tuned for 3h radar image
|
308
|
+
return Cache.add(
|
309
|
+
f"radar-{time}", img_byte_arr.getvalue(), timedelta(minutes=200)
|
310
|
+
)
|
317
311
|
|
318
|
-
|
319
|
-
|
312
|
+
time = frame_time.strftime("%Y-%m-%dT%H:%M:00Z")
|
313
|
+
|
314
|
+
if img := Cache.get(f"radar-{time}"):
|
315
|
+
return img
|
316
|
+
|
317
|
+
base_bytes = await self._get_basemap()
|
318
|
+
legend_bytes = await self._get_legend() if self.show_legend else None
|
320
319
|
|
321
|
-
async def _get_radar_image(self, session, frame_time):
|
322
320
|
params = dict(
|
323
321
|
**radar_params,
|
324
322
|
**self.map_params,
|
325
|
-
layers=precip_layers[self.
|
326
|
-
time=
|
323
|
+
layers=precip_layers[self._precip_type_actual],
|
324
|
+
time=time,
|
327
325
|
)
|
328
|
-
|
329
|
-
return await
|
326
|
+
radar_bytes = await _get_resource(geomet_url, params)
|
327
|
+
return await asyncio.get_event_loop().run_in_executor(None, _create_image)
|
330
328
|
|
331
329
|
async def get_latest_frame(self):
|
332
330
|
"""Get the latest image from Environment Canada."""
|
333
331
|
dimensions = await self._get_dimensions()
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
return await self._combine_layers(frame, latest)
|
332
|
+
if not dimensions:
|
333
|
+
return None
|
334
|
+
return await self._get_radar_image(frame_time=dimensions[1])
|
338
335
|
|
339
336
|
async def update(self):
|
340
|
-
if self.precip_type == "auto":
|
341
|
-
self._auto_precip_type()
|
342
|
-
|
343
337
|
self.image = await self.get_loop()
|
344
338
|
|
345
339
|
async def get_loop(self, fps=5):
|
346
340
|
"""Build an animated GIF of recent radar images."""
|
347
341
|
|
348
|
-
def
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
342
|
+
def create_gif():
|
343
|
+
"""Assemble animated GIF."""
|
344
|
+
duration = 1000 / fps
|
345
|
+
imgs = [Image.open(BytesIO(img)).convert("RGBA") for img in radar_layers]
|
346
|
+
gif = BytesIO()
|
347
|
+
imgs[0].save(
|
348
|
+
gif,
|
353
349
|
format="GIF",
|
350
|
+
save_all=True,
|
351
|
+
append_images=imgs[1:],
|
354
352
|
duration=duration,
|
355
|
-
|
353
|
+
loop=0,
|
356
354
|
)
|
357
|
-
return
|
355
|
+
return gif.getvalue()
|
358
356
|
|
359
|
-
|
360
|
-
|
361
|
-
|
357
|
+
# Without this cache priming the tasks below each compete to load map/legend
|
358
|
+
# at the same time, resulting in them getting retrieved for each radar image.
|
359
|
+
await self._get_basemap()
|
360
|
+
await self._get_legend() if self.show_legend else None
|
362
361
|
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
else:
|
368
|
-
frame_times.append(next_frame)
|
369
|
-
|
370
|
-
"""Fetch frames."""
|
362
|
+
timespan = await self._get_dimensions()
|
363
|
+
if not timespan:
|
364
|
+
logging.error("Cannot retrieve radar times.")
|
365
|
+
return None
|
371
366
|
|
372
367
|
tasks = []
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
frames = []
|
379
|
-
|
380
|
-
for i, f in enumerate(radar_layers):
|
381
|
-
frames.append(await self._combine_layers(f, frame_times[i]))
|
382
|
-
|
383
|
-
for f in range(3):
|
384
|
-
frames.append(frames[-1])
|
368
|
+
curr = timespan[0]
|
369
|
+
while curr <= timespan[1]:
|
370
|
+
tasks.append(self._get_radar_image(frame_time=curr))
|
371
|
+
curr = curr + radar_interval
|
372
|
+
radar_layers = await asyncio.gather(*tasks)
|
385
373
|
|
386
|
-
|
387
|
-
|
374
|
+
for _ in range(3):
|
375
|
+
radar_layers.append(radar_layers[-1])
|
388
376
|
|
389
|
-
|
390
|
-
gif_bytes = await loop.run_in_executor(None, build_image)
|
391
|
-
return gif_bytes
|
377
|
+
return await asyncio.get_running_loop().run_in_executor(None, create_gif)
|
env_canada/ec_weather.py
CHANGED
@@ -451,6 +451,7 @@ class ECWeather(object):
|
|
451
451
|
self.hourly_forecasts = []
|
452
452
|
|
453
453
|
# Update daily forecasts
|
454
|
+
forecast_time = self.forecast_time
|
454
455
|
for f in weather_tree.findall("./forecastGroup/forecast"):
|
455
456
|
self.daily_forecasts.append(
|
456
457
|
{
|
@@ -464,8 +465,11 @@ class ECWeather(object):
|
|
464
465
|
"precip_probability": int(
|
465
466
|
f.findtext("./abbreviatedForecast/pop") or "0"
|
466
467
|
),
|
468
|
+
"timestamp": forecast_time,
|
467
469
|
}
|
468
470
|
)
|
471
|
+
if self.daily_forecasts[-1]["temperature_class"] == "low":
|
472
|
+
forecast_time = forecast_time + datetime.timedelta(days=1)
|
469
473
|
|
470
474
|
# Update hourly forecasts
|
471
475
|
for f in weather_tree.findall("./hourlyForecastGroup/hourlyForecast"):
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: env_canada
|
3
|
-
Version: 0.7.
|
3
|
+
Version: 0.7.2
|
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.7.
|
28
|
+
[](https://snyk.io/vuln/pip:env-canada@0.7.2?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
|
|
@@ -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=DlW7o9XEpZNQry99jZ1vF0wX7Xm7ZRcK41n0YAwzp9U,32
|
5
|
+
env_canada/ec_aqhi.py,sha256=zEEt2U8gCxaLlePexl23r9zCfQYgmfhsP0ur2ZiupZc,7793
|
6
|
+
env_canada/ec_cache.py,sha256=xPlXBRLyrD6dTJWLRBy12J8kzBxMUC-20-xRuc56Hts,722
|
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=zh0tbazBbvLpuxrY0yfRm9EIaXNkM6HXPe1us99h4xM,12982
|
11
|
+
env_canada/ec_weather.py,sha256=cLWMTuvASrxYeGnXvGwsNZxd_SfD1sdfInBU51QLFCo,17026
|
12
|
+
env_canada-0.7.2.dist-info/LICENSE,sha256=c037dTHQWAgRgDqZNN-5d-CZvcteSYN37u39SNklO0I,1072
|
13
|
+
env_canada-0.7.2.dist-info/METADATA,sha256=M-fb4_i9Je1a2e6T5M7RIYDW33nknvWN6GHSKg60Sds,10707
|
14
|
+
env_canada-0.7.2.dist-info/WHEEL,sha256=Z4pYXqR_rTB7OWNDYFOm1qRk0RX6GFP2o8LgvP453Hk,91
|
15
|
+
env_canada-0.7.2.dist-info/top_level.txt,sha256=fw7Pcl9ULBXYvqnAdyBdmwPXW8GSRFmhO0sLZWVfOCc,11
|
16
|
+
env_canada-0.7.2.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=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,,
|
File without changes
|
File without changes
|