feat: add import routes and ingestion service for project and scenario imports
This commit is contained in:
@@ -21,6 +21,7 @@ from services.session import (
|
||||
extract_session_tokens,
|
||||
)
|
||||
from services.unit_of_work import UnitOfWork
|
||||
from services.importers import ImportIngestionService
|
||||
|
||||
|
||||
def get_unit_of_work() -> Generator[UnitOfWork, None, None]:
|
||||
@@ -30,6 +31,15 @@ def get_unit_of_work() -> Generator[UnitOfWork, None, None]:
|
||||
yield uow
|
||||
|
||||
|
||||
_IMPORT_INGESTION_SERVICE = ImportIngestionService(lambda: UnitOfWork())
|
||||
|
||||
|
||||
def get_import_ingestion_service() -> ImportIngestionService:
|
||||
"""Provide singleton import ingestion service."""
|
||||
|
||||
return _IMPORT_INGESTION_SERVICE
|
||||
|
||||
|
||||
def get_application_settings() -> Settings:
|
||||
"""Provide cached application settings instance."""
|
||||
|
||||
|
||||
2
main.py
2
main.py
@@ -16,6 +16,7 @@ from models import (
|
||||
)
|
||||
from routes.auth import router as auth_router
|
||||
from routes.dashboard import router as dashboard_router
|
||||
from routes.imports import router as imports_router
|
||||
from routes.projects import router as projects_router
|
||||
from routes.scenarios import router as scenarios_router
|
||||
from services.bootstrap import bootstrap_admin
|
||||
@@ -61,6 +62,7 @@ async def ensure_admin_bootstrap() -> None:
|
||||
|
||||
app.include_router(dashboard_router)
|
||||
app.include_router(auth_router)
|
||||
app.include_router(imports_router)
|
||||
app.include_router(projects_router)
|
||||
app.include_router(scenarios_router)
|
||||
|
||||
|
||||
143
routes/imports.py
Normal file
143
routes/imports.py
Normal file
@@ -0,0 +1,143 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
||||
|
||||
from dependencies import get_import_ingestion_service, require_roles
|
||||
from models import User
|
||||
from schemas.imports import (
|
||||
ImportCommitRequest,
|
||||
ProjectImportCommitResponse,
|
||||
ProjectImportPreviewResponse,
|
||||
ScenarioImportCommitResponse,
|
||||
ScenarioImportPreviewResponse,
|
||||
)
|
||||
from services.importers import ImportIngestionService, UnsupportedImportFormat
|
||||
|
||||
router = APIRouter(prefix="/imports", tags=["Imports"])
|
||||
|
||||
MANAGE_ROLES = ("project_manager", "admin")
|
||||
|
||||
|
||||
async def _read_upload_file(upload: UploadFile) -> BytesIO:
|
||||
content = await upload.read()
|
||||
if not content:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Uploaded file is empty.",
|
||||
)
|
||||
return BytesIO(content)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/projects/preview",
|
||||
response_model=ProjectImportPreviewResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
async def preview_project_import(
|
||||
file: UploadFile = File(...,
|
||||
description="Project import file (CSV or Excel)"),
|
||||
_: User = Depends(require_roles(*MANAGE_ROLES)),
|
||||
ingestion_service: ImportIngestionService = Depends(
|
||||
get_import_ingestion_service),
|
||||
) -> ProjectImportPreviewResponse:
|
||||
if not file.filename:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Filename is required for import.",
|
||||
)
|
||||
|
||||
stream = await _read_upload_file(file)
|
||||
|
||||
try:
|
||||
preview = ingestion_service.preview_projects(stream, file.filename)
|
||||
except UnsupportedImportFormat as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
|
||||
return ProjectImportPreviewResponse.model_validate(preview)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/scenarios/preview",
|
||||
response_model=ScenarioImportPreviewResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
async def preview_scenario_import(
|
||||
file: UploadFile = File(...,
|
||||
description="Scenario import file (CSV or Excel)"),
|
||||
_: User = Depends(require_roles(*MANAGE_ROLES)),
|
||||
ingestion_service: ImportIngestionService = Depends(
|
||||
get_import_ingestion_service),
|
||||
) -> ScenarioImportPreviewResponse:
|
||||
if not file.filename:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Filename is required for import.",
|
||||
)
|
||||
|
||||
stream = await _read_upload_file(file)
|
||||
|
||||
try:
|
||||
preview = ingestion_service.preview_scenarios(stream, file.filename)
|
||||
except UnsupportedImportFormat as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
|
||||
return ScenarioImportPreviewResponse.model_validate(preview)
|
||||
|
||||
|
||||
def _value_error_status(exc: ValueError) -> int:
|
||||
detail = str(exc)
|
||||
if detail.lower().startswith("unknown"):
|
||||
return status.HTTP_404_NOT_FOUND
|
||||
return status.HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
@router.post(
|
||||
"/projects/commit",
|
||||
response_model=ProjectImportCommitResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
async def commit_project_import_endpoint(
|
||||
payload: ImportCommitRequest,
|
||||
_: User = Depends(require_roles(*MANAGE_ROLES)),
|
||||
ingestion_service: ImportIngestionService = Depends(
|
||||
get_import_ingestion_service),
|
||||
) -> ProjectImportCommitResponse:
|
||||
try:
|
||||
result = ingestion_service.commit_project_import(payload.token)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=_value_error_status(exc),
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
|
||||
return ProjectImportCommitResponse.model_validate(result)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/scenarios/commit",
|
||||
response_model=ScenarioImportCommitResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
async def commit_scenario_import_endpoint(
|
||||
payload: ImportCommitRequest,
|
||||
_: User = Depends(require_roles(*MANAGE_ROLES)),
|
||||
ingestion_service: ImportIngestionService = Depends(
|
||||
get_import_ingestion_service),
|
||||
) -> ScenarioImportCommitResponse:
|
||||
try:
|
||||
result = ingestion_service.commit_scenario_import(payload.token)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=_value_error_status(exc),
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
|
||||
return ScenarioImportCommitResponse.model_validate(result)
|
||||
Reference in New Issue
Block a user