Complete API implementation (BE-006 to BE-010):
BE-006: API Dependencies & Configuration
- Add core/config.py with Settings and environment variables
- Add core/exceptions.py with AppException hierarchy
- Add api/deps.py with get_db() and get_running_scenario() dependencies
- Add pydantic-settings dependency
BE-007: Services Layer
- Add services/pii_detector.py: PIIDetector with email/SSN/credit card patterns
- Add services/cost_calculator.py: AWS cost calculation (SQS, Lambda, Bedrock)
- Add services/ingest_service.py: Log processing with hash, PII detection, metrics
BE-008: Scenarios API Endpoints
- POST /api/v1/scenarios - Create scenario
- GET /api/v1/scenarios - List with filters and pagination
- GET /api/v1/scenarios/{id} - Get single scenario
- PUT /api/v1/scenarios/{id} - Update scenario
- DELETE /api/v1/scenarios/{id} - Delete scenario
- POST /api/v1/scenarios/{id}/start - Start (draft->running)
- POST /api/v1/scenarios/{id}/stop - Stop (running->completed)
- POST /api/v1/scenarios/{id}/archive - Archive (completed->archived)
BE-009: Ingest API
- POST /ingest with X-Scenario-ID header validation
- Depends on get_running_scenario() for status check
- Returns LogResponse with processed metrics
- POST /flush for backward compatibility
BE-010: Metrics API
- GET /api/v1/scenarios/{id}/metrics - Full metrics endpoint
- Aggregates data from scenario_logs
- Calculates costs using CostCalculator
- Returns cost breakdown (SQS/Lambda/Bedrock)
- Returns timeseries data grouped by hour
Refactored main.py:
- Simplified to use api_router
- Added exception handlers
- Added health check endpoint
All endpoints tested and working.
Tasks: BE-006, BE-007, BE-008, BE-009, BE-010 complete
87 lines
2.9 KiB
Python
87 lines
2.9 KiB
Python
"""Cost calculation service."""
|
|
|
|
from decimal import Decimal
|
|
from typing import Optional
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, and_
|
|
from datetime import date
|
|
|
|
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."""
|
|
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:
|
|
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")
|
|
|
|
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")
|
|
|
|
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()
|