Skip to content

Commit 2f930a6

Browse files
committed
Caching
1 parent 0d519b7 commit 2f930a6

File tree

3 files changed

+41
-3
lines changed

3 files changed

+41
-3
lines changed

epidatpy/_model.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from dataclasses import dataclass, field
22
from datetime import date
33
from enum import Enum
4+
from os import environ
45
from typing import (
56
Final,
67
List,
@@ -146,6 +147,7 @@ class AEpiDataCall:
146147
meta: Final[Sequence[EpidataFieldInfo]]
147148
meta_by_name: Final[Mapping[str, EpidataFieldInfo]]
148149
only_supports_classic: Final[bool]
150+
use_cache: Final[bool]
149151

150152
def __init__(
151153
self,
@@ -154,13 +156,18 @@ def __init__(
154156
params: Mapping[str, Optional[EpiRangeParam]],
155157
meta: Optional[Sequence[EpidataFieldInfo]] = None,
156158
only_supports_classic: bool = False,
159+
use_cache: Optional[bool] = None,
157160
) -> None:
158161
self._base_url = base_url
159162
self._endpoint = endpoint
160163
self._params = params
161164
self.only_supports_classic = only_supports_classic
162165
self.meta = meta or []
163166
self.meta_by_name = {k.name: k for k in self.meta}
167+
# Set the use_cache value from the constructor if present.
168+
# Otherwise check the USE_EPIDATPY_CACHE variable, accepting various "truthy" values.
169+
self.use_cache = use_cache \
170+
or (environ.get("USE_EPIDATPY_CACHE", "").lower() in ['true', 't', '1'])
164171

165172
def _verify_parameters(self) -> None:
166173
# hook for verifying parameters before sending

epidatpy/request.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
cast,
1111
)
1212

13+
from appdirs import user_cache_dir
14+
from diskcache import Cache
15+
from json import dumps
1316
from pandas import CategoricalDtype, DataFrame, Series, to_datetime
1417
from requests import Response, Session
1518
from requests.auth import HTTPBasicAuth
@@ -33,7 +36,7 @@
3336

3437
# Make the linter happy about the unused variables
3538
__all__ = ["Epidata", "EpiDataCall", "EpiDataContext", "EpiRange", "CovidcastEpidata"]
36-
39+
CACHE_DIRECTORY = user_cache_dir(appname="epidatpy", appauthor="delphi")
3740

3841
@retry(reraise=True, stop=stop_after_attempt(2))
3942
def _request_with_retry(
@@ -73,8 +76,9 @@ def __init__(
7376
params: Mapping[str, Optional[EpiRangeParam]],
7477
meta: Optional[Sequence[EpidataFieldInfo]] = None,
7578
only_supports_classic: bool = False,
79+
use_cache = None,
7680
) -> None:
77-
super().__init__(base_url, endpoint, params, meta, only_supports_classic)
81+
super().__init__(base_url, endpoint, params, meta, only_supports_classic, use_cache)
7882
self._session = session
7983

8084
def with_base_url(self, base_url: str) -> "EpiDataCall":
@@ -100,13 +104,23 @@ def classic(
100104
"""Request and parse epidata in CLASSIC message format."""
101105
self._verify_parameters()
102106
try:
107+
if self.use_cache:
108+
with Cache(CACHE_DIRECTORY) as cache:
109+
cache_key = str(self._endpoint) + str(self._params)
110+
if cache_key in cache:
111+
return cache[cache_key]
103112
response = self._call(fields)
104113
r = cast(EpiDataResponse, response.json())
105114
if disable_type_parsing:
106115
return r
107116
epidata = r.get("epidata")
108117
if epidata and isinstance(epidata, list) and len(epidata) > 0 and isinstance(epidata[0], dict):
109118
r["epidata"] = [self._parse_row(row, disable_date_parsing=disable_date_parsing) for row in epidata]
119+
if self.use_cache:
120+
with Cache(CACHE_DIRECTORY) as cache:
121+
cache_key = str(self._endpoint) + str(self._params)
122+
# Set TTL to 7 days (TODO: configurable?)
123+
cache.set(cache_key, r, expire=7*24*60*60)
110124
return r
111125
except Exception as e: # pylint: disable=broad-except
112126
return {"result": 0, "message": f"error: {e}", "epidata": []}
@@ -130,6 +144,13 @@ def df(
130144
if self.only_supports_classic:
131145
raise OnlySupportsClassicFormatException()
132146
self._verify_parameters()
147+
148+
if self.use_cache:
149+
with Cache(CACHE_DIRECTORY) as cache:
150+
cache_key = str(self._endpoint) + str(self._params)
151+
if cache_key in cache:
152+
return cache[cache_key]
153+
133154
json = self.classic(fields, disable_type_parsing=True)
134155
rows = json.get("epidata", [])
135156
pred = fields_to_predicate(fields)
@@ -175,6 +196,13 @@ def df(
175196
df[info.name] = to_datetime(df[info.name], format="%Y%m%d")
176197
except ValueError:
177198
pass
199+
200+
if self.use_cache:
201+
with Cache(CACHE_DIRECTORY) as cache:
202+
cache_key = str(self._endpoint) + str(self._params)
203+
# Set TTL to 7 days (TODO: configurable?)
204+
cache.set(cache_key, df, expire=7*24*60*60)
205+
178206
return df
179207

180208

@@ -203,8 +231,9 @@ def _create_call(
203231
params: Mapping[str, Optional[EpiRangeParam]],
204232
meta: Optional[Sequence[EpidataFieldInfo]] = None,
205233
only_supports_classic: bool = False,
234+
use_cache: bool = False,
206235
) -> EpiDataCall:
207-
return EpiDataCall(self._base_url, self._session, endpoint, params, meta, only_supports_classic)
236+
return EpiDataCall(self._base_url, self._session, endpoint, params, meta, only_supports_classic, use_cache)
208237

209238

210239
Epidata = EpiDataContext()

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ classifiers = [
3131
requires-python = ">=3.8"
3232
dependencies = [
3333
"aiohttp",
34+
"appdirs",
35+
"diskcache",
3436
"epiweeks>=2.1",
3537
"pandas>=1",
3638
"requests>=2.25",

0 commit comments

Comments
 (0)