diff --git a/toolkits/google/arcade_google/tools/gmail.py b/toolkits/google/arcade_google/tools/gmail.py index 9a43a254..f90aa17b 100644 --- a/toolkits/google/arcade_google/tools/gmail.py +++ b/toolkits/google/arcade_google/tools/gmail.py @@ -8,7 +8,6 @@ from arcade.sdk.auth import Google from arcade.sdk.errors import RetryableToolError from google.oauth2.credentials import Credentials from googleapiclient.discovery import build -from googleapiclient.errors import HttpError from arcade_google.tools.utils import ( DateRange, @@ -19,6 +18,7 @@ from arcade_google.tools.utils import ( get_sent_email_url, parse_draft_email, parse_email, + process_email_messages, remove_none_values, ) @@ -356,22 +356,10 @@ async def list_emails_by_header( if not messages: return {"emails": []} - emails = process_messages(service, messages) + emails = process_email_messages(service, messages) return {"emails": emails} -def process_messages(service: Any, messages: list[dict[str, Any]]) -> list[dict[str, Any]]: - emails = [] - for msg in messages: - try: - email_data = service.users().messages().get(userId="me", id=msg["id"]).execute() - email_details = parse_email(email_data) - emails += [email_details] if email_details else [] - except HttpError as e: - print(f"Error reading email {msg['id']}: {e}") - return emails - - @tool( requires_auth=Google( scopes=["https://www.googleapis.com/auth/gmail.readonly"], diff --git a/toolkits/google/arcade_google/tools/utils.py b/toolkits/google/arcade_google/tools/utils.py index e0076fda..c9bd42e6 100644 --- a/toolkits/google/arcade_google/tools/utils.py +++ b/toolkits/google/arcade_google/tools/utils.py @@ -8,6 +8,7 @@ from zoneinfo import ZoneInfo from bs4 import BeautifulSoup from google.oauth2.credentials import Credentials from googleapiclient.discovery import Resource, build +from googleapiclient.errors import HttpError from arcade_google.tools.models import Day, TimeSlot @@ -72,6 +73,18 @@ class DateRange(Enum): return result + comparison_date.strftime("%Y/%m/%d") +def process_email_messages(service: Any, messages: list[dict[str, Any]]) -> list[dict[str, Any]]: + emails = [] + for msg in messages: + try: + email_data = service.users().messages().get(userId="me", id=msg["id"]).execute() + email_details = parse_email(email_data) + emails += [email_details] if email_details else [] + except HttpError as e: + print(f"Error reading email {msg['id']}: {e}") + return emails + + def parse_email(email_data: dict[str, Any]) -> dict[str, Any]: """ Parse email data and extract relevant information. diff --git a/toolkits/google/pyproject.toml b/toolkits/google/pyproject.toml index 516e0de4..80ad757b 100644 --- a/toolkits/google/pyproject.toml +++ b/toolkits/google/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "arcade_google" -version = "0.1.9" +version = "0.1.10" description = "Arcade tools for the entire google suite" authors = ["Arcade AI "] diff --git a/toolkits/linkedin/arcade_linkedin/tools/constants.py b/toolkits/linkedin/arcade_linkedin/tools/constants.py new file mode 100644 index 00000000..187eeaed --- /dev/null +++ b/toolkits/linkedin/arcade_linkedin/tools/constants.py @@ -0,0 +1 @@ +LINKEDIN_BASE_URL = "https://api.linkedin.com/v2" diff --git a/toolkits/linkedin/arcade_linkedin/tools/share.py b/toolkits/linkedin/arcade_linkedin/tools/share.py index 4005e7bb..ac642835 100644 --- a/toolkits/linkedin/arcade_linkedin/tools/share.py +++ b/toolkits/linkedin/arcade_linkedin/tools/share.py @@ -1,74 +1,10 @@ from typing import Annotated -import httpx from arcade.sdk import ToolContext, tool from arcade.sdk.auth import LinkedIn from arcade.sdk.errors import ToolExecutionError -LINKEDIN_BASE_URL = "https://api.linkedin.com/v2" - - -async def _send_linkedin_request( - context: ToolContext, - method: str, - endpoint: str, - params: dict | None = None, - json_data: dict | None = None, -) -> httpx.Response: - """ - Send an asynchronous request to the LinkedIn API. - - Args: - context: The tool context containing the authorization token. - method: The HTTP method (GET, POST, PUT, DELETE, etc.). - endpoint: The API endpoint path (e.g., "/ugcPosts"). - params: Query parameters to include in the request. - json_data: JSON data to include in the request body. - - Returns: - The response object from the API request. - - Raises: - ToolExecutionError: If the request fails for any reason. - """ - url = f"{LINKEDIN_BASE_URL}{endpoint}" - token = ( - context.authorization.token if context.authorization and context.authorization.token else "" - ) - headers = {"Authorization": f"Bearer {token}"} - - async with httpx.AsyncClient() as client: - try: - response = await client.request( - method, url, headers=headers, params=params, json=json_data - ) - response.raise_for_status() - except httpx.RequestError as e: - raise ToolExecutionError(f"Failed to send request to LinkedIn API: {e}") - - return response - - -def _handle_linkedin_api_error(response: httpx.Response) -> None: - """ - Handle errors from the LinkedIn API by mapping common status codes to ToolExecutionErrors. - - Args: - response: The response object from the API request. - - Raises: - ToolExecutionError: If the response contains an error status code. - """ - status_code_map = { - 401: ToolExecutionError("Unauthorized: Invalid or expired token"), - 403: ToolExecutionError("Forbidden: User does not have Spotify Premium"), - 429: ToolExecutionError("Too Many Requests: Rate limit exceeded"), - } - - if response.status_code in status_code_map: - raise status_code_map[response.status_code] - elif response.status_code >= 400: - raise ToolExecutionError(f"Error: {response.status_code} - {response.text}") +from arcade_linkedin.tools.utils import _handle_linkedin_api_error, _send_linkedin_request @tool( @@ -90,6 +26,7 @@ async def create_text_post( # LinkedIn calls the user ID "sub" in their user_info data payload. See: # https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2#api-request-to-retreive-member-details user_id = context.authorization.user_info.get("sub") if context.authorization else None + if not user_id: raise ToolExecutionError( "User ID not found.", @@ -110,9 +47,11 @@ async def create_text_post( } response = await _send_linkedin_request(context, "POST", endpoint, json_data=payload) + if response.status_code >= 200 and response.status_code < 300: share_id = response.json().get("id") return f"https://www.linkedin.com/feed/update/{share_id}/" _handle_linkedin_api_error(response) + return "" diff --git a/toolkits/linkedin/arcade_linkedin/tools/utils.py b/toolkits/linkedin/arcade_linkedin/tools/utils.py new file mode 100644 index 00000000..6f56a00f --- /dev/null +++ b/toolkits/linkedin/arcade_linkedin/tools/utils.py @@ -0,0 +1,68 @@ +import httpx +from arcade.sdk import ToolContext +from arcade.sdk.errors import ToolExecutionError + +from arcade_linkedin.tools.constants import LINKEDIN_BASE_URL + + +async def _send_linkedin_request( + context: ToolContext, + method: str, + endpoint: str, + params: dict | None = None, + json_data: dict | None = None, +) -> httpx.Response: + """ + Send an asynchronous request to the LinkedIn API. + + Args: + context: The tool context containing the authorization token. + method: The HTTP method (GET, POST, PUT, DELETE, etc.). + endpoint: The API endpoint path (e.g., "/ugcPosts"). + params: Query parameters to include in the request. + json_data: JSON data to include in the request body. + + Returns: + The response object from the API request. + + Raises: + ToolExecutionError: If the request fails for any reason. + """ + url = f"{LINKEDIN_BASE_URL}{endpoint}" + token = ( + context.authorization.token if context.authorization and context.authorization.token else "" + ) + headers = {"Authorization": f"Bearer {token}"} + + async with httpx.AsyncClient() as client: + try: + response = await client.request( + method, url, headers=headers, params=params, json=json_data + ) + response.raise_for_status() + except httpx.RequestError as e: + raise ToolExecutionError(f"Failed to send request to LinkedIn API: {e}") + + return response + + +def _handle_linkedin_api_error(response: httpx.Response) -> None: + """ + Handle errors from the LinkedIn API by mapping common status codes to ToolExecutionErrors. + + Args: + response: The response object from the API request. + + Raises: + ToolExecutionError: If the response contains an error status code. + """ + status_code_map = { + 401: ToolExecutionError("Unauthorized: Invalid or expired token"), + 403: ToolExecutionError("Forbidden: User does not have Spotify Premium"), + 429: ToolExecutionError("Too Many Requests: Rate limit exceeded"), + } + + if response.status_code in status_code_map: + raise status_code_map[response.status_code] + elif response.status_code >= 400: + raise ToolExecutionError(f"Error: {response.status_code} - {response.text}") diff --git a/toolkits/linkedin/pyproject.toml b/toolkits/linkedin/pyproject.toml index 6e966381..15788af3 100644 --- a/toolkits/linkedin/pyproject.toml +++ b/toolkits/linkedin/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "arcade_linkedin" -version = "0.1.8" +version = "0.1.9" description = "Arcade tools for LinkedIn" authors = ["Arcade AI "] diff --git a/toolkits/search/arcade_search/tools/google.py b/toolkits/search/arcade_search/tools/google.py index 5e8af541..bf4821d2 100644 --- a/toolkits/search/arcade_search/tools/google.py +++ b/toolkits/search/arcade_search/tools/google.py @@ -1,10 +1,11 @@ import json -import os -from typing import Annotated, Any, Optional +from typing import Annotated import serpapi from arcade.sdk import tool +from arcade_search.tools.utils import get_secret + @tool async def search_google( @@ -25,12 +26,3 @@ async def search_google( organic_results = results.get("organic_results", []) return json.dumps(organic_results[:n_results]) - - -def get_secret(name: str, default: Optional[Any] = None) -> Any: - secret = os.getenv(name) - if secret is None: - if default is not None: - return default - raise ValueError(f"Secret {name} is not set.") - return secret diff --git a/toolkits/search/arcade_search/tools/utils.py b/toolkits/search/arcade_search/tools/utils.py new file mode 100644 index 00000000..94394baf --- /dev/null +++ b/toolkits/search/arcade_search/tools/utils.py @@ -0,0 +1,11 @@ +import os +from typing import Any, Optional + + +def get_secret(name: str, default: Optional[Any] = None) -> Any: + secret = os.getenv(name) + if secret is None: + if default is not None: + return default + raise ValueError(f"Secret {name} is not set.") + return secret diff --git a/toolkits/search/pyproject.toml b/toolkits/search/pyproject.toml index 76c7636f..df265dc4 100644 --- a/toolkits/search/pyproject.toml +++ b/toolkits/search/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "arcade_search" -version = "0.1.8" +version = "0.1.9" description = "Tools for searching the web" authors = ["Arcade AI "] diff --git a/toolkits/x/arcade_x/tools/constants.py b/toolkits/x/arcade_x/tools/constants.py new file mode 100644 index 00000000..12004059 --- /dev/null +++ b/toolkits/x/arcade_x/tools/constants.py @@ -0,0 +1 @@ +TWEETS_URL = "https://api.x.com/2/tweets" diff --git a/toolkits/x/arcade_x/tools/tweets.py b/toolkits/x/arcade_x/tools/tweets.py index 302aecb6..9d8393a9 100644 --- a/toolkits/x/arcade_x/tools/tweets.py +++ b/toolkits/x/arcade_x/tools/tweets.py @@ -5,6 +5,7 @@ from arcade.sdk import ToolContext, tool from arcade.sdk.auth import X from arcade.sdk.errors import RetryableToolError +from arcade_x.tools.constants import TWEETS_URL from arcade_x.tools.utils import ( expand_attached_media, expand_long_tweet, @@ -15,9 +16,6 @@ from arcade_x.tools.utils import ( remove_none_values, ) -TWEETS_URL = "https://api.x.com/2/tweets" - - # Manage Tweets Tools. See developer docs for additional available parameters: # https://developer.x.com/en/docs/x-api/tweets/manage-tweets/api-reference diff --git a/toolkits/x/pyproject.toml b/toolkits/x/pyproject.toml index f31a84ba..62c7404f 100644 --- a/toolkits/x/pyproject.toml +++ b/toolkits/x/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "arcade_x" -version = "0.1.9" +version = "0.1.10" description = "LLM tools for interacting with X (Twitter)" authors = ["Arcade AI "] diff --git a/toolkits/zoom/arcade_zoom/tools/constants.py b/toolkits/zoom/arcade_zoom/tools/constants.py new file mode 100644 index 00000000..1eab3cc2 --- /dev/null +++ b/toolkits/zoom/arcade_zoom/tools/constants.py @@ -0,0 +1 @@ +ZOOM_BASE_URL = "https://api.zoom.us/v2" diff --git a/toolkits/zoom/arcade_zoom/tools/meetings.py b/toolkits/zoom/arcade_zoom/tools/meetings.py index 03ea1ff1..1c9c2024 100644 --- a/toolkits/zoom/arcade_zoom/tools/meetings.py +++ b/toolkits/zoom/arcade_zoom/tools/meetings.py @@ -1,74 +1,9 @@ from typing import Annotated, Optional -import httpx from arcade.sdk import ToolContext, tool from arcade.sdk.auth import Zoom -from arcade.sdk.errors import ToolExecutionError -ZOOM_BASE_URL = "https://api.zoom.us/v2" - - -async def _send_zoom_request( - context: ToolContext, - method: str, - endpoint: str, - params: dict | None = None, - json_data: dict | None = None, -) -> httpx.Response: - """ - Send an asynchronous request to the Zoom API. - - Args: - context: The tool context containing the authorization token. - method: The HTTP method (GET, POST, PUT, DELETE, etc.). - endpoint: The API endpoint path (e.g., "/users/me/upcoming_meetings"). - params: Query parameters to include in the request. - json_data: JSON data to include in the request body. - - Returns: - The response object from the API request. - - Raises: - ToolExecutionError: If the request fails for any reason. - """ - url = f"{ZOOM_BASE_URL}{endpoint}" - token = ( - context.authorization.token if context.authorization and context.authorization.token else "" - ) - headers = {"Authorization": f"Bearer {token}"} - - async with httpx.AsyncClient() as client: - try: - response = await client.request( - method, url, headers=headers, params=params, json=json_data - ) - response.raise_for_status() - except httpx.RequestError as e: - raise ToolExecutionError(f"Failed to send request to Zoom API: {e}") - - return response - - -def _handle_zoom_api_error(response: httpx.Response) -> None: - """ - Handle errors from the Zoom API by mapping common status codes to ToolExecutionErrors. - - Args: - response: The response object from the API request. - - Raises: - ToolExecutionError: If the response contains an error status code. - """ - status_code_map = { - 401: ToolExecutionError("Unauthorized: Invalid or expired token"), - 403: ToolExecutionError("Forbidden: Access denied"), - 429: ToolExecutionError("Too Many Requests: Rate limit exceeded"), - } - - if response.status_code in status_code_map: - raise status_code_map[response.status_code] - elif response.status_code >= 400: - raise ToolExecutionError(f"Error: {response.status_code} - {response.text}") +from arcade_zoom.tools.utils import _handle_zoom_api_error, _send_zoom_request @tool( diff --git a/toolkits/zoom/arcade_zoom/tools/utils.py b/toolkits/zoom/arcade_zoom/tools/utils.py new file mode 100644 index 00000000..47294d8b --- /dev/null +++ b/toolkits/zoom/arcade_zoom/tools/utils.py @@ -0,0 +1,68 @@ +import httpx +from arcade.sdk import ToolContext +from arcade.sdk.errors import ToolExecutionError + +from arcade_zoom.tools.constants import ZOOM_BASE_URL + + +async def _send_zoom_request( + context: ToolContext, + method: str, + endpoint: str, + params: dict | None = None, + json_data: dict | None = None, +) -> httpx.Response: + """ + Send an asynchronous request to the Zoom API. + + Args: + context: The tool context containing the authorization token. + method: The HTTP method (GET, POST, PUT, DELETE, etc.). + endpoint: The API endpoint path (e.g., "/users/me/upcoming_meetings"). + params: Query parameters to include in the request. + json_data: JSON data to include in the request body. + + Returns: + The response object from the API request. + + Raises: + ToolExecutionError: If the request fails for any reason. + """ + url = f"{ZOOM_BASE_URL}{endpoint}" + token = ( + context.authorization.token if context.authorization and context.authorization.token else "" + ) + headers = {"Authorization": f"Bearer {token}"} + + async with httpx.AsyncClient() as client: + try: + response = await client.request( + method, url, headers=headers, params=params, json=json_data + ) + response.raise_for_status() + except httpx.RequestError as e: + raise ToolExecutionError(f"Failed to send request to Zoom API: {e}") + + return response + + +def _handle_zoom_api_error(response: httpx.Response) -> None: + """ + Handle errors from the Zoom API by mapping common status codes to ToolExecutionErrors. + + Args: + response: The response object from the API request. + + Raises: + ToolExecutionError: If the response contains an error status code. + """ + status_code_map = { + 401: ToolExecutionError("Unauthorized: Invalid or expired token"), + 403: ToolExecutionError("Forbidden: Access denied"), + 429: ToolExecutionError("Too Many Requests: Rate limit exceeded"), + } + + if response.status_code in status_code_map: + raise status_code_map[response.status_code] + elif response.status_code >= 400: + raise ToolExecutionError(f"Error: {response.status_code} - {response.text}") diff --git a/toolkits/zoom/pyproject.toml b/toolkits/zoom/pyproject.toml index 35d786ef..d821e586 100644 --- a/toolkits/zoom/pyproject.toml +++ b/toolkits/zoom/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "arcade_zoom" -version = "0.1.8" +version = "0.1.9" description = "Arcade tools for Zoom" authors = ["Arcade AI "]