""" Cloud API package — router aggregator. One parent router owns the /api/cloud prefix. Sub-routers (connections, browse) carry no prefix — paths are declared relative to the parent (D-04 pattern, same as api/documents/__init__.py). main.py imports: from api.cloud import router as cloud_router, users_router app.include_router(cloud_router) app.include_router(users_router) """ from fastapi import APIRouter, Depends, HTTPException, Request, status from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from api.cloud.connections import router as connections_router, _VALID_BACKENDS from api.cloud.browse import router as browse_router from db.models import User from deps.auth import get_regular_user from deps.db import get_db from services.rate_limiting import account_limiter router = APIRouter(prefix="/api/cloud", tags=["cloud"]) router.include_router(connections_router) router.include_router(browse_router) # ── Users router (default storage) ─────────────────────────────────────────── users_router = APIRouter(prefix="/api/users", tags=["users"]) class DefaultStorageRequest(BaseModel): backend: str @users_router.patch("/me/default-storage") @account_limiter.limit("100/minute") async def update_default_storage( request: Request, body: DefaultStorageRequest, session: AsyncSession = Depends(get_db), current_user: User = Depends(get_regular_user), ) -> dict: """Update the current user's default storage backend. Validated against the allowlist before storage. """ if body.backend not in _VALID_BACKENDS: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Invalid backend. Valid values: {sorted(_VALID_BACKENDS)}", ) request.state.current_user = current_user user = await session.get(User, current_user.id) if user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") user.default_storage_backend = body.backend session.add(user) await session.commit() return {"default_storage_backend": user.default_storage_backend}