|
|
|
"""
|
|
|
|
LRU-Cache implementation for formatted (`format=`) answers
|
|
|
|
"""
|
|
|
|
|
|
|
|
import datetime
|
|
|
|
import re
|
|
|
|
import time
|
|
|
|
import os
|
|
|
|
import hashlib
|
|
|
|
import random
|
|
|
|
|
|
|
|
import pytz
|
|
|
|
import pylru
|
|
|
|
|
|
|
|
from globals import LRU_CACHE
|
|
|
|
|
|
|
|
CACHE_SIZE = 10000
|
|
|
|
CACHE = pylru.lrucache(CACHE_SIZE)
|
|
|
|
|
|
|
|
# strings longer than this are stored not in ram
|
|
|
|
# but in the file cache
|
|
|
|
MIN_SIZE_FOR_FILECACHE = 80
|
|
|
|
|
|
|
|
def _update_answer(answer):
|
|
|
|
def _now_in_tz(timezone):
|
|
|
|
return datetime.datetime.now(pytz.timezone(timezone)).strftime("%H:%M:%S%z")
|
|
|
|
|
|
|
|
if isinstance(answer, str) and "%{{NOW(" in answer:
|
|
|
|
answer = re.sub(r"%{{NOW\(([^}]*)\)}}", lambda x: _now_in_tz(x.group(1)), answer)
|
|
|
|
|
|
|
|
return answer
|
|
|
|
|
|
|
|
def get_signature(user_agent, query_string, client_ip_address, lang):
|
|
|
|
"""
|
|
|
|
Get cache signature based on `user_agent`, `url_string`,
|
|
|
|
`lang`, and `client_ip_address`
|
|
|
|
Return `None` if query should not be cached.
|
|
|
|
"""
|
|
|
|
|
|
|
|
if "?" in query_string:
|
|
|
|
location = query_string.split("?", 1)[0]
|
|
|
|
else:
|
|
|
|
location = query_string
|
|
|
|
if ":" in location:
|
|
|
|
return None
|
|
|
|
|
|
|
|
signature = "%s:%s:%s:%s" % \
|
|
|
|
(user_agent, query_string, client_ip_address, lang)
|
|
|
|
print(signature)
|
|
|
|
return signature
|
|
|
|
|
|
|
|
def get(signature):
|
|
|
|
"""
|
|
|
|
If `update_answer` is not True, return answer as it is
|
|
|
|
stored in the cache. Otherwise update it, using
|
|
|
|
the `_update_answer` function.
|
|
|
|
"""
|
|
|
|
|
|
|
|
if not signature:
|
|
|
|
return None
|
|
|
|
|
|
|
|
value_record = CACHE.get(signature)
|
|
|
|
if not value_record:
|
|
|
|
return None
|
|
|
|
|
|
|
|
value = value_record["val"]
|
|
|
|
expiry = value_record["expiry"]
|
|
|
|
if value and time.time() < expiry:
|
|
|
|
if value.startswith("file:") or value.startswith("bfile:"):
|
|
|
|
value = _read_from_file(signature, sighash=value)
|
|
|
|
if not value:
|
|
|
|
return None
|
|
|
|
return _update_answer(value)
|
|
|
|
return None
|
|
|
|
|
|
|
|
def _randint(minimum, maximum):
|
|
|
|
return random.randrange(maximum - minimum)
|
|
|
|
|
|
|
|
def store(signature, value):
|
|
|
|
"""
|
|
|
|
Store in cache `value` for `signature`
|
|
|
|
"""
|
|
|
|
if not signature:
|
|
|
|
return value
|
|
|
|
|
|
|
|
if len(value) >= MIN_SIZE_FOR_FILECACHE:
|
|
|
|
value_to_store = _store_in_file(signature, value)
|
|
|
|
else:
|
|
|
|
value_to_store = value
|
|
|
|
|
|
|
|
value_record = {
|
|
|
|
"val": value_to_store,
|
|
|
|
"expiry": time.time() + _randint(1000, 2000),
|
|
|
|
}
|
|
|
|
|
|
|
|
CACHE[signature] = value_record
|
|
|
|
|
|
|
|
return _update_answer(value)
|
|
|
|
|
|
|
|
def _hash(signature):
|
|
|
|
return hashlib.md5(signature.encode("utf-8")).hexdigest()
|
|
|
|
|
|
|
|
def _store_in_file(signature, value):
|
|
|
|
"""Store `value` for `signature` in cache file.
|
|
|
|
Return file name (signature_hash) as the result.
|
|
|
|
`value` can be string as well as bytes.
|
|
|
|
Returned filename is prefixed with "file:" (for text files)
|
|
|
|
or "bfile:" (for binary files).
|
|
|
|
"""
|
|
|
|
|
|
|
|
signature_hash = _hash(signature)
|
|
|
|
filename = os.path.join(LRU_CACHE, signature_hash)
|
|
|
|
if not os.path.exists(LRU_CACHE):
|
|
|
|
os.makedirs(LRU_CACHE)
|
|
|
|
|
|
|
|
if isinstance(value, bytes):
|
|
|
|
mode = "wb"
|
|
|
|
signature_hash = "bfile:%s" % signature_hash
|
|
|
|
else:
|
|
|
|
mode = "w"
|
|
|
|
signature_hash = "file:%s" % signature_hash
|
|
|
|
|
|
|
|
with open(filename, mode) as f_cache:
|
|
|
|
f_cache.write(value)
|
|
|
|
return signature_hash
|
|
|
|
|
|
|
|
def _read_from_file(signature, sighash=None):
|
|
|
|
"""Read value for `signature` from cache file,
|
|
|
|
or return None if file is not found.
|
|
|
|
If `sighash` is specified, do not calculate file name
|
|
|
|
from signature, but use `sighash` instead.
|
|
|
|
|
|
|
|
`sigash` can be prefixed with "file:" (for text files)
|
|
|
|
or "bfile:" (for binary files).
|
|
|
|
"""
|
|
|
|
|
|
|
|
mode = "r"
|
|
|
|
if sighash:
|
|
|
|
if sighash.startswith("file:"):
|
|
|
|
sighash = sighash[5:]
|
|
|
|
elif sighash.startswith("bfile:"):
|
|
|
|
sighash = sighash[6:]
|
|
|
|
mode = "rb"
|
|
|
|
else:
|
|
|
|
sighash = _hash(signature)
|
|
|
|
|
|
|
|
filename = os.path.join(LRU_CACHE, sighash)
|
|
|
|
if not os.path.exists(filename):
|
|
|
|
return None
|
|
|
|
|
|
|
|
with open(filename, mode) as f_cache:
|
|
|
|
return f_cache.read()
|