mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-29 05:21:48 +00:00
* merge branch develop with main * bump desktop version * fix the merge * sync Cargo.lock to desktop 2.0.0 * fix cli conftest.py --------- Co-authored-by: deeleeramone <>
242 lines
5.1 KiB
Python
242 lines
5.1 KiB
Python
"""On-disk response cache for the FRED API."""
|
|
|
|
import os
|
|
import re
|
|
from typing import Any
|
|
|
|
ROOT_URL = "https://api.stlouisfed.org"
|
|
|
|
HOUR = 60 * 60
|
|
DAY = 24 * HOUR
|
|
|
|
DEFAULT_TTL = HOUR
|
|
|
|
TTL_BY_PATH = {
|
|
"/fred/series/observations": 4 * HOUR,
|
|
"/fred/series/search": DAY,
|
|
"/fred/series/updates": HOUR,
|
|
"/fred/series": DAY,
|
|
"/fred/release/tables": 6 * HOUR,
|
|
"/fred/release/series": DAY,
|
|
"/fred/releases": DAY,
|
|
"/fred/category": 7 * DAY,
|
|
"/geofred/series/group": 7 * DAY,
|
|
"/geofred/series/data": DAY,
|
|
"/geofred/regional/data": DAY,
|
|
}
|
|
|
|
_API_KEY_RE = re.compile(r"([?&])api_key=[^&]*")
|
|
|
|
_cache = None
|
|
_cache_directory = ""
|
|
|
|
DEFAULT_SIZE_LIMIT = 512 * 1024 * 1024
|
|
|
|
|
|
def _float_env(name: str, default: float) -> float:
|
|
"""Read a float from the environment, falling back to a default."""
|
|
raw = os.environ.get(name)
|
|
|
|
if not raw:
|
|
return default
|
|
|
|
try:
|
|
return float(raw)
|
|
except ValueError:
|
|
return default
|
|
|
|
|
|
def enabled() -> bool:
|
|
"""Return whether responses are cached on disk.
|
|
|
|
Returns
|
|
-------
|
|
bool
|
|
False when ``OPENBB_FRED_DISK_CACHE`` switches the cache off.
|
|
"""
|
|
return os.environ.get("OPENBB_FRED_DISK_CACHE", "1") not in ("0", "false", "False")
|
|
|
|
|
|
def size_limit() -> int:
|
|
"""Return the size the cache is held to, in bytes.
|
|
|
|
Returns
|
|
-------
|
|
int
|
|
The limit, from ``OPENBB_FRED_DISK_CACHE_SIZE``.
|
|
"""
|
|
return int(_float_env("OPENBB_FRED_DISK_CACHE_SIZE", DEFAULT_SIZE_LIMIT))
|
|
|
|
|
|
def ttl_multiplier() -> float:
|
|
"""Return the factor every cache lifetime is scaled by.
|
|
|
|
Returns
|
|
-------
|
|
float
|
|
The multiplier, from ``OPENBB_FRED_DISK_TTL_MULTIPLIER``.
|
|
"""
|
|
return _float_env("OPENBB_FRED_DISK_TTL_MULTIPLIER", 1.0)
|
|
|
|
|
|
def cache_directory() -> str:
|
|
"""Return the directory the cache is held in.
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
The directory, from ``OPENBB_FRED_DISK_CACHE_DIR`` when it is set.
|
|
"""
|
|
from openbb_core.app.utils import get_user_cache_directory
|
|
|
|
return (
|
|
os.environ.get("OPENBB_FRED_DISK_CACHE_DIR")
|
|
or f"{get_user_cache_directory()}/fred"
|
|
)
|
|
|
|
|
|
def redact(url: str) -> str:
|
|
"""Return a URL with the API key replaced.
|
|
|
|
Parameters
|
|
----------
|
|
url : str
|
|
The request URL.
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
The URL with any ``api_key`` value redacted.
|
|
"""
|
|
return _API_KEY_RE.sub(r"\1api_key=__redacted__", url)
|
|
|
|
|
|
def ttl_for(url: str) -> float:
|
|
"""Return the cache lifetime for a FRED URL.
|
|
|
|
Parameters
|
|
----------
|
|
url : str
|
|
The request URL.
|
|
|
|
Returns
|
|
-------
|
|
float
|
|
The time to live, in seconds.
|
|
"""
|
|
path = url.split("?", 1)[0].replace(ROOT_URL, "")
|
|
matches = [p for p in TTL_BY_PATH if path.startswith(p)]
|
|
ttl = TTL_BY_PATH[max(matches, key=len)] if matches else DEFAULT_TTL
|
|
|
|
return ttl * ttl_multiplier()
|
|
|
|
|
|
def get_disk_cache():
|
|
"""Return the process-wide on-disk cache.
|
|
|
|
Returns
|
|
-------
|
|
Cache or None
|
|
The cache, or None when disk caching is switched off. The cache is
|
|
rebuilt when the configured directory changes.
|
|
"""
|
|
global _cache, _cache_directory # noqa: PLW0603
|
|
|
|
if not enabled():
|
|
return None
|
|
|
|
directory = cache_directory()
|
|
|
|
if _cache is None or _cache_directory != directory:
|
|
from diskcache import Cache
|
|
|
|
close()
|
|
_cache = Cache(directory=directory, size_limit=size_limit())
|
|
_cache_directory = directory
|
|
|
|
return _cache
|
|
|
|
|
|
async def disk_get(url: str) -> Any:
|
|
"""Read a cached payload for a URL.
|
|
|
|
Parameters
|
|
----------
|
|
url : str
|
|
The request URL.
|
|
|
|
Returns
|
|
-------
|
|
Any
|
|
The cached payload, or None when absent or expired.
|
|
"""
|
|
import asyncio
|
|
|
|
cache = get_disk_cache()
|
|
|
|
if cache is None:
|
|
return None
|
|
|
|
return await asyncio.to_thread(cache.get, redact(url))
|
|
|
|
|
|
async def disk_set(url: str, value: Any) -> None:
|
|
"""Write a payload to the cache under its endpoint's lifetime.
|
|
|
|
Parameters
|
|
----------
|
|
url : str
|
|
The request URL.
|
|
value : Any
|
|
The payload to store.
|
|
"""
|
|
import asyncio
|
|
|
|
cache = get_disk_cache()
|
|
|
|
if cache is None:
|
|
return
|
|
|
|
await asyncio.to_thread(cache.set, redact(url), value, expire=ttl_for(url))
|
|
|
|
|
|
def clear() -> None:
|
|
"""Empty the on-disk cache."""
|
|
cache = get_disk_cache()
|
|
|
|
if cache is not None:
|
|
cache.clear()
|
|
|
|
|
|
def close() -> None:
|
|
"""Release the on-disk cache, so the next read opens it again."""
|
|
global _cache, _cache_directory # noqa: PLW0603
|
|
|
|
if _cache is not None:
|
|
_cache.close()
|
|
|
|
_cache = None
|
|
_cache_directory = ""
|
|
|
|
|
|
def stats() -> dict:
|
|
"""Return the cache's size and item count.
|
|
|
|
Returns
|
|
-------
|
|
dict
|
|
Whether caching is on, and the directory, item count, and volume in
|
|
bytes when it is.
|
|
"""
|
|
cache = get_disk_cache()
|
|
|
|
if cache is None:
|
|
return {"enabled": False, "items": 0, "bytes": 0}
|
|
|
|
return {
|
|
"enabled": True,
|
|
"directory": cache.directory,
|
|
"items": len(cache),
|
|
"bytes": cache.volume(),
|
|
}
|