REFACTOR API gateway

This commit is contained in:
Urtzi Alfaro
2025-07-26 18:46:52 +02:00
parent e49893e10a
commit e4885db828
24 changed files with 1049 additions and 1080 deletions

View File

@@ -21,7 +21,7 @@ from app.core.security import SecurityManager
from shared.monitoring.decorators import track_execution_time
logger = structlog.get_logger()
router = APIRouter()
router = APIRouter(tags=["authentication"])
security = HTTPBearer(auto_error=False)
def get_metrics_collector(request: Request):

View File

@@ -21,7 +21,7 @@ from shared.auth.decorators import (
)
logger = structlog.get_logger()
router = APIRouter()
router = APIRouter(tags=["users"])
@router.get("/me", response_model=UserResponse)
async def get_current_user_info(
@@ -77,46 +77,4 @@ async def update_current_user(
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update user"
)
@router.post("/change-password")
async def change_password(
password_data: PasswordChange,
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Change user password"""
try:
await UserService.change_password(
current_user.id,
password_data.current_password,
password_data.new_password,
db
)
return {"message": "Password changed successfully"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Password change error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to change password"
)
@router.delete("/me")
async def delete_current_user(
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Delete current user account"""
try:
await UserService.delete_user(current_user.id, db)
return {"message": "User account deleted successfully"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Delete user error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to delete user account"
)

View File

@@ -1,13 +1,13 @@
# ================================================================
# services/data/app/api/sales.py - UPDATED WITH UNIFIED AUTH
# services/data/app/api/sales.py - FIXED FOR NEW TENANT-SCOPED ARCHITECTURE
# ================================================================
"""Sales data API endpoints with unified authentication"""
"""Sales data API endpoints with tenant-scoped URLs"""
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Query, Response
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Query, Response, Path
from fastapi.responses import StreamingResponse
from sqlalchemy.ext.asyncio import AsyncSession
from typing import List, Optional, Dict, Any
import uuid
from uuid import UUID
from datetime import datetime
import base64
import structlog
@@ -31,22 +31,23 @@ from app.services.messaging import (
)
# Import unified authentication from shared library
from shared.auth.decorators import (
get_current_user_dep,
get_current_tenant_id_dep
)
from shared.auth.decorators import get_current_user_dep
router = APIRouter(tags=["sales"])
logger = structlog.get_logger()
@router.post("/", response_model=SalesDataResponse)
# ================================================================
# TENANT-SCOPED SALES ENDPOINTS
# ================================================================
@router.post("/tenants/{tenant_id}/sales", response_model=SalesDataResponse)
async def create_sales_record(
sales_data: SalesDataCreate,
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Create a new sales record"""
"""Create a new sales record for tenant"""
try:
logger.debug("Creating sales record",
product=sales_data.product_name,
@@ -54,7 +55,7 @@ async def create_sales_record(
tenant_id=tenant_id,
user_id=current_user["user_id"])
# Override tenant_id from token/header
# Override tenant_id from URL path (gateway already verified access)
sales_data.tenant_id = tenant_id
record = await SalesService.create_sales_record(sales_data, db)
@@ -85,14 +86,14 @@ async def create_sales_record(
tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to create sales record: {str(e)}")
@router.post("/bulk", response_model=List[SalesDataResponse])
@router.post("/tenants/{tenant_id}/sales/bulk", response_model=List[SalesDataResponse])
async def create_bulk_sales(
sales_data: List[SalesDataCreate],
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Create multiple sales records"""
"""Create multiple sales records for tenant"""
try:
logger.debug("Creating bulk sales records",
count=len(sales_data),
@@ -127,16 +128,16 @@ async def create_bulk_sales(
tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to create bulk sales records: {str(e)}")
@router.get("/", response_model=List[SalesDataResponse])
@router.get("/tenants/{tenant_id}/sales", response_model=List[SalesDataResponse])
async def get_sales_data(
start_date: Optional[datetime] = Query(None),
end_date: Optional[datetime] = Query(None),
product_name: Optional[str] = Query(None),
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
start_date: Optional[datetime] = Query(None, description="Start date filter"),
end_date: Optional[datetime] = Query(None, description="End date filter"),
product_name: Optional[str] = Query(None, description="Product name filter"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get sales data with filters"""
"""Get sales data for tenant with filters"""
try:
logger.debug("Querying sales data",
tenant_id=tenant_id,
@@ -164,15 +165,15 @@ async def get_sales_data(
tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to query sales data: {str(e)}")
@router.post("/import", response_model=SalesImportResult)
@router.post("/tenants/{tenant_id}/sales/import", response_model=SalesImportResult)
async def import_sales_data(
tenant_id: UUID = Path(..., description="Tenant ID"),
file: UploadFile = File(...),
file_format: str = Form(...),
tenant_id: str = Depends(get_current_tenant_id_dep),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Import sales data from file"""
"""Import sales data from file for tenant"""
try:
logger.info("Importing sales data",
tenant_id=tenant_id,
@@ -220,26 +221,27 @@ async def import_sales_data(
tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to import sales data: {str(e)}")
@router.post("/import/validate", response_model=SalesValidationResult)
@router.post("/tenants/{tenant_id}/sales/import/validate", response_model=SalesValidationResult)
async def validate_import_data(
import_data: SalesDataImport,
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep)
):
"""Validate import data before processing"""
"""Validate import data - Gateway already verified tenant access"""
try:
logger.debug("Validating import data", tenant_id=tenant_id)
logger.debug("Validating import data",
tenant_id=tenant_id,
user_id=current_user["user_id"])
# Override tenant_id
# Set tenant context from URL path
import_data.tenant_id = tenant_id
validation = await DataImportService.validate_import_data(
import_data.model_dump()
)
validation = await DataImportService.validate_import_data(import_data.model_dump())
logger.debug("Validation completed",
is_valid=validation.get("is_valid", False),
tenant_id=tenant_id)
return validation
except Exception as e:
@@ -248,15 +250,17 @@ async def validate_import_data(
tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to validate import data: {str(e)}")
@router.get("/import/template/{format_type}")
@router.get("/tenants/{tenant_id}/sales/import/template/{format_type}")
async def get_import_template(
format_type: str,
tenant_id: UUID = Path(..., description="Tenant ID"),
format_type: str = Path(..., description="Template format: csv, json, excel"),
current_user: Dict[str, Any] = Depends(get_current_user_dep)
):
"""Get import template for specified format"""
try:
logger.debug("Getting import template",
format=format_type,
tenant_id=tenant_id,
user_id=current_user["user_id"])
template = await DataImportService.get_import_template(format_type)
@@ -265,7 +269,9 @@ async def get_import_template(
logger.warning("Template generation error", error=template["error"])
raise HTTPException(status_code=400, detail=template["error"])
logger.debug("Template generated successfully", format=format_type)
logger.debug("Template generated successfully",
format=format_type,
tenant_id=tenant_id)
if format_type.lower() == "csv":
return Response(
@@ -291,14 +297,16 @@ async def get_import_template(
except HTTPException:
raise
except Exception as e:
logger.error("Failed to generate import template", error=str(e))
logger.error("Failed to generate import template",
error=str(e),
tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to generate template: {str(e)}")
@router.get("/analytics")
@router.get("/tenants/{tenant_id}/sales/analytics")
async def get_sales_analytics(
tenant_id: UUID = Path(..., description="Tenant ID"),
start_date: Optional[datetime] = Query(None, description="Start date"),
end_date: Optional[datetime] = Query(None, description="End date"),
tenant_id: str = Depends(get_current_tenant_id_dep),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
@@ -322,17 +330,17 @@ async def get_sales_analytics(
tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to generate analytics: {str(e)}")
@router.post("/export")
@router.post("/tenants/{tenant_id}/sales/export")
async def export_sales_data(
tenant_id: UUID = Path(..., description="Tenant ID"),
export_format: str = Query("csv", description="Export format: csv, excel, json"),
start_date: Optional[datetime] = Query(None, description="Start date"),
end_date: Optional[datetime] = Query(None, description="End date"),
products: Optional[List[str]] = Query(None, description="Filter by products"),
tenant_id: str = Depends(get_current_tenant_id_dep),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Export sales data in specified format"""
"""Export sales data in specified format for tenant"""
try:
logger.info("Exporting sales data",
tenant_id=tenant_id,
@@ -376,14 +384,14 @@ async def export_sales_data(
tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to export sales data: {str(e)}")
@router.delete("/{record_id}")
@router.delete("/tenants/{tenant_id}/sales/{record_id}")
async def delete_sales_record(
record_id: str,
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
record_id: str = Path(..., description="Sales record ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Delete a sales record"""
"""Delete a sales record for tenant"""
try:
logger.info("Deleting sales record",
record_id=record_id,
@@ -413,14 +421,14 @@ async def delete_sales_record(
tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to delete sales record: {str(e)}")
@router.get("/summary")
@router.get("/tenants/{tenant_id}/sales/summary")
async def get_sales_summary(
tenant_id: UUID = Path(..., description="Tenant ID"),
period: str = Query("daily", description="Summary period: daily, weekly, monthly"),
tenant_id: str = Depends(get_current_tenant_id_dep),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get sales summary for specified period"""
"""Get sales summary for specified period for tenant"""
try:
logger.debug("Getting sales summary",
tenant_id=tenant_id,
@@ -437,13 +445,13 @@ async def get_sales_summary(
tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to generate summary: {str(e)}")
@router.get("/products")
@router.get("/tenants/{tenant_id}/sales/products")
async def get_products_list(
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get list of all products with sales data"""
"""Get list of all products with sales data for tenant"""
try:
logger.debug("Getting products list", tenant_id=tenant_id)
@@ -458,4 +466,78 @@ async def get_products_list(
logger.error("Failed to get products list",
error=str(e),
tenant_id=tenant_id)
raise HTTPException(status_code=500, detail=f"Failed to get products list: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to get products list: {str(e)}")
@router.get("/tenants/{tenant_id}/sales/{record_id}", response_model=SalesDataResponse)
async def get_sales_record(
tenant_id: UUID = Path(..., description="Tenant ID"),
record_id: str = Path(..., description="Sales record ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Get a specific sales record for tenant"""
try:
logger.debug("Getting sales record",
record_id=record_id,
tenant_id=tenant_id)
record = await SalesService.get_sales_record(record_id, db)
if not record or record.tenant_id != tenant_id:
raise HTTPException(status_code=404, detail="Sales record not found")
logger.debug("Sales record retrieved",
record_id=record_id,
tenant_id=tenant_id)
return record
except HTTPException:
raise
except Exception as e:
logger.error("Failed to get sales record",
error=str(e),
tenant_id=tenant_id,
record_id=record_id)
raise HTTPException(status_code=500, detail=f"Failed to get sales record: {str(e)}")
@router.put("/tenants/{tenant_id}/sales/{record_id}", response_model=SalesDataResponse)
async def update_sales_record(
sales_data: SalesDataCreate,
record_id: str = Path(..., description="Sales record ID"),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
"""Update a sales record for tenant"""
try:
logger.info("Updating sales record",
record_id=record_id,
tenant_id=tenant_id,
user_id=current_user["user_id"])
# Verify record exists and belongs to tenant
existing_record = await SalesService.get_sales_record(record_id, db)
if not existing_record or existing_record.tenant_id != tenant_id:
raise HTTPException(status_code=404, detail="Sales record not found")
# Override tenant_id from URL path
sales_data.tenant_id = tenant_id
updated_record = await SalesService.update_sales_record(record_id, sales_data, db)
if not updated_record:
raise HTTPException(status_code=404, detail="Sales record not found")
logger.info("Sales record updated successfully",
record_id=record_id,
tenant_id=tenant_id)
return updated_record
except HTTPException:
raise
except Exception as e:
logger.error("Failed to update sales record",
error=str(e),
tenant_id=tenant_id,
record_id=record_id)
raise HTTPException(status_code=500, detail=f"Failed to update sales record: {str(e)}")

View File

@@ -3,11 +3,12 @@
# ================================================================
"""Traffic data API endpoints with improved error handling"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from fastapi import APIRouter, Depends, HTTPException, Query, Path
from typing import List, Dict, Any
from datetime import datetime, timedelta
import structlog
from uuid import UUID
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.services.traffic_service import TrafficService
@@ -23,14 +24,15 @@ from shared.auth.decorators import (
get_current_tenant_id_dep
)
router = APIRouter()
router = APIRouter(tags=["traffic"])
traffic_service = TrafficService()
logger = structlog.get_logger()
@router.get("/current", response_model=TrafficDataResponse)
@router.get("/tenants/{tenant_id}/current", response_model=TrafficDataResponse)
async def get_current_traffic(
latitude: float = Query(..., description="Latitude"),
longitude: float = Query(..., description="Longitude"),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
):
"""Get current traffic data for location"""
@@ -69,13 +71,14 @@ async def get_current_traffic(
logger.error("Traffic API traceback", traceback=traceback.format_exc())
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
@router.get("/historical", response_model=List[TrafficDataResponse])
@router.get("/tenants/{tenant_id}/historical", response_model=List[TrafficDataResponse])
async def get_historical_traffic(
latitude: float = Query(..., description="Latitude"),
longitude: float = Query(..., description="Longitude"),
start_date: datetime = Query(..., description="Start date"),
end_date: datetime = Query(..., description="End date"),
db: AsyncSession = Depends(get_db),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
):
"""Get historical traffic data"""
@@ -115,11 +118,12 @@ async def get_historical_traffic(
logger.error("Unexpected error in historical traffic API", error=str(e))
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
@router.post("/store")
@router.post("/tenants/{tenant_id}/store")
async def store_traffic_data(
latitude: float = Query(..., description="Latitude"),
longitude: float = Query(..., description="Longitude"),
db: AsyncSession = Depends(get_db),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep)
):
"""Store current traffic data to database"""

View File

@@ -1,10 +1,11 @@
# services/data/app/api/weather.py - UPDATED WITH UNIFIED AUTH
"""Weather data API endpoints with unified authentication"""
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks, Path
from typing import List, Optional, Dict, Any
from datetime import datetime, date
import structlog
from uuid import UUID
from app.schemas.external import (
WeatherDataResponse,
@@ -19,14 +20,14 @@ from shared.auth.decorators import (
get_current_tenant_id_dep
)
router = APIRouter(prefix="/weather", tags=["weather"])
router = APIRouter(tags=["weather"])
logger = structlog.get_logger()
@router.get("/current", response_model=WeatherDataResponse)
@router.get("/tenants/{tenant_id}/current", response_model=WeatherDataResponse)
async def get_current_weather(
latitude: float = Query(..., description="Latitude"),
longitude: float = Query(..., description="Longitude"),
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
):
"""Get current weather data for location"""
@@ -64,12 +65,12 @@ async def get_current_weather(
logger.error("Failed to get current weather", error=str(e))
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
@router.get("/forecast", response_model=List[WeatherForecastResponse])
@router.get("/tenants/{tenant_id}/forecast", response_model=List[WeatherForecastResponse])
async def get_weather_forecast(
latitude: float = Query(..., description="Latitude"),
longitude: float = Query(..., description="Longitude"),
days: int = Query(7, description="Number of forecast days", ge=1, le=14),
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
):
"""Get weather forecast for location"""
@@ -108,13 +109,13 @@ async def get_weather_forecast(
logger.error("Failed to get weather forecast", error=str(e))
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
@router.get("/history", response_model=List[WeatherDataResponse])
@router.get("/tenants/{tenant_id}/history", response_model=List[WeatherDataResponse])
async def get_weather_history(
start_date: date = Query(..., description="Start date"),
end_date: date = Query(..., description="End date"),
latitude: float = Query(..., description="Latitude"),
longitude: float = Query(..., description="Longitude"),
tenant_id: str = Depends(get_current_tenant_id_dep)
tenant_id: str = Path(..., description="Tenant ID")
):
"""Get historical weather data"""
try:
@@ -134,11 +135,11 @@ async def get_weather_history(
logger.error("Failed to get weather history", error=str(e))
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
@router.post("/sync")
@router.post("/tenants/{tenant_id}/sync")
async def sync_weather_data(
background_tasks: BackgroundTasks,
force: bool = Query(False, description="Force sync even if recently synced"),
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
):
"""Manually trigger weather data synchronization"""

View File

@@ -108,9 +108,9 @@ app.add_middleware(
)
# Include routers
app.include_router(sales_router, prefix="/api/v1/sales", tags=["sales"])
app.include_router(weather_router, prefix="/api/v1/weather", tags=["weather"])
app.include_router(traffic_router, prefix="/api/v1/traffic", tags=["traffic"])
app.include_router(sales_router, prefix="/api/v1", tags=["sales"])
app.include_router(weather_router, prefix="/api/v1", tags=["weather"])
app.include_router(traffic_router, prefix="/api/v1", tags=["traffic"])
# Health check endpoint
@app.get("/health")

View File

@@ -3,10 +3,11 @@
Tenant API endpoints
"""
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi import APIRouter, Depends, HTTPException, status, Path
from sqlalchemy.ext.asyncio import AsyncSession
from typing import List, Dict, Any
import structlog
from uuid import UUID
from app.core.database import get_db
from app.schemas.tenants import (
@@ -45,8 +46,8 @@ async def register_bakery(
@router.get("/tenants/{tenant_id}/access/{user_id}", response_model=TenantAccessResponse)
async def verify_tenant_access(
tenant_id: str,
user_id: str,
tenant_id: UUID = Path(..., description="Tenant ID"),
db: AsyncSession = Depends(get_db)
):
"""Verify if user has access to tenant - Called by Gateway"""
@@ -62,7 +63,7 @@ async def verify_tenant_access(
detail="Access verification failed"
)
@router.get("/users/{user_id}/tenants", response_model=List[TenantResponse])
@router.get("/tenants/users/{user_id}", response_model=List[TenantResponse])
async def get_user_tenants(
user_id: str,
current_user: Dict[str, Any] = Depends(get_current_user_dep),
@@ -89,7 +90,7 @@ async def get_user_tenants(
@router.get("/tenants/{tenant_id}", response_model=TenantResponse)
async def get_tenant(
tenant_id: str,
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
@@ -113,8 +114,8 @@ async def get_tenant(
@router.put("/tenants/{tenant_id}", response_model=TenantResponse)
async def update_tenant(
tenant_id: str,
update_data: TenantUpdate,
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):
@@ -134,9 +135,9 @@ async def update_tenant(
@router.post("/tenants/{tenant_id}/members", response_model=TenantMemberResponse)
async def add_team_member(
tenant_id: str,
user_id: str,
role: str,
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
):

View File

@@ -89,7 +89,7 @@ async def health_check():
@app.get("/metrics")
async def metrics():
"""Prometheus metrics endpoint"""
return metrics_collector.generate_latest()
return metrics_collector.get_metrics()
if __name__ == "__main__":
import uvicorn

View File

@@ -3,11 +3,11 @@
# ================================================================
"""Training API endpoints with unified authentication"""
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks, Query
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks, Query, Path
from typing import List, Optional, Dict, Any
from datetime import datetime
import structlog
import uuid
from uuid import UUID, uuid4
from app.schemas.training import (
TrainingJobRequest,
@@ -49,7 +49,7 @@ def get_training_service() -> TrainingService:
async def start_training_job(
request: TrainingJobRequest,
background_tasks: BackgroundTasks,
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
training_service: TrainingService = Depends(get_training_service),
db: AsyncSession = Depends(get_db_session) # Ensure db is available
@@ -57,11 +57,11 @@ async def start_training_job(
"""Start a new training job for all products"""
try:
new_job_id = str(uuid.uuid4())
new_job_id = str(uuid4())
logger.info("Starting training job",
tenant_id=tenant_id,
job_id=uuid.uuid4(),
job_id=uuid4(),
config=request.dict())
# Create training job
@@ -115,7 +115,7 @@ async def get_training_jobs(
status: Optional[TrainingStatus] = Query(None, description="Filter jobs by status"),
limit: int = Query(100, ge=1, le=1000),
offset: int = Query(0, ge=0),
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
training_service: TrainingService = Depends(get_training_service)
):
@@ -149,7 +149,7 @@ async def get_training_jobs(
@router.get("/jobs/{job_id}", response_model=TrainingJobResponse)
async def get_training_job(
job_id: str,
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
training_service: TrainingService = Depends(get_training_service)
):
@@ -182,7 +182,7 @@ async def get_training_job(
@router.get("/jobs/{job_id}/progress", response_model=TrainingJobProgress)
async def get_training_progress(
job_id: str,
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
training_service: TrainingService = Depends(get_training_service)
):
@@ -212,7 +212,7 @@ async def get_training_progress(
@router.post("/jobs/{job_id}/cancel")
async def cancel_training_job(
job_id: str,
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
training_service: TrainingService = Depends(get_training_service)
):
@@ -259,7 +259,7 @@ async def train_single_product(
product_name: str,
request: SingleProductTrainingRequest,
background_tasks: BackgroundTasks,
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
training_service: TrainingService = Depends(get_training_service),
db: AsyncSession = Depends(get_db_session)
@@ -312,7 +312,7 @@ async def train_single_product(
@router.post("/validate", response_model=DataValidationResponse)
async def validate_training_data(
request: DataValidationRequest,
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
training_service: TrainingService = Depends(get_training_service)
):
@@ -343,7 +343,7 @@ async def validate_training_data(
@router.get("/models")
async def get_trained_models(
product_name: Optional[str] = Query(None),
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
training_service: TrainingService = Depends(get_training_service)
):
@@ -374,7 +374,7 @@ async def get_trained_models(
@require_role("admin") # Only admins can delete models
async def delete_model(
model_id: str,
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
training_service: TrainingService = Depends(get_training_service)
):
@@ -411,7 +411,7 @@ async def delete_model(
async def get_training_stats(
start_date: Optional[datetime] = Query(None),
end_date: Optional[datetime] = Query(None),
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
training_service: TrainingService = Depends(get_training_service)
):
@@ -442,7 +442,7 @@ async def get_training_stats(
async def retrain_all_products(
request: TrainingJobRequest,
background_tasks: BackgroundTasks,
tenant_id: str = Depends(get_current_tenant_id_dep),
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
training_service: TrainingService = Depends(get_training_service)
):

View File

@@ -173,18 +173,10 @@ async def global_exception_handler(request: Request, exc: Exception):
}
)
# Include API routers - NO AUTH DEPENDENCIES HERE
# Authentication is handled by API Gateway
app.include_router(
training.router,
tags=["training"]
)
# Include API routers
app.include_router(training.router, prefix="/api/v1", tags=["training"])
app.include_router(models.router, prefix="/api/v1", tags=["models"])
app.include_router(
models.router,
prefix="/models",
tags=["models"]
)
# Health check endpoints
@app.get("/health")

View File

@@ -488,7 +488,7 @@ class TrainingService:
params["end_date"] = request.end_date.isoformat()
response = await client.get(
f"{settings.DATA_SERVICE_URL}/weather/history",
f"{settings.DATA_SERVICE_URL}/tenants/{tenant_id}/weather/history",
params=params,
timeout=30.0
)
@@ -516,7 +516,7 @@ class TrainingService:
params["end_date"] = request.end_date.isoformat()
response = await client.get(
f"{settings.DATA_SERVICE_URL}/traffic/historical",
f"{settings.DATA_SERVICE_URL}/tenants/{tenant_id}/traffic/historical",
params=params,
timeout=30.0
)