Add DEMO feature to the project

This commit is contained in:
Urtzi Alfaro
2025-10-03 14:09:34 +02:00
parent 1243c2ca6d
commit dc8221bd2f
77 changed files with 6251 additions and 1074 deletions

View File

@@ -0,0 +1,7 @@
"""Demo Session Service Core"""
from .config import settings
from .database import DatabaseManager, get_db
from .redis_client import RedisClient, get_redis
__all__ = ["settings", "DatabaseManager", "get_db", "RedisClient", "get_redis"]

View File

@@ -0,0 +1,66 @@
"""
Demo Session Service Configuration
"""
import os
from pydantic_settings import BaseSettings
from typing import Optional
class Settings(BaseSettings):
"""Demo Session Service Settings"""
# Service info
SERVICE_NAME: str = "demo-session"
VERSION: str = "1.0.0"
DEBUG: bool = os.getenv("DEBUG", "false").lower() == "true"
# Database
DATABASE_URL: str = os.getenv(
"DEMO_SESSION_DATABASE_URL",
"postgresql+asyncpg://postgres:postgres@localhost:5432/demo_session_db"
)
# Redis
REDIS_URL: str = os.getenv("REDIS_URL", "redis://localhost:6379/0")
REDIS_KEY_PREFIX: str = "demo:session"
REDIS_SESSION_TTL: int = 1800 # 30 minutes
# Demo session configuration
DEMO_SESSION_DURATION_MINUTES: int = 30
DEMO_SESSION_MAX_EXTENSIONS: int = 3
DEMO_SESSION_CLEANUP_INTERVAL_MINUTES: int = 60
# Demo account credentials (public)
DEMO_ACCOUNTS: dict = {
"individual_bakery": {
"email": "demo.individual@panaderiasanpablo.com",
"name": "Panadería San Pablo - Demo",
"subdomain": "demo-sanpablo"
},
"central_baker": {
"email": "demo.central@panaderialaespiga.com",
"name": "Panadería La Espiga - Demo",
"subdomain": "demo-laespiga"
}
}
# Service URLs
AUTH_SERVICE_URL: str = os.getenv("AUTH_SERVICE_URL", "http://auth-service:8000")
TENANT_SERVICE_URL: str = os.getenv("TENANT_SERVICE_URL", "http://tenant-service:8000")
INVENTORY_SERVICE_URL: str = os.getenv("INVENTORY_SERVICE_URL", "http://inventory-service:8000")
RECIPES_SERVICE_URL: str = os.getenv("RECIPES_SERVICE_URL", "http://recipes-service:8000")
SALES_SERVICE_URL: str = os.getenv("SALES_SERVICE_URL", "http://sales-service:8000")
ORDERS_SERVICE_URL: str = os.getenv("ORDERS_SERVICE_URL", "http://orders-service:8000")
PRODUCTION_SERVICE_URL: str = os.getenv("PRODUCTION_SERVICE_URL", "http://production-service:8000")
SUPPLIERS_SERVICE_URL: str = os.getenv("SUPPLIERS_SERVICE_URL", "http://suppliers-service:8000")
# Logging
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
class Config:
env_file = ".env"
case_sensitive = True
settings = Settings()

View File

@@ -0,0 +1,61 @@
"""
Database connection management for Demo Session Service
"""
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.pool import NullPool
import structlog
from .config import settings
logger = structlog.get_logger()
class DatabaseManager:
"""Database connection manager"""
def __init__(self, database_url: str = None):
self.database_url = database_url or settings.DATABASE_URL
self.engine = None
self.session_factory = None
def initialize(self):
"""Initialize database engine and session factory"""
self.engine = create_async_engine(
self.database_url,
echo=settings.DEBUG,
poolclass=NullPool,
pool_pre_ping=True
)
self.session_factory = async_sessionmaker(
self.engine,
class_=AsyncSession,
expire_on_commit=False,
autocommit=False,
autoflush=False
)
logger.info("Database manager initialized", database_url=self.database_url.split("@")[-1])
async def close(self):
"""Close database connections"""
if self.engine:
await self.engine.dispose()
logger.info("Database connections closed")
async def get_session(self) -> AsyncSession:
"""Get database session"""
if not self.session_factory:
self.initialize()
async with self.session_factory() as session:
yield session
db_manager = DatabaseManager()
async def get_db() -> AsyncSession:
"""Dependency for FastAPI"""
async for session in db_manager.get_session():
yield session

