From 5784c7fdf5a10fbad63de1ab280deb3ce2223204 Mon Sep 17 00:00:00 2001 From: Chelseaquiin Date: Mon, 11 Aug 2025 11:39:10 +0100 Subject: [PATCH 1/4] Feat: Implemented Caching --- app/services/cache.py | 81 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 app/services/cache.py diff --git a/app/services/cache.py b/app/services/cache.py new file mode 100644 index 0000000..df5361a --- /dev/null +++ b/app/services/cache.py @@ -0,0 +1,81 @@ +# app/services/cache.py +import json +from typing import Any, Optional, List +import redis.asyncio as aioredis +from app.config.settings import settings + +class Cache: + def __init__(self, url: Optional[str] = None, prefix: Optional[str] = None): + self._url = url or settings.REDIS_URL + self._prefix = prefix or settings.REDIS_PREFIX + self._client: Optional[aioredis.Redis] = None + + async def init(self): + if self._client is None: + # decode_responses=True returns str instead of bytes + self._client = aioredis.from_url(self._url, decode_responses=True) + + def _key(self, key: str) -> str: + return f"{self._prefix}{key}" + + async def get(self, key: str) -> Optional[Any]: + await self.init() + value = await self._client.get(self._key(key)) + if value is None: + return None + try: + return json.loads(value) + except Exception: + return value + + async def set(self, key: str, value: Any, ex: Optional[int] = None): + await self.init() + ex = ex if ex is not None else settings.REDIS_DEFAULT_TTL + if isinstance(value, (dict, list)): + to_store = json.dumps(value) + else: + to_store = str(value) + await self._client.set(self._key(key), to_store, ex=ex) + + async def delete(self, key: str): + await self.init() + await self._client.delete(self._key(key)) + + async def scan_keys(self, pattern: str = "*") -> List[str]: + """Safely iterate keys using scan (avoid KEYS in production).""" + await self.init() + full_pattern = f"{self._prefix}{pattern}" + keys = [] + async for k in self._client.scan_iter(match=full_pattern): + # remove prefix for readability + keys.append(k.replace(self._prefix, "", 1)) + return keys + + async def flush_prefix(self): + await self.init() + keys = [] + async for k in self._client.scan_iter(match=f"{self._prefix}*"): + keys.append(k) + if keys: + await self._client.delete(*keys) + + async def info(self) -> dict: + await self.init() + return await self._client.info() + + async def close(self): + if self._client is not None: + await self._client.close() + self._client = None + + # helper: get or set (atomically read-if-exists else set) + async def get_or_set(self, key: str, factory, ex: Optional[int] = None): + val = await self.get(key) + if val is not None: + return val + result = await factory() + await self.set(key, result, ex=ex) + return result + +# create a module-level instance you can import +cache = Cache() From 9b2064e51aef0948f8e6134d8e645eacf4f28dbe Mon Sep 17 00:00:00 2001 From: Chelseaquiin Date: Mon, 11 Aug 2025 11:41:03 +0100 Subject: [PATCH 2/4] Added the config settings for Redis --- app/config/settings.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/config/settings.py b/app/config/settings.py index ef87a96..90a4150 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -49,6 +49,12 @@ class Settings(BaseSettings): # Security secret_key: str = Field(..., env="SECRET_KEY") access_token_expire_minutes: int = Field(30, env="ACCESS_TOKEN_EXPIRE_MINUTES") + + # Redis + redis_host: str = Field("localhost", env="REDIS_HOST") + redis_port: int = Field(6379, env="REDIS_PORT") + redis_db: int = Field(0, env="REDIS_DB") + redis_password: str | None = Field(None, env="REDIS_PASSWORD") @field_validator("cors_origins", mode="before") @classmethod From 5fa0e86a98cf79c56b531f9be3c836cd8e90c7c4 Mon Sep 17 00:00:00 2001 From: Chelseaquiin Date: Mon, 11 Aug 2025 11:41:44 +0100 Subject: [PATCH 3/4] Initialized Redis --- main.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/main.py b/main.py index fba1d9f..3e56552 100644 --- a/main.py +++ b/main.py @@ -12,6 +12,7 @@ from app.config.logger import Logger from app.config.settings import settings from app.api.v1.agents_router import router as agents_router +from app.services.cache import init_cache # Setup logging Logger.setup_root_logger() @@ -28,6 +29,11 @@ async def lifespan(app: FastAPI): logger.info(f"Environment: {settings.environment}") logger.info(f"Model: {settings.google_model_name}") logger.info(f"Venue: {settings.conference_venue_name}") + + # Initialize Redis + logger.info("Connecting to Redis...") + init_cache() + logger.info("Redis connected.") yield From e625dc0590a4dae23a5928f7177c577d976ae415 Mon Sep 17 00:00:00 2001 From: Chelseaquiin Date: Mon, 11 Aug 2025 11:44:05 +0100 Subject: [PATCH 4/4] Created redis connection. Set, get, delete redis key --- app/services/cache.py | 90 ++++++++----------------------------------- 1 file changed, 17 insertions(+), 73 deletions(-) diff --git a/app/services/cache.py b/app/services/cache.py index df5361a..83228c2 100644 --- a/app/services/cache.py +++ b/app/services/cache.py @@ -1,81 +1,25 @@ # app/services/cache.py -import json -from typing import Any, Optional, List -import redis.asyncio as aioredis +import redis from app.config.settings import settings -class Cache: - def __init__(self, url: Optional[str] = None, prefix: Optional[str] = None): - self._url = url or settings.REDIS_URL - self._prefix = prefix or settings.REDIS_PREFIX - self._client: Optional[aioredis.Redis] = None - async def init(self): - if self._client is None: - # decode_responses=True returns str instead of bytes - self._client = aioredis.from_url(self._url, decode_responses=True) +redis_client = None - def _key(self, key: str) -> str: - return f"{self._prefix}{key}" +def init_cache(): + global redis_client + redis_client = redis.Redis( + host=settings.redis_host, + port=settings.redis_port, + db=settings.redis_db, + password=settings.redis_password, + decode_responses=True + ) - async def get(self, key: str) -> Optional[Any]: - await self.init() - value = await self._client.get(self._key(key)) - if value is None: - return None - try: - return json.loads(value) - except Exception: - return value +def set_cache(key, value, expire_seconds=3600): + redis_client.set(key, value, ex=expire_seconds) - async def set(self, key: str, value: Any, ex: Optional[int] = None): - await self.init() - ex = ex if ex is not None else settings.REDIS_DEFAULT_TTL - if isinstance(value, (dict, list)): - to_store = json.dumps(value) - else: - to_store = str(value) - await self._client.set(self._key(key), to_store, ex=ex) +def get_cache(key): + return redis_client.get(key) - async def delete(self, key: str): - await self.init() - await self._client.delete(self._key(key)) - - async def scan_keys(self, pattern: str = "*") -> List[str]: - """Safely iterate keys using scan (avoid KEYS in production).""" - await self.init() - full_pattern = f"{self._prefix}{pattern}" - keys = [] - async for k in self._client.scan_iter(match=full_pattern): - # remove prefix for readability - keys.append(k.replace(self._prefix, "", 1)) - return keys - - async def flush_prefix(self): - await self.init() - keys = [] - async for k in self._client.scan_iter(match=f"{self._prefix}*"): - keys.append(k) - if keys: - await self._client.delete(*keys) - - async def info(self) -> dict: - await self.init() - return await self._client.info() - - async def close(self): - if self._client is not None: - await self._client.close() - self._client = None - - # helper: get or set (atomically read-if-exists else set) - async def get_or_set(self, key: str, factory, ex: Optional[int] = None): - val = await self.get(key) - if val is not None: - return val - result = await factory() - await self.set(key, result, ex=ex) - return result - -# create a module-level instance you can import -cache = Cache() +def delete_cache(key): + redis_client.delete(key)