env-canada 0.7.0__py3-none-any.whl → 0.7.1__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 CHANGED
@@ -1 +1 @@
1
- USER_AGENT = "env_canada/0.7.0"
1
+ USER_AGENT = "env_canada/0.7.1"
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
- timestamp_to_datetime(f.attrib["UTCTime"])
219
- ] = int(f.text or 0)
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 aiohttp import ClientSession
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
- class CacheClientSession(ClientSession):
10
- """Shim to cache ClientSession requests."""
7
+ @classmethod
8
+ def add(cls, cache_key, item, cache_time):
9
+ """Add an entry to the cache."""
11
10
 
12
- _cache = {}
11
+ cls._cache[cache_key] = (datetime.now() + cache_time, item)
12
+ return item # Returning item useful for chaining calls
13
13
 
14
- def _flush_cache(self):
15
- """Flush expired cache entries."""
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 self._cache.items() if value[0] < now]
20
+ expired = [key for key, value in cls._cache.items() if value[0] < now]
19
21
  for key in expired:
20
- del self._cache[key]
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
- return result[1]
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 datetime
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 .ec_cache import CacheClientSession as ClientSession
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 compute_bounding_box(distance, latittude, longitude):
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
- # Set precipitation type
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 = compute_bounding_box(kwargs["radius"], *kwargs["coordinates"])
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
- self.font = None
166
+
167
+ self._font = None
166
168
 
167
169
  @property
168
170
  def precip_type(self):
169
- return self._precip_setting
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._precip_setting = user_input
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
- basemap_params.update(self.map_params)
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
- except ClientConnectorError as e:
199
- logging.warning("NRCan base map could not be retrieved: %s" % e)
191
+ basemap_params.update(self.map_params)
192
+ for map_url in [basemap_url, backup_map_url]:
200
193
  try:
201
- async with ClientSession(raise_for_status=True) as session:
202
- response = await session.get(
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
- return base_bytes
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.layer_key], style=legend_style[self.layer_key]
209
+ layer=precip_layers[self._precip_type_actual],
210
+ style=legend_style[self._precip_type_actual],
217
211
  )
218
212
  )
219
213
  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()
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 data."""
229
- capabilities_params["layer"] = precip_layers[self.layer_key]
230
-
231
- async with ClientSession(raise_for_status=True) as session:
232
- response = await session.get(
233
- url=geomet_url,
234
- params=capabilities_params,
235
- cache_time=datetime.timedelta(minutes=5),
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 = await response.text()
231
+ Cache.add(capabilities_cache_key, capabilities_xml, timedelta(minutes=5))
238
232
 
239
- capabilities_tree = et.fromstring(capabilities_xml)
240
- dimension_string = capabilities_tree.find(
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
- ).text
244
- start, end = [
245
- dateutil.parser.isoparse(t) for t in dimension_string.split("/")[:2]
246
- ]
247
- self.timestamp = end.isoformat()
248
- return start, end
249
-
250
- async def _combine_layers(self, radar_bytes, frame_time):
251
- """Add radar overlay to base layer and add timestamp."""
252
-
253
- base_bytes = None
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
- radar = Image.open(BytesIO(radar_bytes)).convert("RGBA")
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
- self.map_image = Image.open(BytesIO(base_bytes)).convert("RGBA")
254
+ map_image = Image.open(BytesIO(base_bytes)).convert("RGBA")
268
255
 
269
256
  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
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 = radar.copy()
266
+ radar_copy = radar_image.copy()
279
267
  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
- )
268
+ radar_image.paste(radar_copy, radar_image)
286
269
 
287
270
  # Overlay radar on basemap
288
- if self.map_image:
289
- frame = Image.alpha_composite(self.map_image, radar)
271
+ if map_image:
272
+ frame = Image.alpha_composite(map_image, radar_image)
290
273
  else:
291
- frame = radar
274
+ frame = radar_image
292
275
 
293
276
  # Add legend
294
- if self.show_legend and self.legend_image:
295
- frame.paste(self.legend_image, self.legend_position)
277
+ if legend_image:
278
+ frame.paste(legend_image, legend_position)
296
279
 
297
280
  # 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
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
- return frame_bytes
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
- # Since PIL is synchronous, run it all in another thread
319
- return await asyncio.get_event_loop().run_in_executor(None, _create_image)
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.layer_key],
326
- time=frame_time.strftime("%Y-%m-%dT%H:%M:00Z")
323
+ layers=precip_layers[self._precip_type_actual],
324
+ time=time,
327
325
  )
328
- response = await session.get(url=geomet_url, params=params)
329
- return await response.read()
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
- latest = dimensions[1]
335
- async with ClientSession(raise_for_status=True) as session:
336
- frame = await self._get_radar_image(session=session, frame_time=latest)
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 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,
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
- subrectangles=True,
353
+ loop=0,
356
354
  )
357
- return gif_bytes
355
+ return gif.getvalue()
358
356
 
359
- """Build list of frame timestamps."""
360
- start, end = await self._get_dimensions()
361
- frame_times = [start]
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
- while True:
364
- next_frame = frame_times[-1] + datetime.timedelta(minutes=radar_interval)
365
- if next_frame > end:
366
- break
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
- async with ClientSession(raise_for_status=True) as session:
374
- for t in frame_times:
375
- tasks.append(self._get_radar_image(session=session, frame_time=t))
376
- radar_layers = await asyncio.gather(*tasks)
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
- """Assemble animated GIF."""
387
- duration = 1000 / fps
374
+ for _ in range(3):
375
+ radar_layers.append(radar_layers[-1])
388
376
 
389
- loop = asyncio.get_running_loop()
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: env_canada
3
- Version: 0.7.0
3
+ Version: 0.7.1
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
  [![PyPI version](https://badge.fury.io/py/env-canada.svg)](https://badge.fury.io/py/env-canada)
28
- [![Snyk rating](https://snyk-widget.herokuapp.com/badge/pip/env-canada/badge.svg)](https://snyk.io/vuln/pip:env-canada@0.7.0?utm_source=badge)
28
+ [![Snyk rating](https://snyk-widget.herokuapp.com/badge/pip/env-canada/badge.svg)](https://snyk.io/vuln/pip:env-canada@0.7.1?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=6oDJe86s1hRPyA0pj_MK0S_HHM8kCXd_RuHMWu0fh3E,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=M7nPeZIKLirRIcCENB8z2B8aBDZHrjltzMYPgRz9lz0,16789
12
+ env_canada-0.7.1.dist-info/LICENSE,sha256=c037dTHQWAgRgDqZNN-5d-CZvcteSYN37u39SNklO0I,1072
13
+ env_canada-0.7.1.dist-info/METADATA,sha256=y5JONQes6DXUWQlw4zN-XL4TjmOBdYvlVmuoxT4M0B8,10707
14
+ env_canada-0.7.1.dist-info/WHEEL,sha256=mguMlWGMX-VHnMpKOjjQidIo1ssRlCFu4a4mBpz1s2M,91
15
+ env_canada-0.7.1.dist-info/top_level.txt,sha256=fw7Pcl9ULBXYvqnAdyBdmwPXW8GSRFmhO0sLZWVfOCc,11
16
+ env_canada-0.7.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (70.1.0)
2
+ Generator: setuptools (70.1.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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,,