env_canada 0.15.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/DejaVuSans.ttf +0 -0
- env_canada/__init__.py +19 -0
- env_canada/constants.py +1 -0
- env_canada/ec_alerts.py +164 -0
- env_canada/ec_aqhi.py +233 -0
- env_canada/ec_cache.py +47 -0
- env_canada/ec_exc.py +2 -0
- env_canada/ec_historical.py +502 -0
- env_canada/ec_hydro.py +152 -0
- env_canada/ec_legend.py +183 -0
- env_canada/ec_map.py +380 -0
- env_canada/ec_radar.py +105 -0
- env_canada/ec_weather.py +704 -0
- env_canada-0.15.0.dist-info/METADATA +342 -0
- env_canada-0.15.0.dist-info/RECORD +18 -0
- env_canada-0.15.0.dist-info/WHEEL +5 -0
- env_canada-0.15.0.dist-info/licenses/LICENSE +19 -0
- env_canada-0.15.0.dist-info/top_level.txt +1 -0
|
Binary file
|
env_canada/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"ECAirQuality",
|
|
3
|
+
"ECAlerts",
|
|
4
|
+
"ECHistorical",
|
|
5
|
+
"ECHistoricalRange",
|
|
6
|
+
"ECHydro",
|
|
7
|
+
"ECMap",
|
|
8
|
+
"ECRadar",
|
|
9
|
+
"ECWeather",
|
|
10
|
+
"ECWeatherUpdateFailed",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
from .ec_aqhi import ECAirQuality
|
|
14
|
+
from .ec_alerts import ECAlerts
|
|
15
|
+
from .ec_historical import ECHistorical, ECHistoricalRange
|
|
16
|
+
from .ec_hydro import ECHydro
|
|
17
|
+
from .ec_radar import ECRadar
|
|
18
|
+
from .ec_map import ECMap
|
|
19
|
+
from .ec_weather import ECWeather, ECWeatherUpdateFailed
|
env_canada/constants.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
USER_AGENT = "env_canada/0.15.0"
|
env_canada/ec_alerts.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import logging
|
|
3
|
+
import math
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime, timedelta, UTC
|
|
6
|
+
|
|
7
|
+
from aiohttp import ClientSession, ClientTimeout
|
|
8
|
+
|
|
9
|
+
from .constants import USER_AGENT
|
|
10
|
+
from .ec_cache import Cache
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _point_in_polygon(lon, lat, ring):
|
|
14
|
+
"""Ray-casting point-in-polygon test for a single ring."""
|
|
15
|
+
inside = False
|
|
16
|
+
j = len(ring) - 1
|
|
17
|
+
for i, (xi, yi) in enumerate(ring):
|
|
18
|
+
xj, yj = ring[j]
|
|
19
|
+
if (yi > lat) != (yj > lat) and lon < (xj - xi) * (lat - yi) / (yj - yi) + xi:
|
|
20
|
+
inside = not inside
|
|
21
|
+
j = i
|
|
22
|
+
return inside
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _point_in_geometry(lon, lat, geometry):
|
|
26
|
+
"""Return True if (lon, lat) falls within a GeoJSON Polygon or MultiPolygon."""
|
|
27
|
+
gtype = geometry.get("type")
|
|
28
|
+
coords = geometry.get("coordinates", [])
|
|
29
|
+
if gtype == "Polygon":
|
|
30
|
+
return _point_in_polygon(lon, lat, coords[0])
|
|
31
|
+
if gtype == "MultiPolygon":
|
|
32
|
+
return any(_point_in_polygon(lon, lat, poly[0]) for poly in coords)
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
GEOMET_WFS_URL = "https://geo.weather.gc.ca/geomet"
|
|
37
|
+
|
|
38
|
+
ALERTS_WFS_PARAMS = {
|
|
39
|
+
"SERVICE": "WFS",
|
|
40
|
+
"VERSION": "2.0.0",
|
|
41
|
+
"REQUEST": "GetFeature",
|
|
42
|
+
"TYPENAMES": "Current-Alerts",
|
|
43
|
+
"outputFormat": "application/json",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
CLIENT_TIMEOUT = ClientTimeout(10)
|
|
47
|
+
|
|
48
|
+
CACHE_TTL = timedelta(minutes=5)
|
|
49
|
+
|
|
50
|
+
ATTRIBUTION = {
|
|
51
|
+
"english": "Data provided by Environment Canada",
|
|
52
|
+
"french": "Données fournies par Environnement Canada",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
LOG = logging.getLogger(__name__)
|
|
56
|
+
|
|
57
|
+
__all__ = ["ECAlerts"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class MetaData:
|
|
62
|
+
attribution: str
|
|
63
|
+
timestamp: datetime = datetime(1970, 1, 1, 0, 0, tzinfo=UTC)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ECAlerts:
|
|
67
|
+
"""Get weather alerts from Environment Canada GeoMet WFS."""
|
|
68
|
+
|
|
69
|
+
def __init__(self, coordinates, language="english"):
|
|
70
|
+
"""Initialize ECAlerts with coordinates and language."""
|
|
71
|
+
from .ec_weather import ALERTS_INIT # lazy import to avoid circular dependency
|
|
72
|
+
|
|
73
|
+
self.lat = coordinates[0]
|
|
74
|
+
self.lon = coordinates[1]
|
|
75
|
+
self.language = language
|
|
76
|
+
self.metadata = MetaData(ATTRIBUTION[language])
|
|
77
|
+
self.alerts = copy.deepcopy(ALERTS_INIT[language])
|
|
78
|
+
self.alert_features = []
|
|
79
|
+
|
|
80
|
+
async def update(self):
|
|
81
|
+
"""Fetch current alerts from Environment Canada GeoMet WFS."""
|
|
82
|
+
from .ec_weather import ALERTS_INIT, ALERT_TYPE_TO_NAME # lazy import
|
|
83
|
+
|
|
84
|
+
cache_key = f"alerts-{self.language}-{self.lat:.4f}-{self.lon:.4f}"
|
|
85
|
+
cached = Cache.get(cache_key)
|
|
86
|
+
if cached is not None:
|
|
87
|
+
self.alerts, self.alert_features = cached
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
km = 50
|
|
91
|
+
lat_delta = km / 111.0
|
|
92
|
+
lon_delta = km / (111.0 * math.cos(math.radians(self.lat)))
|
|
93
|
+
bbox = (
|
|
94
|
+
f"{self.lat - lat_delta:.4f},{self.lon - lon_delta:.4f},"
|
|
95
|
+
f"{self.lat + lat_delta:.4f},{self.lon + lon_delta:.4f},EPSG:4326"
|
|
96
|
+
)
|
|
97
|
+
params = {**ALERTS_WFS_PARAMS, "BBOX": bbox}
|
|
98
|
+
|
|
99
|
+
async with ClientSession(raise_for_status=True) as session:
|
|
100
|
+
response = await session.get(
|
|
101
|
+
GEOMET_WFS_URL,
|
|
102
|
+
params=params,
|
|
103
|
+
headers={"User-Agent": USER_AGENT},
|
|
104
|
+
timeout=CLIENT_TIMEOUT,
|
|
105
|
+
)
|
|
106
|
+
data = await response.json()
|
|
107
|
+
|
|
108
|
+
if not isinstance(data, dict) or data.get("type") != "FeatureCollection":
|
|
109
|
+
raise ValueError(f"Unexpected WFS response type: {type(data)}")
|
|
110
|
+
|
|
111
|
+
lang_suffix = "en" if self.language == "english" else "fr"
|
|
112
|
+
|
|
113
|
+
self.alerts = copy.deepcopy(ALERTS_INIT[self.language])
|
|
114
|
+
self.alert_features = []
|
|
115
|
+
|
|
116
|
+
for feature in data.get("features", []):
|
|
117
|
+
props = feature.get("properties", {})
|
|
118
|
+
if not isinstance(props, dict):
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
geometry = feature.get("geometry") or {}
|
|
122
|
+
if not _point_in_geometry(self.lon, self.lat, geometry):
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
self.alert_features.append(props)
|
|
126
|
+
|
|
127
|
+
status_en = (props.get("status_en") or "").lower()
|
|
128
|
+
alert_type = (props.get("alert_type") or "").lower()
|
|
129
|
+
|
|
130
|
+
# Ended alerts go to endings category regardless of alert_type
|
|
131
|
+
if status_en == "ended":
|
|
132
|
+
category = "endings"
|
|
133
|
+
else:
|
|
134
|
+
category = ALERT_TYPE_TO_NAME.get(alert_type)
|
|
135
|
+
if category is None:
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
title = props.get(f"alert_name_{lang_suffix}") or ""
|
|
139
|
+
|
|
140
|
+
alert_dict = {
|
|
141
|
+
"title": title.strip().title() if title else title,
|
|
142
|
+
"date": props.get("publication_datetime"),
|
|
143
|
+
"alertColourLevel": props.get(f"risk_colour_{lang_suffix}"),
|
|
144
|
+
"expiryTime": props.get("expiration_datetime"),
|
|
145
|
+
# Additive new fields
|
|
146
|
+
"text": props.get(f"alert_text_{lang_suffix}"),
|
|
147
|
+
"area": props.get(f"feature_name_{lang_suffix}"),
|
|
148
|
+
"status": props.get(f"status_{lang_suffix}"),
|
|
149
|
+
"confidence": props.get(f"confidence_{lang_suffix}"),
|
|
150
|
+
"impact": props.get(f"impact_{lang_suffix}"),
|
|
151
|
+
"alert_code": props.get("alert_code"),
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
self.alerts[category]["value"].append(alert_dict)
|
|
155
|
+
|
|
156
|
+
Cache.add(cache_key, (self.alerts, self.alert_features), CACHE_TTL)
|
|
157
|
+
self.metadata.timestamp = datetime.now(UTC)
|
|
158
|
+
|
|
159
|
+
LOG.debug(
|
|
160
|
+
"update(): fetched %d alert features for (%f, %f)",
|
|
161
|
+
len(self.alert_features),
|
|
162
|
+
self.lat,
|
|
163
|
+
self.lon,
|
|
164
|
+
)
|
env_canada/ec_aqhi.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import datetime, UTC
|
|
4
|
+
|
|
5
|
+
import voluptuous as vol
|
|
6
|
+
from aiohttp import ClientSession, ClientTimeout
|
|
7
|
+
from geopy import distance
|
|
8
|
+
from lxml import etree as et
|
|
9
|
+
|
|
10
|
+
from .constants import USER_AGENT
|
|
11
|
+
|
|
12
|
+
AQHI_SITE_LIST_URL = (
|
|
13
|
+
"https://dd.weather.gc.ca/today/air_quality/doc/AQHI_XML_File_List.xml"
|
|
14
|
+
)
|
|
15
|
+
AQHI_OBSERVATION_URL = "https://dd.weather.gc.ca/today/air_quality/aqhi/{}/observation/realtime/xml/AQ_OBS_{}_CURRENT.xml"
|
|
16
|
+
AQHI_FORECAST_URL = "https://dd.weather.gc.ca/today/air_quality/aqhi/{}/forecast/realtime/xml/AQ_FCST_{}_CURRENT.xml"
|
|
17
|
+
|
|
18
|
+
CLIENT_TIMEOUT = ClientTimeout(10)
|
|
19
|
+
|
|
20
|
+
LOG = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
ATTRIBUTION = {
|
|
23
|
+
"EN": "Data provided by Environment Canada",
|
|
24
|
+
"FR": "Données fournies par Environnement Canada",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class MetaData:
|
|
30
|
+
attribution: str
|
|
31
|
+
timestamp: datetime | None = None
|
|
32
|
+
location: str | None = None
|
|
33
|
+
|
|
34
|
+
# Not used; needed for compatability with ec_weather metadata
|
|
35
|
+
station: str | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = ["ECAirQuality"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def timestamp_to_datetime(timestamp):
|
|
42
|
+
dt = datetime.strptime(timestamp, "%Y%m%d%H%M%S")
|
|
43
|
+
dt = dt.replace(tzinfo=UTC)
|
|
44
|
+
return dt
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def get_aqhi_regions(language):
|
|
48
|
+
"""Get list of all AQHI regions from Environment Canada, for auto-config."""
|
|
49
|
+
zone_name_tag = f"name_{language.lower()}_CA"
|
|
50
|
+
region_name_tag = f"name{language.title()}"
|
|
51
|
+
|
|
52
|
+
LOG.debug("get_aqhi_regions() started")
|
|
53
|
+
|
|
54
|
+
regions = []
|
|
55
|
+
async with ClientSession(raise_for_status=True) as session:
|
|
56
|
+
response = await session.get(
|
|
57
|
+
AQHI_SITE_LIST_URL,
|
|
58
|
+
headers={"User-Agent": USER_AGENT},
|
|
59
|
+
timeout=CLIENT_TIMEOUT,
|
|
60
|
+
)
|
|
61
|
+
result = await response.read()
|
|
62
|
+
|
|
63
|
+
site_xml = result
|
|
64
|
+
xml_object = et.fromstring(site_xml)
|
|
65
|
+
|
|
66
|
+
for zone in xml_object.findall("./EC_administrativeZone"):
|
|
67
|
+
_zone_attribs = zone.attrib
|
|
68
|
+
_zone_attrib = {
|
|
69
|
+
"abbreviation": _zone_attribs["abreviation"],
|
|
70
|
+
"zone_name": _zone_attribs[zone_name_tag],
|
|
71
|
+
}
|
|
72
|
+
for region in zone.findall("./regionList/region"):
|
|
73
|
+
_region_attribs = region.attrib
|
|
74
|
+
|
|
75
|
+
_region_attrib = {
|
|
76
|
+
"region_name": _region_attribs[region_name_tag],
|
|
77
|
+
"cgndb": _region_attribs["cgndb"],
|
|
78
|
+
"latitude": float(_region_attribs["latitude"]),
|
|
79
|
+
"longitude": float(_region_attribs["longitude"]),
|
|
80
|
+
}
|
|
81
|
+
_children = list(region)
|
|
82
|
+
for child in _children:
|
|
83
|
+
_region_attrib[child.tag] = child.text
|
|
84
|
+
_region_attrib.update(_zone_attrib)
|
|
85
|
+
regions.append(_region_attrib)
|
|
86
|
+
|
|
87
|
+
LOG.debug("get_aqhi_regions(): found %d regions", len(regions))
|
|
88
|
+
|
|
89
|
+
return regions
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def find_closest_region(language, lat, lon):
|
|
93
|
+
"""Return the AQHI region and site ID of the closest site."""
|
|
94
|
+
region_list = await get_aqhi_regions(language)
|
|
95
|
+
|
|
96
|
+
def site_distance(site):
|
|
97
|
+
"""Calculate distance to a region."""
|
|
98
|
+
return distance.distance((lat, lon), (site["latitude"], site["longitude"]))
|
|
99
|
+
|
|
100
|
+
return min(region_list, key=site_distance)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class ECAirQuality:
|
|
104
|
+
"""Get air quality data from Environment Canada."""
|
|
105
|
+
|
|
106
|
+
def __init__(self, **kwargs):
|
|
107
|
+
"""Initialize the data object."""
|
|
108
|
+
|
|
109
|
+
init_schema = vol.Schema(
|
|
110
|
+
vol.All(
|
|
111
|
+
vol.Any(
|
|
112
|
+
{
|
|
113
|
+
vol.Required("coordinates"): object,
|
|
114
|
+
vol.Optional("language"): object,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
vol.Required("zone_id"): object,
|
|
118
|
+
vol.Required("region_id"): object,
|
|
119
|
+
vol.Optional("language"): object,
|
|
120
|
+
},
|
|
121
|
+
),
|
|
122
|
+
{
|
|
123
|
+
vol.Optional("zone_id"): vol.In(
|
|
124
|
+
["atl", "ont", "pnr", "pyr", "que"]
|
|
125
|
+
),
|
|
126
|
+
vol.Optional("region_id"): vol.All(str, vol.Length(5)),
|
|
127
|
+
vol.Optional("coordinates"): (
|
|
128
|
+
vol.All(vol.Or(int, float), vol.Range(-90, 90)),
|
|
129
|
+
vol.All(vol.Or(int, float), vol.Range(-180, 180)),
|
|
130
|
+
),
|
|
131
|
+
vol.Optional("language", default="EN"): vol.In(["EN", "FR"]),
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
kwargs = init_schema(kwargs)
|
|
137
|
+
|
|
138
|
+
self.language = kwargs["language"]
|
|
139
|
+
|
|
140
|
+
if (
|
|
141
|
+
"zone_id" in kwargs
|
|
142
|
+
and "region_id" in kwargs
|
|
143
|
+
and kwargs["zone_id"] is not None
|
|
144
|
+
and kwargs["region_id"] is not None
|
|
145
|
+
):
|
|
146
|
+
self.zone_id = kwargs["zone_id"]
|
|
147
|
+
self.region_id = kwargs["region_id"].upper()
|
|
148
|
+
else:
|
|
149
|
+
self.zone_id = None
|
|
150
|
+
self.region_id = None
|
|
151
|
+
self.coordinates = kwargs["coordinates"]
|
|
152
|
+
|
|
153
|
+
self.metadata = MetaData(ATTRIBUTION[self.language])
|
|
154
|
+
self.region_name = None
|
|
155
|
+
self.current = None
|
|
156
|
+
self.current_timestamp = None
|
|
157
|
+
self.forecasts = dict(daily={}, hourly={})
|
|
158
|
+
|
|
159
|
+
async def get_aqhi_data(self, url):
|
|
160
|
+
async with ClientSession(raise_for_status=True) as session:
|
|
161
|
+
try:
|
|
162
|
+
response = await session.get(
|
|
163
|
+
url.format(self.zone_id, self.region_id),
|
|
164
|
+
headers={"User-Agent": USER_AGENT},
|
|
165
|
+
timeout=CLIENT_TIMEOUT,
|
|
166
|
+
)
|
|
167
|
+
except Exception:
|
|
168
|
+
LOG.debug("Retrieving AQHI failed", exc_info=True)
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
result = await response.read()
|
|
172
|
+
aqhi_xml = result
|
|
173
|
+
return et.fromstring(aqhi_xml)
|
|
174
|
+
|
|
175
|
+
async def update(self):
|
|
176
|
+
# Find closest site if not identified
|
|
177
|
+
|
|
178
|
+
if not (self.zone_id and self.region_id):
|
|
179
|
+
closest = await find_closest_region(self.language, *self.coordinates)
|
|
180
|
+
self.zone_id = closest["abbreviation"]
|
|
181
|
+
self.region_id = closest["cgndb"]
|
|
182
|
+
LOG.debug(
|
|
183
|
+
"update() closest region returned: zone_id '%s' region_id '%s'",
|
|
184
|
+
self.zone_id,
|
|
185
|
+
self.region_id,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Fetch current measurement
|
|
189
|
+
aqhi_current = await self.get_aqhi_data(url=AQHI_OBSERVATION_URL)
|
|
190
|
+
|
|
191
|
+
if aqhi_current is not None:
|
|
192
|
+
# Update region name
|
|
193
|
+
element = aqhi_current.find("region")
|
|
194
|
+
self.region_name = element.attrib[f"name{self.language.title()}"]
|
|
195
|
+
self.metadata.location = self.region_name
|
|
196
|
+
|
|
197
|
+
# Update AQHI current condition
|
|
198
|
+
element = aqhi_current.find("airQualityHealthIndex")
|
|
199
|
+
if element is not None:
|
|
200
|
+
self.current = float(element.text)
|
|
201
|
+
else:
|
|
202
|
+
self.current = None
|
|
203
|
+
|
|
204
|
+
element = aqhi_current.find("./dateStamp/UTCStamp")
|
|
205
|
+
if element is not None:
|
|
206
|
+
self.current_timestamp = timestamp_to_datetime(element.text)
|
|
207
|
+
else:
|
|
208
|
+
self.current_timestamp = None
|
|
209
|
+
self.metadata.timestamp = self.current_timestamp
|
|
210
|
+
LOG.debug(
|
|
211
|
+
"update(): aqhi_current %d timestamp %s",
|
|
212
|
+
self.current,
|
|
213
|
+
self.current_timestamp,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Update AQHI forecasts
|
|
217
|
+
aqhi_forecast = await self.get_aqhi_data(url=AQHI_FORECAST_URL)
|
|
218
|
+
|
|
219
|
+
if aqhi_forecast is not None:
|
|
220
|
+
# Update AQHI daily forecasts
|
|
221
|
+
for f in aqhi_forecast.findall("./forecastGroup/forecast"):
|
|
222
|
+
for p in f.findall("./period"):
|
|
223
|
+
if self.language == p.attrib["lang"]:
|
|
224
|
+
period = p.attrib["forecastName"]
|
|
225
|
+
self.forecasts["daily"][period] = int(
|
|
226
|
+
f.findtext("./airQualityHealthIndex") or 0
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Update AQHI hourly forecasts
|
|
230
|
+
for f in aqhi_forecast.findall("./hourlyForecastGroup/hourlyForecast"):
|
|
231
|
+
self.forecasts["hourly"][timestamp_to_datetime(f.attrib["UTCTime"])] = (
|
|
232
|
+
int(f.text or 0)
|
|
233
|
+
)
|
env_canada/ec_cache.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Any, ClassVar
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Cache:
|
|
6
|
+
_cache: ClassVar[dict[str, tuple[datetime, Any]]] = {}
|
|
7
|
+
|
|
8
|
+
@classmethod
|
|
9
|
+
def add(cls, cache_key, item, cache_time):
|
|
10
|
+
"""Add an entry to the cache."""
|
|
11
|
+
|
|
12
|
+
cls._cache[cache_key] = (datetime.now() + cache_time, item)
|
|
13
|
+
return item # Returning item useful for chaining calls
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def get(cls, cache_key):
|
|
17
|
+
"""Get an entry from the cache."""
|
|
18
|
+
|
|
19
|
+
# Delete expired entries at start so we don't use expired entries
|
|
20
|
+
now = datetime.now()
|
|
21
|
+
expired = [key for key, value in cls._cache.items() if value[0] < now]
|
|
22
|
+
for key in expired:
|
|
23
|
+
del cls._cache[key]
|
|
24
|
+
|
|
25
|
+
result = cls._cache.get(cache_key)
|
|
26
|
+
return result[1] if result else None
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def clear(cls, prefix: str | None = None) -> int:
|
|
30
|
+
"""Clear cache entries.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
prefix: If provided, only clear entries whose keys start with this prefix.
|
|
34
|
+
If None, clear all entries.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Number of entries cleared.
|
|
38
|
+
"""
|
|
39
|
+
if prefix is None:
|
|
40
|
+
count = len(cls._cache)
|
|
41
|
+
cls._cache.clear()
|
|
42
|
+
return count
|
|
43
|
+
|
|
44
|
+
keys_to_delete = [key for key in cls._cache if key.startswith(prefix)]
|
|
45
|
+
for key in keys_to_delete:
|
|
46
|
+
del cls._cache[key]
|
|
47
|
+
return len(keys_to_delete)
|
env_canada/ec_exc.py
ADDED