env-canada 0.6.3__tar.gz → 0.7.1__tar.gz

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.
Files changed (31) hide show
  1. {env_canada-0.6.3/env_canada.egg-info → env_canada-0.7.1}/PKG-INFO +2 -2
  2. {env_canada-0.6.3 → env_canada-0.7.1}/README.md +1 -1
  3. env_canada-0.7.1/env_canada/constants.py +1 -0
  4. {env_canada-0.6.3 → env_canada-0.7.1}/env_canada/ec_aqhi.py +5 -6
  5. env_canada-0.7.1/env_canada/ec_cache.py +25 -0
  6. env_canada-0.7.1/env_canada/ec_radar.py +377 -0
  7. {env_canada-0.6.3 → env_canada-0.7.1}/env_canada/ec_weather.py +7 -22
  8. {env_canada-0.6.3 → env_canada-0.7.1/env_canada.egg-info}/PKG-INFO +2 -2
  9. {env_canada-0.6.3 → env_canada-0.7.1}/env_canada.egg-info/SOURCES.txt +0 -1
  10. {env_canada-0.6.3 → env_canada-0.7.1}/setup.py +1 -1
  11. {env_canada-0.6.3 → env_canada-0.7.1}/tests/test_ec_radar.py +3 -3
  12. env_canada-0.6.3/env_canada/constants.py +0 -1
  13. env_canada-0.6.3/env_canada/ec_cache.py +0 -38
  14. env_canada-0.6.3/env_canada/ec_data.py +0 -501
  15. env_canada-0.6.3/env_canada/ec_radar.py +0 -391
  16. {env_canada-0.6.3 → env_canada-0.7.1}/LICENSE +0 -0
  17. {env_canada-0.6.3 → env_canada-0.7.1}/MANIFEST.in +0 -0
  18. {env_canada-0.6.3 → env_canada-0.7.1}/env_canada/10x20.pbm +0 -0
  19. {env_canada-0.6.3 → env_canada-0.7.1}/env_canada/10x20.pil +0 -0
  20. {env_canada-0.6.3 → env_canada-0.7.1}/env_canada/__init__.py +0 -0
  21. {env_canada-0.6.3 → env_canada-0.7.1}/env_canada/ec_exc.py +0 -0
  22. {env_canada-0.6.3 → env_canada-0.7.1}/env_canada/ec_historical.py +0 -0
  23. {env_canada-0.6.3 → env_canada-0.7.1}/env_canada/ec_hydro.py +0 -0
  24. {env_canada-0.6.3 → env_canada-0.7.1}/env_canada.egg-info/dependency_links.txt +0 -0
  25. {env_canada-0.6.3 → env_canada-0.7.1}/env_canada.egg-info/requires.txt +0 -0
  26. {env_canada-0.6.3 → env_canada-0.7.1}/env_canada.egg-info/top_level.txt +0 -0
  27. {env_canada-0.6.3 → env_canada-0.7.1}/setup.cfg +0 -0
  28. {env_canada-0.6.3 → env_canada-0.7.1}/tests/test_ec_aqhi.py +0 -0
  29. {env_canada-0.6.3 → env_canada-0.7.1}/tests/test_ec_historical.py +0 -0
  30. {env_canada-0.6.3 → env_canada-0.7.1}/tests/test_ec_hydro.py +0 -0
  31. {env_canada-0.6.3 → env_canada-0.7.1}/tests/test_ec_weather.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: env_canada
3
- Version: 0.6.3
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.6.3?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
 
@@ -1,7 +1,7 @@
1
1
  # Environment Canada (env_canada)
2
2
 