View File

@@ -0,0 +1,164 @@
"""
Redis client for demo session data caching
"""
import redis.asyncio as redis
from typing import Optional, Any
import json
import structlog
from datetime import timedelta
from .config import settings
logger = structlog.get_logger()
class RedisClient:
"""Redis client for session data"""
def __init__(self, redis_url: str = None):
self.redis_url = redis_url or settings.REDIS_URL
self.client: Optional[redis.Redis] = None
self.key_prefix = settings.REDIS_KEY_PREFIX
async def connect(self):
"""Connect to Redis"""
if not self.client:
self.client = await redis.from_url(
self.redis_url,
encoding="utf-8",
decode_responses=True
)
logger.info("Redis client connected", redis_url=self.redis_url.split("@")[-1])
async def close(self):
"""Close Redis connection"""
if self.client:
await self.client.close()
logger.info("Redis connection closed")
async def ping(self) -> bool:
"""Check Redis connection"""
try:
if not self.client:
await self.connect()
return await self.client.ping()
except Exception as e:
logger.error("Redis ping failed", error=str(e))
return False
def _make_key(self, *parts: str) -> str:
"""Create Redis key with prefix"""
return f"{self.key_prefix}:{':'.join(parts)}"
async def set_session_data(self, session_id: str, key: str, data: Any, ttl: int = None):
"""Store session data in Redis"""
if not self.client:
await self.connect()
redis_key = self._make_key(session_id, key)
serialized = json.dumps(data) if not isinstance(data, str) else data
if ttl:
await self.client.setex(redis_key, ttl, serialized)
else:
await self.client.set(redis_key, serialized)
logger.debug("Session data stored", session_id=session_id, key=key)
async def get_session_data(self, session_id: str, key: str) -> Optional[Any]:
"""Retrieve session data from Redis"""
if not self.client:
await self.connect()
redis_key = self._make_key(session_id, key)
data = await self.client.get(redis_key)
if data:
try:
return json.loads(data)
except json.JSONDecodeError:
return data
return None
async def delete_session_data(self, session_id: str, key: str = None):
"""Delete session data"""
if not self.client:
await self.connect()
if key:
redis_key = self._make_key(session_id, key)
await self.client.delete(redis_key)
else:
pattern = self._make_key(session_id, "*")
keys = await self.client.keys(pattern)
if keys:
await self.client.delete(*keys)
logger.debug("Session data deleted", session_id=session_id, key=key)
async def extend_session_ttl(self, session_id: str, ttl: int):
"""Extend TTL for all session keys"""
if not self.client:
await self.connect()
pattern = self._make_key(session_id, "*")
keys = await self.client.keys(pattern)
for key in keys:
await self.client.expire(key, ttl)
logger.debug("Session TTL extended", session_id=session_id, ttl=ttl)
async def set_hash(self, session_id: str, hash_key: str, field: str, value: Any):
"""Store hash field in Redis"""
if not self.client:
await self.connect()
redis_key = self._make_key(session_id, hash_key)
serialized = json.dumps(value) if not isinstance(value, str) else value
await self.client.hset(redis_key, field, serialized)
async def get_hash(self, session_id: str, hash_key: str, field: str) -> Optional[Any]:
"""Get hash field from Redis"""
if not self.client:
await self.connect()
redis_key = self._make_key(session_id, hash_key)
data = await self.client.hget(redis_key, field)
if data:
try:
return json.loads(data)
except json.JSONDecodeError:
return data
return None
async def get_all_hash(self, session_id: str, hash_key: str) -> dict:
"""Get all hash fields"""
if not self.client:
await self.connect()
redis_key = self._make_key(session_id, hash_key)
data = await self.client.hgetall(redis_key)
result = {}
for field, value in data.items():
try:
result[field] = json.loads(value)
except json.JSONDecodeError:
result[field] = value
return result
redis_client = RedisClient()
async def get_redis() -> RedisClient:
"""Dependency for FastAPI"""
if not redis_client.client:
await redis_client.connect()
return redis_client