Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| from typing import Annotated, Any | |
| from uuid import UUID | |
| from fastapi import APIRouter, Header, Path, Query, Request, Response, status | |
| from app.projects.editor_schemas import ( | |
| EditorSaveRequest, | |
| EditorStateResponse, | |
| ProjectRenderCreate, | |
| ProjectRenderListResponse, | |
| ProjectRenderResponse, | |
| ) | |
| from app.projects.schemas import ( | |
| ProjectAssetAttach, | |
| ProjectAssetListResponse, | |
| ProjectAssetResponse, | |
| ProjectCreate, | |
| ProjectGenerationJobAttach, | |
| ProjectGenerationJobListResponse, | |
| ProjectGenerationJobResponse, | |
| ProjectListResponse, | |
| ProjectResponse, | |
| ProjectStatus, | |
| ProjectUpdate, | |
| ) | |
| from app.projects.schemas.collaboration import ( | |
| TeamResponse, | |
| InvitationCreate, | |
| InvitationResponse, | |
| TeamBase, | |
| MemberResponse | |
| ) | |
| from app.projects.schemas.approval import ApprovalRequest, ReviewComment | |
| from app.security.errors import ForbiddenError | |
| router = APIRouter(prefix="/v1/projects", tags=["projects"]) | |
| async def create_team(request: Request, payload: TeamBase) -> TeamResponse: | |
| workspace_id, _, _, _ = _identity(request) | |
| return await request.app.state.container.collaboration.create_team(workspace_id, payload.name) | |
| async def list_teams(request: Request) -> list[TeamResponse]: | |
| workspace_id, _, _, _ = _identity(request) | |
| return await request.app.state.container.collaboration.list_teams(workspace_id) | |
| async def list_members(request: Request) -> list[MemberResponse]: | |
| workspace_id, _, _, _ = _identity(request) | |
| return await request.app.state.container.collaboration.list_members(workspace_id) | |
| async def list_notification_preferences(request: Request) -> list[dict[str, Any]]: | |
| workspace_id, user_id, _, _ = _identity(request) | |
| return await request.app.state.container.notifications.get_preferences(workspace_id, user_id) | |
| async def update_notification_preference(request: Request, payload: dict[str, Any]) -> dict[str, Any]: | |
| workspace_id, user_id, _, _ = _identity(request) | |
| return await request.app.state.container.notifications.update_preference( | |
| workspace_id, user_id, payload["event_type"], payload["enabled"] | |
| ) | |
| async def invite_member(request: Request, payload: InvitationCreate) -> InvitationResponse: | |
| workspace_id, _, _, _ = _identity(request) | |
| return await request.app.state.container.collaboration.invite_member(workspace_id, payload.email, payload.role) | |
| async def update_team(request: Request, team_id: str, payload: TeamBase) -> TeamResponse: | |
| workspace_id, _, _, _ = _identity(request) | |
| return await request.app.state.container.collaboration.update_team(workspace_id, team_id, payload.name) | |
| async def archive_team(request: Request, team_id: str) -> Response: | |
| workspace_id, _, _, _ = _identity(request) | |
| await request.app.state.container.collaboration.archive_team(workspace_id, team_id) | |
| return Response(status_code=status.HTTP_204_NO_CONTENT) | |
| async def list_approval_requests(request: Request, workflow_id: str) -> list[ApprovalRequest]: | |
| return await request.app.state.container.approval.list_requests(workflow_id) | |
| async def approve_request( | |
| request: Request, | |
| request_id: str | |
| ) -> ApprovalRequest: | |
| _, user_id, _, _ = _identity(request) | |
| return await request.app.state.container.approval.approve_request(request_id, user_id) | |
| async def reject_request( | |
| request: Request, | |
| request_id: str | |
| ) -> ApprovalRequest: | |
| _, user_id, _, _ = _identity(request) | |
| return await request.app.state.container.approval.reject_request(request_id, user_id) | |
| async def add_review_comment( | |
| request: Request, | |
| request_id: str, | |
| content: str | |
| ) -> ReviewComment: | |
| workspace_id, user_id, _, _ = _identity(request) | |
| return await request.app.state.container.approval.add_comment( | |
| request_id, user_id, workspace_id, content | |
| ) | |
| async def remove_member(request: Request, user_id: str) -> Response: | |
| workspace_id, actor_user_id, _, _ = _identity(request) | |
| await request.app.state.container.collaboration.remove_member(workspace_id, actor_user_id, user_id) | |
| return Response(status_code=status.HTTP_204_NO_CONTENT) | |
| async def update_member_role(request: Request, user_id: str, new_role: str) -> MemberResponse: | |
| workspace_id, actor_user_id, _, _ = _identity(request) | |
| await request.app.state.container.collaboration.update_member_role(workspace_id, actor_user_id, user_id, new_role) | |
| # Return updated membership | |
| membership = await request.app.state.container.collaboration.get_membership(workspace_id, user_id) | |
| return MemberResponse(id=membership.id, workspace_id=membership.workspace_id, user_id=membership.user_id, role=membership.role, created_at=membership.created_at.isoformat()) | |
| def _identity(request: Request) -> tuple[str, str, str, str]: | |
| context = request.state.auth | |
| if not context.workspace_id or not context.user_id: | |
| # API-key middleware resolves this authoritative membership. Client | |
| # request bodies and query strings are never tenant selectors. | |
| raise ForbiddenError | |
| return ( | |
| context.workspace_id, | |
| context.user_id, | |
| context.api_key_id, | |
| request.state.request_id, | |
| ) | |
| async def list_projects( | |
| request: Request, | |
| project_status: Annotated[ProjectStatus | None, Query(alias="status")] = ProjectStatus.ACTIVE, | |
| search: Annotated[str | None, Query(min_length=1, max_length=200)] = None, | |
| limit: Annotated[int, Query(ge=1, le=100)] = 50, | |
| cursor: Annotated[str | None, Query(min_length=1, max_length=1024)] = None, | |
| ) -> ProjectListResponse: | |
| workspace_id, user_id, _, _ = _identity(request) | |
| return await request.app.state.container.projects.list( | |
| workspace_id=workspace_id, | |
| user_id=user_id, | |
| status=project_status, | |
| search=search, | |
| limit=limit, | |
| cursor=cursor, | |
| ) | |
| async def create_project(request: Request, payload: ProjectCreate) -> ProjectResponse: | |
| workspace_id, user_id, api_key_id, request_id = _identity(request) | |
| return await request.app.state.container.projects.create( | |
| workspace_id=workspace_id, | |
| user_id=user_id, | |
| api_key_id=api_key_id, | |
| request_id=request_id, | |
| payload=payload, | |
| ) | |
| async def get_project_editor( | |
| request: Request, | |
| project_id: Annotated[UUID, Path(description="Canonical project UUID")], | |
| ) -> EditorStateResponse: | |
| workspace_id, user_id, _, _ = _identity(request) | |
| return await request.app.state.container.editor.get( | |
| workspace_id=workspace_id, user_id=user_id, project_id=str(project_id) | |
| ) | |
| async def save_project_editor( | |
| request: Request, | |
| payload: EditorSaveRequest, | |
| project_id: Annotated[UUID, Path(description="Canonical project UUID")], | |
| ) -> EditorStateResponse: | |
| workspace_id, user_id, api_key_id, request_id = _identity(request) | |
| return await request.app.state.container.editor.save( | |
| workspace_id=workspace_id, | |
| user_id=user_id, | |
| api_key_id=api_key_id, | |
| request_id=request_id, | |
| project_id=str(project_id), | |
| payload=payload, | |
| ) | |
| async def list_project_renders( | |
| request: Request, | |
| project_id: Annotated[UUID, Path(description="Canonical project UUID")], | |
| ) -> ProjectRenderListResponse: | |
| workspace_id, user_id, _, _ = _identity(request) | |
| return ProjectRenderListResponse( | |
| items=await request.app.state.container.renders.list( | |
| workspace_id=workspace_id, user_id=user_id, project_id=str(project_id) | |
| ) | |
| ) | |
| async def create_project_render( | |
| request: Request, | |
| payload: ProjectRenderCreate, | |
| project_id: Annotated[UUID, Path(description="Canonical project UUID")], | |
| idempotency_key: Annotated[str | None, Header(alias="Idempotency-Key")] = None, | |
| ) -> ProjectRenderResponse: | |
| workspace_id, user_id, api_key_id, request_id = _identity(request) | |
| request.app.state.container.api_keys.authorize(request.state.auth, "jobs:create") | |
| return await request.app.state.container.renders.create( | |
| workspace_id=workspace_id, | |
| user_id=user_id, | |
| api_key_id=api_key_id, | |
| request_id=request_id, | |
| project_id=str(project_id), | |
| payload=payload, | |
| idempotency_key=idempotency_key or "", | |
| ) | |
| async def get_project_render( | |
| request: Request, | |
| project_id: Annotated[UUID, Path(description="Canonical project UUID")], | |
| render_id: Annotated[UUID, Path(description="Canonical render job UUID")], | |
| ) -> ProjectRenderResponse: | |
| workspace_id, user_id, _, _ = _identity(request) | |
| return await request.app.state.container.renders.get( | |
| workspace_id=workspace_id, | |
| user_id=user_id, | |
| project_id=str(project_id), | |
| render_id=str(render_id), | |
| ) | |
| async def cancel_project_render( | |
| request: Request, | |
| project_id: Annotated[UUID, Path(description="Canonical project UUID")], | |
| render_id: Annotated[UUID, Path(description="Canonical render job UUID")], | |
| ) -> ProjectRenderResponse: | |
| workspace_id, user_id, api_key_id, request_id = _identity(request) | |
| request.app.state.container.api_keys.authorize(request.state.auth, "jobs:cancel") | |
| return await request.app.state.container.renders.cancel( | |
| workspace_id=workspace_id, | |
| user_id=user_id, | |
| api_key_id=api_key_id, | |
| request_id=request_id, | |
| project_id=str(project_id), | |
| render_id=str(render_id), | |
| ) | |
| async def list_project_assets( | |
| request: Request, | |
| project_id: Annotated[UUID, Path(description="Canonical project UUID")], | |
| ) -> ProjectAssetListResponse: | |
| workspace_id, user_id, _, _ = _identity(request) | |
| return await request.app.state.container.projects.list_assets( | |
| workspace_id=workspace_id, | |
| user_id=user_id, | |
| project_id=str(project_id), | |
| ) | |
| async def attach_project_asset( | |
| request: Request, | |
| payload: ProjectAssetAttach, | |
| project_id: Annotated[UUID, Path(description="Canonical project UUID")], | |
| ) -> ProjectAssetResponse: | |
| workspace_id, user_id, api_key_id, request_id = _identity(request) | |
| return await request.app.state.container.projects.attach_asset( | |
| workspace_id=workspace_id, | |
| user_id=user_id, | |
| api_key_id=api_key_id, | |
| request_id=request_id, | |
| project_id=str(project_id), | |
| asset_id=payload.asset_id, | |
| ) | |
| async def list_project_collaborators( | |
| request: Request, | |
| project_id: UUID | |
| ) -> list[MemberResponse]: | |
| return await request.app.state.container.collaboration.list_project_collaborators(str(project_id)) | |
| async def add_project_collaborator( | |
| request: Request, | |
| project_id: UUID, | |
| user_id: str, | |
| role: str | |
| ) -> MemberResponse: | |
| workspace_id, _, _, _ = _identity(request) | |
| return await request.app.state.container.collaboration.add_project_collaborator( | |
| workspace_id, str(project_id), user_id, role | |
| ) | |
| async def remove_project_collaborator( | |
| request: Request, | |
| project_id: UUID, | |
| user_id: str | |
| ) -> Response: | |
| await request.app.state.container.collaboration.remove_project_collaborator(str(project_id), user_id) | |
| return Response(status_code=status.HTTP_204_NO_CONTENT) | |
| async def list_project_generation_jobs( | |
| request: Request, | |
| project_id: Annotated[UUID, Path(description="Canonical project UUID")], | |
| ) -> ProjectGenerationJobListResponse: | |
| workspace_id, user_id, _, _ = _identity(request) | |
| return await request.app.state.container.projects.list_generation_jobs( | |
| workspace_id=workspace_id, | |
| user_id=user_id, | |
| project_id=str(project_id), | |
| ) | |
| async def attach_project_generation_job( | |
| request: Request, | |
| payload: ProjectGenerationJobAttach, | |
| project_id: Annotated[UUID, Path(description="Canonical project UUID")], | |
| ) -> ProjectGenerationJobResponse: | |
| workspace_id, user_id, api_key_id, request_id = _identity(request) | |
| return await request.app.state.container.projects.attach_generation_job( | |
| workspace_id=workspace_id, | |
| user_id=user_id, | |
| api_key_id=api_key_id, | |
| request_id=request_id, | |
| project_id=str(project_id), | |
| generation_job_id=payload.generation_job_id, | |
| ) | |
| async def detach_project_generation_job( | |
| request: Request, | |
| project_id: Annotated[UUID, Path(description="Canonical project UUID")], | |
| job_id: Annotated[UUID, Path(description="Durable generation job UUID")], | |
| ) -> Response: | |
| workspace_id, user_id, api_key_id, request_id = _identity(request) | |
| await request.app.state.container.projects.detach_generation_job( | |
| workspace_id=workspace_id, | |
| user_id=user_id, | |
| api_key_id=api_key_id, | |
| request_id=request_id, | |
| project_id=str(project_id), | |
| generation_job_id=str(job_id), | |
| ) | |
| return Response(status_code=status.HTTP_204_NO_CONTENT) | |
| async def get_project( | |
| request: Request, | |
| project_id: Annotated[UUID, Path(description="Canonical project UUID")], | |
| ) -> ProjectResponse: | |
| workspace_id, user_id, _, _ = _identity(request) | |
| return await request.app.state.container.projects.get( | |
| workspace_id=workspace_id, | |
| user_id=user_id, | |
| project_id=str(project_id), | |
| ) | |
| async def update_project( | |
| request: Request, | |
| payload: ProjectUpdate, | |
| project_id: Annotated[UUID, Path(description="Canonical project UUID")], | |
| ) -> ProjectResponse: | |
| workspace_id, user_id, api_key_id, request_id = _identity(request) | |
| return await request.app.state.container.projects.update( | |
| workspace_id=workspace_id, | |
| user_id=user_id, | |
| api_key_id=api_key_id, | |
| request_id=request_id, | |
| project_id=str(project_id), | |
| payload=payload, | |
| ) | |
| async def delete_project( | |
| request: Request, | |
| project_id: Annotated[UUID, Path(description="Canonical project UUID")], | |
| ) -> Response: | |
| workspace_id, user_id, api_key_id, request_id = _identity(request) | |
| await request.app.state.container.projects.delete( | |
| workspace_id=workspace_id, | |
| user_id=user_id, | |
| api_key_id=api_key_id, | |
| request_id=request_id, | |
| project_id=str(project_id), | |
| ) | |
| return Response(status_code=status.HTTP_204_NO_CONTENT) | |