3
3
  [![PyPI version](https://badge.fury.io/py/env-canada.svg)](https://badge.fury.io/py/env-canada)
4
- [![Snyk rating](https://snyk-widget.herokuapp.com/badge/pip/env-canada/badge.svg)](https://snyk.io/vuln/pip:env-canada@0.6.3?utm_source=badge)
4
+ [![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)
5
5
 
6
6
  This package provides access to various data sources published by [Environment and Climate Change Canada](https://www.canada.ca/en/environment-climate-change.html).
7
7
 
@@ -0,0 +1 @@
1
+ USER_AGENT = "env_canada/0.7.1"
@@ -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
+ )
@@ -0,0 +1,25 @@
1
+ from datetime import datetime
2
+
3
+
4
+ class Cache:
5
+ _cache = {}
6
+
7
+ @classmethod
8
+ def add(cls, cache_key, item, cache_time):
9
+ """Add an entry to the cache."""
10
+
11
+ cls._cache[cache_key] = (datetime.now() + cache_time, item)
12
+ return item # Returning item useful for chaining calls
13
+
14
+ @classmethod
15
+ def get(cls, cache_key):
16
+ """Get an entry from the cache."""
17
+
18
+ # Delete expired entries at start so we don't use expired entries
19
+ now = datetime.now()
20
+ expired = [key for key, value in cls._cache.items() if value[0] < now]
21
+ for key in expired:
22
+ del cls._cache[key]
23
+
24
+ result = cls._cache.get(cache_key)
25
+ return result[1] if result else None
@@ -0,0 +1,377 @@
1
+ import asyncio
2
+ from datetime import date, timedelta
3
+ import logging
4
+ import math
5
+ import os
6
+ from io import BytesIO
7
+ from typing import cast
8
+
9
+ import dateutil.parser
10
+ import defusedxml.ElementTree as et
11
+ import voluptuous as vol
12
+ from aiohttp import ClientSession
13
+ from aiohttp.client_exceptions import ClientConnectorError
14
+ from PIL import Image, ImageDraw, ImageFont
15
+
16
+ from .constants import USER_AGENT
17
+ from .ec_cache import Cache
18
+
19
+ ATTRIBUTION = {
20
+ "english": "Data provided by Environment Canada",
21
+ "french": "Données fournies par Environnement Canada",
22
+ }
23
+
24
+ __all__ = ["ECRadar"]
25
+
26
+ # Natural Resources Canada
27
+
28
+ basemap_url = "https://maps.geogratis.gc.ca/wms/CBMT"
29
+ basemap_params = {
30
+ "service": "wms",
31
+ "version": "1.3.0",
32
+ "request": "GetMap",
33
+ "layers": "CBMT",
34
+ "styles": "",
35
+ "CRS": "epsg:4326",
36
+ "format": "image/png",
37
+ }
38
+
39
+ # Mapbox Proxy
40
+
41
+ backup_map_url = (
42
+ "https://0wmiyoko9f.execute-api.ca-central-1.amazonaws.com/mapbox-proxy"
43
+ )
44
+
45
+ # Environment Canada
46
+
47
+ precip_layers = {"rain": "RADAR_1KM_RRAI", "snow": "RADAR_1KM_RSNO"}
48
+
49
+ legend_style = {"rain": "RADARURPPRECIPR", "snow": "RADARURPPRECIPS14"}
50
+
51
+ geomet_url = "https://geo.weather.gc.ca/geomet"
52
+ capabilities_params = {
53
+ "lang": "en",
54
+ "service": "WMS",
55
+ "version": "1.3.0",
56
+ "request": "GetCapabilities",
57
+ }
58
+ wms_namespace = {"wms": "http://www.opengis.net/wms"}
59
+ dimension_xpath = './/wms:Layer[wms:Name="{layer}"]/wms:Dimension'
60
+ radar_params = {
61
+ "service": "WMS",
62
+ "version": "1.3.0",
63
+ "request": "GetMap",
64
+ "crs": "EPSG:4326",
65
+ "format": "image/png",
66
+ }
67
+ legend_params = {
68
+ "service": "WMS",
69
+ "version": "1.3.0",
70
+ "request": "GetLegendGraphic",
71
+ "sld_version": "1.1.0",
72
+ "format": "image/png",
73
+ }
74
+ radar_interval = timedelta(minutes=6)
75
+
76
+ timestamp_label = {
77
+ "rain": {"english": "Rain", "french": "Pluie"},
78
+ "snow": {"english": "Snow", "french": "Neige"},
79
+ }
80
+
81
+
82
+ def _compute_bounding_box(distance, latittude, longitude):
83
+ """
84
+ Modified from https://gist.github.com/alexcpn/f95ae83a7ee0293a5225
85
+ """
86
+ latittude = math.radians(latittude)
87
+ longitude = math.radians(longitude)
88
+
89
+ distance_from_point_km = distance
90
+ angular_distance = distance_from_point_km / 6371.01
91
+
92
+ lat_min = latittude - angular_distance
93
+ lat_max = latittude + angular_distance
94
+
95
+ delta_longitude = math.asin(math.sin(angular_distance) / math.cos(latittude))
96
+
97
+ lon_min = longitude - delta_longitude
98
+ lon_max = longitude + delta_longitude
99
+ lon_min = round(math.degrees(lon_min), 5)
100
+ lat_max = round(math.degrees(lat_max), 5)
101
+ lon_max = round(math.degrees(lon_max), 5)
102
+ lat_min = round(math.degrees(lat_min), 5)
103
+
104
+ return lat_min, lon_min, lat_max, lon_max
105
+
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
+
117
+ class ECRadar(object):
118
+ def __init__(self, **kwargs):
119
+ """Initialize the radar object."""
120
+
121
+ init_schema = vol.Schema(
122
+ {
123
+ vol.Required("coordinates"): (
124
+ vol.All(vol.Or(int, float), vol.Range(-90, 90)),
125
+ vol.All(vol.Or(int, float), vol.Range(-180, 180)),
126
+ ),
127
+ vol.Required("radius", default=200): vol.All(int, vol.Range(min=10)),
128
+ vol.Required("width", default=800): vol.All(int, vol.Range(min=10)),
129
+ vol.Required("height", default=800): vol.All(int, vol.Range(min=10)),
130
+ vol.Required("legend", default=True): bool,
131
+ vol.Required("timestamp", default=True): bool,
132
+ vol.Required("radar_opacity", default=65): vol.All(
133
+ int, vol.Range(0, 100)
134
+ ),
135
+ vol.Optional("precip_type"): vol.Any(
136
+ None, vol.In(["rain", "snow", "auto"])
137
+ ),
138
+ vol.Optional("language", default="english"): vol.In(
139
+ ["english", "french"]
140
+ ),
141
+ }
142
+ )
143
+
144
+ kwargs = init_schema(kwargs)
145
+ self.language = kwargs["language"]
146
+ self.metadata = {"attribution": ATTRIBUTION[self.language]}
147
+
148
+ self._precip_type_setting = kwargs.get("precip_type")
149
+ self._precip_type_actual = self.precip_type[1]
150
+
151
+ # Get map parameters
152
+ self.image = None
153
+ self.width = kwargs["width"]
154
+ self.height = kwargs["height"]
155
+ self.bbox = _compute_bounding_box(kwargs["radius"], *kwargs["coordinates"])
156
+ self.map_params = {
157
+ "bbox": ",".join([str(coord) for coord in self.bbox]),
158
+ "width": self.width,
159
+ "height": self.height,
160
+ }
161
+ self.radar_opacity = kwargs["radar_opacity"]
162
+
163
+ # Get overlay parameters
164
+ self.show_legend = kwargs["legend"]
165
+ self.show_timestamp = kwargs["timestamp"]
166
+
167
+ self._font = None
168
+
169
+ @property
170
+ def precip_type(self):
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)
178
+
179
+ @precip_type.setter
180
+ def precip_type(self, user_input):
181
+ if user_input not in ["rain", "snow", "auto"]:
182
+ raise ValueError("precip_type must be 'rain', 'snow', or 'auto'")
183
+ self._precip_type_setting = user_input
184
+ self._precip_type_actual = self.precip_type[1]
185
+
186
+ async def _get_basemap(self):
187
+ """Fetch the background map image."""
188
+ if base_bytes := Cache.get("basemap"):
189
+ return base_bytes
190
+
191
+ basemap_params.update(self.map_params)
192
+ for map_url in [basemap_url, backup_map_url]:
193
+ try:
194
+ base_bytes = await _get_resource(map_url, basemap_params)
195
+ return Cache.add("basemap", base_bytes, timedelta(days=7))
196
+
197
+ except ClientConnectorError as e:
198
+ logging.warning("Map from %s could not be retrieved: %s" % map_url, e)
199
+
200
+ async def _get_legend(self):
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
+
207
+ legend_params.update(
208
+ dict(
209
+ layer=precip_layers[self._precip_type_actual],
210
+ style=legend_style[self._precip_type_actual],
211
+ )
212
+ )
213
+ try:
214
+ legend = await _get_resource(geomet_url, legend_params)
215
+ return Cache.add(legend_cache_key, legend, timedelta(days=7))
216
+
217
+ except ClientConnectorError:
218
+ logging.warning("Legend could not be retrieved")
219
+ return None
220
+
221
+ async def _get_dimensions(self):
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
230
+ )
231
+ Cache.add(capabilities_cache_key, capabilities_xml, timedelta(minutes=5))
232
+
233
+ dimension_string = et.fromstring(capabilities_xml).find(
234
+ dimension_xpath.format(layer=precip_layers[self._precip_type_actual]),
235
+ namespaces=wms_namespace,
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):
247
+ def _create_image():
248
+ """Contains all the PIL calls; run in another thread."""
249
+
250
+ radar_image = Image.open(BytesIO(cast(bytes, radar_bytes))).convert("RGBA")
251
+
252
+ map_image = None
253
+ if base_bytes:
254
+ map_image = Image.open(BytesIO(base_bytes)).convert("RGBA")
255
+
256
+ if legend_bytes:
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
262
+
263
+ # Add transparency to radar
264
+ if self.radar_opacity < 100:
265
+ alpha = round((self.radar_opacity / 100) * 255)
266
+ radar_copy = radar_image.copy()
267
+ radar_copy.putalpha(alpha)
268
+ radar_image.paste(radar_copy, radar_image)
269
+
270
+ # Overlay radar on basemap
271
+ if map_image:
272
+ frame = Image.alpha_composite(map_image, radar_image)
273
+ else:
274
+ frame = radar_image
275
+
276
+ # Add legend
277
+ if legend_image:
278
+ frame.paste(legend_image, legend_position)
279
+
280
+ # Add timestamp
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
304
+ img_byte_arr = BytesIO()
305
+ frame.save(img_byte_arr, format="PNG")
306
+
307
+ # Time is tuned for 3h radar image
308
+ return Cache.add(
309
+ f"radar-{time}", img_byte_arr.getvalue(), timedelta(minutes=200)
310
+ )
311
+
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
319
+
320
+ params = dict(
321
+ **radar_params,
322
+ **self.map_params,
323
+ layers=precip_layers[self._precip_type_actual],
324
+ time=time,
325
+ )
326
+ radar_bytes = await _get_resource(geomet_url, params)
327
+ return await asyncio.get_event_loop().run_in_executor(None, _create_image)
328
+
329
+ async def get_latest_frame(self):
330
+ """Get the latest image from Environment Canada."""
331
+ dimensions = await self._get_dimensions()
332
+ if not dimensions:
333
+ return None
334
+ return await self._get_radar_image(frame_time=dimensions[1])
335
+
336
+ async def update(self):
337
+ self.image = await self.get_loop()
338
+
339
+ async def get_loop(self, fps=5):
340
+ """Build an animated GIF of recent radar images."""
341
+
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,
349
+ format="GIF",
350
+ save_all=True,
351
+ append_images=imgs[1:],
352
+ duration=duration,
353
+ loop=0,
354
+ )
355
+ return gif.getvalue()
356
+
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
361
+
362
+ timespan = await self._get_dimensions()
363
+ if not timespan:
364
+ logging.error("Cannot retrieve radar times.")
365
+ return None
366
+
367
+ tasks = []
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)
373
+
374
+ for _ in range(3):
375
+ radar_layers.append(radar_layers[-1])
376
+
377
+ return await asyncio.get_running_loop().run_in_executor(None, create_gif)
@@ -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("./currentConditions/station").text
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(wind_speed_text if wind_speed_text.isnumeric() else 0),
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
2
  Name: env_canada
3
- Version: 0.6.3
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.6.3?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
 
@@ -8,7 +8,6 @@ env_canada/__init__.py
8
8
  env_canada/constants.py
9
9
  env_canada/ec_aqhi.py
10
10
  env_canada/ec_cache.py
11
- env_canada/ec_data.py
12
11
  env_canada/ec_exc.py
13
12
  env_canada/ec_historical.py
14
13
  env_canada/ec_hydro.py
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setuptools.setup(
7
7
  name="env_canada",
8
- version="0.6.3",
8
+ version="0.7.1",
9
9
  author="Michael Davie",
10
10
  author_email="michael.davie@gmail.com",
11
11
  description="A package to access meteorological data from Environment Canada",
@@ -48,9 +48,9 @@ def test_get_loop(test_radar):
48
48
 
49
49
  def test_set_precip_type(test_radar):
50
50
  test_radar.precip_type = "auto"
51
- assert test_radar.precip_type == "auto"
51
+ assert test_radar.precip_type[0] == "auto"
52
52
 
53
53
  if date.today().month in range(4, 11):
54
- assert test_radar.layer_key == "rain"
54
+ assert test_radar.precip_type[1] == "rain"
55
55
  else:
56
- assert test_radar.layer_key == "snow"
56
+ assert test_radar.precip_type[1] == "snow"
@@ -1 +0,0 @@
1
- USER_AGENT = "env_canada/0.6.3"
@@ -1,38 +0,0 @@
1
- from aiohttp import ClientSession
2
- from datetime import datetime, timedelta
3
-
4
- from .constants import USER_AGENT
5
-
6
- CACHE_EXPIRE_TIME = timedelta(minutes=200) # Time is tuned for 3h radar image
7
-
8
-
9
- class CacheClientSession(ClientSession):
10
- """Shim to cache ClientSession requests."""
11
-
12
- _cache = {}
13
-
14
- def _flush_cache(self):
15
- """Flush expired cache entries."""
16
-
17
- now = datetime.now()
18
- expired = [key for key, value in self._cache.items() if value[0] < now]
19
- 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
37
-
38
- return result[1]