288 lines
10 KiB
Python
288 lines
10 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
"""ClickUp API routes — teams, hierarchy, lists, tasks (connection-scoped)."""
|
|
|
|
import logging
|
|
from typing import Any, Dict, Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request, status
|
|
from pydantic import BaseModel
|
|
|
|
from modules.auth import getCurrentUser, limiter
|
|
from modules.datamodels.datamodelUam import AuthAuthority, User, UserConnection
|
|
from modules.interfaces.interfaceDbApp import getInterface
|
|
from modules.serviceHub import getInterface as getServices
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(
|
|
prefix="/api/clickup",
|
|
tags=["ClickUp"],
|
|
responses={
|
|
404: {"description": "Not found"},
|
|
400: {"description": "Bad request"},
|
|
401: {"description": "Unauthorized"},
|
|
500: {"description": "Internal server error"},
|
|
},
|
|
)
|
|
|
|
|
|
def _getUserConnection(interface, connection_id: str, user_id: str) -> Optional[UserConnection]:
|
|
try:
|
|
connections = interface.getUserConnections(user_id)
|
|
for conn in connections:
|
|
if conn.id == connection_id:
|
|
return conn
|
|
return None
|
|
except Exception as e:
|
|
logger.error(f"Error getting user connection: {e}")
|
|
return None
|
|
|
|
|
|
def _clickup_connection_or_404(interface, connection_id: str, user_id: str) -> UserConnection:
|
|
connection = _getUserConnection(interface, connection_id, user_id)
|
|
if not connection:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Connection not found")
|
|
authority = connection.authority.value if hasattr(connection.authority, "value") else str(connection.authority)
|
|
if authority.lower() != AuthAuthority.CLICKUP.value:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Connection is not a ClickUp connection",
|
|
)
|
|
return connection
|
|
|
|
|
|
def _svc_for_connection(current_user: User, connection: UserConnection):
|
|
services = getServices(current_user, None)
|
|
if not services.clickup.setAccessTokenFromConnection(connection):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Failed to set ClickUp access token",
|
|
)
|
|
return services.clickup
|
|
|
|
|
|
# --- Routes (prefix is /api/clickup; OAuth lives under /api/clickup/auth/* in routeSecurityClickup) ---
|
|
|
|
|
|
@router.get("/{connectionId}/teams", response_model=Dict[str, Any])
|
|
@limiter.limit("30/minute")
|
|
async def get_teams(
|
|
request: Request,
|
|
connectionId: str = Path(..., description="ClickUp UserConnection id"),
|
|
currentUser: User = Depends(getCurrentUser),
|
|
) -> Dict[str, Any]:
|
|
interface = getInterface(currentUser)
|
|
conn = _clickup_connection_or_404(interface, connectionId, currentUser.id)
|
|
cu = _svc_for_connection(currentUser, conn)
|
|
return await cu.getAuthorizedTeams()
|
|
|
|
|
|
@router.get("/{connectionId}/teams/{teamId}", response_model=Dict[str, Any])
|
|
@limiter.limit("60/minute")
|
|
async def get_team(
|
|
request: Request,
|
|
connectionId: str = Path(...),
|
|
teamId: str = Path(...),
|
|
currentUser: User = Depends(getCurrentUser),
|
|
) -> Dict[str, Any]:
|
|
"""Workspace/team details including members (for assignee pickers)."""
|
|
interface = getInterface(currentUser)
|
|
conn = _clickup_connection_or_404(interface, connectionId, currentUser.id)
|
|
cu = _svc_for_connection(currentUser, conn)
|
|
return await cu.getTeam(teamId)
|
|
|
|
|
|
@router.get("/{connectionId}/teams/{teamId}/spaces", response_model=Dict[str, Any])
|
|
@limiter.limit("60/minute")
|
|
async def get_spaces(
|
|
request: Request,
|
|
connectionId: str = Path(...),
|
|
teamId: str = Path(...),
|
|
currentUser: User = Depends(getCurrentUser),
|
|
) -> Dict[str, Any]:
|
|
interface = getInterface(currentUser)
|
|
conn = _clickup_connection_or_404(interface, connectionId, currentUser.id)
|
|
cu = _svc_for_connection(currentUser, conn)
|
|
return await cu.getSpaces(teamId)
|
|
|
|
|
|
@router.get("/{connectionId}/spaces/{spaceId}/folders", response_model=Dict[str, Any])
|
|
@limiter.limit("60/minute")
|
|
async def get_folders(
|
|
request: Request,
|
|
connectionId: str = Path(...),
|
|
spaceId: str = Path(...),
|
|
currentUser: User = Depends(getCurrentUser),
|
|
) -> Dict[str, Any]:
|
|
interface = getInterface(currentUser)
|
|
conn = _clickup_connection_or_404(interface, connectionId, currentUser.id)
|
|
cu = _svc_for_connection(currentUser, conn)
|
|
return await cu.getFolders(spaceId)
|
|
|
|
|
|
@router.get("/{connectionId}/spaces/{spaceId}/lists", response_model=Dict[str, Any])
|
|
@limiter.limit("60/minute")
|
|
async def get_folderless_lists(
|
|
request: Request,
|
|
connectionId: str = Path(...),
|
|
spaceId: str = Path(...),
|
|
currentUser: User = Depends(getCurrentUser),
|
|
) -> Dict[str, Any]:
|
|
interface = getInterface(currentUser)
|
|
conn = _clickup_connection_or_404(interface, connectionId, currentUser.id)
|
|
cu = _svc_for_connection(currentUser, conn)
|
|
return await cu.getFolderlessLists(spaceId)
|
|
|
|
|
|
@router.get("/{connectionId}/folders/{folderId}/lists", response_model=Dict[str, Any])
|
|
@limiter.limit("60/minute")
|
|
async def get_lists_in_folder(
|
|
request: Request,
|
|
connectionId: str = Path(...),
|
|
folderId: str = Path(...),
|
|
currentUser: User = Depends(getCurrentUser),
|
|
) -> Dict[str, Any]:
|
|
interface = getInterface(currentUser)
|
|
conn = _clickup_connection_or_404(interface, connectionId, currentUser.id)
|
|
cu = _svc_for_connection(currentUser, conn)
|
|
return await cu.getListsInFolder(folderId)
|
|
|
|
|
|
@router.get("/{connectionId}/lists/{listId}", response_model=Dict[str, Any])
|
|
@limiter.limit("60/minute")
|
|
async def get_list(
|
|
request: Request,
|
|
connectionId: str = Path(...),
|
|
listId: str = Path(...),
|
|
currentUser: User = Depends(getCurrentUser),
|
|
) -> Dict[str, Any]:
|
|
interface = getInterface(currentUser)
|
|
conn = _clickup_connection_or_404(interface, connectionId, currentUser.id)
|
|
cu = _svc_for_connection(currentUser, conn)
|
|
return await cu.getList(listId)
|
|
|
|
|
|
@router.get("/{connectionId}/lists/{listId}/fields", response_model=Dict[str, Any])
|
|
@limiter.limit("60/minute")
|
|
async def get_list_fields(
|
|
request: Request,
|
|
connectionId: str = Path(...),
|
|
listId: str = Path(...),
|
|
currentUser: User = Depends(getCurrentUser),
|
|
) -> Dict[str, Any]:
|
|
interface = getInterface(currentUser)
|
|
conn = _clickup_connection_or_404(interface, connectionId, currentUser.id)
|
|
cu = _svc_for_connection(currentUser, conn)
|
|
return await cu.getListFields(listId)
|
|
|
|
|
|
@router.get("/{connectionId}/lists/{listId}/tasks", response_model=Dict[str, Any])
|
|
@limiter.limit("60/minute")
|
|
async def get_list_tasks(
|
|
request: Request,
|
|
connectionId: str = Path(...),
|
|
listId: str = Path(...),
|
|
page: int = Query(0),
|
|
include_closed: bool = Query(False),
|
|
currentUser: User = Depends(getCurrentUser),
|
|
) -> Dict[str, Any]:
|
|
interface = getInterface(currentUser)
|
|
conn = _clickup_connection_or_404(interface, connectionId, currentUser.id)
|
|
cu = _svc_for_connection(currentUser, conn)
|
|
return await cu.getTasksInList(listId, page=page, include_closed=include_closed)
|
|
|
|
|
|
class TaskCreateBody(BaseModel):
|
|
body: Dict[str, Any]
|
|
|
|
|
|
@router.post("/{connectionId}/lists/{listId}/tasks", response_model=Dict[str, Any])
|
|
@limiter.limit("30/minute")
|
|
async def create_list_task(
|
|
request: Request,
|
|
payload: TaskCreateBody,
|
|
connectionId: str = Path(...),
|
|
listId: str = Path(...),
|
|
currentUser: User = Depends(getCurrentUser),
|
|
) -> Dict[str, Any]:
|
|
interface = getInterface(currentUser)
|
|
conn = _clickup_connection_or_404(interface, connectionId, currentUser.id)
|
|
cu = _svc_for_connection(currentUser, conn)
|
|
return await cu.createTask(listId, payload.body)
|
|
|
|
|
|
class TaskUpdateBody(BaseModel):
|
|
body: Dict[str, Any]
|
|
|
|
|
|
@router.get("/{connectionId}/tasks/{taskId}", response_model=Dict[str, Any])
|
|
@limiter.limit("60/minute")
|
|
async def get_task(
|
|
request: Request,
|
|
connectionId: str = Path(...),
|
|
taskId: str = Path(...),
|
|
currentUser: User = Depends(getCurrentUser),
|
|
) -> Dict[str, Any]:
|
|
interface = getInterface(currentUser)
|
|
conn = _clickup_connection_or_404(interface, connectionId, currentUser.id)
|
|
cu = _svc_for_connection(currentUser, conn)
|
|
return await cu.getTask(taskId)
|
|
|
|
|
|
@router.put("/{connectionId}/tasks/{taskId}", response_model=Dict[str, Any])
|
|
@limiter.limit("30/minute")
|
|
async def update_task(
|
|
request: Request,
|
|
payload: TaskUpdateBody,
|
|
connectionId: str = Path(...),
|
|
taskId: str = Path(...),
|
|
currentUser: User = Depends(getCurrentUser),
|
|
) -> Dict[str, Any]:
|
|
interface = getInterface(currentUser)
|
|
conn = _clickup_connection_or_404(interface, connectionId, currentUser.id)
|
|
cu = _svc_for_connection(currentUser, conn)
|
|
return await cu.updateTask(taskId, payload.body)
|
|
|
|
|
|
@router.delete("/{connectionId}/tasks/{taskId}", response_model=Dict[str, Any])
|
|
@limiter.limit("30/minute")
|
|
async def delete_task(
|
|
request: Request,
|
|
connectionId: str = Path(...),
|
|
taskId: str = Path(...),
|
|
currentUser: User = Depends(getCurrentUser),
|
|
) -> Dict[str, Any]:
|
|
interface = getInterface(currentUser)
|
|
conn = _clickup_connection_or_404(interface, connectionId, currentUser.id)
|
|
cu = _svc_for_connection(currentUser, conn)
|
|
return await cu.deleteTask(taskId)
|
|
|
|
|
|
@router.get("/{connectionId}/teams/{teamId}/tasks/search", response_model=Dict[str, Any])
|
|
@limiter.limit("30/minute")
|
|
async def search_team_tasks(
|
|
request: Request,
|
|
connectionId: str = Path(...),
|
|
teamId: str = Path(...),
|
|
query: str = Query(..., description="Search query"),
|
|
page: int = Query(0),
|
|
currentUser: User = Depends(getCurrentUser),
|
|
) -> Dict[str, Any]:
|
|
interface = getInterface(currentUser)
|
|
conn = _clickup_connection_or_404(interface, connectionId, currentUser.id)
|
|
cu = _svc_for_connection(currentUser, conn)
|
|
return await cu.searchTeamTasks(teamId, query=query, page=page)
|
|
|
|
|
|
@router.get("/{connectionId}/user", response_model=Dict[str, Any])
|
|
@limiter.limit("30/minute")
|
|
async def get_authorized_user(
|
|
request: Request,
|
|
connectionId: str = Path(...),
|
|
currentUser: User = Depends(getCurrentUser),
|
|
) -> Dict[str, Any]:
|
|
interface = getInterface(currentUser)
|
|
conn = _clickup_connection_or_404(interface, connectionId, currentUser.id)
|
|
cu = _svc_for_connection(currentUser, conn)
|
|
return await cu.getAuthorizedUser()
|