Source code for langchain_community.utilities.clickup
"""Util that calls clickup."""importjsonimportwarningsfromdataclassesimportasdict,dataclass,fieldsfromtypingimportAny,Dict,List,Mapping,Optional,Tuple,Type,Unionimportrequestsfromlangchain_core.utilsimportget_from_dict_or_envfrompydanticimportBaseModel,ConfigDict,model_validatorDEFAULT_URL="https://api.clickup.com/api/v2"
[docs]@dataclassclassComponent:"""Base class for all components."""
[docs]@dataclassclassTask(Component):"""Class for a task."""id:intname:strtext_content:strdescription:strstatus:strcreator_id:intcreator_username:strcreator_email:strassignees:List[Dict[str,Any]]watchers:List[Dict[str,Any]]priority:Optional[str]due_date:Optional[str]start_date:Optional[str]points:intteam_id:intproject_id:int
[docs]@dataclassclassCUList(Component):"""Component class for a list."""folder_id:floatname:strcontent:Optional[str]=Nonedue_date:Optional[int]=Nonedue_date_time:Optional[bool]=Nonepriority:Optional[int]=Noneassignee:Optional[int]=Nonestatus:Optional[str]=None
[docs]defparse_dict_through_component(data:dict,component:Type[Component],fault_tolerant:bool=False)->Dict:"""Parse a dictionary by creating a component and then turning it back into a dictionary. This helps with two things 1. Extract and format data from a dictionary according to schema 2. Provide a central place to do this in a fault-tolerant way """try:returnasdict(component.from_data(data))exceptExceptionase:iffault_tolerant:warning_str=f"""Error encountered while trying to parse{str(data)}: {str(e)}\n Falling back to returning input data."""warnings.warn(warning_str)returndataelse:raisee
[docs]defextract_dict_elements_from_component_fields(data:dict,component:Type[Component])->dict:"""Extract elements from a dictionary. Args: data: The dictionary to extract elements from. component: The component to extract elements from. Returns: A dictionary containing the elements from the input dictionary that are also in the component. """output={}forattributeinfields(component):ifattribute.nameindata:output[attribute.name]=data[attribute.name]returnoutput
[docs]defload_query(query:str,fault_tolerant:bool=False)->Tuple[Optional[Dict],Optional[str]]:"""Parse a JSON string and return the parsed object. If parsing fails, returns an error message. :param query: The JSON string to parse. :return: A tuple containing the parsed object or None and an error message or None. Exceptions: json.JSONDecodeError: If the input is not a valid JSON string. """try:returnjson.loads(query),Noneexceptjson.JSONDecodeErrorase:iffault_tolerant:return(None,f"""Input must be a valid JSON. Got the following error: {str(e)}. "Please reformat and try again.""",)else:raisee
[docs]deffetch_first_id(data:dict,key:str)->Optional[int]:"""Fetch the first id from a dictionary."""ifkeyindataandlen(data[key])>0:iflen(data[key])>1:warnings.warn(f"Found multiple {key}: {data[key]}. Defaulting to first.")returndata[key][0]["id"]returnNone
[docs]deffetch_data(url:str,access_token:str,query:Optional[dict]=None)->dict:"""Fetch data from a URL."""headers={"Authorization":access_token}response=requests.get(url,headers=headers,params=query)response.raise_for_status()returnresponse.json()
[docs]deffetch_team_id(access_token:str)->Optional[int]:"""Fetch the team id."""url=f"{DEFAULT_URL}/team"data=fetch_data(url,access_token)returnfetch_first_id(data,"teams")
[docs]deffetch_space_id(team_id:int,access_token:str)->Optional[int]:"""Fetch the space id."""url=f"{DEFAULT_URL}/team/{team_id}/space"data=fetch_data(url,access_token,query={"archived":"false"})returnfetch_first_id(data,"spaces")
[docs]deffetch_folder_id(space_id:int,access_token:str)->Optional[int]:"""Fetch the folder id."""url=f"{DEFAULT_URL}/space/{space_id}/folder"data=fetch_data(url,access_token,query={"archived":"false"})returnfetch_first_id(data,"folders")
[docs]deffetch_list_id(space_id:int,folder_id:int,access_token:str)->Optional[int]:"""Fetch the list id."""iffolder_id:url=f"{DEFAULT_URL}/folder/{folder_id}/list"else:url=f"{DEFAULT_URL}/space/{space_id}/list"data=fetch_data(url,access_token,query={"archived":"false"})# The structure to fetch list id differs based if its folderlessiffolder_idand"id"indata:returndata["id"]else:returnfetch_first_id(data,"lists")
[docs]classClickupAPIWrapper(BaseModel):"""Wrapper for Clickup API."""access_token:Optional[str]=Noneteam_id:Optional[str]=Nonespace_id:Optional[str]=Nonefolder_id:Optional[str]=Nonelist_id:Optional[str]=Nonemodel_config=ConfigDict(extra="forbid",)
[docs]@classmethoddefget_access_code_url(cls,oauth_client_id:str,redirect_uri:str="https://google.com")->str:"""Get the URL to get an access code."""url=f"https://app.clickup.com/api?client_id={oauth_client_id}"returnf"{url}&redirect_uri={redirect_uri}"
[docs]@classmethoddefget_access_token(cls,oauth_client_id:str,oauth_client_secret:str,code:str)->Optional[str]:"""Get the access token."""url=f"{DEFAULT_URL}/oauth/token"params={"client_id":oauth_client_id,"client_secret":oauth_client_secret,"code":code,}response=requests.post(url,params=params)data=response.json()if"access_token"notindata:print(f"Error: {data}")# noqa: T201if"ECODE"indataanddata["ECODE"]=="OAUTH_014":url=ClickupAPIWrapper.get_access_code_url(oauth_client_id)print(# noqa: T201"You already used this code once. Generate a new one.",f"Our best guess for the url to get a new code is:\n{url}",)returnNonereturndata["access_token"]
@model_validator(mode="before")@classmethoddefvalidate_environment(cls,values:Dict)->Any:"""Validate that api key and python package exists in environment."""values["access_token"]=get_from_dict_or_env(values,"access_token","CLICKUP_ACCESS_TOKEN")values["team_id"]=fetch_team_id(values["access_token"])values["space_id"]=fetch_space_id(values["team_id"],values["access_token"])values["folder_id"]=fetch_folder_id(values["space_id"],values["access_token"])values["list_id"]=fetch_list_id(values["space_id"],values["folder_id"],values["access_token"])returnvalues
[docs]defattempt_parse_teams(self,input_dict:dict)->Dict[str,List[dict]]:"""Parse appropriate content from the list of teams."""parsed_teams:Dict[str,List[dict]]={"teams":[]}forteamininput_dict["teams"]:try:team=parse_dict_through_component(team,Team,fault_tolerant=False)parsed_teams["teams"].append(team)exceptExceptionase:warnings.warn(f"Error parsing a team {e}")returnparsed_teams
[docs]defget_headers(self,)->Mapping[str,Union[str,bytes]]:"""Get the headers for the request."""ifnotisinstance(self.access_token,str):raiseTypeError(f"Access Token: {self.access_token}, must be str.")headers={"Authorization":str(self.access_token),"Content-Type":"application/json",}returnheaders
[docs]defget_authorized_teams(self)->Dict[Any,Any]:"""Get all teams for the user."""url=f"{DEFAULT_URL}/team"response=requests.get(url,headers=self.get_headers())data=response.json()parsed_teams=self.attempt_parse_teams(data)returnparsed_teams
[docs]defget_folders(self)->Dict:""" Get all the folders for the team. """url=f"{DEFAULT_URL}/team/"+str(self.team_id)+"/space"params=self.get_default_params()response=requests.get(url,headers=self.get_headers(),params=params)return{"response":response}
[docs]defget_task(self,query:str,fault_tolerant:bool=True)->Dict:""" Retrieve a specific task. """params,error=load_query(query,fault_tolerant=True)ifparamsisNone:return{"Error":error}url=f"{DEFAULT_URL}/task/{params['task_id']}"params={"custom_task_ids":"true","team_id":self.team_id,"include_subtasks":"true",}response=requests.get(url,headers=self.get_headers(),params=params)data=response.json()parsed_task=parse_dict_through_component(data,Task,fault_tolerant=fault_tolerant)returnparsed_task
[docs]defget_lists(self)->Dict:""" Get all available lists. """url=f"{DEFAULT_URL}/folder/{self.folder_id}/list"params=self.get_default_params()response=requests.get(url,headers=self.get_headers(),params=params)return{"response":response}
[docs]defquery_tasks(self,query:str)->Dict:""" Query tasks that match certain fields """params,error=load_query(query,fault_tolerant=True)ifparamsisNone:return{"Error":error}url=f"{DEFAULT_URL}/list/{params['list_id']}/task"params=self.get_default_params()response=requests.get(url,headers=self.get_headers(),params=params)return{"response":response}
[docs]defget_spaces(self)->Dict:""" Get all spaces for the team. """url=f"{DEFAULT_URL}/team/{self.team_id}/space"response=requests.get(url,headers=self.get_headers(),params=self.get_default_params())data=response.json()parsed_spaces=parse_dict_through_component(data,Space,fault_tolerant=True)returnparsed_spaces
[docs]defget_task_attribute(self,query:str)->Dict:""" Update an attribute of a specified task. """task=self.get_task(query,fault_tolerant=True)params,error=load_query(query,fault_tolerant=True)ifnotisinstance(params,dict):return{"Error":error}ifparams["attribute_name"]notintask:return{"Error":f"""attribute_name = {params["attribute_name"]} was not found in task keys {task.keys()}. Please call again with one of the key names."""}return{params["attribute_name"]:task[params["attribute_name"]]}
[docs]defupdate_task(self,query:str)->Dict:""" Update an attribute of a specified task. """query_dict,error=load_query(query,fault_tolerant=True)ifquery_dictisNone:return{"Error":error}url=f"{DEFAULT_URL}/task/{query_dict['task_id']}"params={"custom_task_ids":"true","team_id":self.team_id,"include_subtasks":"true",}headers=self.get_headers()payload={query_dict["attribute_name"]:query_dict["value"]}response=requests.put(url,headers=headers,params=params,json=payload)return{"response":response}
[docs]defupdate_task_assignees(self,query:str)->Dict:""" Add or remove assignees of a specified task. """query_dict,error=load_query(query,fault_tolerant=True)ifquery_dictisNone:return{"Error":error}foruserinquery_dict["users"]:ifnotisinstance(user,int):return{"Error":f"""All users must be integers, not strings!"Got user {user} if type {type(user)}"""}url=f"{DEFAULT_URL}/task/{query_dict['task_id']}"headers=self.get_headers()ifquery_dict["operation"]=="add":assigne_payload={"add":query_dict["users"],"rem":[]}elifquery_dict["operation"]=="rem":assigne_payload={"add":[],"rem":query_dict["users"]}else:raiseValueError(f"Invalid operation ({query_dict['operation']}). ","Valid options ['add', 'rem'].",)params={"custom_task_ids":"true","team_id":self.team_id,"include_subtasks":"true",}payload={"assignees":assigne_payload}response=requests.put(url,headers=headers,params=params,json=payload)return{"response":response}
[docs]defcreate_task(self,query:str)->Dict:""" Creates a new task. """query_dict,error=load_query(query,fault_tolerant=True)ifquery_dictisNone:return{"Error":error}list_id=self.list_idurl=f"{DEFAULT_URL}/list/{list_id}/task"params={"custom_task_ids":"true","team_id":self.team_id}payload=extract_dict_elements_from_component_fields(query_dict,Task)headers=self.get_headers()response=requests.post(url,json=payload,headers=headers,params=params)data:Dict=response.json()returnparse_dict_through_component(data,Task,fault_tolerant=True)
[docs]defcreate_list(self,query:str)->Dict:""" Creates a new list. """query_dict,error=load_query(query,fault_tolerant=True)ifquery_dictisNone:return{"Error":error}# Default to using folder as location if it exists.# If not, fall back to using the space.location=self.folder_idifself.folder_idelseself.space_idurl=f"{DEFAULT_URL}/folder/{location}/list"payload=extract_dict_elements_from_component_fields(query_dict,Task)headers=self.get_headers()response=requests.post(url,json=payload,headers=headers)data=response.json()parsed_list=parse_dict_through_component(data,CUList,fault_tolerant=True)# set list id to new listif"id"inparsed_list:self.list_id=parsed_list["id"]returnparsed_list
[docs]defcreate_folder(self,query:str)->Dict:""" Creates a new folder. """query_dict,error=load_query(query,fault_tolerant=True)ifquery_dictisNone:return{"Error":error}space_id=self.space_idurl=f"{DEFAULT_URL}/space/{space_id}/folder"payload={"name":query_dict["name"],}headers=self.get_headers()response=requests.post(url,json=payload,headers=headers)data=response.json()if"id"indata:self.list_id=data["id"]returndata
[docs]defrun(self,mode:str,query:str)->str:"""Run the API."""ifmode=="get_task":output=self.get_task(query)elifmode=="get_task_attribute":output=self.get_task_attribute(query)elifmode=="get_teams":output=self.get_authorized_teams()elifmode=="create_task":output=self.create_task(query)elifmode=="create_list":output=self.create_list(query)elifmode=="create_folder":output=self.create_folder(query)elifmode=="get_lists":output=self.get_lists()elifmode=="get_folders":output=self.get_folders()elifmode=="get_spaces":output=self.get_spaces()elifmode=="update_task":output=self.update_task(query)elifmode=="update_task_assignees":output=self.update_task_assignees(query)else:output={"ModeError":f"Got unexpected mode {mode}."}try:returnjson.dumps(output)exceptException:returnstr(output)