# πŸš€ @backend-dev - API Implementation ## πŸ“Š Stato Progetto **Data:** 2026-04-07 **Fase:** 1 - Database & Backend Core **Database:** βœ… COMPLETATO **Models/Schemas:** βœ… COMPLETATI **Repositories:** βœ… COMPLETI ### βœ… Cosa Γ¨ pronto - Database PostgreSQL con tutte le tabelle - SQLAlchemy Models (5 modelli) - Pydantic Schemas - Repository Pattern (Base + Scenario) - Alembic migrations ### 🎯 I tuoi task (PrioritΓ  P1) --- ## BE-006: API Dependencies & Configuration **Stima:** S (30-60 min) **Dipende da:** BE-005 completato βœ… ### Obiettivo Configurare dependencies FastAPI per database e preparare struttura API. ### Files da creare/modificare ``` src/ β”œβ”€β”€ api/ β”‚ β”œβ”€β”€ __init__.py β”‚ └── deps.py # NUOVO └── core/ β”œβ”€β”€ config.py # NUOVO └── exceptions.py # NUOVO ``` ### Implementazione richiesta **1. `src/core/config.py`** ```python """Application configuration.""" from pydantic_settings import BaseSettings from functools import lru_cache class Settings(BaseSettings): """Application settings from environment variables.""" # Database database_url: str = "postgresql+asyncpg://app:changeme@localhost:5432/mockupaws" # Application app_name: str = "mockupAWS" debug: bool = False # Pagination default_page_size: int = 20 max_page_size: int = 100 class Config: env_file = ".env" case_sensitive = False @lru_cache() def get_settings() -> Settings: """Get cached settings instance.""" return Settings() settings = get_settings() ``` **2. `src/core/exceptions.py`** ```python """Custom exceptions for the application.""" from fastapi import HTTPException, status class AppException(Exception): """Base application exception.""" status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR code: str = "internal_error" def __init__(self, message: str = None): self.message = message or "An internal error occurred" super().__init__(self.message) class NotFoundException(AppException): """Resource not found exception.""" status_code = status.HTTP_404_NOT_FOUND code = "not_found" def __init__(self, resource: str = "Resource"): super().__init__(f"{resource} not found") class ValidationException(AppException): """Validation error exception.""" status_code = status.HTTP_400_BAD_REQUEST code = "validation_error" class ConflictException(AppException): """Conflict error exception.""" status_code = status.HTTP_409_CONFLICT code = "conflict" class ScenarioNotRunningException(AppException): """Scenario is not in running state.""" status_code = status.HTTP_400_BAD_REQUEST code = "scenario_not_running" def __init__(self): super().__init__("Scenario is not in 'running' state. Cannot ingest logs.") def setup_exception_handlers(app): """Setup exception handlers for FastAPI app.""" from fastapi import Request from fastapi.responses import JSONResponse @app.exception_handler(AppException) async def app_exception_handler(request: Request, exc: AppException): return JSONResponse( status_code=exc.status_code, content={ "error": exc.code, "message": exc.message, "status_code": exc.status_code } ) ``` **3. `src/api/deps.py`** ```python """API dependencies.""" from typing import AsyncGenerator from fastapi import Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from src.core.database import AsyncSessionLocal from src.repositories.scenario import scenario_repository, ScenarioStatus from src.core.exceptions import NotFoundException, ScenarioNotRunningException async def get_db() -> AsyncGenerator[AsyncSession, None]: """Dependency that provides a database session.""" async with AsyncSessionLocal() as session: try: yield session finally: await session.close() async def get_running_scenario( scenario_id: str, db: AsyncSession = Depends(get_db) ): """Dependency that validates scenario exists and is running.""" from uuid import UUID try: scenario_uuid = UUID(scenario_id) except ValueError: raise NotFoundException("Scenario") scenario = await scenario_repository.get(db, scenario_uuid) if not scenario: raise NotFoundException("Scenario") if scenario.status != ScenarioStatus.RUNNING.value: raise ScenarioNotRunningException() return scenario ``` ### Criteri di accettazione - [ ] Configurazione da environment variables - [ ] Exception hierarchy completa - [ ] Global exception handlers - [ ] get_db() dependency funzionante - [ ] get_running_scenario() dependency --- ## BE-007: Services Layer **Stima:** L (2-4 ore) **Dipende da:** BE-006 ### Obiettivo Implementare Services per business logic (Cost Calculator, Ingest Processor, PII Detector). ### Files da creare ``` src/services/ β”œβ”€β”€ __init__.py β”œβ”€β”€ cost_calculator.py # Calcolo costi AWS β”œβ”€β”€ pii_detector.py # Rilevamento PII └── ingest_service.py # Logica ingestione log ``` ### Implementazione richiesta **1. `src/services/pii_detector.py`** ```python """PII detection service.""" import re from typing import Dict, List, Optional from dataclasses import dataclass @dataclass class PIIDetectionResult: """Result of PII detection.""" has_pii: bool pii_types: List[str] total_matches: int details: Dict[str, List[str]] class PIIDetector: """Service for detecting PII in messages.""" # Regex patterns for common PII PATTERNS = { 'email': r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', 'ssn': r'\b\d{3}-\d{2}-\d{4}\b', 'credit_card': r'\b(?:\d[ -]*?){13,16}\b', 'phone': r'\b\d{3}[-.]?\d{3}[-.]?\d{4}\b', 'ip_address': r'\b(?:\d{1,3}\.){3}\d{1,3}\b', } def detect(self, message: str) -> PIIDetectionResult: """Detect PII in a message.""" results = {} for pii_type, pattern in self.PATTERNS.items(): matches = re.findall(pattern, message) if matches: results[pii_type] = matches return PIIDetectionResult( has_pii=len(results) > 0, pii_types=list(results.keys()), total_matches=sum(len(matches) for matches in results.values()), details=results ) def detect_simple(self, message: str) -> bool: """Simple PII detection - returns True if PII found.""" # Quick check for email (most common) return '@' in message and '.com' in message # Singleton instance pii_detector = PIIDetector() ``` **2. `src/services/cost_calculator.py`** ```python """Cost calculation service.""" from decimal import Decimal from typing import Optional from sqlalchemy.ext.asyncio import AsyncSession from src.repositories.base import BaseRepository from src.models.aws_pricing import AwsPricing class CostCalculator: """Service for calculating AWS costs.""" def __init__(self): self.pricing_repo = BaseRepository(AwsPricing) async def get_pricing( self, db: AsyncSession, service: str, region: str, tier: str ) -> Optional[Decimal]: """Get active pricing for a service/region/tier.""" from sqlalchemy import select, and_ from datetime import date result = await db.execute( select(AwsPricing).where( and_( AwsPricing.service == service, AwsPricing.region == region, AwsPricing.tier == tier, AwsPricing.is_active == True ) ) ) pricing = result.scalar_one_or_none() return pricing.price_per_unit if pricing else None async def calculate_sqs_cost( self, db: AsyncSession, blocks: int, region: str ) -> Decimal: """Calculate SQS cost.""" price = await self.get_pricing(db, 'sqs', region, 'standard') if price is None: # Default price price = Decimal('0.40') # Formula: blocks * price_per_million / 1,000,000 return Decimal(blocks) * price / Decimal('1000000') async def calculate_lambda_cost( self, db: AsyncSession, invocations: int, gb_seconds: float, region: str ) -> Decimal: """Calculate Lambda cost (requests + compute).""" request_price = await self.get_pricing(db, 'lambda', region, 'x86_request') compute_price = await self.get_pricing(db, 'lambda', region, 'x86_compute') if request_price is None: request_price = Decimal('0.20') if compute_price is None: compute_price = Decimal('0.0000166667') # Formula: (invocations * price_per_million / 1M) + (gb_seconds * price_per_gb_second) request_cost = Decimal(invocations) * request_price / Decimal('1000000') compute_cost = Decimal(str(gb_seconds)) * compute_price return request_cost + compute_cost async def calculate_bedrock_cost( self, db: AsyncSession, input_tokens: int, output_tokens: int, region: str, model: str = 'claude_3_sonnet' ) -> Decimal: """Calculate Bedrock LLM cost.""" input_price = await self.get_pricing(db, 'bedrock', region, f'{model}_input') output_price = await self.get_pricing(db, 'bedrock', region, f'{model}_output') if input_price is None: input_price = Decimal('0.003') if output_price is None: output_price = Decimal('0.015') # Formula: (tokens * price_per_1k / 1000) input_cost = Decimal(input_tokens) * input_price / Decimal('1000') output_cost = Decimal(output_tokens) * output_price / Decimal('1000') return input_cost + output_cost # Singleton instance cost_calculator = CostCalculator() ``` **3. `src/services/ingest_service.py`** ```python """Log ingestion service.""" import hashlib from datetime import datetime from sqlalchemy.ext.asyncio import AsyncSession from src.models.scenario_log import ScenarioLog from src.models.scenario import Scenario from src.repositories.scenario import scenario_repository from src.services.pii_detector import pii_detector from src.services.cost_calculator import cost_calculator from src.profiler import count_tokens, calculate_sqs_blocks class IngestService: """Service for processing and ingesting logs.""" async def ingest_log( self, db: AsyncSession, scenario: Scenario, message: str, source: str = "unknown" ) -> ScenarioLog: """Process and save a log entry.""" # Calculate message hash for deduplication message_hash = hashlib.sha256(message.encode()).hexdigest() # Truncate message for preview (privacy) message_preview = message[:500] if len(message) > 500 else message # Detect PII has_pii = pii_detector.detect_simple(message) # Calculate metrics token_count = count_tokens(message) payload_size = len(message.encode('utf-8')) sqs_blocks = calculate_sqs_blocks(message) # Create log entry log_entry = ScenarioLog( scenario_id=scenario.id, received_at=datetime.utcnow(), message_hash=message_hash, message_preview=message_preview, source=source, size_bytes=payload_size, has_pii=has_pii, token_count=token_count, sqs_blocks=sqs_blocks ) db.add(log_entry) # Update scenario metrics await scenario_repository.increment_total_requests(db, scenario.id) await db.commit() await db.refresh(log_entry) return log_entry # Singleton instance ingest_service = IngestService() ``` **4. `src/services/__init__.py`** ```python """Services package.""" from src.services.pii_detector import PIIDetector, pii_detector, PIIDetectionResult from src.services.cost_calculator import CostCalculator, cost_calculator from src.services.ingest_service import IngestService, ingest_service __all__ = [ "PIIDetector", "pii_detector", "PIIDetectionResult", "CostCalculator", "cost_calculator", "IngestService", "ingest_service", ] ``` ### Criteri di accettazione - [ ] PIIDetector con multipli pattern - [ ] CostCalculator con tutte le formule AWS - [ ] IngestService con hash, metrics, PII detection - [ ] Integrazione con repositories esistenti - [ ] Test unitari per services --- ## BE-008: Scenarios API Endpoints **Stima:** L (2-4 ore) **Dipende da:** BE-007 ### Obiettivo Implementare CRUD API per scenarios. ### Files da creare ``` src/api/ β”œβ”€β”€ __init__.py └── v1/ β”œβ”€β”€ __init__.py └── scenarios.py # CRUD endpoints ``` ### Implementazione richiesta **1. `src/api/__init__.py`** ```python """API package.""" ``` **2. `src/api/v1/__init__.py`** ```python """API v1 routes.""" from fastapi import APIRouter from src.api.v1.scenarios import router as scenarios_router api_router = APIRouter() api_router.include_router(scenarios_router, prefix="/scenarios", tags=["scenarios"]) ``` **3. `src/api/v1/scenarios.py`** ```python """Scenario API endpoints.""" from typing import List from uuid import UUID from fastapi import APIRouter, Depends, Query, status from sqlalchemy.ext.asyncio import AsyncSession from src.api.deps import get_db from src.repositories.scenario import scenario_repository, ScenarioStatus from src.schemas.scenario import ( ScenarioCreate, ScenarioUpdate, ScenarioResponse, ScenarioList ) from src.core.exceptions import NotFoundException, ValidationException from src.core.config import settings router = APIRouter() @router.post("", response_model=ScenarioResponse, status_code=status.HTTP_201_CREATED) async def create_scenario( scenario_in: ScenarioCreate, db: AsyncSession = Depends(get_db) ): """Create a new scenario.""" # Check for duplicate name existing = await scenario_repository.get_by_name(db, scenario_in.name) if existing: raise ValidationException(f"Scenario with name '{scenario_in.name}' already exists") scenario = await scenario_repository.create(db, obj_in=scenario_in.model_dump()) return scenario @router.get("", response_model=ScenarioList) async def list_scenarios( status: str = Query(None, description="Filter by status"), region: str = Query(None, description="Filter by region"), page: int = Query(1, ge=1, description="Page number"), page_size: int = Query( settings.default_page_size, ge=1, le=settings.max_page_size, description="Items per page" ), db: AsyncSession = Depends(get_db) ): """List scenarios with optional filtering.""" skip = (page - 1) * page_size filters = {} if status: filters['status'] = status if region: filters['region'] = region scenarios = await scenario_repository.get_multi( db, skip=skip, limit=page_size, **filters ) total = await scenario_repository.count(db, **filters) return ScenarioList( items=scenarios, total=total, page=page, page_size=page_size ) @router.get("/{scenario_id}", response_model=ScenarioResponse) async def get_scenario( scenario_id: UUID, db: AsyncSession = Depends(get_db) ): """Get a specific scenario by ID.""" scenario = await scenario_repository.get(db, scenario_id) if not scenario: raise NotFoundException("Scenario") return scenario @router.put("/{scenario_id}", response_model=ScenarioResponse) async def update_scenario( scenario_id: UUID, scenario_in: ScenarioUpdate, db: AsyncSession = Depends(get_db) ): """Update a scenario.""" scenario = await scenario_repository.get(db, scenario_id) if not scenario: raise NotFoundException("Scenario") # Check name uniqueness if updating name if scenario_in.name and scenario_in.name != scenario.name: existing = await scenario_repository.get_by_name(db, scenario_in.name) if existing: raise ValidationException(f"Scenario with name '{scenario_in.name}' already exists") updated = await scenario_repository.update( db, db_obj=scenario, obj_in=scenario_in.model_dump(exclude_unset=True) ) return updated @router.delete("/{scenario_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_scenario( scenario_id: UUID, db: AsyncSession = Depends(get_db) ): """Delete a scenario.""" scenario = await scenario_repository.get(db, scenario_id) if not scenario: raise NotFoundException("Scenario") await scenario_repository.delete(db, id=scenario_id) return None @router.post("/{scenario_id}/start", response_model=ScenarioResponse) async def start_scenario( scenario_id: UUID, db: AsyncSession = Depends(get_db) ): """Start a scenario (draft -> running).""" scenario = await scenario_repository.get(db, scenario_id) if not scenario: raise NotFoundException("Scenario") if scenario.status != ScenarioStatus.DRAFT.value: raise ValidationException(f"Cannot start scenario with status '{scenario.status}'") from datetime import datetime await scenario_repository.update( db, db_obj=scenario, obj_in={ 'status': ScenarioStatus.RUNNING.value, 'started_at': datetime.utcnow() } ) await db.refresh(scenario) return scenario @router.post("/{scenario_id}/stop", response_model=ScenarioResponse) async def stop_scenario( scenario_id: UUID, db: AsyncSession = Depends(get_db) ): """Stop a scenario (running -> completed).""" scenario = await scenario_repository.get(db, scenario_id) if not scenario: raise NotFoundException("Scenario") if scenario.status != ScenarioStatus.RUNNING.value: raise ValidationException(f"Cannot stop scenario with status '{scenario.status}'") from datetime import datetime await scenario_repository.update( db, db_obj=scenario, obj_in={ 'status': ScenarioStatus.COMPLETED.value, 'completed_at': datetime.utcnow() } ) await db.refresh(scenario) return scenario @router.post("/{scenario_id}/archive", response_model=ScenarioResponse) async def archive_scenario( scenario_id: UUID, db: AsyncSession = Depends(get_db) ): """Archive a scenario (completed -> archived).""" scenario = await scenario_repository.get(db, scenario_id) if not scenario: raise NotFoundException("Scenario") if scenario.status != ScenarioStatus.COMPLETED.value: raise ValidationException(f"Cannot archive scenario with status '{scenario.status}'") await scenario_repository.update_status(db, scenario_id, ScenarioStatus.ARCHIVED) await db.refresh(scenario) return scenario ``` ### Criteri di accettazione - [ ] POST /api/v1/scenarios - Create - [ ] GET /api/v1/scenarios - List (con filtri e pagination) - [ ] GET /api/v1/scenarios/{id} - Get - [ ] PUT /api/v1/scenarios/{id} - Update - [ ] DELETE /api/v1/scenarios/{id} - Delete - [ ] POST /api/v1/scenarios/{id}/start - Start - [ ] POST /api/v1/scenarios/{id}/stop - Stop - [ ] POST /api/v1/scenarios/{id}/archive - Archive - [ ] Validazione errori appropriata - [ ] Response models corretti --- ## BE-009: Ingest API (Updated) **Stima:** M (1-2 ore) **Dipende da:** BE-008 ### Obiettivo Aggiornare endpoint /ingest per usare il database. ### Files da modificare ``` src/api/v1/ β”œβ”€β”€ __init__.py # Aggiornare └── ingest.py # NUOVO ``` ### Implementazione richiesta **1. `src/api/v1/ingest.py`** ```python """Ingest API endpoints.""" from uuid import UUID from fastapi import APIRouter, Depends, Header, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from src.api.deps import get_db, get_running_scenario from src.schemas.log import LogIngest, LogResponse from src.services.ingest_service import ingest_service from src.core.exceptions import ScenarioNotRunningException router = APIRouter() @router.post("/ingest", response_model=LogResponse, status_code=status.HTTP_202_ACCEPTED) async def ingest_log( log_data: LogIngest, x_scenario_id: str = Header(..., alias="X-Scenario-ID"), db: AsyncSession = Depends(get_db) ): """ Ingest a log message for processing. - **message**: The log message content - **source**: Optional source identifier - **X-Scenario-ID**: Header with the scenario UUID """ # Validate scenario is running scenario = await get_running_scenario(x_scenario_id, db) # Process and save log log_entry = await ingest_service.ingest_log( db=db, scenario=scenario, message=log_data.message, source=log_data.source ) return LogResponse( id=log_entry.id, scenario_id=log_entry.scenario_id, received_at=log_entry.received_at, message_preview=log_entry.message_preview, source=log_entry.source, size_bytes=log_entry.size_bytes, has_pii=log_entry.has_pii, token_count=log_entry.token_count, sqs_blocks=log_entry.sqs_blocks ) @router.post("/flush") async def flush_queue(): """Force immediate processing of queued messages.""" # Since we're now synchronous in DB, this is a no-op # Kept for backward compatibility return {"status": "flushed", "message": "Processing is now synchronous"} ``` **2. Aggiorna `src/api/v1/__init__.py`**: ```python """API v1 routes.""" from fastapi import APIRouter from src.api.v1.scenarios import router as scenarios_router from src.api.v1.ingest import router as ingest_router api_router = APIRouter() api_router.include_router(scenarios_router, prefix="/scenarios", tags=["scenarios"]) api_router.include_router(ingest_router, tags=["ingest"]) ``` ### Criteri di accettazione - [ ] Endpoint /ingest usa X-Scenario-ID header - [ ] Valida scenario esiste e status=running - [ ] Usa IngestService per processare log - [ ] Salva log in database - [ ] Ritorna LogResponse - [ ] Mantiene compatibilitΓ  con /flush --- ## BE-010: Metrics API **Stima:** M (1-2 ore) **Dipende da:** BE-009 ### Obiettivo Implementare endpoint per ottenere metriche di uno scenario. ### Files da creare ``` src/api/v1/ └── metrics.py # Metrics endpoints ``` ### Implementazione richiesta **`src/api/v1/metrics.py`**: ```python """Metrics API endpoints.""" from uuid import UUID from decimal import Decimal from fastapi import APIRouter, Depends, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, and_ from src.api.deps import get_db from src.repositories.scenario import scenario_repository from src.schemas.metric import MetricsResponse, MetricSummary, CostBreakdown, TimeseriesPoint from src.core.exceptions import NotFoundException from src.services.cost_calculator import cost_calculator router = APIRouter() @router.get("/scenarios/{scenario_id}/metrics", response_model=MetricsResponse) async def get_scenario_metrics( scenario_id: UUID, db: AsyncSession = Depends(get_db) ): """Get aggregated metrics for a scenario.""" # Verify scenario exists scenario = await scenario_repository.get(db, scenario_id) if not scenario: raise NotFoundException("Scenario") from src.models.scenario_log import ScenarioLog from src.models.scenario_metric import ScenarioMetric # Get summary metrics summary_result = await db.execute( select( func.count(ScenarioLog.id).label('total_logs'), func.sum(ScenarioLog.sqs_blocks).label('total_sqs_blocks'), func.sum(ScenarioLog.token_count).label('total_tokens'), func.count(ScenarioLog.id).filter(ScenarioLog.has_pii == True).label('pii_violations') ).where(ScenarioLog.scenario_id == scenario_id) ) summary_row = summary_result.one() # Calculate costs region = scenario.region sqs_cost = await cost_calculator.calculate_sqs_cost( db, summary_row.total_sqs_blocks or 0, region ) # For demo: assume 1 lambda invocation per 100 logs lambda_invocations = (summary_row.total_logs or 0) // 100 + 1 lambda_cost = await cost_calculator.calculate_lambda_cost( db, lambda_invocations, 1.0, region # 1 GB-second per invocation ) # Bedrock cost for tokens bedrock_cost = await cost_calculator.calculate_bedrock_cost( db, summary_row.total_tokens or 0, 0, region # No output tokens for now ) total_cost = sqs_cost + lambda_cost + bedrock_cost # Cost breakdown cost_breakdown = [ CostBreakdown(service='SQS', cost_usd=sqs_cost, percentage=float(sqs_cost / total_cost * 100) if total_cost > 0 else 0), CostBreakdown(service='Lambda', cost_usd=lambda_cost, percentage=float(lambda_cost / total_cost * 100) if total_cost > 0 else 0), CostBreakdown(service='Bedrock', cost_usd=bedrock_cost, percentage=float(bedrock_cost / total_cost * 100) if total_cost > 0 else 0), ] summary = MetricSummary( total_requests=scenario.total_requests, total_cost_usd=total_cost, sqs_blocks=summary_row.total_sqs_blocks or 0, lambda_invocations=lambda_invocations, llm_tokens=summary_row.total_tokens or 0, pii_violations=summary_row.pii_violations or 0 ) # Get timeseries data (grouped by hour) timeseries_result = await db.execute( select( func.date_trunc('hour', ScenarioLog.received_at).label('hour'), func.count(ScenarioLog.id).label('count') ) .where(ScenarioLog.scenario_id == scenario_id) .group_by(func.date_trunc('hour', ScenarioLog.received_at)) .order_by(func.date_trunc('hour', ScenarioLog.received_at)) ) timeseries = [ TimeseriesPoint( timestamp=row.hour, metric_type='requests', value=Decimal(row.count) ) for row in timeseries_result.all() ] return MetricsResponse( scenario_id=scenario_id, summary=summary, cost_breakdown=cost_breakdown, timeseries=timeseries ) ``` **Aggiorna `src/api/v1/__init__.py`**: ```python api_router.include_router(metrics_router, prefix="/scenarios", tags=["metrics"]) ``` ### Criteri di accettazione - [ ] GET /api/v1/scenarios/{id}/metrics - Full metrics - [ ] Calcolo costi usando CostCalculator - [ ] Aggregazioni da scenario_logs - [ ] Timeseries raggruppato per ora - [ ] Response conforme a MetricsResponse schema --- ## πŸ“‹ Checklist Completamento Prima di procedere, verifica: - [ ] Tutti i services implementati e testati - [ ] API endpoints funzionanti - [ ] Error handling appropriato - [ ] Response schemas corrette - [ ] Logging aggiunto dove necessario - [ ] Documentazione OpenAPI visibile su /docs --- ## πŸ§ͺ Testing ```bash # Test API curl -X POST http://localhost:8000/api/v1/scenarios \ -H "Content-Type: application/json" \ -d '{"name": "Test", "region": "us-east-1"}' # Test ingest curl -X POST http://localhost:8000/ingest \ -H "Content-Type: application/json" \ -H "X-Scenario-ID: " \ -d '{"message": "test log", "source": "test"}' # Check docs open http://localhost:8000/docs ``` --- ## πŸš€ Comandi Utili ```bash # Run server uv run uvicorn src.main:app --reload # Run tests uv run pytest tests/integration/test_api_scenarios.py -v # Check types uv run mypy src/ ``` --- **@backend-dev: CONTINUIAMO! πŸš€** Procedi in ordine: BE-006 β†’ BE-007 β†’ BE-008 β†’ BE-009 β†’ BE-010 **Domande?** Riferiti ai files giΓ  creati in `src/`. **Commit convenzioni:** - `feat: add API dependencies and exception handling` - `feat: implement services (cost calculator, PII detector, ingest)` - `feat: add scenarios CRUD API endpoints` - `feat: update ingest endpoint to use database` - `feat: add metrics API with cost calculations` **Buon lavoro! πŸ’ͺ**