parent
d620ffd389
commit
a369c44ae9
2 changed files with 149 additions and 77 deletions
77
agenda/fx.py
77
agenda/fx.py
|
|
@ -9,6 +9,9 @@ from decimal import Decimal
|
|||
import flask
|
||||
import httpx
|
||||
|
||||
DEFAULT_FX_CACHE_TTL_HOURS = 24
|
||||
DEFAULT_FX_FAILURE_RETRY_HOURS = 24
|
||||
|
||||
|
||||
async def get_gbpusd(config: flask.config.Config) -> Decimal:
|
||||
"""Get the current value for GBPUSD, with caching."""
|
||||
|
|
@ -54,18 +57,51 @@ def read_cached_rates(
|
|||
with open(filename) as file:
|
||||
data = json.load(file, parse_float=Decimal)
|
||||
|
||||
quotes = data.get("quotes")
|
||||
if not isinstance(quotes, dict):
|
||||
return {}
|
||||
|
||||
return {
|
||||
cur: Decimal(data["quotes"][f"GBP{cur}"])
|
||||
for cur in currencies
|
||||
if f"GBP{cur}" in data["quotes"]
|
||||
cur: Decimal(quotes[f"GBP{cur}"]) for cur in currencies if f"GBP{cur}" in quotes
|
||||
}
|
||||
|
||||
|
||||
def _fx_cache_datetime(filename: str) -> datetime:
|
||||
"""Extract the cache timestamp from an FX cache filename."""
|
||||
return datetime.strptime(filename[:16], "%Y-%m-%d_%H:%M")
|
||||
|
||||
|
||||
def _has_required_quotes(filename: str, currencies: list[str]) -> bool:
|
||||
"""Return true if the cache file contains the requested GBP quotes."""
|
||||
try:
|
||||
return len(read_cached_rates(filename, currencies)) == len(currencies)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return False
|
||||
|
||||
|
||||
def _latest_file(
|
||||
fx_dir: str, filenames: list[str], currencies: list[str], *, valid_only: bool
|
||||
) -> str | None:
|
||||
"""Return the newest matching FX cache filename."""
|
||||
matching_files = [
|
||||
filename
|
||||
for filename in filenames
|
||||
if not valid_only
|
||||
or _has_required_quotes(os.path.join(fx_dir, filename), currencies)
|
||||
]
|
||||
|
||||
return max(matching_files) if matching_files else None
|
||||
|
||||
|
||||
def get_rates(config: flask.config.Config) -> dict[str, Decimal]:
|
||||
"""Get current values of exchange rates for a list of currencies against GBP."""
|
||||
currencies = config["CURRENCIES"]
|
||||
access_key = config["EXCHANGERATE_ACCESS_KEY"]
|
||||
data_dir = config["DATA_DIR"]
|
||||
cache_ttl_hours = int(config.get("FX_CACHE_TTL_HOURS", DEFAULT_FX_CACHE_TTL_HOURS))
|
||||
failure_retry_hours = int(
|
||||
config.get("FX_FAILURE_RETRY_HOURS", DEFAULT_FX_FAILURE_RETRY_HOURS)
|
||||
)
|
||||
|
||||
now = datetime.now()
|
||||
now_str = now.strftime("%Y-%m-%d_%H:%M")
|
||||
|
|
@ -75,20 +111,26 @@ def get_rates(config: flask.config.Config) -> dict[str, Decimal]:
|
|||
currency_string = ",".join(sorted(currencies))
|
||||
file_suffix = f"{currency_string}_to_GBP.json"
|
||||
existing_data = os.listdir(fx_dir)
|
||||
existing_files = [f for f in existing_data if f.endswith(".json")]
|
||||
existing_files = [f for f in existing_data if f.endswith(file_suffix)]
|
||||
|
||||
full_path: str | None = None
|
||||
latest_attempt = _latest_file(fx_dir, existing_files, currencies, valid_only=False)
|
||||
latest_valid = _latest_file(fx_dir, existing_files, currencies, valid_only=True)
|
||||
latest_valid_path = (
|
||||
os.path.join(fx_dir, latest_valid) if latest_valid is not None else None
|
||||
)
|
||||
|
||||
if existing_files:
|
||||
recent_filename = max(existing_files)
|
||||
recent = datetime.strptime(recent_filename[:16], "%Y-%m-%d_%H:%M")
|
||||
if latest_valid is not None:
|
||||
recent = _fx_cache_datetime(latest_valid)
|
||||
delta = now - recent
|
||||
|
||||
full_path = os.path.join(fx_dir, recent_filename)
|
||||
if recent_filename.endswith(file_suffix) and (
|
||||
delta < timedelta(hours=12) or config["OFFLINE_MODE"]
|
||||
):
|
||||
return read_cached_rates(full_path, currencies)
|
||||
if delta < timedelta(hours=cache_ttl_hours) or config["OFFLINE_MODE"]:
|
||||
return read_cached_rates(latest_valid_path, currencies)
|
||||
|
||||
if latest_attempt is not None:
|
||||
recent_attempt = _fx_cache_datetime(latest_attempt)
|
||||
attempt_delta = now - recent_attempt
|
||||
if attempt_delta < timedelta(hours=failure_retry_hours):
|
||||
return read_cached_rates(latest_valid_path, currencies)
|
||||
|
||||
url = "http://api.exchangerate.host/live"
|
||||
params = {"currencies": currency_string, "source": "GBP", "access_key": access_key}
|
||||
|
|
@ -98,12 +140,17 @@ def get_rates(config: flask.config.Config) -> dict[str, Decimal]:
|
|||
with httpx.Client() as client:
|
||||
response = client.get(url, params=params, timeout=10)
|
||||
except (httpx.ConnectError, httpx.ReadTimeout):
|
||||
return read_cached_rates(full_path, currencies)
|
||||
return read_cached_rates(latest_valid_path, currencies)
|
||||
|
||||
try:
|
||||
data = json.loads(response.text, parse_float=Decimal)
|
||||
except json.decoder.JSONDecodeError:
|
||||
return read_cached_rates(full_path, currencies)
|
||||
return read_cached_rates(latest_valid_path, currencies)
|
||||
|
||||
if not data.get("success", True) or not isinstance(data.get("quotes"), dict):
|
||||
with open(os.path.join(fx_dir, filename), "w") as file:
|
||||
file.write(response.text)
|
||||
return read_cached_rates(latest_valid_path, currencies)
|
||||
|
||||
with open(os.path.join(fx_dir, filename), "w") as file:
|
||||
file.write(response.text)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue