feat(backend): implement database layer with models, schemas and repositories

Complete backend core implementation (BE-001 to BE-005):

BE-001: Database Connection & Session Management
- Create src/core/database.py with async SQLAlchemy 2.0
- Configure engine with pool_size=20
- Implement get_db() FastAPI dependency

BE-002: SQLAlchemy Models (5 models)
- Base model with TimestampMixin
- Scenario: status enum, relationships, cost tracking
- ScenarioLog: message hash, PII detection, metrics
- ScenarioMetric: time-series with extra_data (JSONB)
- AwsPricing: service pricing with region support
- Report: format enum, file tracking, extra_data

BE-003: Pydantic Schemas
- Scenario: Create, Update, Response, List schemas
- Log: Ingest, Response schemas
- Metric: Summary, CostBreakdown, MetricsResponse
- Common: PaginatedResponse generic type

BE-004: Base Repository Pattern
- Generic BaseRepository[T] with CRUD operations
- Methods: get, get_multi, count, create, update, delete
- Dynamic filter support

BE-005: Scenario Repository
- Extends BaseRepository[Scenario]
- Specific methods: get_by_name, list_by_status, list_by_region
- Business methods: update_status, increment_total_requests, update_total_cost
- ScenarioStatus enum
- Singleton instance: scenario_repository

All models, schemas and repositories tested and working.

Tasks: BE-001, BE-002, BE-003, BE-004, BE-005 complete
This commit is contained in:
Luca Sacchi Ricciardi
2026-04-07 14:20:02 +02:00
parent 216f9e229c
commit ebefc323c3
18 changed files with 1322 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
"""Repositories package."""
from src.repositories.base import BaseRepository
from src.repositories.scenario import (
ScenarioRepository,
scenario_repository,
ScenarioStatus,
)
__all__ = [
"BaseRepository",
"ScenarioRepository",
"scenario_repository",
"ScenarioStatus",
]
+75
View File
@@ -0,0 +1,75 @@
"""Base repository with generic CRUD operations."""
from typing import Generic, TypeVar, Optional, List, Any
from uuid import UUID
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, delete, update, func
from src.models.base import Base
ModelType = TypeVar("ModelType", bound=Base)
class BaseRepository(Generic[ModelType]):
"""Generic base repository with common CRUD operations."""
def __init__(self, model: type[ModelType]):
self.model = model
async def get(self, db: AsyncSession, id: UUID) -> Optional[ModelType]:
"""Get a single record by ID."""
result = await db.execute(select(self.model).where(self.model.id == id))
return result.scalar_one_or_none()
async def get_multi(
self, db: AsyncSession, *, skip: int = 0, limit: int = 100, **filters
) -> List[ModelType]:
"""Get multiple records with optional filtering."""
query = select(self.model)
# Apply filters
for key, value in filters.items():
if hasattr(self.model, key) and value is not None:
query = query.where(getattr(self.model, key) == value)
query = query.offset(skip).limit(limit)
result = await db.execute(query)
return result.scalars().all()
async def count(self, db: AsyncSession, **filters) -> int:
"""Count records with optional filtering."""
query = select(func.count(self.model.id))
for key, value in filters.items():
if hasattr(self.model, key) and value is not None:
query = query.where(getattr(self.model, key) == value)
result = await db.execute(query)
return result.scalar()
async def create(self, db: AsyncSession, *, obj_in: dict) -> ModelType:
"""Create a new record."""
db_obj = self.model(**obj_in)
db.add(db_obj)
await db.commit()
await db.refresh(db_obj)
return db_obj
async def update(
self, db: AsyncSession, *, db_obj: ModelType, obj_in: dict
) -> ModelType:
"""Update a record."""
for field, value in obj_in.items():
if hasattr(db_obj, field) and value is not None:
setattr(db_obj, field, value)
db.add(db_obj)
await db.commit()
await db.refresh(db_obj)
return db_obj
async def delete(self, db: AsyncSession, *, id: UUID) -> bool:
"""Delete a record by ID."""
result = await db.execute(delete(self.model).where(self.model.id == id))
await db.commit()
return result.rowcount > 0
+82
View File
@@ -0,0 +1,82 @@
"""Scenario repository with specific methods."""
from typing import Optional, List
from uuid import UUID
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update
from enum import Enum
from src.models.scenario import Scenario
from src.repositories.base import BaseRepository
class ScenarioStatus(str, Enum):
"""Scenario status enum."""
DRAFT = "draft"
RUNNING = "running"
COMPLETED = "completed"
ARCHIVED = "archived"
class ScenarioRepository(BaseRepository[Scenario]):
"""Repository for Scenario model with specific methods."""
def __init__(self):
super().__init__(Scenario)
async def get_by_name(self, db: AsyncSession, name: str) -> Optional[Scenario]:
"""Get scenario by name."""
result = await db.execute(select(Scenario).where(Scenario.name == name))
return result.scalar_one_or_none()
async def list_by_status(
self, db: AsyncSession, status: ScenarioStatus, skip: int = 0, limit: int = 100
) -> List[Scenario]:
"""List scenarios by status."""
return await self.get_multi(db, status=status.value, skip=skip, limit=limit)
async def list_by_region(
self, db: AsyncSession, region: str, skip: int = 0, limit: int = 100
) -> List[Scenario]:
"""List scenarios by region."""
return await self.get_multi(db, region=region, skip=skip, limit=limit)
async def update_status(
self, db: AsyncSession, scenario_id: UUID, new_status: ScenarioStatus
) -> Optional[Scenario]:
"""Update scenario status."""
result = await db.execute(
update(Scenario)
.where(Scenario.id == scenario_id)
.values(status=new_status.value)
.returning(Scenario)
)
await db.commit()
return result.scalar_one_or_none()
async def increment_total_requests(
self, db: AsyncSession, scenario_id: UUID, increment: int = 1
) -> None:
"""Atomically increment total_requests counter."""
await db.execute(
update(Scenario)
.where(Scenario.id == scenario_id)
.values(total_requests=Scenario.total_requests + increment)
)
await db.commit()
async def update_total_cost(
self, db: AsyncSession, scenario_id: UUID, new_cost: float
) -> None:
"""Update total cost estimate."""
await db.execute(
update(Scenario)
.where(Scenario.id == scenario_id)
.values(total_cost_estimate=new_cost)
)
await db.commit()
# Singleton instance
scenario_repository = ScenarioRepository()