# 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 clickup_authorization_header(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}")