|
@@ -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): |
|
|