Initial commit - production deployment
This commit is contained in:
7
services/demo_session/app/core/__init__.py
Normal file
7
services/demo_session/app/core/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Demo Session Service Core"""
|
||||
|
||||
from .config import settings
|
||||
from .database import DatabaseManager, get_db
|
||||
from .redis_wrapper import DemoRedisWrapper, get_redis
|
||||
|
||||
__all__ = ["settings", "DatabaseManager", "get_db", "DemoRedisWrapper", "get_redis"]
|
||||
132
services/demo_session/app/core/config.py
Normal file
132
services/demo_session/app/core/config.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
Demo Session Service Configuration
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
from shared.config.base import BaseServiceSettings
|
||||
|
||||
|
||||
class Settings(BaseServiceSettings):
|
||||
"""Demo Session Service Settings"""
|
||||
|
||||
# Service info (override base settings)
|
||||
APP_NAME: str = "Demo Session Service"
|
||||
SERVICE_NAME: str = "demo-session"
|
||||
VERSION: str = "1.0.0"
|
||||
DESCRIPTION: str = "Demo session management and orchestration service"
|
||||
|
||||
# Database (override base property)
|
||||
@property
|
||||
def DATABASE_URL(self) -> str:
|
||||
"""Build database URL from environment"""
|
||||
return os.getenv(
|
||||
"DEMO_SESSION_DATABASE_URL",
|
||||
"postgresql+asyncpg://postgres:postgres@localhost:5432/demo_session_db"
|
||||
)
|
||||
|
||||
# Redis configuration (demo-specific)
|
||||
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)
|
||||
# Contains complete user, tenant, and subscription data matching fixture files
|
||||
DEMO_ACCOUNTS: dict = {
|
||||
"professional": {
|
||||
"email": "demo.professional@panaderiaartesana.com",
|
||||
"name": "Panadería Artesana Madrid - Demo",
|
||||
"subdomain": "demo-artesana",
|
||||
"base_tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"subscription_tier": "professional",
|
||||
"tenant_type": "standalone",
|
||||
# User data from fixtures/professional/01-tenant.json
|
||||
"user": {
|
||||
"id": "c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"email": "maria.garcia@panaderiaartesana.com",
|
||||
"full_name": "María García López",
|
||||
"role": "owner",
|
||||
"is_active": True,
|
||||
"is_verified": True
|
||||
},
|
||||
# Tenant data
|
||||
"tenant": {
|
||||
"business_type": "bakery",
|
||||
"business_model": "production_retail",
|
||||
"description": "Professional tier demo tenant for bakery operations"
|
||||
}
|
||||
},
|
||||
"enterprise": {
|
||||
"email": "central@panaderiaartesana.es",
|
||||
"name": "Panadería Artesana España - Central",
|
||||
"subdomain": "artesana-central",
|
||||
"base_tenant_id": "80000000-0000-4000-a000-000000000001",
|
||||
"subscription_tier": "enterprise",
|
||||
"tenant_type": "parent",
|
||||
# User data from fixtures/enterprise/parent/01-tenant.json
|
||||
"user": {
|
||||
"id": "d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7",
|
||||
"email": "director@panaderiaartesana.es",
|
||||
"full_name": "Director",
|
||||
"role": "owner",
|
||||
"is_active": True,
|
||||
"is_verified": True
|
||||
},
|
||||
# Tenant data
|
||||
"tenant": {
|
||||
"business_type": "bakery_chain",
|
||||
"business_model": "multi_location",
|
||||
"description": "Central production facility and parent tenant for multi-location bakery chain"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"name": "Madrid - Salamanca",
|
||||
"base_tenant_id": "A0000000-0000-4000-a000-000000000001",
|
||||
"location": {"city": "Madrid", "zone": "Salamanca", "latitude": 40.4284, "longitude": -3.6847},
|
||||
"description": "Premium location in upscale Salamanca district"
|
||||
},
|
||||
{
|
||||
"name": "Barcelona - Eixample",
|
||||
"base_tenant_id": "B0000000-0000-4000-a000-000000000001",
|
||||
"location": {"city": "Barcelona", "zone": "Eixample", "latitude": 41.3947, "longitude": 2.1616},
|
||||
"description": "High-volume tourist and local area in central Barcelona"
|
||||
},
|
||||
{
|
||||
"name": "Valencia - Ruzafa",
|
||||
"base_tenant_id": "C0000000-0000-4000-a000-000000000001",
|
||||
"location": {"city": "Valencia", "zone": "Ruzafa", "latitude": 39.4623, "longitude": -0.3645},
|
||||
"description": "Trendy artisan neighborhood with focus on quality"
|
||||
},
|
||||
{
|
||||
"name": "Seville - Triana",
|
||||
"base_tenant_id": "D0000000-0000-4000-a000-000000000001",
|
||||
"location": {"city": "Seville", "zone": "Triana", "latitude": 37.3828, "longitude": -6.0026},
|
||||
"description": "Traditional Andalusian location with local specialties"
|
||||
},
|
||||
{
|
||||
"name": "Bilbao - Casco Viejo",
|
||||
"base_tenant_id": "E0000000-0000-4000-a000-000000000001",
|
||||
"location": {"city": "Bilbao", "zone": "Casco Viejo", "latitude": 43.2567, "longitude": -2.9272},
|
||||
"description": "Basque region location with focus on quality and local culture"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# Service URLs - these are inherited from BaseServiceSettings
|
||||
# but we can override defaults if needed:
|
||||
# - GATEWAY_URL (inherited)
|
||||
# - AUTH_SERVICE_URL, TENANT_SERVICE_URL, etc. (inherited)
|
||||
# - JWT_SECRET_KEY, JWT_ALGORITHM (inherited)
|
||||
# - LOG_LEVEL (inherited)
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = True
|
||||
|
||||
|
||||
settings = Settings()
|
||||
61
services/demo_session/app/core/database.py
Normal file
61
services/demo_session/app/core/database.py
Normal 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
|
||||
131
services/demo_session/app/core/redis_wrapper.py
Normal file
131
services/demo_session/app/core/redis_wrapper.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
Redis wrapper for demo session service using shared Redis implementation
|
||||
Provides a compatibility layer for session-specific operations
|
||||
"""
|
||||
|
||||
import json
|
||||
import structlog
|
||||
from typing import Optional, Any
|
||||
from shared.redis_utils import get_redis_client
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class DemoRedisWrapper:
|
||||
"""Wrapper around shared Redis client for demo session operations"""
|
||||
|
||||
def __init__(self, key_prefix: str = "demo_session"):
|
||||
self.key_prefix = key_prefix
|
||||
|
||||
async def get_client(self):
|
||||
"""Get the underlying Redis client"""
|
||||
return await get_redis_client()
|
||||
|
||||
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"""
|
||||
client = await get_redis_client()
|
||||
redis_key = self._make_key(session_id, key)
|
||||
serialized = json.dumps(data) if not isinstance(data, str) else data
|
||||
|
||||
if ttl:
|
||||
await client.setex(redis_key, ttl, serialized)
|
||||
else:
|
||||
await 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"""
|
||||
client = await get_redis_client()
|
||||
redis_key = self._make_key(session_id, key)
|
||||
data = await 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"""
|
||||
client = await get_redis_client()
|
||||
|
||||
if key:
|
||||
redis_key = self._make_key(session_id, key)
|
||||
await client.delete(redis_key)
|
||||
else:
|
||||
pattern = self._make_key(session_id, "*")
|
||||
keys = await client.keys(pattern)
|
||||
if keys:
|
||||
await 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"""
|
||||
client = await get_redis_client()
|
||||
pattern = self._make_key(session_id, "*")
|
||||
keys = await client.keys(pattern)
|
||||
|
||||
for key in keys:
|
||||
await 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"""
|
||||
client = await get_redis_client()
|
||||
redis_key = self._make_key(session_id, hash_key)
|
||||
serialized = json.dumps(value) if not isinstance(value, str) else value
|
||||
await 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"""
|
||||
client = await get_redis_client()
|
||||
redis_key = self._make_key(session_id, hash_key)
|
||||
data = await 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"""
|
||||
client = await get_redis_client()
|
||||
redis_key = self._make_key(session_id, hash_key)
|
||||
data = await 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
|
||||
|
||||
async def get_client(self):
|
||||
"""Get raw Redis client for direct operations"""
|
||||
return await get_redis_client()
|
||||
|
||||
|
||||
# Cached instance
|
||||
_redis_wrapper = None
|
||||
|
||||
|
||||
async def get_redis() -> DemoRedisWrapper:
|
||||
"""Dependency for FastAPI - returns wrapper around shared Redis"""
|
||||
global _redis_wrapper
|
||||
if _redis_wrapper is None:
|
||||
_redis_wrapper = DemoRedisWrapper()
|
||||
return _redis_wrapper
|
||||
Reference in New Issue
Block a user