Common changes in all toolkits (#345)

Addresses general improvements to all toolkits including changing ruff
from python 3.9 to python 3.10 which is the reason for the removal of
Optional[] among others.

Also, turns out that our `make install` for toolkits wasn't correctly
checking for whether poetry was installed (&> /dev/null syntax isn't
supported by our check-toolkits GitHub action, so we were installing
poetry twice. I replaced with the more portable >/dev/null 2>&1)

Question: Should we also change ruff to py310 for the `arcade/` package
in a later PR?

-------------------

CU-86b4gzyp6
This commit is contained in:
Eric Gustin 2025-04-04 08:32:37 -08:00 committed by GitHub
parent 7c6a739f25
commit 6af49ef068
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
84 changed files with 532 additions and 468 deletions

View file

@ -1,4 +1,4 @@
target-version = "py39"
target-version = "py310"
line-length = 100
fix = true

View file

@ -7,7 +7,12 @@ help:
.PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed"
@if ! command -v poetry &> /dev/null; then \
@if ! command -v poetry >/dev/null 2>&1; then \
echo "📦 Poetry not found. Checking if pip is available"; \
if ! command -v pip >/dev/null 2>&1; then \
echo "❌ pip is not installed. Please install pip first."; \
exit 1; \
fi; \
echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \
else \

View file

@ -17,7 +17,7 @@ tox = "^4.11.1"
ruff = "^0.7.4"
[build-system]
requires = ["poetry-core>=1.0.0"]
requires = ["poetry-core>=1.0.0,<2.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.mypy]

View file

@ -3,7 +3,7 @@ VERSION ?= "0.1.0"
.PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed"
@if ! command -v poetry &> /dev/null; then \
@if ! command -v poetry >/dev/null 2>&1; then \
echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \
else \

View file

@ -1,4 +1,4 @@
target-version = "py39"
target-version = "py310"
line-length = 100
fix = true

View file

@ -7,7 +7,12 @@ help:
.PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed"
@if ! command -v poetry &> /dev/null; then \
@if ! command -v poetry >/dev/null 2>&1; then \
echo "📦 Poetry not found. Checking if pip is available"; \
if ! command -v pip >/dev/null 2>&1; then \
echo "❌ pip is not installed. Please install pip first."; \
exit 1; \
fi; \
echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \
else \

View file

@ -1,7 +1,7 @@
[tool.poetry]
name = "arcade_code_sandbox"
version = "1.0.0"
description = "LLM tools for running code in a sandbox"
description = "Arcade.dev LLM tools for running code in a sandbox"
authors = ["Arcade <dev@arcade.dev>"]
[tool.poetry.dependencies]
@ -20,7 +20,7 @@ tox = "^4.11.1"
ruff = "^0.7.4"
[build-system]
requires = ["poetry-core>=1.0.0"]
requires = ["poetry-core>=1.0.0,<2.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.mypy]

View file

@ -1,4 +1,4 @@
target-version = "py39"
target-version = "py310"
line-length = 100
fix = true

View file

@ -7,7 +7,12 @@ help:
.PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed"
@if ! command -v poetry &> /dev/null; then \
@if ! command -v poetry >/dev/null 2>&1; then \
echo "📦 Poetry not found. Checking if pip is available"; \
if ! command -v pip >/dev/null 2>&1; then \
echo "❌ pip is not installed. Please install pip first."; \
exit 1; \
fi; \
echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \
else \

View file

@ -1,4 +1,4 @@
from typing import Annotated, Optional
from typing import Annotated
import httpx
from arcade.sdk import ToolContext, tool
@ -50,7 +50,7 @@ async def list_stargazers(
owner: Annotated[str, "The owner of the repository"],
repo: Annotated[str, "The name of the repository"],
limit: Annotated[
Optional[int],
int | None,
"The maximum number of stargazers to return. "
"If not provided, all stargazers will be returned.",
] = None,

View file

@ -1,5 +1,5 @@
import json
from typing import Annotated, Optional
from typing import Annotated
import httpx
from arcade.sdk import ToolContext, tool
@ -27,12 +27,12 @@ async def create_issue(
"The name of the repository without the .git extension. The name is not case sensitive.",
],
title: Annotated[str, "The title of the issue."],
body: Annotated[Optional[str], "The contents of the issue."] = None,
assignees: Annotated[Optional[list[str]], "Logins for Users to assign to this issue."] = None,
body: Annotated[str | None, "The contents of the issue."] = None,
assignees: Annotated[list[str] | None, "Logins for Users to assign to this issue."] = None,
milestone: Annotated[
Optional[int], "The number of the milestone to associate this issue with."
int | None, "The number of the milestone to associate this issue with."
] = None,
labels: Annotated[Optional[list[str]], "Labels to associate with this issue."] = None,
labels: Annotated[list[str] | None, "Labels to associate with this issue."] = None,
include_extra_data: Annotated[
bool,
"If true, return all the data available about the pull requests. "

View file

@ -1,5 +1,5 @@
import json
from typing import Annotated, Optional
from typing import Annotated
import httpx
from arcade.sdk import ToolContext, tool
@ -38,17 +38,17 @@ async def list_pull_requests(
str,
"The name of the repository without the .git extension. The name is not case sensitive.",
],
state: Annotated[Optional[PRState], "The state of the pull requests to return."] = PRState.OPEN,
state: Annotated[PRState | None, "The state of the pull requests to return."] = PRState.OPEN,
head: Annotated[
Optional[str],
str | None,
"Filter pulls by head user or head organization and branch name in the format of "
"user:ref-name or organization:ref-name.",
] = None,
base: Annotated[Optional[str], "Filter pulls by base branch name."] = "main",
base: Annotated[str | None, "Filter pulls by base branch name."] = "main",
sort: Annotated[
Optional[PRSortProperty], "The property to sort the results by."
PRSortProperty | None, "The property to sort the results by."
] = PRSortProperty.CREATED,
direction: Annotated[Optional[SortDirection], "The direction of the sort."] = None,
direction: Annotated[SortDirection | None, "The direction of the sort."] = None,
per_page: Annotated[int, "The number of results per page (max 100)."] = 30,
page: Annotated[int, "The page number of the results to fetch."] = 1,
include_extra_data: Annotated[
@ -120,11 +120,11 @@ async def get_pull_request(
],
pull_number: Annotated[int, "The number that identifies the pull request."],
include_diff_content: Annotated[
Optional[bool],
bool | None,
"If true, return the diff content of the pull request.",
] = False,
include_extra_data: Annotated[
Optional[bool],
bool | None,
"If true, return all the data available about the pull requests. "
"This is a large payload and may impact performance - use with caution.",
] = False,
@ -201,16 +201,12 @@ async def update_pull_request(
"The name of the repository without the .git extension. The name is not case sensitive.",
],
pull_number: Annotated[int, "The number that identifies the pull request."],
title: Annotated[Optional[str], "The title of the pull request."] = None,
body: Annotated[Optional[str], "The contents of the pull request."] = None,
state: Annotated[
Optional[PRState], "State of this Pull Request. Either open or closed."
] = None,
base: Annotated[
Optional[str], "The name of the branch you want your changes pulled into."
] = None,
title: Annotated[str | None, "The title of the pull request."] = None,
body: Annotated[str | None, "The contents of the pull request."] = None,
state: Annotated[PRState | None, "State of this Pull Request. Either open or closed."] = None,
base: Annotated[str | None, "The name of the branch you want your changes pulled into."] = None,
maintainer_can_modify: Annotated[
Optional[bool], "Indicates whether maintainers can modify the pull request."
bool | None, "Indicates whether maintainers can modify the pull request."
] = None,
) -> Annotated[str, "JSON string containing updated information about the pull request"]:
"""
@ -398,14 +394,14 @@ async def list_review_comments_on_pull_request(
],
pull_number: Annotated[int, "The number that identifies the pull request."],
sort: Annotated[
Optional[ReviewCommentSortProperty],
ReviewCommentSortProperty | None,
"The property to sort the results by. Can be one of: created, updated.",
] = ReviewCommentSortProperty.CREATED,
direction: Annotated[
Optional[SortDirection], "The direction to sort results. Can be one of: asc, desc."
SortDirection | None, "The direction to sort results. Can be one of: asc, desc."
] = SortDirection.DESC,
since: Annotated[
Optional[str],
str | None,
"Only show results that were last updated after the given time. "
"This is a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ.",
] = None,
@ -494,31 +490,31 @@ async def create_review_comment(
body: Annotated[str, "The text of the review comment."],
path: Annotated[str, "The relative path to the file that necessitates a comment."],
commit_id: Annotated[
Optional[str],
str | None,
"The SHA of the commit needing a comment. If not provided, the latest commit SHA of the "
"PR's base branch will be used.",
] = None,
start_line: Annotated[
Optional[int],
int | None,
"The start line of the range of lines in the pull request diff that the "
"comment applies to. Required unless 'subject_type' is 'file'.",
] = None,
end_line: Annotated[
Optional[int],
int | None,
"The end line of the range of lines in the pull request diff that the "
"comment applies to. Required unless 'subject_type' is 'file'.",
] = None,
side: Annotated[
Optional[DiffSide],
DiffSide | None,
"The side of the diff that the pull request's changes appear on. "
"Use LEFT for deletions that appear in red. Use RIGHT for additions that appear in green "
"or unchanged lines that appear in white and are shown for context",
] = DiffSide.RIGHT,
start_side: Annotated[
Optional[str], "The starting side of the diff that the comment applies to."
str | None, "The starting side of the diff that the comment applies to."
] = None,
subject_type: Annotated[
Optional[ReviewCommentSubjectType],
ReviewCommentSubjectType | None,
"The type of subject that the comment applies to. Can be one of: file, hunk, or line.",
] = ReviewCommentSubjectType.FILE,
include_extra_data: Annotated[

View file

@ -1,5 +1,5 @@
import json
from typing import Annotated, Optional
from typing import Annotated
import httpx
from arcade.sdk import ToolContext, tool
@ -191,28 +191,28 @@ async def list_repository_activities(
"The name of the repository without the .git extension. The name is not case sensitive.",
],
direction: Annotated[
Optional[SortDirection], "The direction to sort the results by."
SortDirection | None, "The direction to sort the results by."
] = SortDirection.DESC,
per_page: Annotated[int, "The number of results per page (max 100)."] = 30,
before: Annotated[
Optional[str],
str | None,
"A cursor (unique ID, e.g., a SHA of a commit) to search for results before this cursor.",
] = None,
after: Annotated[
Optional[str],
str | None,
"A cursor (unique ID, e.g., a SHA of a commit) to search for results after this cursor.",
] = None,
ref: Annotated[
Optional[str],
str | None,
"The Git reference for the activities you want to list. The ref for a branch can be "
"formatted either as refs/heads/BRANCH_NAME or BRANCH_NAME, where BRANCH_NAME is the name "
"of your branch.",
] = None,
actor: Annotated[
Optional[str], "The GitHub username to filter by the actor who performed the activity."
str | None, "The GitHub username to filter by the actor who performed the activity."
] = None,
time_period: Annotated[Optional[RepoTimePeriod], "The time period to filter by."] = None,
activity_type: Annotated[Optional[ActivityType], "The activity type to filter by."] = None,
time_period: Annotated[RepoTimePeriod | None, "The time period to filter by."] = None,
activity_type: Annotated[ActivityType | None, "The activity type to filter by."] = None,
include_extra_data: Annotated[
bool,
"If true, return all the data available about the repository activities. "
@ -293,14 +293,14 @@ async def list_review_comments_in_a_repository(
"The name of the repository without the .git extension. The name is not case sensitive.",
],
sort: Annotated[
Optional[ReviewCommentSortProperty], "Can be one of: created, updated."
ReviewCommentSortProperty | None, "Can be one of: created, updated."
] = ReviewCommentSortProperty.CREATED,
direction: Annotated[
Optional[SortDirection],
SortDirection | None,
"The direction to sort results. Ignored without sort parameter. Can be one of: asc, desc.",
] = SortDirection.DESC,
since: Annotated[
Optional[str],
str | None,
"Only show results that were last updated after the given time. "
"This is a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ.",
] = None,

View file

@ -1,7 +1,7 @@
[tool.poetry]
name = "arcade_github"
version = "0.1.10"
description = "LLM tools for interacting with Github"
description = "Arcade.dev LLM tools for Github"
authors = ["Arcade <dev@arcade.dev>"]
[tool.poetry.dependencies]
@ -20,7 +20,7 @@ tox = "^4.11.1"
ruff = "^0.7.4"
[build-system]
requires = ["poetry-core>=1.0.0"]
requires = ["poetry-core>=1.0.0,<2.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.mypy]

View file

@ -1,4 +1,4 @@
target-version = "py39"
target-version = "py310"
line-length = 100
fix = true

View file

@ -7,7 +7,12 @@ help:
.PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed"
@if ! command -v poetry &> /dev/null; then \
@if ! command -v poetry >/dev/null 2>&1; then \
echo "📦 Poetry not found. Checking if pip is available"; \
if ! command -v pip >/dev/null 2>&1; then \
echo "❌ pip is not installed. Please install pip first."; \
exit 1; \
fi; \
echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \
else \

View file

@ -20,10 +20,10 @@ class DateRange(Enum):
def to_datetime_range(
self,
start_time: Optional[time] = None,
end_time: Optional[time] = None,
time_zone: Optional[ZoneInfo] = None,
today: Optional[date] = None,
start_time: time | None = None,
end_time: time | None = None,
time_zone: ZoneInfo | None = None,
today: date | None = None,
) -> tuple[datetime, datetime]:
"""
Convert a DateRange enum value to a tuple with two datetime objects representing the start
@ -427,10 +427,10 @@ class CellExtendedValue(BaseModel):
Implementation of https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#ExtendedValue
"""
numberValue: Optional[float] = None
stringValue: Optional[str] = None
boolValue: Optional[bool] = None
formulaValue: Optional[str] = None
numberValue: float | None = None
stringValue: str | None = None
boolValue: bool | None = None
formulaValue: str | None = None
errorValue: Optional["CellErrorValue"] = None
@model_validator(mode="after")
@ -476,7 +476,7 @@ class CellData(BaseModel):
"""
userEnteredValue: CellExtendedValue
userEnteredFormat: Optional[CellFormat] = None
userEnteredFormat: CellFormat | None = None
class RowData(BaseModel):
@ -517,7 +517,7 @@ class SheetProperties(BaseModel):
sheetId: int
title: str
gridProperties: Optional[GridProperties] = None
gridProperties: GridProperties | None = None
class Sheet(BaseModel):
@ -527,7 +527,7 @@ class Sheet(BaseModel):
"""
properties: SheetProperties
data: Optional[list[GridData]] = None
data: list[GridData] | None = None
class SpreadsheetProperties(BaseModel):
@ -612,7 +612,7 @@ class SheetDataInput(BaseModel):
col_string = col_key.upper()
if not col_string.isalpha():
raise TypeError(f"Column key '{col_key}' is invalid. Must be alphabetic.")
if not isinstance(cell_value, (int, float, str, bool)):
if not isinstance(cell_value, int | float | str | bool):
raise TypeError(
f"Cell value for {col_string}{row_int} must be an int, float, str, or bool."
)

View file

@ -1,6 +1,6 @@
import json
from datetime import datetime, timedelta
from typing import Annotated, Any, Optional
from typing import Annotated, Any
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from arcade.sdk import ToolContext, tool
@ -373,18 +373,18 @@ async def delete_event(
async def find_time_slots_when_everyone_is_free(
context: ToolContext,
email_addresses: Annotated[
Optional[list[str]],
list[str] | None,
"The list of email addresses from people in the same organization domain (apart from the "
"currently logged in user) to search for free time slots. Defaults to None, which will "
"return free time slots for the current user only.",
] = None,
start_date: Annotated[
Optional[str],
str | None,
"The start date to search for time slots in the format 'YYYY-MM-DD'. Defaults to today's "
"date. It will search starting from this date at the time 00:00:00.",
] = None,
end_date: Annotated[
Optional[str],
str | None,
"The end date to search for time slots in the format 'YYYY-MM-DD'. Defaults to seven days "
"from the start date. It will search until this date at the time 23:59:59.",
] = None,

View file

@ -1,5 +1,5 @@
import asyncio
from typing import Annotated, Optional
from typing import Annotated
from arcade.sdk import ToolContext, tool
from arcade.sdk.auth import Google
@ -25,7 +25,7 @@ async def search_contacts_by_email(
context: ToolContext,
email: Annotated[str, "The email address to search for"],
limit: Annotated[
Optional[int],
int | None,
"The maximum number of contacts to return (30 is the max allowed by Google API)",
] = DEFAULT_SEARCH_CONTACTS_LIMIT,
) -> Annotated[dict, "A dictionary containing the list of matching contacts"]:
@ -47,7 +47,7 @@ async def search_contacts_by_name(
context: ToolContext,
name: Annotated[str, "The full name to search for"],
limit: Annotated[
Optional[int],
int | None,
"The maximum number of contacts to return (30 is the max allowed by Google API)",
] = DEFAULT_SEARCH_CONTACTS_LIMIT,
) -> Annotated[dict, "A dictionary containing the list of matching contacts"]:
@ -67,8 +67,8 @@ async def search_contacts_by_name(
async def create_contact(
context: ToolContext,
given_name: Annotated[str, "The given name of the contact"],
family_name: Annotated[Optional[str], "The optional family name of the contact"],
email: Annotated[Optional[str], "The optional email address of the contact"],
family_name: Annotated[str | None, "The optional family name of the contact"],
email: Annotated[str | None, "The optional email address of the contact"],
) -> Annotated[dict, "A dictionary containing the details of the created contact"]:
"""
Create a new contact record in Google Contacts.

View file

@ -1,4 +1,4 @@
from typing import Annotated, Any, Optional
from typing import Annotated, Any
from arcade.sdk import ToolContext, tool
from arcade.sdk.auth import Google
@ -27,7 +27,7 @@ async def get_file_tree_structure(
bool, "Whether to include shared drives in the file tree structure. Defaults to False."
] = False,
restrict_to_shared_drive_id: Annotated[
Optional[str],
str | None,
"If provided, only include files from this shared drive in the file tree structure. "
"Defaults to None, which will include files and folders from all drives.",
] = None,
@ -38,11 +38,11 @@ async def get_file_tree_structure(
"account. Defaults to False.",
] = False,
order_by: Annotated[
Optional[list[OrderBy]],
list[OrderBy] | None,
"Sort order. Defaults to listing the most recently modified documents first",
] = None,
limit: Annotated[
Optional[int],
int | None,
"The number of files and folders to list. Defaults to None, "
"which will list all files and folders.",
] = None,
@ -121,17 +121,17 @@ async def get_file_tree_structure(
async def search_documents(
context: ToolContext,
document_contains: Annotated[
Optional[list[str]],
list[str] | None,
"Keywords or phrases that must be in the document title or body. Provide a list of "
"keywords or phrases if needed.",
] = None,
document_not_contains: Annotated[
Optional[list[str]],
list[str] | None,
"Keywords or phrases that must NOT be in the document title or body. Provide a list of "
"keywords or phrases if needed.",
] = None,
search_only_in_shared_drive_id: Annotated[
Optional[str],
str | None,
"The ID of the shared drive to restrict the search to. If provided, the search will only "
"return documents from this drive. Defaults to None, which searches across all drives.",
] = None,
@ -147,12 +147,12 @@ async def search_documents(
"account. Defaults to False.",
] = False,
order_by: Annotated[
Optional[list[OrderBy]],
list[OrderBy] | None,
"Sort order. Defaults to listing the most recently modified documents first",
] = None,
limit: Annotated[int, "The number of documents to list"] = 50,
pagination_token: Annotated[
Optional[str], "The pagination token to continue a previous request"
str | None, "The pagination token to continue a previous request"
] = None,
) -> Annotated[
dict,
@ -215,17 +215,17 @@ async def search_and_retrieve_documents(
"The format of the document to return. Defaults to Markdown.",
] = DocumentFormat.MARKDOWN,
document_contains: Annotated[
Optional[list[str]],
list[str] | None,
"Keywords or phrases that must be in the document title or body. Provide a list of "
"keywords or phrases if needed.",
] = None,
document_not_contains: Annotated[
Optional[list[str]],
list[str] | None,
"Keywords or phrases that must NOT be in the document title or body. Provide a list of "
"keywords or phrases if needed.",
] = None,
search_only_in_shared_drive_id: Annotated[
Optional[str],
str | None,
"The ID of the shared drive to restrict the search to. If provided, the search will only "
"return documents from this drive. Defaults to None, which searches across all drives.",
] = None,
@ -241,12 +241,12 @@ async def search_and_retrieve_documents(
"account. Defaults to False.",
] = False,
order_by: Annotated[
Optional[list[OrderBy]],
list[OrderBy] | None,
"Sort order. Defaults to listing the most recently modified documents first",
] = None,
limit: Annotated[int, "The number of documents to list"] = 50,
pagination_token: Annotated[
Optional[str], "The pagination token to continue a previous request"
str | None, "The pagination token to continue a previous request"
] = None,
) -> Annotated[
dict,

View file

@ -1,6 +1,6 @@
import base64
from email.mime.text import MIMEText
from typing import Annotated, Any, Optional
from typing import Annotated, Any
from arcade.sdk import ToolContext, tool
from arcade.sdk.auth import Google
@ -40,8 +40,8 @@ async def send_email(
subject: Annotated[str, "The subject of the email"],
body: Annotated[str, "The body of the email"],
recipient: Annotated[str, "The recipient of the email"],
cc: Annotated[Optional[list[str]], "CC recipients of the email"] = None,
bcc: Annotated[Optional[list[str]], "BCC recipients of the email"] = None,
cc: Annotated[list[str] | None, "CC recipients of the email"] = None,
bcc: Annotated[list[str] | None, "BCC recipients of the email"] = None,
) -> Annotated[dict, "A dictionary containing the sent email details"]:
"""
Send an email using the Gmail API.
@ -96,7 +96,7 @@ async def reply_to_email(
"Whether to reply to every recipient (including cc) or only to the original sender. "
f"Defaults to '{GMAIL_DEFAULT_REPLY_TO}'.",
] = GMAIL_DEFAULT_REPLY_TO,
bcc: Annotated[Optional[list[str]], "BCC recipients of the email"] = None,
bcc: Annotated[list[str] | None, "BCC recipients of the email"] = None,
) -> Annotated[dict, "A dictionary containing the sent email details"]:
"""
Send a reply to an email message.
@ -156,8 +156,8 @@ async def write_draft_email(
subject: Annotated[str, "The subject of the draft email"],
body: Annotated[str, "The body of the draft email"],
recipient: Annotated[str, "The recipient of the draft email"],
cc: Annotated[Optional[list[str]], "CC recipients of the draft email"] = None,
bcc: Annotated[Optional[list[str]], "BCC recipients of the draft email"] = None,
cc: Annotated[list[str] | None, "CC recipients of the draft email"] = None,
bcc: Annotated[list[str] | None, "BCC recipients of the draft email"] = None,
) -> Annotated[dict, "A dictionary containing the created draft email details"]:
"""
Compose a new email draft using the Gmail API.
@ -193,7 +193,7 @@ async def write_draft_reply_email(
"Whether to reply to every recipient (including cc) or only to the original sender. "
f"Defaults to '{GMAIL_DEFAULT_REPLY_TO}'.",
] = GMAIL_DEFAULT_REPLY_TO,
bcc: Annotated[Optional[list[str]], "BCC recipients of the draft reply email"] = None,
bcc: Annotated[list[str] | None, "BCC recipients of the draft reply email"] = None,
) -> Annotated[dict, "A dictionary containing the created draft reply email details"]:
"""
Compose a draft reply to an email message.
@ -256,8 +256,8 @@ async def update_draft_email(
subject: Annotated[str, "The subject of the draft email"],
body: Annotated[str, "The body of the draft email"],
recipient: Annotated[str, "The recipient of the draft email"],
cc: Annotated[Optional[list[str]], "CC recipients of the draft email"] = None,
bcc: Annotated[Optional[list[str]], "BCC recipients of the draft email"] = None,
cc: Annotated[list[str] | None, "CC recipients of the draft email"] = None,
bcc: Annotated[list[str] | None, "BCC recipients of the draft email"] = None,
) -> Annotated[dict, "A dictionary containing the updated draft email details"]:
"""
Update an existing email draft using the Gmail API.
@ -374,12 +374,12 @@ async def list_draft_emails(
)
async def list_emails_by_header(
context: ToolContext,
sender: Annotated[Optional[str], "The name or email address of the sender of the email"] = None,
recipient: Annotated[Optional[str], "The name or email address of the recipient"] = None,
subject: Annotated[Optional[str], "Words to find in the subject of the email"] = None,
body: Annotated[Optional[str], "Words to find in the body of the email"] = None,
date_range: Annotated[Optional[DateRange], "The date range of the email"] = None,
label: Annotated[Optional[str], "The label name to filter by"] = None,
sender: Annotated[str | None, "The name or email address of the sender of the email"] = None,
recipient: Annotated[str | None, "The name or email address of the recipient"] = None,
subject: Annotated[str | None, "Words to find in the subject of the email"] = None,
body: Annotated[str | None, "Words to find in the body of the email"] = None,
date_range: Annotated[DateRange | None, "The date range of the email"] = None,
label: Annotated[str | None, "The label name to filter by"] = None,
max_results: Annotated[int, "The maximum number of emails to return"] = 25,
) -> Annotated[
dict, "A dictionary containing a list of email details matching the search criteria"
@ -475,16 +475,16 @@ async def list_emails(
async def search_threads(
context: ToolContext,
page_token: Annotated[
Optional[str], "Page token to retrieve a specific page of results in the list"
str | None, "Page token to retrieve a specific page of results in the list"
] = None,
max_results: Annotated[int, "The maximum number of threads to return"] = 10,
include_spam_trash: Annotated[bool, "Whether to include spam and trash in the results"] = False,
label_ids: Annotated[Optional[list[str]], "The IDs of labels to filter by"] = None,
sender: Annotated[Optional[str], "The name or email address of the sender of the email"] = None,
recipient: Annotated[Optional[str], "The name or email address of the recipient"] = None,
subject: Annotated[Optional[str], "Words to find in the subject of the email"] = None,
body: Annotated[Optional[str], "Words to find in the body of the email"] = None,
date_range: Annotated[Optional[DateRange], "The date range of the email"] = None,
label_ids: Annotated[list[str] | None, "The IDs of labels to filter by"] = None,
sender: Annotated[str | None, "The name or email address of the sender of the email"] = None,
recipient: Annotated[str | None, "The name or email address of the recipient"] = None,
subject: Annotated[str | None, "Words to find in the subject of the email"] = None,
body: Annotated[str | None, "Words to find in the body of the email"] = None,
date_range: Annotated[DateRange | None, "The date range of the email"] = None,
) -> Annotated[dict, "A dictionary containing a list of thread details"]:
"""Search for threads in the user's mailbox"""
service = _build_gmail_service(context)
@ -535,7 +535,7 @@ async def search_threads(
async def list_threads(
context: ToolContext,
page_token: Annotated[
Optional[str], "Page token to retrieve a specific page of results in the list"
str | None, "Page token to retrieve a specific page of results in the list"
] = None,
max_results: Annotated[int, "The maximum number of threads to return"] = 10,
include_spam_trash: Annotated[bool, "Whether to include spam and trash in the results"] = False,

View file

@ -1,4 +1,4 @@
from typing import Annotated, Optional
from typing import Annotated
from arcade.sdk import ToolContext, tool
from arcade.sdk.auth import Google
@ -27,7 +27,7 @@ def create_spreadsheet(
context: ToolContext,
title: Annotated[str, "The title of the new spreadsheet"] = "Untitled spreadsheet",
data: Annotated[
Optional[str],
str | None,
"The data to write to the spreadsheet. A JSON string "
"(property names enclosed in double quotes) representing a dictionary that "
"maps row numbers to dictionaries that map column letters to cell values. "

View file

@ -5,7 +5,7 @@ from datetime import date, datetime, time, timedelta, timezone
from email.message import EmailMessage
from email.mime.text import MIMEText
from enum import Enum
from typing import Any, Optional, Union, cast
from typing import Any, cast
from zoneinfo import ZoneInfo
from arcade.sdk import ToolContext
@ -114,15 +114,15 @@ def build_email_message(
recipient: str,
subject: str,
body: str,
cc: Optional[list[str]] = None,
bcc: Optional[list[str]] = None,
replying_to: Optional[dict[str, Any]] = None,
cc: list[str] | None = None,
bcc: list[str] | None = None,
replying_to: dict[str, Any] | None = None,
action: GmailAction = GmailAction.SEND,
) -> dict[str, Any]:
if replying_to:
body = build_reply_body(body, replying_to)
message: Union[EmailMessage, MIMEText]
message: EmailMessage | MIMEText
if action == GmailAction.SEND:
message = EmailMessage()
@ -183,10 +183,10 @@ def parse_plain_text_email(email_data: dict[str, Any]) -> dict[str, Any]:
Only returns the plain text body.
Args:
email_data (Dict[str, Any]): Raw email data from Gmail API.
email_data (dict[str, Any]): Raw email data from Gmail API.
Returns:
Optional[Dict[str, str]]: Parsed email details or None if parsing fails.
dict[str, str]: Parsed email details
"""
payload = email_data.get("payload", {})
headers = {d["name"].lower(): d["value"] for d in payload.get("headers", [])}
@ -223,7 +223,7 @@ def parse_multipart_email(email_data: dict[str, Any]) -> dict[str, Any]:
email_data (Dict[str, Any]): Raw email data from Gmail API.
Returns:
Optional[Dict[str, Any]]: Parsed email details or None if parsing fails.
dict[str, Any]: Parsed email details
"""
payload = email_data.get("payload", {})
@ -263,7 +263,7 @@ def parse_draft_email(draft_email_data: dict[str, Any]) -> dict[str, str]:
draft_email_data (Dict[str, Any]): Raw draft email data from Gmail API.
Returns:
Optional[Dict[str, str]]: Parsed draft email details or None if parsing fails.
dict[str, str]: Parsed draft email details
"""
message = draft_email_data.get("message", {})
payload = message.get("payload", {})
@ -342,7 +342,7 @@ def _build_gmail_service(context: ToolContext) -> Any:
return build("gmail", "v1", credentials=credentials)
def _extract_plain_body(parts: list) -> Optional[str]:
def _extract_plain_body(parts: list) -> str | None:
"""
Recursively extract the email body from parts, handling both plain text and HTML.
@ -350,7 +350,7 @@ def _extract_plain_body(parts: list) -> Optional[str]:
parts (List[Dict[str, Any]]): List of email parts.
Returns:
Optional[str]: Decoded and cleaned email body or None if not found.
str | None: Decoded and cleaned email body or None if not found.
"""
for part in parts:
mime_type = part.get("mimeType")
@ -367,7 +367,7 @@ def _extract_plain_body(parts: list) -> Optional[str]:
return _extract_html_body(parts)
def _extract_html_body(parts: list) -> Optional[str]:
def _extract_html_body(parts: list) -> str | None:
"""
Recursively extract the email body from parts, handling only HTML.
@ -375,7 +375,7 @@ def _extract_html_body(parts: list) -> Optional[str]:
parts (List[Dict[str, Any]]): List of email parts.
Returns:
Optional[str]: Decoded and cleaned email body or None if not found.
str | None: Decoded and cleaned email body or None if not found.
"""
for part in parts:
mime_type = part.get("mimeType")
@ -393,7 +393,7 @@ def _extract_html_body(parts: list) -> Optional[str]:
return None
def _get_email_images(payload: dict[str, Any]) -> Optional[list[str]]:
def _get_email_images(payload: dict[str, Any]) -> list[str] | None:
"""
Extract the email images from an email payload.
@ -401,7 +401,7 @@ def _get_email_images(payload: dict[str, Any]) -> Optional[list[str]]:
payload (Dict[str, Any]): Email payload data.
Returns:
Optional[List[str]]: List of decoded image contents or None if none found.
list[str] | None: List of decoded image contents or None if none found.
"""
images = []
for part in payload.get("parts", []):
@ -423,7 +423,7 @@ def _get_email_images(payload: dict[str, Any]) -> Optional[list[str]]:
return None
def _get_email_plain_text_body(payload: dict[str, Any]) -> Optional[str]:
def _get_email_plain_text_body(payload: dict[str, Any]) -> str | None:
"""
Extract email body from payload, handling 'multipart/alternative' parts.
@ -431,7 +431,7 @@ def _get_email_plain_text_body(payload: dict[str, Any]) -> Optional[str]:
payload (Dict[str, Any]): Email payload data.
Returns:
Optional[str]: Decoded email body or None if not found.
str | None: Decoded email body or None if not found.
"""
# Direct body extraction
if "body" in payload and payload["body"].get("data"):
@ -441,7 +441,7 @@ def _get_email_plain_text_body(payload: dict[str, Any]) -> Optional[str]:
return _clean_email_body(_extract_plain_body(payload.get("parts", [])))
def _get_email_html_body(payload: dict[str, Any]) -> Optional[str]:
def _get_email_html_body(payload: dict[str, Any]) -> str | None:
"""
Extract email html body from payload, handling 'multipart/alternative' parts.
@ -449,7 +449,7 @@ def _get_email_html_body(payload: dict[str, Any]) -> Optional[str]:
payload (Dict[str, Any]): Email payload data.
Returns:
Optional[str]: Decoded email body or None if not found.
str | None: Decoded email body or None if not found.
"""
# Direct body extraction
if "body" in payload and payload["body"].get("data"):
@ -459,7 +459,7 @@ def _get_email_html_body(payload: dict[str, Any]) -> Optional[str]:
return _extract_html_body(payload.get("parts", []))
def _clean_email_body(body: Optional[str]) -> str:
def _clean_email_body(body: str | None) -> str:
"""
Remove HTML tags and clean up email body text while preserving most content.
@ -608,7 +608,7 @@ def remove_none_values(params: dict) -> dict:
# Drive utils
def build_drive_service(auth_token: Optional[str]) -> Resource: # type: ignore[no-any-unimported]
def build_drive_service(auth_token: str | None) -> Resource: # type: ignore[no-any-unimported]
"""
Build a Drive service object.
"""
@ -618,8 +618,8 @@ def build_drive_service(auth_token: Optional[str]) -> Resource: # type: ignore[
def build_files_list_query(
mime_type: str,
document_contains: Optional[list[str]] = None,
document_not_contains: Optional[list[str]] = None,
document_contains: list[str] | None = None,
document_not_contains: list[str] | None = None,
) -> str:
query = [f"(mimeType = '{mime_type}' and trashed = false)"]
@ -655,12 +655,12 @@ def build_files_list_params(
mime_type: str,
page_size: int,
order_by: list[OrderBy],
pagination_token: Optional[str],
pagination_token: str | None,
include_shared_drives: bool,
search_only_in_shared_drive_id: Optional[str],
search_only_in_shared_drive_id: str | None,
include_organization_domain_documents: bool,
document_contains: Optional[list[str]] = None,
document_not_contains: Optional[list[str]] = None,
document_contains: list[str] | None = None,
document_not_contains: list[str] | None = None,
) -> dict[str, Any]:
query = build_files_list_query(
mime_type=mime_type,
@ -696,11 +696,11 @@ def build_files_list_params(
def build_file_tree_request_params(
order_by: Optional[list[OrderBy]],
page_token: Optional[str],
limit: Optional[int],
order_by: list[OrderBy] | None,
page_token: str | None,
limit: int | None,
include_shared_drives: bool,
restrict_to_shared_drive_id: Optional[str],
restrict_to_shared_drive_id: str | None,
include_organization_domain_documents: bool,
) -> dict[str, Any]:
if order_by is None:
@ -787,7 +787,7 @@ def build_file_tree(files: dict[str, Any]) -> dict[str, Any]:
# Docs utils
def build_docs_service(auth_token: Optional[str]) -> Resource: # type: ignore[no-any-unimported]
def build_docs_service(auth_token: str | None) -> Resource: # type: ignore[no-any-unimported]
"""
Build a Drive service object.
"""
@ -989,7 +989,7 @@ def get_now(tz: ZoneInfo | None = None) -> datetime:
# Contacts utils
def build_people_service(auth_token: Optional[str]) -> Resource: # type: ignore[no-any-unimported]
def build_people_service(auth_token: str | None) -> Resource: # type: ignore[no-any-unimported]
"""
Build a People service object.
"""
@ -997,7 +997,7 @@ def build_people_service(auth_token: Optional[str]) -> Resource: # type: ignore
return build("people", "v1", credentials=Credentials(auth_token))
def search_contacts(service: Any, query: str, limit: Optional[int]) -> list[dict[str, Any]]:
def search_contacts(service: Any, query: str, limit: int | None) -> list[dict[str, Any]]:
"""
Search the user's contacts in Google Contacts.
"""
@ -1029,7 +1029,7 @@ def search_contacts(service: Any, query: str, limit: Optional[int]) -> list[dict
# ----------------------------------------------------------------
def build_sheets_service(auth_token: Optional[str]) -> Resource: # type: ignore[no-any-unimported]
def build_sheets_service(auth_token: str | None) -> Resource: # type: ignore[no-any-unimported]
"""
Build a Sheets service object.
"""

View file

@ -1,7 +1,7 @@
[tool.poetry]
name = "arcade_google"
version = "1.1.0"
description = "Arcade tools for the entire google suite"
description = "Arcade.dev LLM tools for Google Workspace"
authors = ["Arcade <dev@arcade.dev>"]
[tool.poetry.dependencies]
@ -26,7 +26,7 @@ tox = "^4.11.1"
ruff = "^0.7.4"
[build-system]
requires = ["poetry-core>=1.0.0"]
requires = ["poetry-core>=1.0.0,<2.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.mypy]

View file

@ -236,7 +236,7 @@ def test_create_row_data():
row_data = create_row_data(row_data, min_col_index, max_col_index)
assert len(row_data.values) == len(expected_row_data.values)
for cell, expected in zip(row_data.values, expected_row_data.values):
for cell, expected in zip(row_data.values, expected_row_data.values, strict=False):
assert cell.userEnteredValue == expected.userEnteredValue
@ -268,7 +268,7 @@ def test_create_sheet_data():
create_cell_data("row2B"),
create_cell_data(200),
]
for cell, expected in zip(row2_cells, expected_row2):
for cell, expected in zip(row2_cells, expected_row2, strict=False):
assert cell.userEnteredValue == expected.userEnteredValue
row3_cells = group1.rowData[1].values
@ -277,7 +277,7 @@ def test_create_sheet_data():
create_cell_data("row3B"),
CellData(userEnteredValue=CellExtendedValue(stringValue="")),
]
for cell, expected in zip(row3_cells, expected_row3):
for cell, expected in zip(row3_cells, expected_row3, strict=False):
assert cell.userEnteredValue == expected.userEnteredValue
group2 = grid_data_list[1]
@ -291,7 +291,7 @@ def test_create_sheet_data():
CellData(userEnteredValue=CellExtendedValue(stringValue="")),
create_cell_data("row5C"),
]
for cell, expected in zip(row5_cells, expected_row5):
for cell, expected in zip(row5_cells, expected_row5, strict=False):
assert cell.userEnteredValue == expected.userEnteredValue

View file

@ -1,4 +1,4 @@
target-version = "py39"
target-version = "py310"
line-length = 100
fix = true

View file

@ -7,7 +7,12 @@ help:
.PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed"
@if ! command -v poetry &> /dev/null; then \
@if ! command -v poetry >/dev/null 2>&1; then \
echo "📦 Poetry not found. Checking if pip is available"; \
if ! command -v pip >/dev/null 2>&1; then \
echo "❌ pip is not installed. Please install pip first."; \
exit 1; \
fi; \
echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \
else \

View file

@ -1,7 +1,7 @@
[tool.poetry]
name = "arcade_linkedin"
version = "0.1.10"
description = "Arcade tools for LinkedIn"
description = "Arcade.dev LLM tools for LinkedIn"
authors = ["Arcade <dev@arcade.dev>"]
[tool.poetry.dependencies]
@ -20,7 +20,7 @@ tox = "^4.11.1"
ruff = "^0.7.4"
[build-system]
requires = ["poetry-core>=1.0.0"]
requires = ["poetry-core>=1.0.0,<2.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.mypy]

View file

@ -1,4 +1,4 @@
target-version = "py39"
target-version = "py310"
line-length = 100
fix = true

View file

@ -7,7 +7,12 @@ help:
.PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed"
@if ! command -v poetry &> /dev/null; then \
@if ! command -v poetry >/dev/null 2>&1; then \
echo "📦 Poetry not found. Checking if pip is available"; \
if ! command -v pip >/dev/null 2>&1; then \
echo "❌ pip is not installed. Please install pip first."; \
exit 1; \
fi; \
echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \
else \

View file

@ -1,5 +1,5 @@
import random
from typing import Annotated, Optional
from typing import Annotated
from arcade.sdk import tool
@ -9,7 +9,7 @@ def generate_random_int(
min_value: Annotated[str, "The minimum value of the random integer as a string"],
max_value: Annotated[str, "The maximum value of the random integer as a string"],
seed: Annotated[
Optional[str],
str | None,
"The seed for the random number generator as a string."
" If None, the current system time is used.",
] = None,
@ -26,7 +26,7 @@ def generate_random_float(
min_value: Annotated[str, "The minimum value of the random float as a string"],
max_value: Annotated[str, "The maximum value of the random float as a string"],
seed: Annotated[
Optional[str],
str | None,
"The seed for the random number generator as a string."
" If None, the current system time is used.",
] = None,

View file

@ -1,7 +1,7 @@
[tool.poetry]
name = "arcade_math"
version = "1.0.1"
description = "Math toolkit for Arcade"
description = "Arcade.dev LLM tools for doing math"
authors = ["Arcade <dev@arcade.dev>"]
[tool.poetry.dependencies]
@ -19,7 +19,7 @@ tox = "^4.11.1"
ruff = "^0.7.4"
[build-system]
requires = ["poetry-core>=1.0.0"]
requires = ["poetry-core>=1.0.0,<2.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.mypy]

View file

@ -1,4 +1,4 @@
target-version = "py39"
target-version = "py310"
line-length = 100
fix = true

View file

@ -7,7 +7,12 @@ help:
.PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed"
@if ! command -v poetry &> /dev/null; then \
@if ! command -v poetry >/dev/null 2>&1; then \
echo "📦 Poetry not found. Checking if pip is available"; \
if ! command -v pip >/dev/null 2>&1; then \
echo "❌ pip is not installed. Please install pip first."; \
exit 1; \
fi; \
echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \
else \

View file

@ -1,5 +1,5 @@
import asyncio
from typing import Any, Optional
from typing import Any
from arcade.sdk import ToolContext
@ -100,14 +100,14 @@ class BlockToMarkdownConverter:
return md
@staticmethod
def apply_formatting(text: str, annotations: dict[str, Any], link: Optional[str] = None) -> str:
def apply_formatting(text: str, annotations: dict[str, Any], link: str | None = None) -> str:
"""Apply formatting to a text string based on the annotations.
Used when converting rich text to markdown
Args:
text (str): The text to format.
annotations (dict[str, Any]): The annotations to apply to the text.
link (Optional[str]): An optional link for a hyperlink.
link (str | None): An optional link for a hyperlink.
Returns:
str: The formatted text.

View file

@ -1,4 +1,4 @@
from typing import Annotated, Any, Optional
from typing import Annotated, Any
import httpx
from arcade.sdk import ToolContext, tool
@ -95,7 +95,7 @@ async def create_page(
"Title of an existing page/database within which the new page will be created. ",
],
title: Annotated[str, "Title of the new page"],
content: Annotated[Optional[str], "The content of the new page"] = None,
content: Annotated[str | None, "The content of the new page"] = None,
) -> Annotated[str, "The ID of the new page"]:
"""Create a new Notion page by the title of the new page's parent."""
# Notion API does not support creating a page at the root of the workspace... sigh

View file

@ -1,4 +1,4 @@
from typing import Annotated, Any, Optional
from typing import Annotated, Any
import httpx
from arcade.sdk import ToolContext, tool
@ -19,12 +19,12 @@ from arcade_notion_toolkit.utils import (
async def search_by_title(
context: ToolContext,
query: Annotated[
Optional[str],
str | None,
"A substring to search for within page and database titles. "
"If not provided (default), all pages and/or databases are returned.",
] = None,
select: Annotated[
Optional[ObjectType],
ObjectType | None,
"Limit the results to either only pages or only databases. Defaults to both.",
] = None,
order_by: Annotated[
@ -87,11 +87,11 @@ async def search_by_title(
async def get_object_metadata(
context: ToolContext,
object_title: Annotated[
Optional[str], "Title of the page or database whose metadata to get"
str | None, "Title of the page or database whose metadata to get"
] = None,
object_id: Annotated[Optional[str], "ID of the page or database whose metadata to get"] = None,
object_id: Annotated[str | None, "ID of the page or database whose metadata to get"] = None,
object_type: Annotated[
Optional[ObjectType],
ObjectType | None,
"The type of object to match title to. Only used if `object_title` is provided. "
"Defaults to both",
] = None,

View file

@ -1,4 +1,4 @@
from typing import Any, Optional
from typing import Any
import httpx
from arcade.sdk import ToolContext
@ -118,8 +118,8 @@ async def get_next_page(
client: httpx.AsyncClient,
url: str,
headers: dict,
params: Optional[dict] = None,
cursor: Optional[str] = None,
params: dict | None = None,
cursor: str | None = None,
) -> tuple[dict, bool, str]:
"""
Retrieves the next page of results from a Notion API endpoint.
@ -129,8 +129,8 @@ async def get_next_page(
client (httpx.AsyncClient): The HTTP client to use for the request.
url (str): The URL of the endpoint to request.
headers (dict): The headers to use for the request.
params (Optional[dict]): The parameters to use for the request.
cursor (Optional[str]): The cursor to use for the request.
params (dict | None): The parameters to use for the request.
cursor (str | None): The cursor to use for the request.
Returns:
tuple[dict, bool, str]: A tuple containing the results, a boolean indicating if there is a

View file

@ -1,7 +1,7 @@
[tool.poetry]
name = "arcade_notion_toolkit"
version = "0.1.1"
description = "LLM tools for essential Notion interactions such as creating, updating, retrieving, and searching pages."
description = "Arcade.dev LLM tools for Notion"
authors = ["ArcadeAI <dev@arcade.dev>"]
[tool.poetry.dependencies]
@ -17,7 +17,7 @@ tox = "^4.11.1"
ruff = "^0.7.4"
[build-system]
requires = ["poetry-core>=1.0.0"]
requires = ["poetry-core>=1.0.0,<2.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.mypy]

View file

@ -1,4 +1,4 @@
target-version = "py39"
target-version = "py310"
line-length = 100
fix = true

View file

@ -7,7 +7,12 @@ help:
.PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed"
@if ! command -v poetry &> /dev/null; then \
@if ! command -v poetry >/dev/null 2>&1; then \
echo "📦 Poetry not found. Checking if pip is available"; \
if ! command -v pip >/dev/null 2>&1; then \
echo "❌ pip is not installed. Please install pip first."; \
exit 1; \
fi; \
echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \
else \

View file

@ -1,4 +1,4 @@
from typing import Annotated, Optional
from typing import Annotated
from arcade.sdk import ToolContext, tool
from arcade.sdk.auth import Reddit
@ -33,7 +33,7 @@ async def get_posts_in_subreddit(
),
] = SubredditListingType.HOT,
limit: Annotated[int, "The maximum number of posts to fetch. Default is 10, max is 100."] = 10,
cursor: Annotated[Optional[str], "The pagination token from a previous call"] = None,
cursor: Annotated[str | None, "The pagination token from a previous call"] = None,
time_range: Annotated[
RedditTimeFilter,
"The time range for filtering posts. Must be provided if the listing type is "

View file

@ -1,4 +1,4 @@
from typing import Annotated, Optional
from typing import Annotated
from arcade.sdk import ToolContext, tool
from arcade.sdk.auth import Reddit
@ -19,20 +19,20 @@ async def submit_text_post(
subreddit: Annotated[str, "The name of the subreddit to which the post will be submitted"],
title: Annotated[str, "The title of the submission"],
body: Annotated[
Optional[str],
str | None,
"The body of the post in markdown format. Should never be the same as the title",
] = None,
nsfw: Annotated[
Optional[bool],
bool | None,
"Indicates if the submission has content that is 'Not Safe For Work' (NSFW). "
"Default is False",
] = False,
spoiler: Annotated[
Optional[bool],
bool | None,
"Indicates if the post is marked as a spoiler. Default is False",
] = False,
send_replies: Annotated[
Optional[bool], "If true, sends replies to the user's inbox. Default is True"
bool | None, "If true, sends replies to the user's inbox. Default is True"
] = True,
) -> Annotated[dict, "Response from Reddit after submission"]:
"""Submit a text-based post to a subreddit"""

View file

@ -1,7 +1,7 @@
[tool.poetry]
name = "arcade_reddit"
version = "0.0.1"
description = "LLM tools for interacting with Reddit"
description = "Arcade.dev LLM tools Reddit"
authors = ["Arcade <dev@arcade.dev>"]
[tool.poetry.dependencies]
@ -17,7 +17,7 @@ tox = "^4.11.1"
ruff = "^0.7.4"
[build-system]
requires = ["poetry-core>=1.0.0"]
requires = ["poetry-core>=1.0.0,<2.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.mypy]

View file

@ -1,4 +1,4 @@
target-version = "py39"
target-version = "py310"
line-length = 100
fix = true

View file

@ -7,7 +7,12 @@ help:
.PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed"
@if ! command -v poetry &> /dev/null; then \
@if ! command -v poetry >/dev/null 2>&1; then \
echo "📦 Poetry not found. Checking if pip is available"; \
if ! command -v pip >/dev/null 2>&1; then \
echo "❌ pip is not installed. Please install pip first."; \
exit 1; \
fi; \
echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \
else \

View file

@ -1,5 +1,4 @@
from enum import Enum
from typing import Optional
# ------------------------------------------------------------------------------------------------
@ -138,7 +137,7 @@ class WalmartSortBy(Enum):
RATING_HIGH = "highest_rating_first"
NEW_ARRIVALS = "new_arrivals_first"
def to_api_value(self: "WalmartSortBy") -> Optional[str]:
def to_api_value(self: "WalmartSortBy") -> str | None:
_map = {
str(self.RELEVANCE): None,
str(self.PRICE_LOW_TO_HIGH): "price_low",

View file

@ -1,5 +1,4 @@
import json
from typing import Optional
from arcade.sdk.errors import RetryableToolError
@ -11,7 +10,7 @@ class GoogleRetryableError(RetryableToolError):
class CountryNotFoundError(GoogleRetryableError):
def __init__(self, country: Optional[str]) -> None:
def __init__(self, country: str | None) -> None:
valid_countries = json.dumps(COUNTRY_CODES, default=str)
message = f"Country not found: '{country}'."
additional_message = f"Valid countries are: {valid_countries}"
@ -19,7 +18,7 @@ class CountryNotFoundError(GoogleRetryableError):
class LanguageNotFoundError(GoogleRetryableError):
def __init__(self, language: Optional[str]) -> None:
def __init__(self, language: str | None) -> None:
valid_languages = json.dumps(LANGUAGE_CODES, default=str)
message = f"Language not found: '{language}'."
additional_message = f"Valid languages are: {valid_languages}"

View file

@ -1,4 +1,4 @@
from typing import Annotated, Any, Optional
from typing import Annotated, Any
from arcade.sdk import ToolContext, tool
@ -14,16 +14,16 @@ async def search_roundtrip_flights(
],
arrival_airport_code: Annotated[str, "The arrival airport code. An uppercase 3-letter code"],
outbound_date: Annotated[str, "Flight outbound date in YYYY-MM-DD format"],
return_date: Annotated[Optional[str], "Flight return date in YYYY-MM-DD format"],
return_date: Annotated[str | None, "Flight return date in YYYY-MM-DD format"],
currency_code: Annotated[
Optional[str], "Currency of the returned prices. Defaults to 'USD'"
str | None, "Currency of the returned prices. Defaults to 'USD'"
] = "USD",
travel_class: Annotated[
GoogleFlightsTravelClass,
"Travel class of the flight. Defaults to 'ECONOMY'",
] = GoogleFlightsTravelClass.ECONOMY,
num_adults: Annotated[Optional[int], "Number of adult passengers. Defaults to 1"] = 1,
num_children: Annotated[Optional[int], "Number of child passengers. Defaults to 0"] = 0,
num_adults: Annotated[int | None, "Number of adult passengers. Defaults to 1"] = 1,
num_children: Annotated[int | None, "Number of child passengers. Defaults to 0"] = 0,
max_stops: Annotated[
GoogleFlightsMaxStops,
"Maximum number of stops (layovers) for the flight. Defaults to any number of stops",
@ -68,14 +68,14 @@ async def search_one_way_flights(
arrival_airport_code: Annotated[str, "The arrival airport code. An uppercase 3-letter code"],
outbound_date: Annotated[str, "Flight departure date in YYYY-MM-DD format"],
currency_code: Annotated[
Optional[str], "Currency of the returned prices. Defaults to 'USD'"
str | None, "Currency of the returned prices. Defaults to 'USD'"
] = "USD",
travel_class: Annotated[
GoogleFlightsTravelClass,
"Travel class of the flight. Defaults to 'ECONOMY'",
] = GoogleFlightsTravelClass.ECONOMY,
num_adults: Annotated[Optional[int], "Number of adult passengers. Defaults to 1"] = 1,
num_children: Annotated[Optional[int], "Number of child passengers. Defaults to 0"] = 0,
num_adults: Annotated[int | None, "Number of adult passengers. Defaults to 1"] = 1,
num_children: Annotated[int | None, "Number of child passengers. Defaults to 0"] = 0,
max_stops: Annotated[
GoogleFlightsMaxStops,
"Maximum number of stops (layovers) for the flight. Defaults to any number of stops",

View file

@ -1,4 +1,4 @@
from typing import Annotated, Any, Optional
from typing import Annotated, Any
from arcade.sdk import ToolContext, tool
@ -13,18 +13,18 @@ async def search_hotels(
check_in_date: Annotated[str, "Check-in date in YYYY-MM-DD format"],
check_out_date: Annotated[str, "Check-out date in YYYY-MM-DD format"],
query: Annotated[
Optional[str], "Anything that would be used in a regular Google Hotels search"
str | None, "Anything that would be used in a regular Google Hotels search"
] = None,
currency: Annotated[Optional[str], "Currency code for prices. Defaults to 'USD'"] = "USD",
min_price: Annotated[Optional[int], "Minimum price per night. Defaults to no minimum"] = None,
max_price: Annotated[Optional[int], "Maximum price per night. Defaults to no maximum"] = None,
num_adults: Annotated[Optional[int], "Number of adults per room. Defaults to 2"] = 2,
num_children: Annotated[Optional[int], "Number of children per room. Defaults to 0"] = 0,
currency: Annotated[str | None, "Currency code for prices. Defaults to 'USD'"] = "USD",
min_price: Annotated[int | None, "Minimum price per night. Defaults to no minimum"] = None,
max_price: Annotated[int | None, "Maximum price per night. Defaults to no maximum"] = None,
num_adults: Annotated[int | None, "Number of adults per room. Defaults to 2"] = 2,
num_children: Annotated[int | None, "Number of children per room. Defaults to 0"] = 0,
sort_by: Annotated[
GoogleHotelsSortBy, "The sorting order of the results. Defaults to RELEVANCE"
] = GoogleHotelsSortBy.RELEVANCE,
num_results: Annotated[
Optional[int], "Maximum number of results to return. Defaults to 5. Max 20"
int | None, "Maximum number of results to return. Defaults to 5. Max 20"
] = 5,
) -> Annotated[dict[str, Any], "Hotel search results from the Google Hotels API"]:
"""Retrieve hotel search results using the Google Hotels API."""

View file

@ -1,4 +1,4 @@
from typing import Annotated, Optional
from typing import Annotated
from arcade.sdk import ToolContext, tool
@ -18,7 +18,7 @@ async def search_jobs(
"or 'data analyst at Apple'.",
],
location: Annotated[
Optional[str],
str | None,
"Location to search for jobs. E.g. 'United States' or 'New York, NY'. Defaults to None.",
] = None,
language: Annotated[
@ -31,7 +31,7 @@ async def search_jobs(
"Maximum number of results to retrieve. Defaults to 10 (max supported by the API).",
] = 10,
next_page_token: Annotated[
Optional[str],
str | None,
"Next page token to paginate results. Defaults to None (start from the first page).",
] = None,
) -> Annotated[dict, "Google Jobs results"]:

View file

@ -1,4 +1,4 @@
from typing import Annotated, Optional
from typing import Annotated
from arcade.sdk import ToolContext, tool
@ -27,7 +27,7 @@ async def get_directions_between_addresses(
f"Defaults to '{DEFAULT_GOOGLE_MAPS_LANGUAGE}'.",
] = DEFAULT_GOOGLE_MAPS_LANGUAGE,
country: Annotated[
Optional[str],
str | None,
"2-character country code to use in the Google Maps search. "
f"Defaults to '{DEFAULT_GOOGLE_MAPS_COUNTRY}'.",
] = DEFAULT_GOOGLE_MAPS_COUNTRY,
@ -69,7 +69,7 @@ async def get_directions_between_coordinates(
f"Defaults to '{DEFAULT_GOOGLE_MAPS_LANGUAGE}'.",
] = DEFAULT_GOOGLE_MAPS_LANGUAGE,
country: Annotated[
Optional[str],
str | None,
f"2-letter country code to use in the Google Maps search. Defaults to "
f"'{DEFAULT_GOOGLE_MAPS_COUNTRY}'.",
] = DEFAULT_GOOGLE_MAPS_COUNTRY,

View file

@ -1,4 +1,4 @@
from typing import Annotated, Any, Optional
from typing import Annotated, Any
from arcade.sdk import ToolContext, tool
from arcade.sdk.errors import ToolExecutionError
@ -17,7 +17,7 @@ async def search_news_stories(
"Keywords to search for news articles. E.g. 'Apple launches new iPhone'.",
],
country_code: Annotated[
Optional[str],
str | None,
"2-character country code to search for news articles. E.g. 'us' (United States). "
f"Defaults to '{DEFAULT_GOOGLE_NEWS_COUNTRY}'.",
] = None,
@ -27,7 +27,7 @@ async def search_news_stories(
f"Defaults to '{DEFAULT_GOOGLE_NEWS_LANGUAGE}'.",
] = DEFAULT_GOOGLE_NEWS_LANGUAGE,
limit: Annotated[
Optional[int],
int | None,
"Maximum number of news articles to return. Defaults to None "
"(returns all results found by the API).",
] = None,

View file

@ -1,4 +1,4 @@
from typing import Annotated, Any, Optional
from typing import Annotated, Any
from arcade.sdk import ToolContext, tool
from arcade.sdk.errors import ToolExecutionError
@ -25,12 +25,12 @@ async def search_shopping_products(
"Keywords to search for products in Google Shopping. E.g. 'Apple iPhone'.",
],
country_code: Annotated[
Optional[str],
str | None,
"2-character country code to search for products in Google Shopping. "
f"E.g. 'us' (United States). Defaults to '{DEFAULT_GOOGLE_SHOPPING_COUNTRY or 'us'}'.",
] = DEFAULT_GOOGLE_SHOPPING_COUNTRY,
language_code: Annotated[
Optional[str],
str | None,
"2-character language code to search for products on Google Shopping. E.g. 'en' (English). "
f"Defaults to '{DEFAULT_GOOGLE_SHOPPING_LANGUAGE or 'en'}'.",
] = DEFAULT_GOOGLE_SHOPPING_LANGUAGE,

View file

@ -1,4 +1,4 @@
from typing import Annotated, Any, Optional
from typing import Annotated, Any
from arcade.sdk import ToolContext
from arcade.sdk.errors import ToolExecutionError
@ -24,11 +24,11 @@ async def search_walmart_products(
f"Defaults to '{WalmartSortBy.RELEVANCE.value}'.",
] = WalmartSortBy.RELEVANCE,
min_price: Annotated[
Optional[float],
float | None,
"Minimum price to filter the results by. E.g. 100.00",
] = None,
max_price: Annotated[
Optional[float],
float | None,
"Maximum price to filter the results by. E.g. 100.00",
] = None,
next_day_delivery: Annotated[

View file

@ -1,4 +1,4 @@
from typing import Annotated, Any, Optional, cast
from typing import Annotated, Any, cast
from arcade.sdk import ToolContext, tool
from arcade.sdk.errors import ToolExecutionError
@ -24,17 +24,17 @@ async def search_youtube_videos(
"The keywords to search for. E.g. 'Python tutorial'.",
],
language_code: Annotated[
Optional[str],
str | None,
"2-character language code to search for. E.g. 'en' for English. "
f"Defaults to '{default_language_code(DEFAULT_YOUTUBE_SEARCH_LANGUAGE)}'.",
] = None,
country_code: Annotated[
Optional[str],
str | None,
"2-character country code to search for. E.g. 'us' for United States. "
f"Defaults to '{default_country_code(DEFAULT_YOUTUBE_SEARCH_COUNTRY)}'.",
] = None,
next_page_token: Annotated[
Optional[str],
str | None,
"The next page token to use for pagination. "
"Defaults to `None` (start from the first page).",
] = None,
@ -70,12 +70,12 @@ async def get_youtube_video_details(
"The ID of the YouTube video to get details about. E.g. 'dQw4w9WgXcQ'.",
],
language_code: Annotated[
Optional[str],
str | None,
"2-character language code to search for. E.g. 'en' for English. "
f"Defaults to '{default_language_code(DEFAULT_YOUTUBE_SEARCH_LANGUAGE)}'.",
] = None,
country_code: Annotated[
Optional[str],
str | None,
"2-character country code to search for. E.g. 'us' for United States. "
f"Defaults to '{default_country_code(DEFAULT_YOUTUBE_SEARCH_COUNTRY)}'.",
] = None,

View file

@ -1,7 +1,7 @@
import contextlib
import re
from datetime import datetime
from typing import Any, Optional, cast
from typing import Any, cast
from urllib.parse import parse_qs, urlparse
from zoneinfo import ZoneInfo
@ -71,7 +71,7 @@ def call_serpapi(context: ToolContext, params: dict) -> dict:
# ------------------------------------------------------------------------------------------------
# Google general utils
# ------------------------------------------------------------------------------------------------
def default_language_code(default_service_language_code: Optional[str] = None) -> Optional[str]:
def default_language_code(default_service_language_code: str | None = None) -> str | None:
if isinstance(default_service_language_code, str):
return default_service_language_code.lower()
elif isinstance(DEFAULT_GOOGLE_LANGUAGE, str):
@ -79,7 +79,7 @@ def default_language_code(default_service_language_code: Optional[str] = None) -
return None
def default_country_code(default_service_country_code: Optional[str] = None) -> Optional[str]:
def default_country_code(default_service_country_code: str | None = None) -> str | None:
if isinstance(default_service_country_code, str):
return default_service_country_code.lower()
elif isinstance(DEFAULT_GOOGLE_COUNTRY, str):
@ -88,9 +88,9 @@ def default_country_code(default_service_country_code: Optional[str] = None) ->
def resolve_language_code(
language_code: Optional[str] = None,
default_service_language_code: Optional[str] = None,
) -> Optional[str]:
language_code: str | None = None,
default_service_language_code: str | None = None,
) -> str | None:
language_code = language_code or default_language_code(default_service_language_code)
if isinstance(language_code, str):
@ -102,9 +102,9 @@ def resolve_language_code(
def resolve_country_code(
country_code: Optional[str] = None,
default_service_country_code: Optional[str] = None,
) -> Optional[str]:
country_code: str | None = None,
default_service_country_code: str | None = None,
) -> str | None:
country_code = country_code or default_country_code(default_service_country_code)
if isinstance(country_code, str):
@ -120,14 +120,14 @@ def resolve_country_code(
# ------------------------------------------------------------------------------------------------
def get_google_maps_directions(
context: ToolContext,
origin_address: Optional[str] = None,
destination_address: Optional[str] = None,
origin_latitude: Optional[str] = None,
origin_longitude: Optional[str] = None,
destination_latitude: Optional[str] = None,
destination_longitude: Optional[str] = None,
language: Optional[str] = DEFAULT_GOOGLE_MAPS_LANGUAGE,
country: Optional[str] = DEFAULT_GOOGLE_MAPS_COUNTRY,
origin_address: str | None = None,
destination_address: str | None = None,
origin_latitude: str | None = None,
origin_longitude: str | None = None,
destination_latitude: str | None = None,
destination_longitude: str | None = None,
language: str | None = DEFAULT_GOOGLE_MAPS_LANGUAGE,
country: str | None = DEFAULT_GOOGLE_MAPS_COUNTRY,
distance_unit: GoogleMapsDistanceUnit = DEFAULT_GOOGLE_MAPS_DISTANCE_UNIT,
travel_mode: GoogleMapsTravelMode = DEFAULT_GOOGLE_MAPS_TRAVEL_MODE,
) -> list[dict[str, Any]]:
@ -224,7 +224,7 @@ def clean_google_maps_direction(direction: dict[str, Any]) -> None:
del stop["data_id"]
def enrich_google_maps_arrive_around(timestamp: Optional[int]) -> dict[str, Any]:
def enrich_google_maps_arrive_around(timestamp: int | None) -> dict[str, Any]:
if not timestamp:
return {}
@ -258,9 +258,7 @@ def parse_flight_results(results: dict[str, Any]) -> dict[str, Any]:
# ------------------------------------------------------------------------------------------------
# Google News utils
# ------------------------------------------------------------------------------------------------
def extract_news_results(
results: dict[str, Any], limit: Optional[int] = None
) -> list[dict[str, Any]]:
def extract_news_results(results: dict[str, Any], limit: int | None = None) -> list[dict[str, Any]]:
news_results = []
for result in results.get("news_results", []):
news_results.append({
@ -375,7 +373,7 @@ def extract_walmart_variant_options(variant_swatches: list[dict[str, Any]]) -> l
# ------------------------------------------------------------------------------------------------
# YouTube utils
# ------------------------------------------------------------------------------------------------
def extract_video_id_from_link(link: Optional[str]) -> Optional[str]:
def extract_video_id_from_link(link: str | None) -> str | None:
if not isinstance(link, str):
return None
@ -387,7 +385,7 @@ def extract_video_id_from_link(link: Optional[str]) -> Optional[str]:
def extract_video_description(
video: dict[str, Any],
max_description_length: int = YOUTUBE_MAX_DESCRIPTION_LENGTH,
) -> Optional[str]:
) -> str | None:
description = video.get("description", "")
if isinstance(description, dict):
@ -401,7 +399,7 @@ def extract_video_description(
if description is not None:
description = str(description).strip()
return cast(Optional[str], description)
return cast(str | None, description)
def extract_video_results(

View file

@ -1,7 +1,7 @@
[tool.poetry]
name = "arcade_search"
version = "1.4.0"
description = "Tools for searching the web"
description = "Arcade.dev LLM tools for searching the web"
authors = ["Arcade <dev@arcade.dev>"]
[tool.poetry.dependencies]

View file

@ -1,4 +1,4 @@
target-version = "py39"
target-version = "py310"
line-length = 100
fix = true

View file

@ -7,7 +7,12 @@ help:
.PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed"
@if ! command -v poetry &> /dev/null; then \
@if ! command -v poetry >/dev/null 2>&1; then \
echo "📦 Poetry not found. Checking if pip is available"; \
if ! command -v pip >/dev/null 2>&1; then \
echo "❌ pip is not installed. Please install pip first."; \
exit 1; \
fi; \
echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \
else \

View file

@ -1,8 +1,8 @@
from typing import NewType, Optional, Union
from typing import NewType
class PositiveInt(int):
def __new__(cls, value: Union[str, int], name: str = "value") -> "PositiveInt":
def __new__(cls, value: str | int, name: str = "value") -> "PositiveInt":
def validate(val: int) -> int:
if val <= 0:
raise ValueError(f"{name} must be positive, got {val}")
@ -19,7 +19,7 @@ class PositiveInt(int):
SlackOffsetSecondsFromUTC = NewType("SlackOffsetSecondsFromUTC", int) # observe it can be negative
SlackPaginationNextCursor = Optional[str]
SlackPaginationNextCursor = str | None
SlackUserFieldId = NewType("SlackUserFieldId", str)
SlackUserId = NewType("SlackUserId", str)
SlackTeamId = NewType("SlackTeamId", str)

View file

@ -1,5 +1,5 @@
from enum import Enum
from typing import Literal, Optional, TypedDict
from typing import Literal, TypedDict
from arcade_slack.custom_types import (
SlackOffsetSecondsFromUTC,
@ -45,15 +45,15 @@ class SlackUserFieldData(TypedDict, total=False):
Slack type definition: https://api.slack.com/methods/users.profile.set#custom-profile
"""
value: Optional[str]
alt: Optional[bool]
value: str | None
alt: bool | None
class SlackStatusEmojiDisplayInfo(TypedDict, total=False):
"""Type definition for Slack status emoji display info dictionary."""
emoji_name: Optional[str]
display_url: Optional[str]
emoji_name: str | None
display_url: str | None
class SlackUserProfile(TypedDict, total=False):
@ -62,37 +62,37 @@ class SlackUserProfile(TypedDict, total=False):
Slack type definition: https://api.slack.com/types/user#profile (https://archive.is/RUZdL)
"""
title: Optional[str]
phone: Optional[str]
skype: Optional[str]
email: Optional[str]
real_name: Optional[str]
real_name_normalized: Optional[str]
display_name: Optional[str]
display_name_normalized: Optional[str]
first_name: Optional[str]
last_name: Optional[str]
fields: Optional[list[dict[SlackUserFieldId, SlackUserFieldData]]]
image_original: Optional[str]
is_custom_image: Optional[bool]
image_24: Optional[str]
image_32: Optional[str]
image_48: Optional[str]
image_72: Optional[str]
image_192: Optional[str]
image_512: Optional[str]
image_1024: Optional[str]
status_emoji: Optional[str]
status_emoji_display_info: Optional[list[SlackStatusEmojiDisplayInfo]]
status_text: Optional[str]
status_text_canonical: Optional[str]
status_expiration: Optional[int]
avatar_hash: Optional[str]
start_date: Optional[str]
pronouns: Optional[str]
huddle_state: Optional[str]
huddle_state_expiration: Optional[int]
team: Optional[SlackTeamId]
title: str | None
phone: str | None
skype: str | None
email: str | None
real_name: str | None
real_name_normalized: str | None
display_name: str | None
display_name_normalized: str | None
first_name: str | None
last_name: str | None
fields: list[dict[SlackUserFieldId, SlackUserFieldData]] | None
image_original: str | None
is_custom_image: bool | None
image_24: str | None
image_32: str | None
image_48: str | None
image_72: str | None
image_192: str | None
image_512: str | None
image_1024: str | None
status_emoji: str | None
status_emoji_display_info: list[SlackStatusEmojiDisplayInfo] | None
status_text: str | None
status_text_canonical: str | None
status_expiration: int | None
avatar_hash: str | None
start_date: str | None
pronouns: str | None
huddle_state: str | None
huddle_state_expiration: int | None
team: SlackTeamId | None
class SlackUser(TypedDict, total=False):
@ -103,23 +103,23 @@ class SlackUser(TypedDict, total=False):
id: SlackUserId
team_id: SlackTeamId
name: Optional[str]
deleted: Optional[bool]
color: Optional[str]
real_name: Optional[str]
tz: Optional[str]
tz_label: Optional[str]
tz_offset: Optional[SlackOffsetSecondsFromUTC]
profile: Optional[SlackUserProfile]
is_admin: Optional[bool]
is_owner: Optional[bool]
is_primary_owner: Optional[bool]
is_restricted: Optional[bool]
is_ultra_restricted: Optional[bool]
is_bot: Optional[bool]
is_app_user: Optional[bool]
is_email_confirmed: Optional[bool]
who_can_share_contact_card: Optional[str]
name: str | None
deleted: bool | None
color: str | None
real_name: str | None
tz: str | None
tz_label: str | None
tz_offset: SlackOffsetSecondsFromUTC | None
profile: SlackUserProfile | None
is_admin: bool | None
is_owner: bool | None
is_primary_owner: bool | None
is_restricted: bool | None
is_ultra_restricted: bool | None
is_bot: bool | None
is_app_user: bool | None
is_email_confirmed: bool | None
who_can_share_contact_card: str | None
class SlackUserList(TypedDict, total=False):
@ -131,25 +131,25 @@ class SlackUserList(TypedDict, total=False):
class SlackConversationPurpose(TypedDict, total=False):
"""Type definition for the Slack conversation purpose dictionary."""
value: Optional[str]
value: str | None
class SlackConversation(TypedDict, total=False):
"""Type definition for the Slack conversation dictionary."""
id: Optional[str]
name: Optional[str]
is_private: Optional[bool]
is_archived: Optional[bool]
is_member: Optional[bool]
is_channel: Optional[bool]
is_group: Optional[bool]
is_im: Optional[bool]
is_mpim: Optional[bool]
purpose: Optional[SlackConversationPurpose]
num_members: Optional[int]
user: Optional[SlackUser]
is_user_deleted: Optional[bool]
id: str | None
name: str | None
is_private: bool | None
is_archived: bool | None
is_member: bool | None
is_channel: bool | None
is_group: bool | None
is_im: bool | None
is_mpim: bool | None
purpose: SlackConversationPurpose | None
num_members: int | None
user: SlackUser | None
is_user_deleted: bool | None
class SlackMessage(TypedDict, total=True):
@ -175,28 +175,28 @@ class Message(SlackMessage, total=False):
class ConversationMetadata(TypedDict, total=False):
"""Type definition for the conversation metadata dictionary."""
id: Optional[str]
name: Optional[str]
conversation_type: Optional[str]
is_private: Optional[bool]
is_archived: Optional[bool]
is_member: Optional[bool]
purpose: Optional[str]
num_members: Optional[int]
user: Optional[SlackUser]
is_user_deleted: Optional[bool]
id: str | None
name: str | None
conversation_type: str | None
is_private: bool | None
is_archived: bool | None
is_member: bool | None
purpose: str | None
num_members: int | None
user: SlackUser | None
is_user_deleted: bool | None
class BasicUserInfo(TypedDict, total=False):
"""Type definition for the returned basic user info dictionary."""
id: Optional[str]
name: Optional[str]
is_bot: Optional[bool]
email: Optional[str]
display_name: Optional[str]
real_name: Optional[str]
timezone: Optional[str]
id: str | None
name: str | None
is_bot: bool | None
email: str | None
display_name: str | None
real_name: str | None
timezone: str | None
class SlackConversationsToolResponse(TypedDict, total=True):

View file

@ -1,6 +1,6 @@
import asyncio
from datetime import datetime, timezone
from typing import Annotated, Optional, cast
from typing import Annotated, cast
from arcade.sdk import ToolContext, tool
from arcade.sdk.auth import Slack
@ -149,8 +149,8 @@ async def send_message_to_channel(
async def get_members_in_conversation_by_id(
context: ToolContext,
conversation_id: Annotated[str, "The ID of the conversation to get members for"],
limit: Annotated[Optional[int], "The maximum number of members to return."] = None,
next_cursor: Annotated[Optional[str], "The cursor to use for pagination."] = None,
limit: Annotated[int | None, "The maximum number of members to return."] = None,
next_cursor: Annotated[str | None, "The cursor to use for pagination."] = None,
) -> Annotated[dict, "Information about each member in the conversation"]:
"""Get the members of a conversation in Slack by the conversation's ID."""
token = (
@ -209,8 +209,8 @@ async def get_members_in_conversation_by_id(
async def get_members_in_channel_by_name(
context: ToolContext,
channel_name: Annotated[str, "The name of the channel to get members for"],
limit: Annotated[Optional[int], "The maximum number of members to return."] = None,
next_cursor: Annotated[Optional[str], "The cursor to use for pagination."] = None,
limit: Annotated[int | None, "The maximum number of members to return."] = None,
next_cursor: Annotated[str | None, "The cursor to use for pagination."] = None,
) -> Annotated[dict, "The channel members' IDs and Names"]:
"""Get the members of a conversation in Slack by the conversation's name."""
channel = await get_channel_metadata_by_name(context=context, channel_name=channel_name)
@ -235,35 +235,35 @@ async def get_messages_in_conversation_by_id(
context: ToolContext,
conversation_id: Annotated[str, "The ID of the conversation to get history for"],
oldest_relative: Annotated[
Optional[str],
str | None,
(
"The oldest message to include in the results, specified as a time offset from the "
"current time in the format 'DD:HH:MM'"
),
] = None,
latest_relative: Annotated[
Optional[str],
str | None,
(
"The latest message to include in the results, specified as a time offset from the "
"current time in the format 'DD:HH:MM'"
),
] = None,
oldest_datetime: Annotated[
Optional[str],
str | None,
(
"The oldest message to include in the results, specified as a datetime object in the "
"format 'YYYY-MM-DD HH:MM:SS'"
),
] = None,
latest_datetime: Annotated[
Optional[str],
str | None,
(
"The latest message to include in the results, specified as a datetime object in the "
"format 'YYYY-MM-DD HH:MM:SS'"
),
] = None,
limit: Annotated[Optional[int], "The maximum number of messages to return."] = None,
next_cursor: Annotated[Optional[str], "The cursor to use for pagination."] = None,
limit: Annotated[int | None, "The maximum number of messages to return."] = None,
next_cursor: Annotated[str | None, "The cursor to use for pagination."] = None,
) -> Annotated[
dict,
(
@ -367,35 +367,35 @@ async def get_messages_in_channel_by_name(
context: ToolContext,
channel_name: Annotated[str, "The name of the channel"],
oldest_relative: Annotated[
Optional[str],
str | None,
(
"The oldest message to include in the results, specified as a time offset from the "
"current time in the format 'DD:HH:MM'"
),
] = None,
latest_relative: Annotated[
Optional[str],
str | None,
(
"The latest message to include in the results, specified as a time offset from the "
"current time in the format 'DD:HH:MM'"
),
] = None,
oldest_datetime: Annotated[
Optional[str],
str | None,
(
"The oldest message to include in the results, specified as a datetime object in the "
"format 'YYYY-MM-DD HH:MM:SS'"
),
] = None,
latest_datetime: Annotated[
Optional[str],
str | None,
(
"The latest message to include in the results, specified as a datetime object in the "
"format 'YYYY-MM-DD HH:MM:SS'"
),
] = None,
limit: Annotated[Optional[int], "The maximum number of messages to return."] = None,
next_cursor: Annotated[Optional[str], "The cursor to use for pagination."] = None,
limit: Annotated[int | None, "The maximum number of messages to return."] = None,
next_cursor: Annotated[str | None, "The cursor to use for pagination."] = None,
) -> Annotated[
dict,
(
@ -438,35 +438,35 @@ async def get_messages_in_direct_message_conversation_by_username(
context: ToolContext,
username: Annotated[str, "The username of the user to get messages from"],
oldest_relative: Annotated[
Optional[str],
str | None,
(
"The oldest message to include in the results, specified as a time offset from the "
"current time in the format 'DD:HH:MM'"
),
] = None,
latest_relative: Annotated[
Optional[str],
str | None,
(
"The latest message to include in the results, specified as a time offset from the "
"current time in the format 'DD:HH:MM'"
),
] = None,
oldest_datetime: Annotated[
Optional[str],
str | None,
(
"The oldest message to include in the results, specified as a datetime object in the "
"format 'YYYY-MM-DD HH:MM:SS'"
),
] = None,
latest_datetime: Annotated[
Optional[str],
str | None,
(
"The latest message to include in the results, specified as a datetime object in the "
"format 'YYYY-MM-DD HH:MM:SS'"
),
] = None,
limit: Annotated[Optional[int], "The maximum number of messages to return."] = None,
next_cursor: Annotated[Optional[str], "The cursor to use for pagination."] = None,
limit: Annotated[int | None, "The maximum number of messages to return."] = None,
next_cursor: Annotated[str | None, "The cursor to use for pagination."] = None,
) -> Annotated[
dict,
(
@ -511,35 +511,35 @@ async def get_messages_in_multi_person_dm_conversation_by_usernames(
context: ToolContext,
usernames: Annotated[list[str], "The usernames of the users to get messages from"],
oldest_relative: Annotated[
Optional[str],
str | None,
(
"The oldest message to include in the results, specified as a time offset from the "
"current time in the format 'DD:HH:MM'"
),
] = None,
latest_relative: Annotated[
Optional[str],
str | None,
(
"The latest message to include in the results, specified as a time offset from the "
"current time in the format 'DD:HH:MM'"
),
] = None,
oldest_datetime: Annotated[
Optional[str],
str | None,
(
"The oldest message to include in the results, specified as a datetime object in the "
"format 'YYYY-MM-DD HH:MM:SS'"
),
] = None,
latest_datetime: Annotated[
Optional[str],
str | None,
(
"The latest message to include in the results, specified as a datetime object in the "
"format 'YYYY-MM-DD HH:MM:SS'"
),
] = None,
limit: Annotated[Optional[int], "The maximum number of messages to return."] = None,
next_cursor: Annotated[Optional[str], "The cursor to use for pagination."] = None,
limit: Annotated[int | None, "The maximum number of messages to return."] = None,
next_cursor: Annotated[str | None, "The cursor to use for pagination."] = None,
) -> Annotated[
dict,
(
@ -629,7 +629,7 @@ async def get_channel_metadata_by_name(
context: ToolContext,
channel_name: Annotated[str, "The name of the channel to get metadata for"],
next_cursor: Annotated[
Optional[str],
str | None,
"The cursor to use for pagination, if continuing from a previous search.",
] = None,
) -> Annotated[dict, "The channel metadata"]:
@ -698,11 +698,11 @@ async def get_direct_message_conversation_metadata_by_username(
context: ToolContext,
username: Annotated[str, "The username of the user/person to get messages with"],
next_cursor: Annotated[
Optional[str],
str | None,
"The cursor to use for pagination, if continuing from a previous search.",
] = None,
) -> Annotated[
Optional[dict],
dict | None,
"The direct message conversation metadata.",
]:
"""Get the metadata of a direct message conversation in Slack by the username.
@ -750,11 +750,11 @@ async def get_multi_person_dm_conversation_metadata_by_usernames(
context: ToolContext,
usernames: Annotated[list[str], "The usernames of the users/people to get messages with"],
next_cursor: Annotated[
Optional[str],
str | None,
"The cursor to use for pagination, if continuing from a previous search.",
] = None,
) -> Annotated[
Optional[dict],
dict | None,
"The multi-person direct message conversation metadata.",
]:
"""Get the metadata of a multi-person direct message conversation in Slack by the usernames.
@ -818,11 +818,11 @@ async def get_multi_person_dm_conversation_metadata_by_usernames(
async def list_conversations_metadata(
context: ToolContext,
conversation_types: Annotated[
Optional[list[ConversationType]],
list[ConversationType] | None,
"The type(s) of conversations to list. Defaults to all types.",
] = None,
limit: Annotated[Optional[int], "The maximum number of conversations to list."] = None,
next_cursor: Annotated[Optional[str], "The cursor to use for pagination."] = None,
limit: Annotated[int | None, "The maximum number of conversations to list."] = None,
next_cursor: Annotated[str | None, "The cursor to use for pagination."] = None,
) -> Annotated[
dict,
(
@ -873,7 +873,7 @@ async def list_conversations_metadata(
)
async def list_public_channels_metadata(
context: ToolContext,
limit: Annotated[Optional[int], "The maximum number of channels to list."] = None,
limit: Annotated[int | None, "The maximum number of channels to list."] = None,
) -> Annotated[dict, "The public channels"]:
"""List metadata for public channels in Slack that the user is a member of."""
@ -891,7 +891,7 @@ async def list_public_channels_metadata(
)
async def list_private_channels_metadata(
context: ToolContext,
limit: Annotated[Optional[int], "The maximum number of channels to list."] = None,
limit: Annotated[int | None, "The maximum number of channels to list."] = None,
) -> Annotated[dict, "The private channels"]:
"""List metadata for private channels in Slack that the user is a member of."""
@ -909,7 +909,7 @@ async def list_private_channels_metadata(
)
async def list_group_direct_message_conversations_metadata(
context: ToolContext,
limit: Annotated[Optional[int], "The maximum number of conversations to list."] = None,
limit: Annotated[int | None, "The maximum number of conversations to list."] = None,
) -> Annotated[dict, "The group direct message conversations metadata"]:
"""List metadata for group direct message conversations that the user is a member of."""
@ -929,7 +929,7 @@ async def list_group_direct_message_conversations_metadata(
)
async def list_direct_message_conversations_metadata(
context: ToolContext,
limit: Annotated[Optional[int], "The maximum number of conversations to list."] = None,
limit: Annotated[int | None, "The maximum number of conversations to list."] = None,
) -> Annotated[dict, "The direct message conversations metadata"]:
"""List metadata for direct message conversations in Slack that the user is a member of."""

View file

@ -1,4 +1,4 @@
from typing import Annotated, Any, Optional, cast
from typing import Annotated, Any, cast
from arcade.sdk import ToolContext, tool
from arcade.sdk.auth import Slack
@ -59,9 +59,9 @@ async def get_user_info_by_id(
)
async def list_users(
context: ToolContext,
exclude_bots: Annotated[Optional[bool], "Whether to exclude bots from the results"] = True,
limit: Annotated[Optional[int], "The maximum number of users to return."] = None,
next_cursor: Annotated[Optional[str], "The next cursor token to use for pagination."] = None,
exclude_bots: Annotated[bool | None, "Whether to exclude bots from the results"] = True,
limit: Annotated[int | None, "The maximum number of users to return."] = None,
next_cursor: Annotated[str | None, "The next cursor token to use for pagination."] = None,
) -> Annotated[dict, "The users' info"]:
"""List all users in the authenticated user's Slack team."""

View file

@ -1,6 +1,7 @@
import asyncio
from collections.abc import Callable
from datetime import datetime, timezone
from typing import Any, Callable, Optional
from typing import Any
from arcade.sdk import ToolContext
from arcade.sdk.errors import RetryableToolError
@ -137,7 +138,7 @@ def extract_conversation_metadata(conversation: SlackConversation) -> Conversati
"""
conversation_type = get_slack_conversation_type_as_str(conversation)
purpose: Optional[SlackConversationPurpose] = conversation.get("purpose")
purpose: SlackConversationPurpose | None = conversation.get("purpose")
purpose_value = "" if not purpose else purpose.get("value", "")
metadata = ConversationMetadata(
@ -219,8 +220,8 @@ async def retrieve_conversations_by_user_ids(
conversation_types: list[ConversationType],
user_ids: list[str],
exact_match: bool = False,
limit: Optional[int] = None,
next_cursor: Optional[str] = None,
limit: int | None = None,
next_cursor: str | None = None,
) -> list[dict]:
"""
Retrieve conversations filtered by the given user IDs. Includes pagination support
@ -312,13 +313,13 @@ def is_user_deleted(user: SlackUser) -> bool:
async def async_paginate(
func: Callable,
response_key: Optional[str] = None,
limit: Optional[int] = None,
next_cursor: Optional[SlackPaginationNextCursor] = None,
response_key: str | None = None,
limit: int | None = None,
next_cursor: SlackPaginationNextCursor | None = None,
max_pagination_timeout_seconds: int = MAX_PAGINATION_TIMEOUT_SECONDS,
*args: Any,
**kwargs: Any,
) -> tuple[list, Optional[SlackPaginationNextCursor]]:
) -> tuple[list, SlackPaginationNextCursor | None]:
"""Paginate a Slack AsyncWebClient's method results.
The purpose is to abstract the pagination work and make it easier for the LLM to retrieve the
@ -425,7 +426,7 @@ def convert_datetime_to_unix_timestamp(datetime_str: str) -> int:
def convert_relative_datetime_to_unix_timestamp(
relative_datetime: str,
current_unix_timestamp: Optional[int] = None,
current_unix_timestamp: int | None = None,
) -> int:
"""Convert a relative datetime string in the format 'DD:HH:MM' to unix timestamp.

View file

@ -1,7 +1,7 @@
[tool.poetry]
name = "arcade_slack"
version = "0.4.2"
description = "Slack tools for LLMs"
description = "Arcade.dev LLM tools for Slack"
authors = ["Arcade <dev@arcade.dev>"]
[tool.poetry.dependencies]
@ -23,7 +23,7 @@ tox = "^4.11.1"
ruff = "^0.7.4"
[build-system]
requires = ["poetry-core>=1.0.0"]
requires = ["poetry-core>=1.0.0,<2.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.mypy]

View file

@ -1,4 +1,4 @@
target-version = "py39"
target-version = "py310"
line-length = 100
fix = true

View file

@ -7,7 +7,12 @@ help:
.PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed"
@if ! command -v poetry &> /dev/null; then \
@if ! command -v poetry >/dev/null 2>&1; then \
echo "📦 Poetry not found. Checking if pip is available"; \
if ! command -v pip >/dev/null 2>&1; then \
echo "❌ pip is not installed. Please install pip first."; \
exit 1; \
fi; \
echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \
else \

View file

@ -1,33 +1,32 @@
from dataclasses import asdict, dataclass, field
from enum import Enum
from typing import Optional
@dataclass
class PlaybackState:
is_playing: Optional[bool] = None
progress_ms: Optional[int] = (
is_playing: bool | None = None
progress_ms: int | None = (
None # Progress into the currently playing track or episode in milliseconds
)
device_name: Optional[str] = None
device_id: Optional[str] = None
currently_playing_type: Optional[str] = None
album_id: Optional[str] = None
album_name: Optional[str] = None
device_name: str | None = None
device_id: str | None = None
currently_playing_type: str | None = None
album_id: str | None = None
album_name: str | None = None
album_artists: list[str] = field(default_factory=list)
album_spotify_url: Optional[str] = None
track_id: Optional[str] = None
track_name: Optional[str] = None
track_spotify_url: Optional[str] = None
album_spotify_url: str | None = None
track_id: str | None = None
track_name: str | None = None
track_spotify_url: str | None = None
track_artists: list[str] = field(default_factory=list)
track_artists_ids: list[str] = field(default_factory=list)
show_name: Optional[str] = None
show_id: Optional[str] = None
show_spotify_url: Optional[str] = None
episode_name: Optional[str] = None
episode_id: Optional[str] = None
episode_spotify_url: Optional[str] = None
message: Optional[str] = None
show_name: str | None = None
show_id: str | None = None
show_spotify_url: str | None = None
episode_name: str | None = None
episode_id: str | None = None
episode_spotify_url: str | None = None
message: str | None = None
def to_dict(self) -> dict:
"""Convert the PlaybackState instance to a dictionary, excluding None values."""

View file

@ -1,4 +1,4 @@
from typing import Annotated, Optional
from typing import Annotated
import httpx
from arcade.sdk import ToolContext, tool
@ -20,10 +20,10 @@ from arcade_spotify.tools.utils import (
async def adjust_playback_position(
context: ToolContext,
absolute_position_ms: Annotated[
Optional[int], "The absolute position in milliseconds to seek to"
int | None, "The absolute position in milliseconds to seek to"
] = None,
relative_position_ms: Annotated[
Optional[int],
int | None,
"The relative position from the current playback position in milliseconds to seek to",
] = None,
) -> Annotated[str, "Success/failure message"]:
@ -173,7 +173,7 @@ async def start_tracks_playback_by_id(
"A list of Spotify track (song) IDs to play. Order of execution is not guarenteed.",
],
position_ms: Annotated[
Optional[int],
int | None,
"The position in milliseconds to start the first track from",
] = 0,
) -> Annotated[str, "Success/failure message"]:
@ -270,7 +270,7 @@ async def play_artist_by_name(
async def play_track_by_name(
context: ToolContext,
track_name: Annotated[str, "The name of the track to play"],
artist_name: Annotated[Optional[str], "The name of the artist of the track"] = None,
artist_name: Annotated[str | None, "The name of the artist of the track"] = None,
) -> Annotated[str, "Success/failure message"]:
"""Plays a song by name"""
q = f"track:{track_name}"

View file

@ -1,7 +1,7 @@
[tool.poetry]
name = "arcade_spotify"
version = "0.2.1"
description = "Arcade tools for Spotify"
description = "Arcade.dev LLM tools for Spotify"
authors = ["Arcade <dev@arcade.dev>"]
[tool.poetry.dependencies]
@ -20,7 +20,7 @@ tox = "^4.11.1"
ruff = "^0.7.4"
[build-system]
requires = ["poetry-core>=1.0.0"]
requires = ["poetry-core>=1.0.0,<2.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.mypy]

View file

@ -1,4 +1,4 @@
target-version = "py39"
target-version = "py310"
line-length = 100
fix = true

View file

@ -7,7 +7,12 @@ help:
.PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed"
@if ! command -v poetry &> /dev/null; then \
@if ! command -v poetry >/dev/null 2>&1; then \
echo "📦 Poetry not found. Checking if pip is available"; \
if ! command -v pip >/dev/null 2>&1; then \
echo "❌ pip is not installed. Please install pip first."; \
exit 1; \
fi; \
echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \
else \

View file

@ -1,4 +1,4 @@
from typing import Annotated, Any, Optional
from typing import Annotated, Any
from arcade.sdk import ToolContext, tool
from firecrawl import FirecrawlApp
@ -14,20 +14,20 @@ async def scrape_url(
context: ToolContext,
url: Annotated[str, "URL to scrape"],
formats: Annotated[
Optional[list[Formats]], "Formats to retrieve. Defaults to ['markdown']."
list[Formats] | None, "Formats to retrieve. Defaults to ['markdown']."
] = None,
only_main_content: Annotated[
Optional[bool],
bool | None,
"Only return the main content of the page excluding headers, navs, footers, etc.",
] = True,
include_tags: Annotated[list[str] | None, "List of tags to include in the output"] = None,
exclude_tags: Annotated[list[str] | None, "List of tags to exclude from the output"] = None,
wait_for: Annotated[
Optional[int],
int | None,
"Specify a delay in milliseconds before fetching the content, allowing the page "
"sufficient time to load.",
] = 10,
timeout: Annotated[Optional[int], "Timeout in milliseconds for the request"] = 30000,
timeout: Annotated[int | None, "Timeout in milliseconds for the request"] = 30000,
) -> Annotated[dict[str, Any], "Scraped data in specified formats"]:
"""Scrape a URL using Firecrawl and return the data in specified formats."""
@ -66,7 +66,7 @@ async def crawl_website(
] = False,
allow_external_links: Annotated[bool, "Allow following links to external websites"] = False,
webhook: Annotated[
Optional[str],
str | None,
"The URL to send a POST request to when the crawl is started, updated and completed.",
] = None,
async_crawl: Annotated[bool, "Run the crawl asynchronously"] = True,
@ -163,7 +163,7 @@ async def cancel_crawl(
async def map_website(
context: ToolContext,
url: Annotated[str, "The base URL to start crawling from"],
search: Annotated[Optional[str], "Search query to use for mapping"] = None,
search: Annotated[str | None, "Search query to use for mapping"] = None,
ignore_sitemap: Annotated[bool, "Ignore the website sitemap when crawling"] = True,
include_subdomains: Annotated[bool, "Include subdomains of the website"] = False,
limit: Annotated[int, "Maximum number of links to return"] = 5000,

View file

@ -1,7 +1,7 @@
[tool.poetry]
name = "arcade_web"
version = "1.0.1"
description = "LLM tools for web-related tasks"
description = "Arcade.dev LLM tools for web scraping related tasks"
authors = ["Arcade <dev@arcade.dev>"]
[tool.poetry.dependencies]
@ -20,7 +20,7 @@ tox = "^4.11.1"
ruff = "^0.7.4"
[build-system]
requires = ["poetry-core>=1.0.0"]
requires = ["poetry-core>=1.0.0,<2.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.mypy]

View file

@ -1,4 +1,4 @@
target-version = "py39"
target-version = "py310"
line-length = 100
fix = true

View file

@ -7,7 +7,12 @@ help:
.PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed"
@if ! command -v poetry &> /dev/null; then \
@if ! command -v poetry >/dev/null 2>&1; then \
echo "📦 Poetry not found. Checking if pip is available"; \
if ! command -v pip >/dev/null 2>&1; then \
echo "❌ pip is not installed. Please install pip first."; \
exit 1; \
fi; \
echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \
else \

View file

@ -1,4 +1,4 @@
from typing import Annotated, Any, Optional
from typing import Annotated, Any
import httpx
from arcade.sdk import ToolContext, tool
@ -67,7 +67,7 @@ async def search_recent_tweets_by_username(
int, "The maximum number of results to return. Must be in range [1, 100] inclusive"
] = 10,
next_token: Annotated[
Optional[str], "The pagination token starting from which to return results"
str | None, "The pagination token starting from which to return results"
] = None,
) -> Annotated[dict[str, Any], "Dictionary containing the search results"]:
"""Search for recent tweets (last 7 days) on X (Twitter) by username.
@ -121,7 +121,7 @@ async def search_recent_tweets_by_keywords(
int, "The maximum number of results to return. Must be in range [1, 100] inclusive"
] = 10,
next_token: Annotated[
Optional[str], "The pagination token starting from which to return results"
str | None, "The pagination token starting from which to return results"
] = None,
) -> Annotated[dict[str, Any], "Dictionary containing the search results"]:
"""

View file

@ -38,7 +38,9 @@ def parse_search_recent_tweets_response(response_data: dict[str, Any]) -> dict[s
tweet["tweet_url"] = get_tweet_url(tweet["id"])
# Add 'author_username' and 'author_name' to each tweet
for tweet_data, user_data in zip(response_data["data"], response_data["includes"]["users"]):
for tweet_data, user_data in zip(
response_data["data"], response_data["includes"]["users"], strict=False
):
tweet_data["author_username"] = user_data["username"]
tweet_data["author_name"] = user_data["name"]

View file

@ -1,7 +1,7 @@
[tool.poetry]
name = "arcade_x"
version = "0.1.12"
description = "LLM tools for interacting with X (Twitter)"
description = "Arcade.dev LLM tools for X (Twitter)"
authors = ["Arcade <dev@arcade.dev>"]
[tool.poetry.dependencies]
@ -20,7 +20,7 @@ tox = "^4.11.1"
ruff = "^0.7.4"
[build-system]
requires = ["poetry-core>=1.0.0"]
requires = ["poetry-core>=1.0.0,<2.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.mypy]

View file

@ -1,4 +1,4 @@
target-version = "py39"
target-version = "py310"
line-length = 100
fix = true

View file

@ -7,7 +7,12 @@ help:
.PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed"
@if ! command -v poetry &> /dev/null; then \
@if ! command -v poetry >/dev/null 2>&1; then \
echo "📦 Poetry not found. Checking if pip is available"; \
if ! command -v pip >/dev/null 2>&1; then \
echo "❌ pip is not installed. Please install pip first."; \
exit 1; \
fi; \
echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \
else \

View file

@ -1,4 +1,4 @@
from typing import Annotated, Optional
from typing import Annotated
from arcade.sdk import ToolContext, tool
from arcade.sdk.auth import Zoom
@ -14,7 +14,7 @@ from arcade_zoom.tools.utils import _handle_zoom_api_error, _send_zoom_request
async def list_upcoming_meetings(
context: ToolContext,
user_id: Annotated[
Optional[str],
str | None,
"The user's user ID or email address. Defaults to 'me' for the current user.",
] = "me",
) -> Annotated[dict, "List of upcoming meetings within the next 24 hours"]:

View file

@ -1,7 +1,7 @@
[tool.poetry]
name = "arcade_zoom"
version = "0.1.10"
description = "Arcade tools for Zoom"
description = "Arcade.dev LLM tools for Zoom"
authors = ["Arcade <dev@arcade.dev>"]
[tool.poetry.dependencies]
@ -20,7 +20,7 @@ tox = "^4.11.1"
ruff = "^0.7.4"
[build-system]
requires = ["poetry-core>=1.0.0"]
requires = ["poetry-core>=1.0.0,<2.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.mypy]