LifeRPG_v2.0/modern/backend/advanced_cache.py
Copilot 90750ee8df
Strip emoji from docs, fix XSS/hashing vulnerabilities, remediate all failing CI checks (#1)
* Initial plan

* Fix security vulnerabilities: MD5→SHA-256, XSS via dangerouslySetInnerHTML/innerHTML, insecure randomness, CodeQL config

Co-authored-by: TLimoges33 <125313326+TLimoges33@users.noreply.github.com>

* Clean up README: remove decorative emojis for a professional tone

Remove all emojis from section headers, list item prefixes, and
decorative positions. Replace  phase status markers with '(Complete)'
text. Keep the  in the final call-to-action line. No changes to
links, badges, code blocks, or technical content.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: remove emoji characters from CONTRIBUTING.md

Remove all emoji from section headers and closing line while
preserving links, code blocks, and technical content.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: remove emoji characters from documentation files

Remove all emoji characters from 8 documentation files in docs/.
Replace status-marker checkmarks () with '(Done)' text.
Remove decorative emojis from headers and body text entirely.
Preserve emojis inside code blocks unchanged.
Clean up trailing whitespace introduced by removals.

Files modified:
- DEPLOYMENT_GUIDE.md
- IMPLEMENTATION_PLAN.md
- MILESTONE_6_SUMMARY.md
- PRODUCTION_ROADMAP.md
- PROJECT_STATUS.md
- REPOSITORY_ENHANCEMENT.md
- ROADMAP.md
- SECURITY_AUDIT_ROADMAP.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs: remove emoji characters from documentation files

Remove all emoji characters from 9 markdown files while preserving
code block content (box-drawing characters, indentation). Emojis
removed from headers, list items, and body text across READMEs,
issue templates, PR template, runbook, and mobile docs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove excessive emoji from all documentation for professional presentation

Co-authored-by: TLimoges33 <125313326+TLimoges33@users.noreply.github.com>

* Fix PluginWidget initial state and remove || true from security audit steps

Co-authored-by: TLimoges33 <125313326+TLimoges33@users.noreply.github.com>

* Remediate all failing CI checks: update deprecated actions, fix npm vulnerabilities, fix migrations YAML

Co-authored-by: SynOSdev <257853113+SynOSdev@users.noreply.github.com>

* Fix all remaining CI failures: Node 18→20, fix test API contract, fix pytest version, fix Postgres health checks

Co-authored-by: SynOSdev <257853113+SynOSdev@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: TLimoges33 <125313326+TLimoges33@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: SynOSdev <257853113+SynOSdev@users.noreply.github.com>
2026-03-14 08:59:37 -04:00

257 lines
8.1 KiB
Python

"""
Advanced caching system for LifeRPG with multi-level cache strategy.
"""
import json
import time
import hashlib
import functools
from typing import Any, Dict, Optional, Union, Callable
from datetime import datetime, timedelta
from cachetools import TTLCache
import asyncio
import os
try:
import redis
REDIS_AVAILABLE = True
except ImportError:
REDIS_AVAILABLE = False
class AdvancedCacheManager:
"""Multi-level cache manager with Redis and memory fallback."""
def __init__(self, redis_url: Optional[str] = None):
# Memory cache (L1) - fastest access
self.memory_cache = TTLCache(maxsize=1000, ttl=300) # 5 minutes
# Redis cache (L2) - persistent, shared
self.redis_client = None
if REDIS_AVAILABLE and redis_url:
try:
self.redis_client = redis.from_url(redis_url)
# Test connection
self.redis_client.ping()
except Exception:
self.redis_client = None
self.stats = {
'memory_hits': 0,
'memory_misses': 0,
'redis_hits': 0,
'redis_misses': 0,
'total_requests': 0
}
def _generate_cache_key(self, prefix: str, *args, **kwargs) -> str:
"""Generate a consistent cache key from function arguments."""
key_data = f"{prefix}:{str(args)}:{str(sorted(kwargs.items()))}"
return hashlib.sha256(key_data.encode()).hexdigest()
async def get(self, key: str) -> Optional[Any]:
"""Get value from cache with fallback strategy."""
self.stats['total_requests'] += 1
# L1: Check memory cache first
if key in self.memory_cache:
self.stats['memory_hits'] += 1
return self.memory_cache[key]
self.stats['memory_misses'] += 1
# L2: Check Redis cache
if self.redis_client:
try:
value = await self.redis_client.get(key)
if value:
self.stats['redis_hits'] += 1
# Deserialize and populate memory cache
deserialized = json.loads(value)
self.memory_cache[key] = deserialized
return deserialized
except Exception:
pass
self.stats['redis_misses'] += 1
return None
async def set(self, key: str, value: Any, ttl: int = 300) -> bool:
"""Set value in both cache levels."""
try:
# Set in memory cache
self.memory_cache[key] = value
# Set in Redis cache
if self.redis_client:
serialized = json.dumps(value, default=str)
await self.redis_client.setex(key, ttl, serialized)
return True
except Exception:
return False
async def delete(self, key: str) -> bool:
"""Delete from both cache levels."""
try:
# Remove from memory cache
if key in self.memory_cache:
del self.memory_cache[key]
# Remove from Redis
if self.redis_client:
await self.redis_client.delete(key)
return True
except Exception:
return False
async def get_with_fallback(self, key: str, fallback_func: Callable, ttl: int = 300) -> Any:
"""Get from cache or execute fallback function and cache result."""
value = await self.get(key)
if value is not None:
return value
# Execute fallback function
if asyncio.iscoroutinefunction(fallback_func):
result = await fallback_func()
else:
result = fallback_func()
# Cache the result
await self.set(key, result, ttl)
return result
def get_stats(self) -> Dict[str, Any]:
"""Get cache performance statistics."""
total = self.stats['total_requests']
if total == 0:
return self.stats
memory_hit_rate = self.stats['memory_hits'] / total
redis_hit_rate = self.stats['redis_hits'] / total
overall_hit_rate = (self.stats['memory_hits'] + self.stats['redis_hits']) / total
return {
**self.stats,
'memory_hit_rate': memory_hit_rate,
'redis_hit_rate': redis_hit_rate,
'overall_hit_rate': overall_hit_rate,
'memory_cache_size': len(self.memory_cache)
}
async def clear_all(self) -> bool:
"""Clear all cache levels."""
try:
self.memory_cache.clear()
if self.redis_client:
await self.redis_client.flushdb()
return True
except Exception:
return False
# Global cache instance
cache_manager = AdvancedCacheManager(
redis_url=os.getenv('REDIS_URL', 'redis://localhost:6379/0')
)
def cached_response(ttl: int = 300, key_prefix: str = ""):
"""Decorator for caching function responses."""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
async def async_wrapper(*args, **kwargs):
# Generate cache key
cache_key = cache_manager._generate_cache_key(
f"{key_prefix}:{func.__name__}", *args, **kwargs
)
# Try to get from cache
result = await cache_manager.get(cache_key)
if result is not None:
return result
# Execute function and cache result
if asyncio.iscoroutinefunction(func):
result = await func(*args, **kwargs)
else:
result = func(*args, **kwargs)
await cache_manager.set(cache_key, result, ttl)
return result
@functools.wraps(func)
def sync_wrapper(*args, **kwargs):
# For synchronous functions, use asyncio.run
return asyncio.run(async_wrapper(*args, **kwargs))
# Return appropriate wrapper based on function type
if asyncio.iscoroutinefunction(func):
return async_wrapper
else:
return sync_wrapper
return decorator
def cache_key(*key_parts: str) -> str:
"""Generate a cache key from parts."""
return ":".join(str(part) for part in key_parts)
class QueryCache:
"""Specialized cache for database queries."""
@staticmethod
@cached_response(ttl=600, key_prefix="query")
def get_user_habits(user_id: int, status: str = "active"):
"""Cache user habits query."""
# This will be implemented in the analytics module
pass
@staticmethod
@cached_response(ttl=300, key_prefix="analytics")
def get_habit_completion_stats(user_id: int, days: int = 30):
"""Cache habit completion analytics."""
pass
@staticmethod
@cached_response(ttl=900, key_prefix="leaderboard")
def get_leaderboard_data(timeframe: str = "weekly"):
"""Cache leaderboard data."""
pass
class CacheWarmer:
"""Proactively warm cache with commonly requested data."""
def __init__(self, cache_manager: AdvancedCacheManager):
self.cache_manager = cache_manager
async def warm_user_data(self, user_id: int):
"""Pre-populate cache with user's most common data."""
# Warm user habits
await self.cache_manager.get_with_fallback(
f"user:{user_id}:habits",
lambda: self._fetch_user_habits(user_id),
ttl=600
)
# Warm user analytics
await self.cache_manager.get_with_fallback(
f"user:{user_id}:analytics:30d",
lambda: self._fetch_user_analytics(user_id, 30),
ttl=300
)
def _fetch_user_habits(self, user_id: int):
"""Fetch user habits (placeholder - to be implemented)."""
return []
def _fetch_user_analytics(self, user_id: int, days: int):
"""Fetch user analytics (placeholder - to be implemented)."""
return {}
# Initialize cache warmer
cache_warmer = CacheWarmer(cache_manager)