Phase 3 (Part 1): API Infrastructure - FastAPI, Database, Redis, gRPC Client
Completed Tasks (T027-T032): - ✅ FastAPI application with structured logging, CORS, global error handlers - ✅ Pydantic Settings for environment configuration - ✅ SQLAlchemy async engine with session management - ✅ Alembic migration environment setup - ✅ Redis async client with connection pooling - ✅ gRPC SDK Bridge client (placeholder - awaiting protobuf generation) Next: JWT utilities, middleware, database models 🤖 Generated with Claude Code
This commit is contained in:
126
src/api/clients/redis_client.py
Normal file
126
src/api/clients/redis_client.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
Redis client with connection pooling
|
||||
"""
|
||||
import redis.asyncio as redis
|
||||
from typing import Optional, Any
|
||||
import json
|
||||
import structlog
|
||||
from config import settings
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
class RedisClient:
|
||||
"""Async Redis client wrapper"""
|
||||
|
||||
def __init__(self):
|
||||
self._pool: Optional[redis.ConnectionPool] = None
|
||||
self._client: Optional[redis.Redis] = None
|
||||
|
||||
async def connect(self):
|
||||
"""Initialize Redis connection pool"""
|
||||
try:
|
||||
logger.info("redis_connecting", host=settings.REDIS_HOST, port=settings.REDIS_PORT)
|
||||
|
||||
self._pool = redis.ConnectionPool.from_url(
|
||||
settings.redis_url,
|
||||
max_connections=settings.REDIS_MAX_CONNECTIONS,
|
||||
decode_responses=True,
|
||||
)
|
||||
|
||||
self._client = redis.Redis(connection_pool=self._pool)
|
||||
|
||||
# Test connection
|
||||
await self._client.ping()
|
||||
|
||||
logger.info("redis_connected")
|
||||
except Exception as e:
|
||||
logger.error("redis_connection_failed", error=str(e))
|
||||
raise
|
||||
|
||||
async def close(self):
|
||||
"""Close Redis connections"""
|
||||
try:
|
||||
if self._client:
|
||||
await self._client.close()
|
||||
if self._pool:
|
||||
await self._pool.disconnect()
|
||||
logger.info("redis_closed")
|
||||
except Exception as e:
|
||||
logger.error("redis_close_failed", error=str(e))
|
||||
|
||||
async def get(self, key: str) -> Optional[str]:
|
||||
"""Get value by key"""
|
||||
if not self._client:
|
||||
raise RuntimeError("Redis client not connected")
|
||||
return await self._client.get(key)
|
||||
|
||||
async def set(self, key: str, value: Any, expire: Optional[int] = None) -> bool:
|
||||
"""Set value with optional expiration (seconds)"""
|
||||
if not self._client:
|
||||
raise RuntimeError("Redis client not connected")
|
||||
return await self._client.set(key, value, ex=expire)
|
||||
|
||||
async def delete(self, key: str) -> int:
|
||||
"""Delete key"""
|
||||
if not self._client:
|
||||
raise RuntimeError("Redis client not connected")
|
||||
return await self._client.delete(key)
|
||||
|
||||
async def exists(self, key: str) -> bool:
|
||||
"""Check if key exists"""
|
||||
if not self._client:
|
||||
raise RuntimeError("Redis client not connected")
|
||||
return await self._client.exists(key) > 0
|
||||
|
||||
async def get_json(self, key: str) -> Optional[dict]:
|
||||
"""Get JSON value"""
|
||||
value = await self.get(key)
|
||||
if value:
|
||||
return json.loads(value)
|
||||
return None
|
||||
|
||||
async def set_json(self, key: str, value: dict, expire: Optional[int] = None) -> bool:
|
||||
"""Set JSON value"""
|
||||
return await self.set(key, json.dumps(value), expire)
|
||||
|
||||
async def get_many(self, keys: list[str]) -> list[Optional[str]]:
|
||||
"""Get multiple values"""
|
||||
if not self._client:
|
||||
raise RuntimeError("Redis client not connected")
|
||||
return await self._client.mget(keys)
|
||||
|
||||
async def set_many(self, mapping: dict[str, Any]) -> bool:
|
||||
"""Set multiple key-value pairs"""
|
||||
if not self._client:
|
||||
raise RuntimeError("Redis client not connected")
|
||||
return await self._client.mset(mapping)
|
||||
|
||||
async def incr(self, key: str, amount: int = 1) -> int:
|
||||
"""Increment value"""
|
||||
if not self._client:
|
||||
raise RuntimeError("Redis client not connected")
|
||||
return await self._client.incrby(key, amount)
|
||||
|
||||
async def expire(self, key: str, seconds: int) -> bool:
|
||||
"""Set expiration on key"""
|
||||
if not self._client:
|
||||
raise RuntimeError("Redis client not connected")
|
||||
return await self._client.expire(key, seconds)
|
||||
|
||||
async def ttl(self, key: str) -> int:
|
||||
"""Get time to live for key"""
|
||||
if not self._client:
|
||||
raise RuntimeError("Redis client not connected")
|
||||
return await self._client.ttl(key)
|
||||
|
||||
# Global Redis client instance
|
||||
redis_client = RedisClient()
|
||||
|
||||
# Convenience functions
|
||||
async def init_redis():
|
||||
"""Initialize Redis connection (call on startup)"""
|
||||
await redis_client.connect()
|
||||
|
||||
async def close_redis():
|
||||
"""Close Redis connection (call on shutdown)"""
|
||||
await redis_client.close()
|
||||
Reference in New Issue
Block a user