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

17
src/models/__init__.py Normal file
View File

@@ -0,0 +1,17 @@
"""Models package."""
from src.models.base import Base
from src.models.scenario import Scenario
from src.models.scenario_log import ScenarioLog
from src.models.scenario_metric import ScenarioMetric
from src.models.aws_pricing import AwsPricing
from src.models.report import Report
__all__ = [
"Base",
"Scenario",
"ScenarioLog",
"ScenarioMetric",
"AwsPricing",
"Report",
]

25
src/models/aws_pricing.py Normal file
View File

@@ -0,0 +1,25 @@
"""AwsPricing model."""
import uuid
from sqlalchemy import Column, String, DECIMAL, Boolean, Date, Text
from sqlalchemy.dialects.postgresql import UUID
from src.models.base import Base
class AwsPricing(Base):
"""AWS service pricing model."""
__tablename__ = "aws_pricing"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
service = Column(String(50), nullable=False, index=True)
region = Column(String(50), nullable=False, index=True)
tier = Column(String(50), default="standard", nullable=False)
price_per_unit = Column(DECIMAL(15, 10), nullable=False)
unit = Column(String(20), nullable=False)
effective_from = Column(Date, nullable=False)
effective_to = Column(Date, nullable=True)
is_active = Column(Boolean, default=True, nullable=False)
source_url = Column(String(500), nullable=True)
description = Column(Text, nullable=True)

21
src/models/base.py Normal file
View File

@@ -0,0 +1,21 @@
"""Base model with mixins."""
from sqlalchemy import Column, DateTime
from sqlalchemy.orm import declarative_base
from sqlalchemy.sql import func
Base = declarative_base()
class TimestampMixin:
"""Mixin che aggiunge created_at e updated_at."""
created_at = Column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
updated_at = Column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
)

29
src/models/report.py Normal file
View File

@@ -0,0 +1,29 @@
"""Report model."""
import uuid
from sqlalchemy import Column, String, Integer, DateTime, ForeignKey, Enum
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.orm import relationship
from src.models.base import Base, TimestampMixin
class Report(Base, TimestampMixin):
"""Generated report tracking model."""
__tablename__ = "reports"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
scenario_id = Column(
UUID(as_uuid=True),
ForeignKey("scenarios.id", ondelete="CASCADE"),
nullable=False,
)
format = Column(Enum("pdf", "csv", name="report_format"), nullable=False)
file_path = Column(String(500), nullable=False)
file_size_bytes = Column(Integer, nullable=True)
generated_by = Column(String(100), nullable=True)
extra_data = Column(JSONB, default=dict)
# Relationships
scenario = relationship("Scenario", back_populates="reports")

40
src/models/scenario.py Normal file
View File

@@ -0,0 +1,40 @@
"""Scenario model."""
import uuid
from sqlalchemy import Column, String, Text, Enum, DECIMAL, Integer, DateTime
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.orm import relationship
from src.models.base import Base, TimestampMixin
class Scenario(Base, TimestampMixin):
"""Scenario model for cost simulation."""
__tablename__ = "scenarios"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String(255), nullable=False)
description = Column(Text, nullable=True)
tags = Column(JSONB, default=list)
status = Column(
Enum("draft", "running", "completed", "archived", name="scenario_status"),
nullable=False,
default="draft",
)
region = Column(String(50), nullable=False, default="us-east-1")
completed_at = Column(DateTime(timezone=True), nullable=True)
started_at = Column(DateTime(timezone=True), nullable=True)
total_requests = Column(Integer, default=0, nullable=False)
total_cost_estimate = Column(DECIMAL(12, 6), default=0.0, nullable=False)
# Relationships
logs = relationship(
"ScenarioLog", back_populates="scenario", cascade="all, delete-orphan"
)
metrics = relationship(
"ScenarioMetric", back_populates="scenario", cascade="all, delete-orphan"
)
reports = relationship(
"Report", back_populates="scenario", cascade="all, delete-orphan"
)

View File

@@ -0,0 +1,32 @@
"""ScenarioLog model."""
import uuid
from sqlalchemy import Column, String, Integer, Boolean, DateTime, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from src.models.base import Base
class ScenarioLog(Base):
"""Log entry model for received logs."""
__tablename__ = "scenario_logs"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
scenario_id = Column(
UUID(as_uuid=True),
ForeignKey("scenarios.id", ondelete="CASCADE"),
nullable=False,
)
received_at = Column(DateTime(timezone=True), nullable=False)
message_hash = Column(String(64), nullable=False, index=True)
message_preview = Column(String(500), nullable=True)
source = Column(String(100), default="unknown", nullable=False)
size_bytes = Column(Integer, default=0, nullable=False)
has_pii = Column(Boolean, default=False, nullable=False)
token_count = Column(Integer, default=0, nullable=False)
sqs_blocks = Column(Integer, default=1, nullable=False)
# Relationships
scenario = relationship("Scenario", back_populates="logs")

View File

@@ -0,0 +1,32 @@
"""ScenarioMetric model."""
import uuid
from sqlalchemy import Column, String, DECIMAL, DateTime, ForeignKey
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.orm import relationship
from src.models.base import Base
class ScenarioMetric(Base):
"""Metric time-series model for scenario analytics."""
__tablename__ = "scenario_metrics"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
scenario_id = Column(
UUID(as_uuid=True),
ForeignKey("scenarios.id", ondelete="CASCADE"),
nullable=False,
)
timestamp = Column(DateTime(timezone=True), nullable=False)
metric_type = Column(
String(50), nullable=False
) # 'sqs', 'lambda', 'bedrock', 'safety'
metric_name = Column(String(100), nullable=False)
value = Column(DECIMAL(15, 6), default=0.0, nullable=False)
unit = Column(String(20), nullable=False) # 'count', 'bytes', 'tokens', 'usd'
extra_data = Column(JSONB, default=dict)
# Relationships
scenario = relationship("Scenario", back_populates="metrics")