| @@ -18,18 +18,24 @@ class _ESICache: | |||||
| EXPIRATION_RATE = 0.2 | EXPIRATION_RATE = 0.2 | ||||
| def __init__(self): | def __init__(self): | ||||
| self._index = {} | |||||
| self._public = {} | |||||
| self._private = {} | |||||
| self._lock = Lock() | self._lock = Lock() | ||||
| @staticmethod | @staticmethod | ||||
| def _calculate_expiry(resp): | |||||
| """Calculate the expiration date for the given response object.""" | |||||
| def _get_cache_policy(resp): | |||||
| """Calculate the expiry and availability of the given response.""" | |||||
| if "Cache-Control" not in resp.headers: | if "Cache-Control" not in resp.headers: | ||||
| return None | return None | ||||
| directives = resp.headers["Cache-Control"].lower().split(";") | directives = resp.headers["Cache-Control"].lower().split(";") | ||||
| directives = [d.strip() for d in directives] | directives = [d.strip() for d in directives] | ||||
| if "public" not in directives: | |||||
| if "public" in directives: | |||||
| public = True | |||||
| elif "private" in directives: | |||||
| public = False | |||||
| else: | |||||
| return None | return None | ||||
| expires = resp.headers.get("Expires") | expires = resp.headers.get("Expires") | ||||
| @@ -37,10 +43,12 @@ class _ESICache: | |||||
| return None | return None | ||||
| try: | try: | ||||
| return datetime.strptime(expires, "%a, %d %b %Y %H:%M:%S GMT") | |||||
| expiry = datetime.strptime(expires, "%a, %d %b %Y %H:%M:%S GMT") | |||||
| except ValueError: | except ValueError: | ||||
| return None | return None | ||||
| return public, expiry | |||||
| @staticmethod | @staticmethod | ||||
| def freeze_dict(d): | def freeze_dict(d): | ||||
| """Return a hashable string key for the given dictionary.""" | """Return a hashable string key for the given dictionary.""" | ||||
| @@ -51,36 +59,52 @@ class _ESICache: | |||||
| def _expire(self): | def _expire(self): | ||||
| """Remove old entries from the cache. Assumes lock is acquired.""" | """Remove old entries from the cache. Assumes lock is acquired.""" | ||||
| condemned = [] | |||||
| now = datetime.utcnow() | now = datetime.utcnow() | ||||
| for key, (expiry, _) in self._index.items(): | |||||
| if expiry < now: | |||||
| condemned.append(key) | |||||
| for key in condemned: | |||||
| del self._index[key] | |||||
| def fetch(self, key): | |||||
| for index in (self._private, self._public): | |||||
| condemned = [] | |||||
| for key, (expiry, _) in index.items(): | |||||
| if expiry < now: | |||||
| condemned.append(key) | |||||
| for key in condemned: | |||||
| del index[key] | |||||
| def fetch(self, key, token): | |||||
| """Try to look up a key in the cache. Return None if not found. | """Try to look up a key in the cache. Return None if not found. | ||||
| The key should be a string. | |||||
| The key should be a string. The token is used if a route is cached | |||||
| privately. | |||||
| This will periodically clear the cache of expired entries. | This will periodically clear the cache of expired entries. | ||||
| """ | """ | ||||
| with self._lock: | with self._lock: | ||||
| if random.random() < self.EXPIRATION_RATE: | if random.random() < self.EXPIRATION_RATE: | ||||
| self._expire() | self._expire() | ||||
| if key in self._index: | |||||
| expiry, value = self._index[key] | |||||
| if (key, token) in self._private: | |||||
| expiry, value = self._private[key, token] | |||||
| if expiry > datetime.utcnow(): | if expiry > datetime.utcnow(): | ||||
| return value | return value | ||||
| if key in self._public: | |||||
| expiry, value = self._public[key] | |||||
| if expiry > datetime.utcnow(): | |||||
| return value | |||||
| return None | return None | ||||
| def insert(self, key, value, response): | |||||
| def insert(self, key, token, value, response): | |||||
| """Store a key-value pair using the response as cache control.""" | """Store a key-value pair using the response as cache control.""" | ||||
| expiry = self._calculate_expiry(response) | |||||
| if expiry and expiry > datetime.utcnow(): | |||||
| policy = self._get_cache_policy(response) | |||||
| if not policy: | |||||
| return | |||||
| public, expiry = policy | |||||
| if expiry > datetime.utcnow(): | |||||
| with self._lock: | with self._lock: | ||||
| self._index[key] = (expiry, value) | |||||
| if public: | |||||
| self._public[key] = (expiry, value) | |||||
| else: | |||||
| self._private[key, token] = (expiry, value) | |||||
| class _ESIQueryBuilder: | class _ESIQueryBuilder: | ||||
| @@ -150,7 +174,7 @@ class EVESwaggerInterface: | |||||
| if can_cache: | if can_cache: | ||||
| pkey = self._cache.freeze_dict(params) | pkey = self._cache.freeze_dict(params) | ||||
| key = "|".join((method.__name__, self._data_source, query, pkey)) | key = "|".join((method.__name__, self._data_source, query, pkey)) | ||||
| cached = self._cache.fetch(key) | |||||
| cached = self._cache.fetch(key, token) | |||||
| else: | else: | ||||
| cached = None | cached = None | ||||
| @@ -177,7 +201,7 @@ class EVESwaggerInterface: | |||||
| raise EVEAPIError() | raise EVEAPIError() | ||||
| if can_cache and result is not None: | if can_cache and result is not None: | ||||
| self._cache.insert(key, result, resp) | |||||
| self._cache.insert(key, token, result, resp) | |||||
| return result | return result | ||||
| def get(self, query, token, params=None): | def get(self, query, token, params=None): | ||||