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:
17
src/models/__init__.py
Normal file
17
src/models/__init__.py
Normal 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
25
src/models/aws_pricing.py
Normal 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
21
src/models/base.py
Normal 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
29
src/models/report.py
Normal 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
40
src/models/scenario.py
Normal 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"
|
||||
)
|
||||
32
src/models/scenario_log.py
Normal file
32
src/models/scenario_log.py
Normal 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")
|
||||
32
src/models/scenario_metric.py
Normal file
32
src/models/scenario_metric.py
Normal 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")
|
||||
Reference in New Issue
Block a user