basyx's picture
Upload 629 files
1fed801 verified
Raw
History Blame Contribute Delete
16.9 kB
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"])
@router.post("/workspace/teams", response_model=TeamResponse)
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)
@router.get("/workspace/teams", response_model=list[TeamResponse])
async def list_teams(request: Request) -> list[TeamResponse]:
workspace_id, _, _, _ = _identity(request)
return await request.app.state.container.collaboration.list_teams(workspace_id)
@router.get("/workspace/members", response_model=list[MemberResponse])
async def list_members(request: Request) -> list[MemberResponse]:
workspace_id, _, _, _ = _identity(request)
return await request.app.state.container.collaboration.list_members(workspace_id)
@router.get("/workspace/notifications/preferences", response_model=list[dict[str, Any]])
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)
@router.post("/workspace/notifications/preferences", response_model=dict[str, Any])
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"]
)
@router.post("/workspace/invitations", response_model=InvitationResponse)
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)
@router.patch("/workspace/teams/{team_id}", response_model=TeamResponse)
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)
@router.delete("/workspace/teams/{team_id}", status_code=status.HTTP_204_NO_CONTENT)
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)
@router.get("/workspace/workflows/{workflow_id}/requests", response_model=list[ApprovalRequest])
async def list_approval_requests(request: Request, workflow_id: str) -> list[ApprovalRequest]:
return await request.app.state.container.approval.list_requests(workflow_id)
@router.post("/workspace/requests/{request_id}/approve", response_model=ApprovalRequest)
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)
@router.post("/workspace/requests/{request_id}/reject", response_model=ApprovalRequest)
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)
@router.post("/workspace/requests/{request_id}/comments", response_model=ReviewComment)
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
)
@router.delete("/workspace/members/{user_id}", status_code=status.HTTP_204_NO_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)
@router.patch("/workspace/members/{user_id}/role", response_model=MemberResponse)
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,
)
@router.get("", response_model=ProjectListResponse)
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,
)
@router.post("", response_model=ProjectResponse, status_code=status.HTTP_201_CREATED)
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,
)
@router.get("/{project_id}/editor", response_model=EditorStateResponse)
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)
)
@router.put("/{project_id}/editor", response_model=EditorStateResponse)
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,
)
@router.get("/{project_id}/renders", response_model=ProjectRenderListResponse)
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)
)
)
@router.post(
"/{project_id}/renders",
response_model=ProjectRenderResponse,
status_code=status.HTTP_202_ACCEPTED,
)
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 "",
)
@router.get("/{project_id}/renders/{render_id}", response_model=ProjectRenderResponse)
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),
)
@router.post("/{project_id}/renders/{render_id}/cancel", response_model=ProjectRenderResponse)
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),
)
@router.get("/{project_id}/assets", response_model=ProjectAssetListResponse)
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),
)
@router.post(
"/{project_id}/assets",
response_model=ProjectAssetResponse,
status_code=status.HTTP_201_CREATED,
)
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,
)
@router.get("/{project_id}/collaborators", response_model=list[MemberResponse])
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))
@router.post("/{project_id}/collaborators", response_model=MemberResponse)
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
)
@router.delete("/{project_id}/collaborators/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
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)
@router.get("/{project_id}/jobs", response_model=ProjectGenerationJobListResponse)
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),
)
@router.post(
"/{project_id}/jobs",
response_model=ProjectGenerationJobResponse,
status_code=status.HTTP_201_CREATED,
)
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,
)
@router.delete("/{project_id}/jobs/{job_id}", status_code=status.HTTP_204_NO_CONTENT)
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)
@router.get("/{project_id}", response_model=ProjectResponse)
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),
)
@router.patch("/{project_id}", response_model=ProjectResponse)
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,
)
@router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT)
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)