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 line-length = 100
fix = true fix = true

View file

@ -7,7 +7,12 @@ help:
.PHONY: install .PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed" @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"; \ echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \ pip install poetry==1.8.5; \
else \ else \

View file

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

View file

@ -3,7 +3,7 @@ VERSION ?= "0.1.0"
.PHONY: install .PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed" @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"; \ echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \ pip install poetry==1.8.5; \
else \ else \

View file

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

View file

@ -7,7 +7,12 @@ help:
.PHONY: install .PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed" @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"; \ echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \ pip install poetry==1.8.5; \
else \ else \

View file

@ -1,7 +1,7 @@
[tool.poetry] [tool.poetry]
name = "arcade_code_sandbox" name = "arcade_code_sandbox"
version = "1.0.0" 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>"] authors = ["Arcade <dev@arcade.dev>"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
@ -20,7 +20,7 @@ tox = "^4.11.1"
ruff = "^0.7.4" ruff = "^0.7.4"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0,<2.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.mypy] [tool.mypy]

View file

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

View file

@ -7,7 +7,12 @@ help:
.PHONY: install .PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed" @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"; \ echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \ pip install poetry==1.8.5; \
else \ else \

View file

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

View file

@ -1,5 +1,5 @@
import json import json
from typing import Annotated, Optional from typing import Annotated
import httpx import httpx
from arcade.sdk import ToolContext, tool 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.", "The name of the repository without the .git extension. The name is not case sensitive.",
], ],
title: Annotated[str, "The title of the issue."], title: Annotated[str, "The title of the issue."],
body: Annotated[Optional[str], "The contents of the issue."] = None, body: Annotated[str | None, "The contents of the issue."] = None,
assignees: Annotated[Optional[list[str]], "Logins for Users to assign to this issue."] = None, assignees: Annotated[list[str] | None, "Logins for Users to assign to this issue."] = None,
milestone: Annotated[ 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, ] = 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[ include_extra_data: Annotated[
bool, bool,
"If true, return all the data available about the pull requests. " "If true, return all the data available about the pull requests. "

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,12 @@ help:
.PHONY: install .PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed" @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"; \ echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \ pip install poetry==1.8.5; \
else \ else \

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import asyncio import asyncio
from typing import Annotated, Optional from typing import Annotated
from arcade.sdk import ToolContext, tool from arcade.sdk import ToolContext, tool
from arcade.sdk.auth import Google from arcade.sdk.auth import Google
@ -25,7 +25,7 @@ async def search_contacts_by_email(
context: ToolContext, context: ToolContext,
email: Annotated[str, "The email address to search for"], email: Annotated[str, "The email address to search for"],
limit: Annotated[ limit: Annotated[
Optional[int], int | None,
"The maximum number of contacts to return (30 is the max allowed by Google API)", "The maximum number of contacts to return (30 is the max allowed by Google API)",
] = DEFAULT_SEARCH_CONTACTS_LIMIT, ] = DEFAULT_SEARCH_CONTACTS_LIMIT,
) -> Annotated[dict, "A dictionary containing the list of matching contacts"]: ) -> Annotated[dict, "A dictionary containing the list of matching contacts"]:
@ -47,7 +47,7 @@ async def search_contacts_by_name(
context: ToolContext, context: ToolContext,
name: Annotated[str, "The full name to search for"], name: Annotated[str, "The full name to search for"],
limit: Annotated[ limit: Annotated[
Optional[int], int | None,
"The maximum number of contacts to return (30 is the max allowed by Google API)", "The maximum number of contacts to return (30 is the max allowed by Google API)",
] = DEFAULT_SEARCH_CONTACTS_LIMIT, ] = DEFAULT_SEARCH_CONTACTS_LIMIT,
) -> Annotated[dict, "A dictionary containing the list of matching contacts"]: ) -> Annotated[dict, "A dictionary containing the list of matching contacts"]:
@ -67,8 +67,8 @@ async def search_contacts_by_name(
async def create_contact( async def create_contact(
context: ToolContext, context: ToolContext,
given_name: Annotated[str, "The given name of the contact"], given_name: Annotated[str, "The given name of the contact"],
family_name: Annotated[Optional[str], "The optional family name of the contact"], family_name: Annotated[str | None, "The optional family name of the contact"],
email: Annotated[Optional[str], "The optional email address 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"]: ) -> Annotated[dict, "A dictionary containing the details of the created contact"]:
""" """
Create a new contact record in Google Contacts. 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 import ToolContext, tool
from arcade.sdk.auth import Google 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." bool, "Whether to include shared drives in the file tree structure. Defaults to False."
] = False, ] = False,
restrict_to_shared_drive_id: Annotated[ restrict_to_shared_drive_id: Annotated[
Optional[str], str | None,
"If provided, only include files from this shared drive in the file tree structure. " "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.", "Defaults to None, which will include files and folders from all drives.",
] = None, ] = None,
@ -38,11 +38,11 @@ async def get_file_tree_structure(
"account. Defaults to False.", "account. Defaults to False.",
] = False, ] = False,
order_by: Annotated[ order_by: Annotated[
Optional[list[OrderBy]], list[OrderBy] | None,
"Sort order. Defaults to listing the most recently modified documents first", "Sort order. Defaults to listing the most recently modified documents first",
] = None, ] = None,
limit: Annotated[ limit: Annotated[
Optional[int], int | None,
"The number of files and folders to list. Defaults to None, " "The number of files and folders to list. Defaults to None, "
"which will list all files and folders.", "which will list all files and folders.",
] = None, ] = None,
@ -121,17 +121,17 @@ async def get_file_tree_structure(
async def search_documents( async def search_documents(
context: ToolContext, context: ToolContext,
document_contains: Annotated[ 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 that must be in the document title or body. Provide a list of "
"keywords or phrases if needed.", "keywords or phrases if needed.",
] = None, ] = None,
document_not_contains: Annotated[ 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 that must NOT be in the document title or body. Provide a list of "
"keywords or phrases if needed.", "keywords or phrases if needed.",
] = None, ] = None,
search_only_in_shared_drive_id: Annotated[ 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 " "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.", "return documents from this drive. Defaults to None, which searches across all drives.",
] = None, ] = None,
@ -147,12 +147,12 @@ async def search_documents(
"account. Defaults to False.", "account. Defaults to False.",
] = False, ] = False,
order_by: Annotated[ order_by: Annotated[
Optional[list[OrderBy]], list[OrderBy] | None,
"Sort order. Defaults to listing the most recently modified documents first", "Sort order. Defaults to listing the most recently modified documents first",
] = None, ] = None,
limit: Annotated[int, "The number of documents to list"] = 50, limit: Annotated[int, "The number of documents to list"] = 50,
pagination_token: Annotated[ pagination_token: Annotated[
Optional[str], "The pagination token to continue a previous request" str | None, "The pagination token to continue a previous request"
] = None, ] = None,
) -> Annotated[ ) -> Annotated[
dict, dict,
@ -215,17 +215,17 @@ async def search_and_retrieve_documents(
"The format of the document to return. Defaults to Markdown.", "The format of the document to return. Defaults to Markdown.",
] = DocumentFormat.MARKDOWN, ] = DocumentFormat.MARKDOWN,
document_contains: Annotated[ 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 that must be in the document title or body. Provide a list of "
"keywords or phrases if needed.", "keywords or phrases if needed.",
] = None, ] = None,
document_not_contains: Annotated[ 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 that must NOT be in the document title or body. Provide a list of "
"keywords or phrases if needed.", "keywords or phrases if needed.",
] = None, ] = None,
search_only_in_shared_drive_id: Annotated[ 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 " "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.", "return documents from this drive. Defaults to None, which searches across all drives.",
] = None, ] = None,
@ -241,12 +241,12 @@ async def search_and_retrieve_documents(
"account. Defaults to False.", "account. Defaults to False.",
] = False, ] = False,
order_by: Annotated[ order_by: Annotated[
Optional[list[OrderBy]], list[OrderBy] | None,
"Sort order. Defaults to listing the most recently modified documents first", "Sort order. Defaults to listing the most recently modified documents first",
] = None, ] = None,
limit: Annotated[int, "The number of documents to list"] = 50, limit: Annotated[int, "The number of documents to list"] = 50,
pagination_token: Annotated[ pagination_token: Annotated[
Optional[str], "The pagination token to continue a previous request" str | None, "The pagination token to continue a previous request"
] = None, ] = None,
) -> Annotated[ ) -> Annotated[
dict, dict,

View file

@ -1,6 +1,6 @@
import base64 import base64
from email.mime.text import MIMEText 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 import ToolContext, tool
from arcade.sdk.auth import Google from arcade.sdk.auth import Google
@ -40,8 +40,8 @@ async def send_email(
subject: Annotated[str, "The subject of the email"], subject: Annotated[str, "The subject of the email"],
body: Annotated[str, "The body of the email"], body: Annotated[str, "The body of the email"],
recipient: Annotated[str, "The recipient of the email"], recipient: Annotated[str, "The recipient of the email"],
cc: Annotated[Optional[list[str]], "CC recipients of the email"] = None, cc: Annotated[list[str] | None, "CC recipients of the email"] = None,
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"]: ) -> Annotated[dict, "A dictionary containing the sent email details"]:
""" """
Send an email using the Gmail API. 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. " "Whether to reply to every recipient (including cc) or only to the original sender. "
f"Defaults to '{GMAIL_DEFAULT_REPLY_TO}'.", f"Defaults to '{GMAIL_DEFAULT_REPLY_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"]: ) -> Annotated[dict, "A dictionary containing the sent email details"]:
""" """
Send a reply to an email message. 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"], subject: Annotated[str, "The subject of the draft email"],
body: Annotated[str, "The body of the draft email"], body: Annotated[str, "The body of the draft email"],
recipient: Annotated[str, "The recipient 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, cc: Annotated[list[str] | None, "CC recipients of the draft email"] = None,
bcc: Annotated[Optional[list[str]], "BCC 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"]: ) -> Annotated[dict, "A dictionary containing the created draft email details"]:
""" """
Compose a new email draft using the Gmail API. 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. " "Whether to reply to every recipient (including cc) or only to the original sender. "
f"Defaults to '{GMAIL_DEFAULT_REPLY_TO}'.", f"Defaults to '{GMAIL_DEFAULT_REPLY_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"]: ) -> Annotated[dict, "A dictionary containing the created draft reply email details"]:
""" """
Compose a draft reply to an email message. 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"], subject: Annotated[str, "The subject of the draft email"],
body: Annotated[str, "The body of the draft email"], body: Annotated[str, "The body of the draft email"],
recipient: Annotated[str, "The recipient 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, cc: Annotated[list[str] | None, "CC recipients of the draft email"] = None,
bcc: Annotated[Optional[list[str]], "BCC 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"]: ) -> Annotated[dict, "A dictionary containing the updated draft email details"]:
""" """
Update an existing email draft using the Gmail API. Update an existing email draft using the Gmail API.
@ -374,12 +374,12 @@ async def list_draft_emails(
) )
async def list_emails_by_header( async def list_emails_by_header(
context: ToolContext, context: ToolContext,
sender: Annotated[Optional[str], "The name or email address of the sender of the email"] = None, sender: Annotated[str | None, "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, recipient: Annotated[str | None, "The name or email address of the recipient"] = None,
subject: Annotated[Optional[str], "Words to find in the subject of the email"] = None, subject: Annotated[str | None, "Words to find in the subject of the email"] = None,
body: Annotated[Optional[str], "Words to find in the body of the email"] = None, body: Annotated[str | None, "Words to find in the body of the email"] = None,
date_range: Annotated[Optional[DateRange], "The date range of the email"] = None, date_range: Annotated[DateRange | None, "The date range of the email"] = None,
label: Annotated[Optional[str], "The label name to filter by"] = None, label: Annotated[str | None, "The label name to filter by"] = None,
max_results: Annotated[int, "The maximum number of emails to return"] = 25, max_results: Annotated[int, "The maximum number of emails to return"] = 25,
) -> Annotated[ ) -> Annotated[
dict, "A dictionary containing a list of email details matching the search criteria" 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( async def search_threads(
context: ToolContext, context: ToolContext,
page_token: Annotated[ 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, ] = None,
max_results: Annotated[int, "The maximum number of threads to return"] = 10, 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, 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, label_ids: Annotated[list[str] | None, "The IDs of labels to filter by"] = None,
sender: Annotated[Optional[str], "The name or email address of the sender of the email"] = None, sender: Annotated[str | None, "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, recipient: Annotated[str | None, "The name or email address of the recipient"] = None,
subject: Annotated[Optional[str], "Words to find in the subject of the email"] = None, subject: Annotated[str | None, "Words to find in the subject of the email"] = None,
body: Annotated[Optional[str], "Words to find in the body of the email"] = None, body: Annotated[str | None, "Words to find in the body of the email"] = None,
date_range: Annotated[Optional[DateRange], "The date range 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"]: ) -> Annotated[dict, "A dictionary containing a list of thread details"]:
"""Search for threads in the user's mailbox""" """Search for threads in the user's mailbox"""
service = _build_gmail_service(context) service = _build_gmail_service(context)
@ -535,7 +535,7 @@ async def search_threads(
async def list_threads( async def list_threads(
context: ToolContext, context: ToolContext,
page_token: Annotated[ 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, ] = None,
max_results: Annotated[int, "The maximum number of threads to return"] = 10, 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, 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 import ToolContext, tool
from arcade.sdk.auth import Google from arcade.sdk.auth import Google
@ -27,7 +27,7 @@ def create_spreadsheet(
context: ToolContext, context: ToolContext,
title: Annotated[str, "The title of the new spreadsheet"] = "Untitled spreadsheet", title: Annotated[str, "The title of the new spreadsheet"] = "Untitled spreadsheet",
data: Annotated[ data: Annotated[
Optional[str], str | None,
"The data to write to the spreadsheet. A JSON string " "The data to write to the spreadsheet. A JSON string "
"(property names enclosed in double quotes) representing a dictionary that " "(property names enclosed in double quotes) representing a dictionary that "
"maps row numbers to dictionaries that map column letters to cell values. " "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.message import EmailMessage
from email.mime.text import MIMEText from email.mime.text import MIMEText
from enum import Enum from enum import Enum
from typing import Any, Optional, Union, cast from typing import Any, cast
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from arcade.sdk import ToolContext from arcade.sdk import ToolContext
@ -114,15 +114,15 @@ def build_email_message(
recipient: str, recipient: str,
subject: str, subject: str,
body: str, body: str,
cc: Optional[list[str]] = None, cc: list[str] | None = None,
bcc: Optional[list[str]] = None, bcc: list[str] | None = None,
replying_to: Optional[dict[str, Any]] = None, replying_to: dict[str, Any] | None = None,
action: GmailAction = GmailAction.SEND, action: GmailAction = GmailAction.SEND,
) -> dict[str, Any]: ) -> dict[str, Any]:
if replying_to: if replying_to:
body = build_reply_body(body, replying_to) body = build_reply_body(body, replying_to)
message: Union[EmailMessage, MIMEText] message: EmailMessage | MIMEText
if action == GmailAction.SEND: if action == GmailAction.SEND:
message = EmailMessage() 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. Only returns the plain text body.
Args: Args:
email_data (Dict[str, Any]): Raw email data from Gmail API. email_data (dict[str, Any]): Raw email data from Gmail API.
Returns: Returns:
Optional[Dict[str, str]]: Parsed email details or None if parsing fails. dict[str, str]: Parsed email details
""" """
payload = email_data.get("payload", {}) payload = email_data.get("payload", {})
headers = {d["name"].lower(): d["value"] for d in payload.get("headers", [])} 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. email_data (Dict[str, Any]): Raw email data from Gmail API.
Returns: Returns:
Optional[Dict[str, Any]]: Parsed email details or None if parsing fails. dict[str, Any]: Parsed email details
""" """
payload = email_data.get("payload", {}) 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. draft_email_data (Dict[str, Any]): Raw draft email data from Gmail API.
Returns: 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", {}) message = draft_email_data.get("message", {})
payload = message.get("payload", {}) payload = message.get("payload", {})
@ -342,7 +342,7 @@ def _build_gmail_service(context: ToolContext) -> Any:
return build("gmail", "v1", credentials=credentials) 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. 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. parts (List[Dict[str, Any]]): List of email parts.
Returns: 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: for part in parts:
mime_type = part.get("mimeType") mime_type = part.get("mimeType")
@ -367,7 +367,7 @@ def _extract_plain_body(parts: list) -> Optional[str]:
return _extract_html_body(parts) 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. 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. parts (List[Dict[str, Any]]): List of email parts.
Returns: 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: for part in parts:
mime_type = part.get("mimeType") mime_type = part.get("mimeType")
@ -393,7 +393,7 @@ def _extract_html_body(parts: list) -> Optional[str]:
return None 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. 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. payload (Dict[str, Any]): Email payload data.
Returns: 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 = [] images = []
for part in payload.get("parts", []): for part in payload.get("parts", []):
@ -423,7 +423,7 @@ def _get_email_images(payload: dict[str, Any]) -> Optional[list[str]]:
return None 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. 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. payload (Dict[str, Any]): Email payload data.
Returns: Returns:
Optional[str]: Decoded email body or None if not found. str | None: Decoded email body or None if not found.
""" """
# Direct body extraction # Direct body extraction
if "body" in payload and payload["body"].get("data"): 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", []))) 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. 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. payload (Dict[str, Any]): Email payload data.
Returns: Returns:
Optional[str]: Decoded email body or None if not found. str | None: Decoded email body or None if not found.
""" """
# Direct body extraction # Direct body extraction
if "body" in payload and payload["body"].get("data"): 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", [])) 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. 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 # 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. 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( def build_files_list_query(
mime_type: str, mime_type: str,
document_contains: Optional[list[str]] = None, document_contains: list[str] | None = None,
document_not_contains: Optional[list[str]] = None, document_not_contains: list[str] | None = None,
) -> str: ) -> str:
query = [f"(mimeType = '{mime_type}' and trashed = false)"] query = [f"(mimeType = '{mime_type}' and trashed = false)"]
@ -655,12 +655,12 @@ def build_files_list_params(
mime_type: str, mime_type: str,
page_size: int, page_size: int,
order_by: list[OrderBy], order_by: list[OrderBy],
pagination_token: Optional[str], pagination_token: str | None,
include_shared_drives: bool, 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, include_organization_domain_documents: bool,
document_contains: Optional[list[str]] = None, document_contains: list[str] | None = None,
document_not_contains: Optional[list[str]] = None, document_not_contains: list[str] | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
query = build_files_list_query( query = build_files_list_query(
mime_type=mime_type, mime_type=mime_type,
@ -696,11 +696,11 @@ def build_files_list_params(
def build_file_tree_request_params( def build_file_tree_request_params(
order_by: Optional[list[OrderBy]], order_by: list[OrderBy] | None,
page_token: Optional[str], page_token: str | None,
limit: Optional[int], limit: int | None,
include_shared_drives: bool, include_shared_drives: bool,
restrict_to_shared_drive_id: Optional[str], restrict_to_shared_drive_id: str | None,
include_organization_domain_documents: bool, include_organization_domain_documents: bool,
) -> dict[str, Any]: ) -> dict[str, Any]:
if order_by is None: if order_by is None:
@ -787,7 +787,7 @@ def build_file_tree(files: dict[str, Any]) -> dict[str, Any]:
# Docs utils # 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. Build a Drive service object.
""" """
@ -989,7 +989,7 @@ def get_now(tz: ZoneInfo | None = None) -> datetime:
# Contacts utils # 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. 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)) 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. 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. Build a Sheets service object.
""" """

View file

@ -1,7 +1,7 @@
[tool.poetry] [tool.poetry]
name = "arcade_google" name = "arcade_google"
version = "1.1.0" 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>"] authors = ["Arcade <dev@arcade.dev>"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
@ -26,7 +26,7 @@ tox = "^4.11.1"
ruff = "^0.7.4" ruff = "^0.7.4"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0,<2.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.mypy] [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) row_data = create_row_data(row_data, min_col_index, max_col_index)
assert len(row_data.values) == len(expected_row_data.values) 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 assert cell.userEnteredValue == expected.userEnteredValue
@ -268,7 +268,7 @@ def test_create_sheet_data():
create_cell_data("row2B"), create_cell_data("row2B"),
create_cell_data(200), 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 assert cell.userEnteredValue == expected.userEnteredValue
row3_cells = group1.rowData[1].values row3_cells = group1.rowData[1].values
@ -277,7 +277,7 @@ def test_create_sheet_data():
create_cell_data("row3B"), create_cell_data("row3B"),
CellData(userEnteredValue=CellExtendedValue(stringValue="")), 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 assert cell.userEnteredValue == expected.userEnteredValue
group2 = grid_data_list[1] group2 = grid_data_list[1]
@ -291,7 +291,7 @@ def test_create_sheet_data():
CellData(userEnteredValue=CellExtendedValue(stringValue="")), CellData(userEnteredValue=CellExtendedValue(stringValue="")),
create_cell_data("row5C"), 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 assert cell.userEnteredValue == expected.userEnteredValue

View file

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

View file

@ -7,7 +7,12 @@ help:
.PHONY: install .PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed" @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"; \ echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \ pip install poetry==1.8.5; \
else \ else \

View file

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

View file

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

View file

@ -7,7 +7,12 @@ help:
.PHONY: install .PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed" @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"; \ echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \ pip install poetry==1.8.5; \
else \ else \

View file

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

View file

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

View file

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

View file

@ -7,7 +7,12 @@ help:
.PHONY: install .PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed" @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"; \ echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \ pip install poetry==1.8.5; \
else \ else \

View file

@ -1,5 +1,5 @@
import asyncio import asyncio
from typing import Any, Optional from typing import Any
from arcade.sdk import ToolContext from arcade.sdk import ToolContext
@ -100,14 +100,14 @@ class BlockToMarkdownConverter:
return md return md
@staticmethod @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. """Apply formatting to a text string based on the annotations.
Used when converting rich text to markdown Used when converting rich text to markdown
Args: Args:
text (str): The text to format. text (str): The text to format.
annotations (dict[str, Any]): The annotations to apply to the text. 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: Returns:
str: The formatted text. str: The formatted text.

View file

@ -1,4 +1,4 @@
from typing import Annotated, Any, Optional from typing import Annotated, Any
import httpx import httpx
from arcade.sdk import ToolContext, tool 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 of an existing page/database within which the new page will be created. ",
], ],
title: Annotated[str, "Title of the new page"], 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"]: ) -> Annotated[str, "The ID of the new page"]:
"""Create a new Notion page by the title of the new page's parent.""" """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 # 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 import httpx
from arcade.sdk import ToolContext, tool from arcade.sdk import ToolContext, tool
@ -19,12 +19,12 @@ from arcade_notion_toolkit.utils import (
async def search_by_title( async def search_by_title(
context: ToolContext, context: ToolContext,
query: Annotated[ query: Annotated[
Optional[str], str | None,
"A substring to search for within page and database titles. " "A substring to search for within page and database titles. "
"If not provided (default), all pages and/or databases are returned.", "If not provided (default), all pages and/or databases are returned.",
] = None, ] = None,
select: Annotated[ select: Annotated[
Optional[ObjectType], ObjectType | None,
"Limit the results to either only pages or only databases. Defaults to both.", "Limit the results to either only pages or only databases. Defaults to both.",
] = None, ] = None,
order_by: Annotated[ order_by: Annotated[
@ -87,11 +87,11 @@ async def search_by_title(
async def get_object_metadata( async def get_object_metadata(
context: ToolContext, context: ToolContext,
object_title: Annotated[ 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, ] = 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[ object_type: Annotated[
Optional[ObjectType], ObjectType | None,
"The type of object to match title to. Only used if `object_title` is provided. " "The type of object to match title to. Only used if `object_title` is provided. "
"Defaults to both", "Defaults to both",
] = None, ] = None,

View file

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

View file

@ -1,7 +1,7 @@
[tool.poetry] [tool.poetry]
name = "arcade_notion_toolkit" name = "arcade_notion_toolkit"
version = "0.1.1" 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>"] authors = ["ArcadeAI <dev@arcade.dev>"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
@ -17,7 +17,7 @@ tox = "^4.11.1"
ruff = "^0.7.4" ruff = "^0.7.4"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0,<2.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.mypy] [tool.mypy]

View file

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

View file

@ -7,7 +7,12 @@ help:
.PHONY: install .PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed" @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"; \ echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \ pip install poetry==1.8.5; \
else \ 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 import ToolContext, tool
from arcade.sdk.auth import Reddit from arcade.sdk.auth import Reddit
@ -33,7 +33,7 @@ async def get_posts_in_subreddit(
), ),
] = SubredditListingType.HOT, ] = SubredditListingType.HOT,
limit: Annotated[int, "The maximum number of posts to fetch. Default is 10, max is 100."] = 10, 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[ time_range: Annotated[
RedditTimeFilter, RedditTimeFilter,
"The time range for filtering posts. Must be provided if the listing type is " "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 import ToolContext, tool
from arcade.sdk.auth import Reddit 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"], subreddit: Annotated[str, "The name of the subreddit to which the post will be submitted"],
title: Annotated[str, "The title of the submission"], title: Annotated[str, "The title of the submission"],
body: Annotated[ body: Annotated[
Optional[str], str | None,
"The body of the post in markdown format. Should never be the same as the title", "The body of the post in markdown format. Should never be the same as the title",
] = None, ] = None,
nsfw: Annotated[ nsfw: Annotated[
Optional[bool], bool | None,
"Indicates if the submission has content that is 'Not Safe For Work' (NSFW). " "Indicates if the submission has content that is 'Not Safe For Work' (NSFW). "
"Default is False", "Default is False",
] = False, ] = False,
spoiler: Annotated[ spoiler: Annotated[
Optional[bool], bool | None,
"Indicates if the post is marked as a spoiler. Default is False", "Indicates if the post is marked as a spoiler. Default is False",
] = False, ] = False,
send_replies: Annotated[ 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, ] = True,
) -> Annotated[dict, "Response from Reddit after submission"]: ) -> Annotated[dict, "Response from Reddit after submission"]:
"""Submit a text-based post to a subreddit""" """Submit a text-based post to a subreddit"""

View file

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

View file

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

View file

@ -7,7 +7,12 @@ help:
.PHONY: install .PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed" @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"; \ echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \ pip install poetry==1.8.5; \
else \ else \

View file

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

View file

@ -1,5 +1,4 @@
import json import json
from typing import Optional
from arcade.sdk.errors import RetryableToolError from arcade.sdk.errors import RetryableToolError
@ -11,7 +10,7 @@ class GoogleRetryableError(RetryableToolError):
class CountryNotFoundError(GoogleRetryableError): 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) valid_countries = json.dumps(COUNTRY_CODES, default=str)
message = f"Country not found: '{country}'." message = f"Country not found: '{country}'."
additional_message = f"Valid countries are: {valid_countries}" additional_message = f"Valid countries are: {valid_countries}"
@ -19,7 +18,7 @@ class CountryNotFoundError(GoogleRetryableError):
class LanguageNotFoundError(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) valid_languages = json.dumps(LANGUAGE_CODES, default=str)
message = f"Language not found: '{language}'." message = f"Language not found: '{language}'."
additional_message = f"Valid languages are: {valid_languages}" 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 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"], 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"], 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[ currency_code: Annotated[
Optional[str], "Currency of the returned prices. Defaults to 'USD'" str | None, "Currency of the returned prices. Defaults to 'USD'"
] = "USD", ] = "USD",
travel_class: Annotated[ travel_class: Annotated[
GoogleFlightsTravelClass, GoogleFlightsTravelClass,
"Travel class of the flight. Defaults to 'ECONOMY'", "Travel class of the flight. Defaults to 'ECONOMY'",
] = GoogleFlightsTravelClass.ECONOMY, ] = GoogleFlightsTravelClass.ECONOMY,
num_adults: Annotated[Optional[int], "Number of adult passengers. Defaults to 1"] = 1, num_adults: Annotated[int | None, "Number of adult passengers. Defaults to 1"] = 1,
num_children: Annotated[Optional[int], "Number of child passengers. Defaults to 0"] = 0, num_children: Annotated[int | None, "Number of child passengers. Defaults to 0"] = 0,
max_stops: Annotated[ max_stops: Annotated[
GoogleFlightsMaxStops, GoogleFlightsMaxStops,
"Maximum number of stops (layovers) for the flight. Defaults to any number of stops", "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"], 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"], outbound_date: Annotated[str, "Flight departure date in YYYY-MM-DD format"],
currency_code: Annotated[ currency_code: Annotated[
Optional[str], "Currency of the returned prices. Defaults to 'USD'" str | None, "Currency of the returned prices. Defaults to 'USD'"
] = "USD", ] = "USD",
travel_class: Annotated[ travel_class: Annotated[
GoogleFlightsTravelClass, GoogleFlightsTravelClass,
"Travel class of the flight. Defaults to 'ECONOMY'", "Travel class of the flight. Defaults to 'ECONOMY'",
] = GoogleFlightsTravelClass.ECONOMY, ] = GoogleFlightsTravelClass.ECONOMY,
num_adults: Annotated[Optional[int], "Number of adult passengers. Defaults to 1"] = 1, num_adults: Annotated[int | None, "Number of adult passengers. Defaults to 1"] = 1,
num_children: Annotated[Optional[int], "Number of child passengers. Defaults to 0"] = 0, num_children: Annotated[int | None, "Number of child passengers. Defaults to 0"] = 0,
max_stops: Annotated[ max_stops: Annotated[
GoogleFlightsMaxStops, GoogleFlightsMaxStops,
"Maximum number of stops (layovers) for the flight. Defaults to any number of stops", "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 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_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"], check_out_date: Annotated[str, "Check-out date in YYYY-MM-DD format"],
query: Annotated[ 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, ] = None,
currency: Annotated[Optional[str], "Currency code for prices. Defaults to 'USD'"] = "USD", currency: Annotated[str | None, "Currency code for prices. Defaults to 'USD'"] = "USD",
min_price: Annotated[Optional[int], "Minimum price per night. Defaults to no minimum"] = None, min_price: Annotated[int | None, "Minimum price per night. Defaults to no minimum"] = None,
max_price: Annotated[Optional[int], "Maximum price per night. Defaults to no maximum"] = None, max_price: Annotated[int | None, "Maximum price per night. Defaults to no maximum"] = None,
num_adults: Annotated[Optional[int], "Number of adults per room. Defaults to 2"] = 2, num_adults: Annotated[int | None, "Number of adults per room. Defaults to 2"] = 2,
num_children: Annotated[Optional[int], "Number of children per room. Defaults to 0"] = 0, num_children: Annotated[int | None, "Number of children per room. Defaults to 0"] = 0,
sort_by: Annotated[ sort_by: Annotated[
GoogleHotelsSortBy, "The sorting order of the results. Defaults to RELEVANCE" GoogleHotelsSortBy, "The sorting order of the results. Defaults to RELEVANCE"
] = GoogleHotelsSortBy.RELEVANCE, ] = GoogleHotelsSortBy.RELEVANCE,
num_results: Annotated[ 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, ] = 5,
) -> Annotated[dict[str, Any], "Hotel search results from the Google Hotels API"]: ) -> Annotated[dict[str, Any], "Hotel search results from the Google Hotels API"]:
"""Retrieve hotel search results using 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 from arcade.sdk import ToolContext, tool
@ -18,7 +18,7 @@ async def search_jobs(
"or 'data analyst at Apple'.", "or 'data analyst at Apple'.",
], ],
location: Annotated[ location: Annotated[
Optional[str], str | None,
"Location to search for jobs. E.g. 'United States' or 'New York, NY'. Defaults to None.", "Location to search for jobs. E.g. 'United States' or 'New York, NY'. Defaults to None.",
] = None, ] = None,
language: Annotated[ language: Annotated[
@ -31,7 +31,7 @@ async def search_jobs(
"Maximum number of results to retrieve. Defaults to 10 (max supported by the API).", "Maximum number of results to retrieve. Defaults to 10 (max supported by the API).",
] = 10, ] = 10,
next_page_token: Annotated[ next_page_token: Annotated[
Optional[str], str | None,
"Next page token to paginate results. Defaults to None (start from the first page).", "Next page token to paginate results. Defaults to None (start from the first page).",
] = None, ] = None,
) -> Annotated[dict, "Google Jobs results"]: ) -> 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 from arcade.sdk import ToolContext, tool
@ -27,7 +27,7 @@ async def get_directions_between_addresses(
f"Defaults to '{DEFAULT_GOOGLE_MAPS_LANGUAGE}'.", f"Defaults to '{DEFAULT_GOOGLE_MAPS_LANGUAGE}'.",
] = DEFAULT_GOOGLE_MAPS_LANGUAGE, ] = DEFAULT_GOOGLE_MAPS_LANGUAGE,
country: Annotated[ country: Annotated[
Optional[str], str | None,
"2-character country code to use in the Google Maps search. " "2-character country code to use in the Google Maps search. "
f"Defaults to '{DEFAULT_GOOGLE_MAPS_COUNTRY}'.", f"Defaults to '{DEFAULT_GOOGLE_MAPS_COUNTRY}'.",
] = 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}'.", f"Defaults to '{DEFAULT_GOOGLE_MAPS_LANGUAGE}'.",
] = DEFAULT_GOOGLE_MAPS_LANGUAGE, ] = DEFAULT_GOOGLE_MAPS_LANGUAGE,
country: Annotated[ country: Annotated[
Optional[str], str | None,
f"2-letter country code to use in the Google Maps search. Defaults to " f"2-letter country code to use in the Google Maps search. Defaults to "
f"'{DEFAULT_GOOGLE_MAPS_COUNTRY}'.", f"'{DEFAULT_GOOGLE_MAPS_COUNTRY}'.",
] = 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 import ToolContext, tool
from arcade.sdk.errors import ToolExecutionError 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'.", "Keywords to search for news articles. E.g. 'Apple launches new iPhone'.",
], ],
country_code: Annotated[ country_code: Annotated[
Optional[str], str | None,
"2-character country code to search for news articles. E.g. 'us' (United States). " "2-character country code to search for news articles. E.g. 'us' (United States). "
f"Defaults to '{DEFAULT_GOOGLE_NEWS_COUNTRY}'.", f"Defaults to '{DEFAULT_GOOGLE_NEWS_COUNTRY}'.",
] = None, ] = None,
@ -27,7 +27,7 @@ async def search_news_stories(
f"Defaults to '{DEFAULT_GOOGLE_NEWS_LANGUAGE}'.", f"Defaults to '{DEFAULT_GOOGLE_NEWS_LANGUAGE}'.",
] = DEFAULT_GOOGLE_NEWS_LANGUAGE, ] = DEFAULT_GOOGLE_NEWS_LANGUAGE,
limit: Annotated[ limit: Annotated[
Optional[int], int | None,
"Maximum number of news articles to return. Defaults to None " "Maximum number of news articles to return. Defaults to None "
"(returns all results found by the API).", "(returns all results found by the API).",
] = None, ] = 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 import ToolContext, tool
from arcade.sdk.errors import ToolExecutionError 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'.", "Keywords to search for products in Google Shopping. E.g. 'Apple iPhone'.",
], ],
country_code: Annotated[ country_code: Annotated[
Optional[str], str | None,
"2-character country code to search for products in Google Shopping. " "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'}'.", f"E.g. 'us' (United States). Defaults to '{DEFAULT_GOOGLE_SHOPPING_COUNTRY or 'us'}'.",
] = DEFAULT_GOOGLE_SHOPPING_COUNTRY, ] = DEFAULT_GOOGLE_SHOPPING_COUNTRY,
language_code: Annotated[ language_code: Annotated[
Optional[str], str | None,
"2-character language code to search for products on Google Shopping. E.g. 'en' (English). " "2-character language code to search for products on Google Shopping. E.g. 'en' (English). "
f"Defaults to '{DEFAULT_GOOGLE_SHOPPING_LANGUAGE or 'en'}'.", f"Defaults to '{DEFAULT_GOOGLE_SHOPPING_LANGUAGE or 'en'}'.",
] = DEFAULT_GOOGLE_SHOPPING_LANGUAGE, ] = 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 import ToolContext
from arcade.sdk.errors import ToolExecutionError from arcade.sdk.errors import ToolExecutionError
@ -24,11 +24,11 @@ async def search_walmart_products(
f"Defaults to '{WalmartSortBy.RELEVANCE.value}'.", f"Defaults to '{WalmartSortBy.RELEVANCE.value}'.",
] = WalmartSortBy.RELEVANCE, ] = WalmartSortBy.RELEVANCE,
min_price: Annotated[ min_price: Annotated[
Optional[float], float | None,
"Minimum price to filter the results by. E.g. 100.00", "Minimum price to filter the results by. E.g. 100.00",
] = None, ] = None,
max_price: Annotated[ max_price: Annotated[
Optional[float], float | None,
"Maximum price to filter the results by. E.g. 100.00", "Maximum price to filter the results by. E.g. 100.00",
] = None, ] = None,
next_day_delivery: Annotated[ 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 import ToolContext, tool
from arcade.sdk.errors import ToolExecutionError from arcade.sdk.errors import ToolExecutionError
@ -24,17 +24,17 @@ async def search_youtube_videos(
"The keywords to search for. E.g. 'Python tutorial'.", "The keywords to search for. E.g. 'Python tutorial'.",
], ],
language_code: Annotated[ language_code: Annotated[
Optional[str], str | None,
"2-character language code to search for. E.g. 'en' for English. " "2-character language code to search for. E.g. 'en' for English. "
f"Defaults to '{default_language_code(DEFAULT_YOUTUBE_SEARCH_LANGUAGE)}'.", f"Defaults to '{default_language_code(DEFAULT_YOUTUBE_SEARCH_LANGUAGE)}'.",
] = None, ] = None,
country_code: Annotated[ country_code: Annotated[
Optional[str], str | None,
"2-character country code to search for. E.g. 'us' for United States. " "2-character country code to search for. E.g. 'us' for United States. "
f"Defaults to '{default_country_code(DEFAULT_YOUTUBE_SEARCH_COUNTRY)}'.", f"Defaults to '{default_country_code(DEFAULT_YOUTUBE_SEARCH_COUNTRY)}'.",
] = None, ] = None,
next_page_token: Annotated[ next_page_token: Annotated[
Optional[str], str | None,
"The next page token to use for pagination. " "The next page token to use for pagination. "
"Defaults to `None` (start from the first page).", "Defaults to `None` (start from the first page).",
] = None, ] = None,
@ -70,12 +70,12 @@ async def get_youtube_video_details(
"The ID of the YouTube video to get details about. E.g. 'dQw4w9WgXcQ'.", "The ID of the YouTube video to get details about. E.g. 'dQw4w9WgXcQ'.",
], ],
language_code: Annotated[ language_code: Annotated[
Optional[str], str | None,
"2-character language code to search for. E.g. 'en' for English. " "2-character language code to search for. E.g. 'en' for English. "
f"Defaults to '{default_language_code(DEFAULT_YOUTUBE_SEARCH_LANGUAGE)}'.", f"Defaults to '{default_language_code(DEFAULT_YOUTUBE_SEARCH_LANGUAGE)}'.",
] = None, ] = None,
country_code: Annotated[ country_code: Annotated[
Optional[str], str | None,
"2-character country code to search for. E.g. 'us' for United States. " "2-character country code to search for. E.g. 'us' for United States. "
f"Defaults to '{default_country_code(DEFAULT_YOUTUBE_SEARCH_COUNTRY)}'.", f"Defaults to '{default_country_code(DEFAULT_YOUTUBE_SEARCH_COUNTRY)}'.",
] = None, ] = None,

View file

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

View file

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

View file

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

View file

@ -7,7 +7,12 @@ help:
.PHONY: install .PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed" @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"; \ echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \ pip install poetry==1.8.5; \
else \ else \

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
import asyncio import asyncio
from collections.abc import Callable
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, Callable, Optional from typing import Any
from arcade.sdk import ToolContext from arcade.sdk import ToolContext
from arcade.sdk.errors import RetryableToolError 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) 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", "") purpose_value = "" if not purpose else purpose.get("value", "")
metadata = ConversationMetadata( metadata = ConversationMetadata(
@ -219,8 +220,8 @@ async def retrieve_conversations_by_user_ids(
conversation_types: list[ConversationType], conversation_types: list[ConversationType],
user_ids: list[str], user_ids: list[str],
exact_match: bool = False, exact_match: bool = False,
limit: Optional[int] = None, limit: int | None = None,
next_cursor: Optional[str] = None, next_cursor: str | None = None,
) -> list[dict]: ) -> list[dict]:
""" """
Retrieve conversations filtered by the given user IDs. Includes pagination support 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( async def async_paginate(
func: Callable, func: Callable,
response_key: Optional[str] = None, response_key: str | None = None,
limit: Optional[int] = None, limit: int | None = None,
next_cursor: Optional[SlackPaginationNextCursor] = None, next_cursor: SlackPaginationNextCursor | None = None,
max_pagination_timeout_seconds: int = MAX_PAGINATION_TIMEOUT_SECONDS, max_pagination_timeout_seconds: int = MAX_PAGINATION_TIMEOUT_SECONDS,
*args: Any, *args: Any,
**kwargs: Any, **kwargs: Any,
) -> tuple[list, Optional[SlackPaginationNextCursor]]: ) -> tuple[list, SlackPaginationNextCursor | None]:
"""Paginate a Slack AsyncWebClient's method results. """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 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( def convert_relative_datetime_to_unix_timestamp(
relative_datetime: str, relative_datetime: str,
current_unix_timestamp: Optional[int] = None, current_unix_timestamp: int | None = None,
) -> int: ) -> int:
"""Convert a relative datetime string in the format 'DD:HH:MM' to unix timestamp. """Convert a relative datetime string in the format 'DD:HH:MM' to unix timestamp.

View file

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

View file

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

View file

@ -7,7 +7,12 @@ help:
.PHONY: install .PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed" @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"; \ echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \ pip install poetry==1.8.5; \
else \ else \

View file

@ -1,33 +1,32 @@
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from enum import Enum from enum import Enum
from typing import Optional
@dataclass @dataclass
class PlaybackState: class PlaybackState:
is_playing: Optional[bool] = None is_playing: bool | None = None
progress_ms: Optional[int] = ( progress_ms: int | None = (
None # Progress into the currently playing track or episode in milliseconds None # Progress into the currently playing track or episode in milliseconds
) )
device_name: Optional[str] = None device_name: str | None = None
device_id: Optional[str] = None device_id: str | None = None
currently_playing_type: Optional[str] = None currently_playing_type: str | None = None
album_id: Optional[str] = None album_id: str | None = None
album_name: Optional[str] = None album_name: str | None = None
album_artists: list[str] = field(default_factory=list) album_artists: list[str] = field(default_factory=list)
album_spotify_url: Optional[str] = None album_spotify_url: str | None = None
track_id: Optional[str] = None track_id: str | None = None
track_name: Optional[str] = None track_name: str | None = None
track_spotify_url: Optional[str] = None track_spotify_url: str | None = None
track_artists: list[str] = field(default_factory=list) track_artists: list[str] = field(default_factory=list)
track_artists_ids: list[str] = field(default_factory=list) track_artists_ids: list[str] = field(default_factory=list)
show_name: Optional[str] = None show_name: str | None = None
show_id: Optional[str] = None show_id: str | None = None
show_spotify_url: Optional[str] = None show_spotify_url: str | None = None
episode_name: Optional[str] = None episode_name: str | None = None
episode_id: Optional[str] = None episode_id: str | None = None
episode_spotify_url: Optional[str] = None episode_spotify_url: str | None = None
message: Optional[str] = None message: str | None = None
def to_dict(self) -> dict: def to_dict(self) -> dict:
"""Convert the PlaybackState instance to a dictionary, excluding None values.""" """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 import httpx
from arcade.sdk import ToolContext, tool from arcade.sdk import ToolContext, tool
@ -20,10 +20,10 @@ from arcade_spotify.tools.utils import (
async def adjust_playback_position( async def adjust_playback_position(
context: ToolContext, context: ToolContext,
absolute_position_ms: Annotated[ 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, ] = None,
relative_position_ms: Annotated[ relative_position_ms: Annotated[
Optional[int], int | None,
"The relative position from the current playback position in milliseconds to seek to", "The relative position from the current playback position in milliseconds to seek to",
] = None, ] = None,
) -> Annotated[str, "Success/failure message"]: ) -> 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.", "A list of Spotify track (song) IDs to play. Order of execution is not guarenteed.",
], ],
position_ms: Annotated[ position_ms: Annotated[
Optional[int], int | None,
"The position in milliseconds to start the first track from", "The position in milliseconds to start the first track from",
] = 0, ] = 0,
) -> Annotated[str, "Success/failure message"]: ) -> Annotated[str, "Success/failure message"]:
@ -270,7 +270,7 @@ async def play_artist_by_name(
async def play_track_by_name( async def play_track_by_name(
context: ToolContext, context: ToolContext,
track_name: Annotated[str, "The name of the track to play"], 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"]: ) -> Annotated[str, "Success/failure message"]:
"""Plays a song by name""" """Plays a song by name"""
q = f"track:{track_name}" q = f"track:{track_name}"

View file

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

View file

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

View file

@ -7,7 +7,12 @@ help:
.PHONY: install .PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed" @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"; \ echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \ pip install poetry==1.8.5; \
else \ 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 arcade.sdk import ToolContext, tool
from firecrawl import FirecrawlApp from firecrawl import FirecrawlApp
@ -14,20 +14,20 @@ async def scrape_url(
context: ToolContext, context: ToolContext,
url: Annotated[str, "URL to scrape"], url: Annotated[str, "URL to scrape"],
formats: Annotated[ formats: Annotated[
Optional[list[Formats]], "Formats to retrieve. Defaults to ['markdown']." list[Formats] | None, "Formats to retrieve. Defaults to ['markdown']."
] = None, ] = None,
only_main_content: Annotated[ only_main_content: Annotated[
Optional[bool], bool | None,
"Only return the main content of the page excluding headers, navs, footers, etc.", "Only return the main content of the page excluding headers, navs, footers, etc.",
] = True, ] = True,
include_tags: Annotated[list[str] | None, "List of tags to include in the output"] = None, 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, exclude_tags: Annotated[list[str] | None, "List of tags to exclude from the output"] = None,
wait_for: Annotated[ wait_for: Annotated[
Optional[int], int | None,
"Specify a delay in milliseconds before fetching the content, allowing the page " "Specify a delay in milliseconds before fetching the content, allowing the page "
"sufficient time to load.", "sufficient time to load.",
] = 10, ] = 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"]: ) -> Annotated[dict[str, Any], "Scraped data in specified formats"]:
"""Scrape a URL using Firecrawl and return the 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, ] = False,
allow_external_links: Annotated[bool, "Allow following links to external websites"] = False, allow_external_links: Annotated[bool, "Allow following links to external websites"] = False,
webhook: Annotated[ webhook: Annotated[
Optional[str], str | None,
"The URL to send a POST request to when the crawl is started, updated and completed.", "The URL to send a POST request to when the crawl is started, updated and completed.",
] = None, ] = None,
async_crawl: Annotated[bool, "Run the crawl asynchronously"] = True, async_crawl: Annotated[bool, "Run the crawl asynchronously"] = True,
@ -163,7 +163,7 @@ async def cancel_crawl(
async def map_website( async def map_website(
context: ToolContext, context: ToolContext,
url: Annotated[str, "The base URL to start crawling from"], 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, ignore_sitemap: Annotated[bool, "Ignore the website sitemap when crawling"] = True,
include_subdomains: Annotated[bool, "Include subdomains of the website"] = False, include_subdomains: Annotated[bool, "Include subdomains of the website"] = False,
limit: Annotated[int, "Maximum number of links to return"] = 5000, limit: Annotated[int, "Maximum number of links to return"] = 5000,

View file

@ -1,7 +1,7 @@
[tool.poetry] [tool.poetry]
name = "arcade_web" name = "arcade_web"
version = "1.0.1" 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>"] authors = ["Arcade <dev@arcade.dev>"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
@ -20,7 +20,7 @@ tox = "^4.11.1"
ruff = "^0.7.4" ruff = "^0.7.4"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0,<2.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.mypy] [tool.mypy]

View file

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

View file

@ -7,7 +7,12 @@ help:
.PHONY: install .PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed" @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"; \ echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \ pip install poetry==1.8.5; \
else \ else \

View file

@ -1,4 +1,4 @@
from typing import Annotated, Any, Optional from typing import Annotated, Any
import httpx import httpx
from arcade.sdk import ToolContext, tool 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" int, "The maximum number of results to return. Must be in range [1, 100] inclusive"
] = 10, ] = 10,
next_token: Annotated[ 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, ] = None,
) -> Annotated[dict[str, Any], "Dictionary containing the search results"]: ) -> Annotated[dict[str, Any], "Dictionary containing the search results"]:
"""Search for recent tweets (last 7 days) on X (Twitter) by username. """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" int, "The maximum number of results to return. Must be in range [1, 100] inclusive"
] = 10, ] = 10,
next_token: Annotated[ 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, ] = None,
) -> Annotated[dict[str, Any], "Dictionary containing the search results"]: ) -> 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"]) tweet["tweet_url"] = get_tweet_url(tweet["id"])
# Add 'author_username' and 'author_name' to each tweet # 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_username"] = user_data["username"]
tweet_data["author_name"] = user_data["name"] tweet_data["author_name"] = user_data["name"]

View file

@ -1,7 +1,7 @@
[tool.poetry] [tool.poetry]
name = "arcade_x" name = "arcade_x"
version = "0.1.12" 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>"] authors = ["Arcade <dev@arcade.dev>"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
@ -20,7 +20,7 @@ tox = "^4.11.1"
ruff = "^0.7.4" ruff = "^0.7.4"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0,<2.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.mypy] [tool.mypy]

View file

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

View file

@ -7,7 +7,12 @@ help:
.PHONY: install .PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks install: ## Install the poetry environment and install the pre-commit hooks
@echo "📦 Checking if Poetry is installed" @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"; \ echo "📦 Installing Poetry with pip"; \
pip install poetry==1.8.5; \ pip install poetry==1.8.5; \
else \ 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 import ToolContext, tool
from arcade.sdk.auth import Zoom 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( async def list_upcoming_meetings(
context: ToolContext, context: ToolContext,
user_id: Annotated[ user_id: Annotated[
Optional[str], str | None,
"The user's user ID or email address. Defaults to 'me' for the current user.", "The user's user ID or email address. Defaults to 'me' for the current user.",
] = "me", ] = "me",
) -> Annotated[dict, "List of upcoming meetings within the next 24 hours"]: ) -> Annotated[dict, "List of upcoming meetings within the next 24 hours"]:

View file

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