138 lines
5.4 KiB
Python
138 lines
5.4 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
"""ClickUp API service (OAuth or personal token via UserConnection).
|
|
|
|
Extends the low-level ClickupApiClient from connectors with service-layer
|
|
concerns (token resolution via SecurityService, extended query params).
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
from typing import Any, Callable, Dict, List, Optional, Union
|
|
|
|
import aiohttp
|
|
|
|
from modules.connectors.connectorProviderClickup import ClickupApiClient, clickupAuthorizationHeader
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_CLICKUP_API_BASE = "https://api.clickup.com/api/v2"
|
|
|
|
|
|
def _clickupAuthorizationHeader(token: str) -> str:
|
|
"""ClickUp: personal tokens are `pk_...` without Bearer; OAuth uses Bearer."""
|
|
return clickupAuthorizationHeader(token)
|
|
|
|
|
|
class ClickupService(ClickupApiClient):
|
|
"""ClickUp service — adds token resolution and extended API methods on top of the API client."""
|
|
|
|
def __init__(self, context, get_service: Callable[[str], Any]):
|
|
super().__init__(accessToken="")
|
|
self._context = context
|
|
self._get_service = get_service
|
|
|
|
def setAccessTokenFromConnection(self, userConnection) -> bool:
|
|
"""Load OAuth/personal token from SecurityService for this UserConnection."""
|
|
try:
|
|
if not userConnection:
|
|
logger.error("UserConnection is required to set access token")
|
|
return False
|
|
if isinstance(userConnection, dict):
|
|
connection_id = userConnection.get("id")
|
|
else:
|
|
connection_id = getattr(userConnection, "id", None)
|
|
if not connection_id:
|
|
logger.error("UserConnection must have an 'id' field")
|
|
return False
|
|
security = self._get_service("security")
|
|
if not security:
|
|
logger.error("Security service not available for token access")
|
|
return False
|
|
token = security.getFreshToken(connection_id)
|
|
if not token:
|
|
logger.error(f"No token found for connection {connection_id}")
|
|
return False
|
|
self.accessToken = token.tokenAccess
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Error setting ClickUp access token: {e}")
|
|
return False
|
|
|
|
def setAccessToken(self, token: str) -> None:
|
|
"""Set token directly (e.g. connector adapter)."""
|
|
self.accessToken = token
|
|
|
|
async def requestRaw(
|
|
self,
|
|
method: str,
|
|
path: str,
|
|
*,
|
|
params: Optional[Dict[str, Any]] = None,
|
|
json_body: Optional[Dict[str, Any]] = None,
|
|
) -> Union[Dict[str, Any], List[Any], None]:
|
|
"""Escape hatch: call any v2 path under /api/v2 (path without leading /api/v2)."""
|
|
return await self._request(method, path, params=params, json_body=json_body)
|
|
|
|
# --- Extended API methods (beyond base ClickupApiClient) ---
|
|
|
|
async def getAuthorizedUser(self) -> Dict[str, Any]:
|
|
return await self._request("GET", "/user")
|
|
|
|
async def getTeam(self, team_id: str) -> Dict[str, Any]:
|
|
return await self._request("GET", f"/team/{team_id}")
|
|
|
|
async def getSpace(self, space_id: str) -> Dict[str, Any]:
|
|
return await self._request("GET", f"/space/{space_id}")
|
|
|
|
async def getFolder(self, folder_id: str) -> Dict[str, Any]:
|
|
return await self._request("GET", f"/folder/{folder_id}")
|
|
|
|
async def getList(self, list_id: str) -> Dict[str, Any]:
|
|
return await self._request("GET", f"/list/{list_id}")
|
|
|
|
async def getListFields(self, list_id: str) -> Dict[str, Any]:
|
|
return await self._request("GET", f"/list/{list_id}/field")
|
|
|
|
async def getTasksInList(
|
|
self,
|
|
list_id: str,
|
|
*,
|
|
page: int = 0,
|
|
include_closed: bool = False,
|
|
subtasks: bool = True,
|
|
dateCreatedGt: Optional[int] = None,
|
|
dateCreatedLt: Optional[int] = None,
|
|
dateUpdatedGt: Optional[int] = None,
|
|
dateUpdatedLt: Optional[int] = None,
|
|
customFields: Optional[List[Dict[str, Any]]] = None,
|
|
) -> Dict[str, Any]:
|
|
params: Dict[str, Any] = {
|
|
"page": page,
|
|
"subtasks": str(subtasks).lower(),
|
|
"include_closed": str(include_closed).lower(),
|
|
}
|
|
if dateCreatedGt is not None:
|
|
params["date_created_gt"] = dateCreatedGt
|
|
if dateCreatedLt is not None:
|
|
params["date_created_lt"] = dateCreatedLt
|
|
if dateUpdatedGt is not None:
|
|
params["date_updated_gt"] = dateUpdatedGt
|
|
if dateUpdatedLt is not None:
|
|
params["date_updated_lt"] = dateUpdatedLt
|
|
if customFields:
|
|
params["custom_fields"] = json.dumps(customFields)
|
|
return await self._request("GET", f"/list/{list_id}/task", params=params)
|
|
|
|
async def getTask(self, task_id: str, *, include_subtasks: bool = True) -> Dict[str, Any]:
|
|
params = {"include_subtasks": str(include_subtasks).lower()}
|
|
return await self._request("GET", f"/task/{task_id}", params=params)
|
|
|
|
async def createTask(self, list_id: str, body: Dict[str, Any]) -> Dict[str, Any]:
|
|
return await self._request("POST", f"/list/{list_id}/task", json_body=body)
|
|
|
|
async def updateTask(self, task_id: str, body: Dict[str, Any]) -> Dict[str, Any]:
|
|
return await self._request("PUT", f"/task/{task_id}", json_body=body)
|
|
|
|
async def deleteTask(self, task_id: str) -> Dict[str, Any]:
|
|
return await self._request("DELETE", f"/task/{task_id}")
|