Arjit
Add ALL missing src/data Python modules
9d33587
"""
Data caching layer to reduce API calls and improve performance.
"""
import os
import json
import pickle
from typing import Any, Optional, Callable
from datetime import datetime, timedelta
from pathlib import Path
import pandas as pd
from functools import wraps
class DataCache:
"""Simple file-based cache for data fetching."""
def __init__(self, cache_dir: str = ".cache", ttl_minutes: int = 15):
"""
Initialize data cache.
Args:
cache_dir: Directory to store cache files
ttl_minutes: Time-to-live for cached data in minutes
"""
self.cache_dir = Path(cache_dir)
self.cache_dir.mkdir(exist_ok=True)
self.ttl = timedelta(minutes=ttl_minutes)
def _get_cache_path(self, key: str) -> Path:
"""Get path for cache file."""
# Sanitize key for filesystem
safe_key = key.replace('/', '_').replace(':', '_').replace(' ', '_')
return self.cache_dir / f"{safe_key}.pkl"
def _get_metadata_path(self, key: str) -> Path:
"""Get path for metadata file."""
safe_key = key.replace('/', '_').replace(':', '_').replace(' ', '_')
return self.cache_dir / f"{safe_key}_meta.json"
def get(self, key: str) -> Optional[Any]:
"""
Get data from cache if it exists and is not expired.
Args:
key: Cache key
Returns:
Cached data or None if not found or expired
"""
cache_path = self._get_cache_path(key)
meta_path = self._get_metadata_path(key)
# Check if cache exists
if not cache_path.exists() or not meta_path.exists():
return None
try:
# Read metadata
with open(meta_path, 'r') as f:
metadata = json.load(f)
# Check if expired
cached_time = datetime.fromisoformat(metadata['timestamp'])
if datetime.now() - cached_time > self.ttl:
# Cache expired, remove files
cache_path.unlink(missing_ok=True)
meta_path.unlink(missing_ok=True)
return None
# Read cached data
with open(cache_path, 'rb') as f:
data = pickle.load(f)
return data
except Exception as e:
print(f"Warning: Failed to read cache for {key}: {str(e)}")
return None
def set(self, key: str, data: Any) -> None:
"""
Store data in cache.
Args:
key: Cache key
data: Data to cache
"""
cache_path = self._get_cache_path(key)
meta_path = self._get_metadata_path(key)
try:
# Write data
with open(cache_path, 'wb') as f:
pickle.dump(data, f)
# Write metadata
metadata = {
'timestamp': datetime.now().isoformat(),
'key': key
}
with open(meta_path, 'w') as f:
json.dump(metadata, f)
except Exception as e:
print(f"Warning: Failed to write cache for {key}: {str(e)}")
def clear(self, key: Optional[str] = None) -> None:
"""
Clear cache.
Args:
key: Optional specific key to clear. If None, clears all cache.
"""
if key:
# Clear specific key
cache_path = self._get_cache_path(key)
meta_path = self._get_metadata_path(key)
cache_path.unlink(missing_ok=True)
meta_path.unlink(missing_ok=True)
else:
# Clear all cache
for file in self.cache_dir.glob('*'):
file.unlink()
def get_size(self) -> int:
"""Get total cache size in bytes."""
return sum(f.stat().st_size for f in self.cache_dir.glob('*'))
def get_stats(self) -> dict:
"""Get cache statistics."""
files = list(self.cache_dir.glob('*.pkl'))
total_size = self.get_size()
return {
'num_items': len(files),
'total_size_bytes': total_size,
'total_size_mb': total_size / (1024 * 1024),
'cache_dir': str(self.cache_dir)
}
def cached(cache_key_fn: Callable[[Any], str], ttl_minutes: int = 15):
"""
Decorator to cache function results.
Args:
cache_key_fn: Function to generate cache key from function arguments
ttl_minutes: Time-to-live for cached data in minutes
Example:
@cached(lambda ticker, days: f"options_{ticker}_{days}", ttl_minutes=15)
def get_options(ticker, days):
# Expensive API call
return fetch_data(ticker, days)
"""
cache = DataCache(ttl_minutes=ttl_minutes)
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Generate cache key
cache_key = cache_key_fn(*args, **kwargs)
# Try to get from cache
cached_data = cache.get(cache_key)
if cached_data is not None:
return cached_data
# Call function and cache result
result = func(*args, **kwargs)
cache.set(cache_key, result)
return result
return wrapper
return decorator
# Create global cache instance
_global_cache = DataCache()
def get_cache() -> DataCache:
"""Get global cache instance."""
return _global_cache
def clear_cache(key: Optional[str] = None) -> None:
"""
Clear global cache.
Args:
key: Optional specific key to clear. If None, clears all cache.
"""
_global_cache.clear(key)
def get_cache_stats() -> dict:
"""Get global cache statistics."""
return _global_cache.get_stats()