Base code

This commit is contained in:
Kunthawat Greethong
2026-01-08 22:39:53 +07:00
parent 697115c61a
commit c35fa52117
2169 changed files with 626670 additions and 0 deletions

View File

@@ -0,0 +1,201 @@
"""
Analytics Cache Service for Backend
Provides intelligent caching for expensive analytics API calls
"""
import time
import json
from typing import Dict, Any, Optional, List
from datetime import datetime, timedelta
from loguru import logger
import hashlib
class AnalyticsCacheService:
def __init__(self):
# In-memory cache (in production, consider Redis)
self.cache: Dict[str, Dict[str, Any]] = {}
# Cache TTL configurations (in seconds)
self.TTL_CONFIG = {
'platform_status': 30 * 60, # 30 minutes
'analytics_data': 60 * 60, # 60 minutes
'user_sites': 120 * 60, # 2 hours
'bing_analytics': 60 * 60, # 1 hour for expensive Bing calls
'gsc_analytics': 60 * 60, # 1 hour for GSC calls
'bing_sites': 120 * 60, # 2 hours for Bing sites (rarely change)
}
# Cache statistics
self.stats = {
'hits': 0,
'misses': 0,
'sets': 0,
'invalidations': 0
}
logger.info("AnalyticsCacheService initialized with TTL config: {ttl}", ttl=self.TTL_CONFIG)
def _generate_cache_key(self, prefix: str, user_id: str, **kwargs) -> str:
"""Generate a unique cache key from parameters"""
# Create a deterministic key from parameters
params_str = json.dumps(kwargs, sort_keys=True) if kwargs else ""
key_data = f"{prefix}:{user_id}:{params_str}"
# Use hash to keep keys manageable
return hashlib.md5(key_data.encode()).hexdigest()
def _is_expired(self, entry: Dict[str, Any]) -> bool:
"""Check if cache entry is expired"""
if 'timestamp' not in entry:
return True
ttl = entry.get('ttl', 0)
age = time.time() - entry['timestamp']
return age > ttl
def get(self, prefix: str, user_id: str, **kwargs) -> Optional[Any]:
"""Get cached data if valid"""
cache_key = self._generate_cache_key(prefix, user_id, **kwargs)
if cache_key not in self.cache:
logger.debug("Cache MISS: {key}", key=cache_key)
self.stats['misses'] += 1
return None
entry = self.cache[cache_key]
if self._is_expired(entry):
logger.debug("Cache EXPIRED: {key}", key=cache_key)
del self.cache[cache_key]
self.stats['misses'] += 1
return None
logger.debug("Cache HIT: {key} (age: {age}s)",
key=cache_key,
age=int(time.time() - entry['timestamp']))
self.stats['hits'] += 1
return entry['data']
def set(self, prefix: str, user_id: str, data: Any, ttl_override: Optional[int] = None, **kwargs) -> None:
"""Set cached data with TTL"""
cache_key = self._generate_cache_key(prefix, user_id, **kwargs)
ttl = ttl_override or self.TTL_CONFIG.get(prefix, 300) # Default 5 minutes
self.cache[cache_key] = {
'data': data,
'timestamp': time.time(),
'ttl': ttl,
'created_at': datetime.now().isoformat()
}
logger.info("Cache SET: {prefix} for user {user_id} (TTL: {ttl}s)",
prefix=prefix, user_id=user_id, ttl=ttl)
self.stats['sets'] += 1
def invalidate(self, prefix: str, user_id: Optional[str] = None, **kwargs) -> int:
"""Invalidate cache entries matching pattern"""
pattern_key = self._generate_cache_key(prefix, user_id or "*", **kwargs)
pattern_prefix = pattern_key.split(':')[0] + ':'
keys_to_delete = []
for key in self.cache.keys():
if key.startswith(pattern_prefix):
if user_id is None or user_id in key:
keys_to_delete.append(key)
for key in keys_to_delete:
del self.cache[key]
logger.info("Cache INVALIDATED: {count} entries matching {pattern}",
count=len(keys_to_delete), pattern=pattern_prefix)
self.stats['invalidations'] += len(keys_to_delete)
return len(keys_to_delete)
def invalidate_user(self, user_id: str) -> int:
"""Invalidate all cache entries for a specific user"""
keys_to_delete = [key for key in self.cache.keys() if user_id in key]
for key in keys_to_delete:
del self.cache[key]
logger.info("Cache INVALIDATED: {count} entries for user {user_id}",
count=len(keys_to_delete), user_id=user_id)
self.stats['invalidations'] += len(keys_to_delete)
return len(keys_to_delete)
def cleanup_expired(self) -> int:
"""Remove expired entries from cache"""
keys_to_delete = []
for key, entry in self.cache.items():
if self._is_expired(entry):
keys_to_delete.append(key)
for key in keys_to_delete:
del self.cache[key]
if keys_to_delete:
logger.info("Cache CLEANUP: Removed {count} expired entries", count=len(keys_to_delete))
return len(keys_to_delete)
def get_stats(self) -> Dict[str, Any]:
"""Get cache statistics"""
total_requests = self.stats['hits'] + self.stats['misses']
hit_rate = (self.stats['hits'] / total_requests * 100) if total_requests > 0 else 0
return {
'cache_size': len(self.cache),
'hit_rate': round(hit_rate, 2),
'total_requests': total_requests,
'hits': self.stats['hits'],
'misses': self.stats['misses'],
'sets': self.stats['sets'],
'invalidations': self.stats['invalidations'],
'ttl_config': self.TTL_CONFIG
}
def clear_all(self) -> None:
"""Clear all cache entries"""
self.cache.clear()
logger.info("Cache CLEARED: All entries removed")
def get_cache_info(self) -> Dict[str, Any]:
"""Get detailed cache information for debugging"""
cache_info = {}
for key, entry in self.cache.items():
age = int(time.time() - entry['timestamp'])
remaining_ttl = max(0, entry['ttl'] - age)
cache_info[key] = {
'age_seconds': age,
'remaining_ttl_seconds': remaining_ttl,
'created_at': entry.get('created_at', 'unknown'),
'data_size': len(str(entry['data'])) if entry['data'] else 0
}
return cache_info
# Global cache instance
analytics_cache = AnalyticsCacheService()
# Cleanup expired entries every 5 minutes
import threading
import time
def cleanup_worker():
"""Background worker to clean up expired cache entries"""
while True:
try:
time.sleep(300) # 5 minutes
analytics_cache.cleanup_expired()
except Exception as e:
logger.error("Cache cleanup error: {error}", error=e)
# Start cleanup thread
cleanup_thread = threading.Thread(target=cleanup_worker, daemon=True)
cleanup_thread.start()
logger.info("Analytics cache cleanup thread started")