Add New Gmail Tools To The Google Toolkit (#41)
# PR Description ## Summary Changes include renaming the `arcade_gmail` toolkit to `arcade_google`, adding unit tests for Google toolkit, add new tools to the Google toolkit. ## Changes ### Makefile - Added a new `make test-toolkits` target to iterate over all toolkits and run pytest on each one. ### Added new tools for the google toolkit 1. `send_email` This tool sends an email using the Gmail API. 2. `write_draft_email` This tool creates a draft email using the Gmail API. 3. `update_draft_email` This tool updates an existing draft email using the Gmail API. 4. `send_draft_email` This tool sends a draft email using the Gmail API. 5. `delete_draft_email` This tool deletes a draft email using the Gmail API. 6. `list_draft_emails` This tool retrieves a list of draft emails using the Gmail API. 7. `list_emails_by_header` This tool searches for emails by a specific header using the Gmail API. - `sender`: The sender's email address to search for. - `limit`: The maximum number of emails to retrieve. 8. `list_emails` This tool retrieves a list of emails using the Gmail API. 9. `trash_email` This tool moves an email to the trash using the Gmail API.
This commit is contained in:
parent
6d854b0110
commit
43198a3a9b
19 changed files with 1374 additions and 483 deletions
14
.vscode/launch.json
vendored
14
.vscode/launch.json
vendored
|
|
@ -12,7 +12,7 @@
|
|||
"cwd": "${workspaceFolder}/examples/fastapi/arcade_example_fastapi"
|
||||
},
|
||||
{
|
||||
"name": "Debug arcade dev",
|
||||
"name": "Debug `arcade dev`",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/arcade/run_cli.py",
|
||||
|
|
@ -21,6 +21,18 @@
|
|||
"jinja": true,
|
||||
"justMyCode": true,
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
,
|
||||
{
|
||||
"name": "Debug `arcade chat -h localhost`",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/arcade/run_cli.py",
|
||||
"args": ["chat", "-h", "localhost"],
|
||||
"console": "integratedTerminal",
|
||||
"jinja": true,
|
||||
"justMyCode": true,
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
7
Makefile
7
Makefile
|
|
@ -20,6 +20,13 @@ test: ## Test the code with pytest
|
|||
@echo "🚀 Testing code: Running pytest"
|
||||
@cd arcade && poetry run pytest -v --cov --cov-config=pyproject.toml --cov-report=xml
|
||||
|
||||
.PHONY: test-toolkits
|
||||
test-toolkits: ## Iterate over all toolkits and run pytest on each one
|
||||
@echo "🚀 Testing code in toolkits: Running pytest"
|
||||
@for dir in toolkits/*/ ; do \
|
||||
(cd $$dir && poetry run pytest -v --cov --cov-config=pyproject.toml --cov-report=xml || exit 1); \
|
||||
done
|
||||
|
||||
.PHONY: build
|
||||
build: clean-build ## Build wheel file using poetry
|
||||
@echo "🚀 Creating wheel file"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import sys
|
|||
from arcade.cli.main import cli
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Support attaching debugger to cli
|
||||
mode = sys.argv[1] if len(sys.argv) > 1 else "dev"
|
||||
cli([mode])
|
||||
# Supports attaching debugger to cli. Run from ../.vscode/launch.json.
|
||||
if len(sys.argv) < 2:
|
||||
raise ValueError("At least one argument is required.")
|
||||
args = sys.argv[1:]
|
||||
cli(args)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
gmail
|
||||
google
|
||||
slack
|
||||
github
|
||||
websearch
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from arcade_github.tools import repo, user
|
||||
from arcade_gmail.tools import gmail
|
||||
from arcade_google.tools import gmail
|
||||
from arcade_slack.tools import chat
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
|
@ -20,9 +20,9 @@ actor = FastAPIActor(app)
|
|||
# actor.register_tool(arithmetic.multiply)
|
||||
# actor.register_tool(arithmetic.divide)
|
||||
# actor.register_tool(arithmetic.sqrt)
|
||||
actor.register_tool(gmail.get_emails)
|
||||
actor.register_tool(gmail.search_emails_by_header)
|
||||
actor.register_tool(gmail.write_draft)
|
||||
actor.register_tool(gmail.list_emails)
|
||||
actor.register_tool(gmail.list_emails_by_header)
|
||||
actor.register_tool(gmail.write_draft_email)
|
||||
actor.register_tool(repo.count_stargazers)
|
||||
actor.register_tool(repo.search_issues)
|
||||
actor.register_tool(user.set_starred)
|
||||
|
|
|
|||
352
examples/fastapi/poetry.lock
generated
352
examples/fastapi/poetry.lock
generated
|
|
@ -71,7 +71,7 @@ files = []
|
|||
develop = true
|
||||
|
||||
[package.dependencies]
|
||||
arcade-ai = {path = "../../arcade", develop = true}
|
||||
arcade-ai = "*"
|
||||
|
||||
[package.source]
|
||||
type = "directory"
|
||||
|
|
@ -87,7 +87,7 @@ files = []
|
|||
develop = true
|
||||
|
||||
[package.dependencies]
|
||||
arcade-ai = "^0.1.0"
|
||||
arcade-ai = "*"
|
||||
requests = "^2.32.3"
|
||||
|
||||
[package.source]
|
||||
|
|
@ -95,16 +95,17 @@ type = "directory"
|
|||
url = "../../toolkits/github"
|
||||
|
||||
[[package]]
|
||||
name = "arcade-gmail"
|
||||
name = "arcade-google"
|
||||
version = "0.1.0"
|
||||
description = "LLM tools for interating with gmail"
|
||||
description = "Arcade tools for the entire google suite"
|
||||
optional = false
|
||||
python-versions = "^3.10"
|
||||
files = []
|
||||
develop = true
|
||||
|
||||
[package.dependencies]
|
||||
arcade-ai = "^0.1.0"
|
||||
arcade-ai = "*"
|
||||
beautifulsoup4 = "^4.10.0"
|
||||
google-api-core = "2.19.1"
|
||||
google-api-python-client = "2.137.0"
|
||||
google-auth = "2.32.0"
|
||||
|
|
@ -114,7 +115,7 @@ googleapis-common-protos = "1.63.2"
|
|||
|
||||
[package.source]
|
||||
type = "directory"
|
||||
url = "../../toolkits/gmail"
|
||||
url = "../../toolkits/google"
|
||||
|
||||
[[package]]
|
||||
name = "arcade-slack"
|
||||
|
|
@ -133,6 +134,27 @@ slack-sdk = "^3.31.0"
|
|||
type = "directory"
|
||||
url = "../../toolkits/slack"
|
||||
|
||||
[[package]]
|
||||
name = "beautifulsoup4"
|
||||
version = "4.12.3"
|
||||
description = "Screen-scraping library"
|
||||
optional = false
|
||||
python-versions = ">=3.6.0"
|
||||
files = [
|
||||
{file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"},
|
||||
{file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
soupsieve = ">1.2"
|
||||
|
||||
[package.extras]
|
||||
cchardet = ["cchardet"]
|
||||
chardet = ["chardet"]
|
||||
charset-normalizer = ["charset-normalizer"]
|
||||
html5lib = ["html5lib"]
|
||||
lxml = ["lxml"]
|
||||
|
||||
[[package]]
|
||||
name = "cachetools"
|
||||
version = "5.5.0"
|
||||
|
|
@ -146,13 +168,13 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2024.7.4"
|
||||
version = "2024.8.30"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
|
||||
{file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
|
||||
{file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"},
|
||||
{file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -306,13 +328,13 @@ test = ["pytest (>=6)"]
|
|||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.112.1"
|
||||
version = "0.112.4"
|
||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "fastapi-0.112.1-py3-none-any.whl", hash = "sha256:bcbd45817fc2a1cd5da09af66815b84ec0d3d634eb173d1ab468ae3103e183e4"},
|
||||
{file = "fastapi-0.112.1.tar.gz", hash = "sha256:b2537146f8c23389a7faa8b03d0bd38d4986e6983874557d95eed2acc46448ef"},
|
||||
{file = "fastapi-0.112.4-py3-none-any.whl", hash = "sha256:6d4f9c3301825d4620665cace8e2bc34e303f61c05a5382d1d61a048ea7f2f37"},
|
||||
{file = "fastapi-0.112.4.tar.gz", hash = "sha256:b1f72e1f72afe7902ccd639ba320abb5d57a309804f45c10ab0ce3693cadeb33"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
|
@ -321,8 +343,8 @@ starlette = ">=0.37.2,<0.39.0"
|
|||
typing-extensions = ">=4.8.0"
|
||||
|
||||
[package.extras]
|
||||
all = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
||||
standard = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"]
|
||||
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
||||
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "google-api-core"
|
||||
|
|
@ -486,13 +508,13 @@ pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0
|
|||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.27.0"
|
||||
version = "0.27.2"
|
||||
description = "The next generation HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"},
|
||||
{file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"},
|
||||
{file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"},
|
||||
{file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
|
@ -507,18 +529,22 @@ brotli = ["brotli", "brotlicffi"]
|
|||
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.7"
|
||||
version = "3.10"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
|
||||
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
|
||||
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
|
||||
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "jiter"
|
||||
version = "0.5.0"
|
||||
|
|
@ -642,13 +668,13 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
|
|||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "1.42.0"
|
||||
version = "1.45.1"
|
||||
description = "The official Python library for the openai API"
|
||||
optional = false
|
||||
python-versions = ">=3.7.1"
|
||||
files = [
|
||||
{file = "openai-1.42.0-py3-none-any.whl", hash = "sha256:dc91e0307033a4f94931e5d03cc3b29b9717014ad5e73f9f2051b6cb5eda4d80"},
|
||||
{file = "openai-1.42.0.tar.gz", hash = "sha256:c9d31853b4e0bc2dc8bd08003b462a006035655a701471695d0bfdc08529cde3"},
|
||||
{file = "openai-1.45.1-py3-none-any.whl", hash = "sha256:4a6cce402aec803ae57ae7eff4b5b94bf6c0e1703a8d85541c27243c2adeadf8"},
|
||||
{file = "openai-1.45.1.tar.gz", hash = "sha256:f79e384916b219ab2f028bbf9c778e81291c61eb0645ccfa1828a4b18b55d534"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
|
@ -683,44 +709,44 @@ testing = ["google-api-core (>=1.31.5)"]
|
|||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "5.27.3"
|
||||
version = "5.28.1"
|
||||
description = ""
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "protobuf-5.27.3-cp310-abi3-win32.whl", hash = "sha256:dcb307cd4ef8fec0cf52cb9105a03d06fbb5275ce6d84a6ae33bc6cf84e0a07b"},
|
||||
{file = "protobuf-5.27.3-cp310-abi3-win_amd64.whl", hash = "sha256:16ddf3f8c6c41e1e803da7abea17b1793a97ef079a912e42351eabb19b2cffe7"},
|
||||
{file = "protobuf-5.27.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:68248c60d53f6168f565a8c76dc58ba4fa2ade31c2d1ebdae6d80f969cdc2d4f"},
|
||||
{file = "protobuf-5.27.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:b8a994fb3d1c11156e7d1e427186662b64694a62b55936b2b9348f0a7c6625ce"},
|
||||
{file = "protobuf-5.27.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:a55c48f2a2092d8e213bd143474df33a6ae751b781dd1d1f4d953c128a415b25"},
|
||||
{file = "protobuf-5.27.3-cp38-cp38-win32.whl", hash = "sha256:043853dcb55cc262bf2e116215ad43fa0859caab79bb0b2d31b708f128ece035"},
|
||||
{file = "protobuf-5.27.3-cp38-cp38-win_amd64.whl", hash = "sha256:c2a105c24f08b1e53d6c7ffe69cb09d0031512f0b72f812dd4005b8112dbe91e"},
|
||||
{file = "protobuf-5.27.3-cp39-cp39-win32.whl", hash = "sha256:c84eee2c71ed83704f1afbf1a85c3171eab0fd1ade3b399b3fad0884cbcca8bf"},
|
||||
{file = "protobuf-5.27.3-cp39-cp39-win_amd64.whl", hash = "sha256:af7c0b7cfbbb649ad26132e53faa348580f844d9ca46fd3ec7ca48a1ea5db8a1"},
|
||||
{file = "protobuf-5.27.3-py3-none-any.whl", hash = "sha256:8572c6533e544ebf6899c360e91d6bcbbee2549251643d32c52cf8a5de295ba5"},
|
||||
{file = "protobuf-5.27.3.tar.gz", hash = "sha256:82460903e640f2b7e34ee81a947fdaad89de796d324bcbc38ff5430bcdead82c"},
|
||||
{file = "protobuf-5.28.1-cp310-abi3-win32.whl", hash = "sha256:fc063acaf7a3d9ca13146fefb5b42ac94ab943ec6e978f543cd5637da2d57957"},
|
||||
{file = "protobuf-5.28.1-cp310-abi3-win_amd64.whl", hash = "sha256:4c7f5cb38c640919791c9f74ea80c5b82314c69a8409ea36f2599617d03989af"},
|
||||
{file = "protobuf-5.28.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4304e4fceb823d91699e924a1fdf95cde0e066f3b1c28edb665bda762ecde10f"},
|
||||
{file = "protobuf-5.28.1-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:0dfd86d2b5edf03d91ec2a7c15b4e950258150f14f9af5f51c17fa224ee1931f"},
|
||||
{file = "protobuf-5.28.1-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:51f09caab818707ab91cf09cc5c156026599cf05a4520779ccbf53c1b352fb25"},
|
||||
{file = "protobuf-5.28.1-cp38-cp38-win32.whl", hash = "sha256:1b04bde117a10ff9d906841a89ec326686c48ececeb65690f15b8cabe7149495"},
|
||||
{file = "protobuf-5.28.1-cp38-cp38-win_amd64.whl", hash = "sha256:cabfe43044ee319ad6832b2fda332646f9ef1636b0130186a3ae0a52fc264bb4"},
|
||||
{file = "protobuf-5.28.1-cp39-cp39-win32.whl", hash = "sha256:4b4b9a0562a35773ff47a3df823177ab71a1f5eb1ff56d8f842b7432ecfd7fd2"},
|
||||
{file = "protobuf-5.28.1-cp39-cp39-win_amd64.whl", hash = "sha256:f24e5d70e6af8ee9672ff605d5503491635f63d5db2fffb6472be78ba62efd8f"},
|
||||
{file = "protobuf-5.28.1-py3-none-any.whl", hash = "sha256:c529535e5c0effcf417682563719e5d8ac8d2b93de07a56108b4c2d436d7a29a"},
|
||||
{file = "protobuf-5.28.1.tar.gz", hash = "sha256:42597e938f83bb7f3e4b35f03aa45208d49ae8d5bcb4bc10b9fc825e0ab5e423"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"},
|
||||
{file = "pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c"},
|
||||
{file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"},
|
||||
{file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1-modules"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
description = "A collection of ASN.1-based protocols modules"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pyasn1_modules-0.4.0-py3-none-any.whl", hash = "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b"},
|
||||
{file = "pyasn1_modules-0.4.0.tar.gz", hash = "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6"},
|
||||
{file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"},
|
||||
{file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
|
@ -728,18 +754,18 @@ pyasn1 = ">=0.4.6,<0.7.0"
|
|||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.8.2"
|
||||
version = "2.9.1"
|
||||
description = "Data validation using Python type hints"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"},
|
||||
{file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"},
|
||||
{file = "pydantic-2.9.1-py3-none-any.whl", hash = "sha256:7aff4db5fdf3cf573d4b3c30926a510a10e19a0774d38fc4967f78beb6deb612"},
|
||||
{file = "pydantic-2.9.1.tar.gz", hash = "sha256:1363c7d975c7036df0db2b4a61f2e062fbc0aa5ab5f2772e0ffc7191a4f4bce2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
annotated-types = ">=0.4.0"
|
||||
pydantic-core = "2.20.1"
|
||||
annotated-types = ">=0.6.0"
|
||||
pydantic-core = "2.23.3"
|
||||
typing-extensions = [
|
||||
{version = ">=4.12.2", markers = "python_version >= \"3.13\""},
|
||||
{version = ">=4.6.1", markers = "python_version < \"3.13\""},
|
||||
|
|
@ -747,103 +773,104 @@ typing-extensions = [
|
|||
|
||||
[package.extras]
|
||||
email = ["email-validator (>=2.0.0)"]
|
||||
timezone = ["tzdata"]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.20.1"
|
||||
version = "2.23.3"
|
||||
description = "Core functionality for Pydantic validation and serialization"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"},
|
||||
{file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"},
|
||||
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"},
|
||||
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"},
|
||||
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"},
|
||||
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"},
|
||||
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"},
|
||||
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"},
|
||||
{file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"},
|
||||
{file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"},
|
||||
{file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"},
|
||||
{file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"},
|
||||
{file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"},
|
||||
{file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"},
|
||||
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"},
|
||||
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"},
|
||||
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"},
|
||||
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"},
|
||||
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"},
|
||||
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"},
|
||||
{file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"},
|
||||
{file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"},
|
||||
{file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"},
|
||||
{file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"},
|
||||
{file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"},
|
||||
{file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"},
|
||||
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"},
|
||||
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"},
|
||||
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"},
|
||||
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"},
|
||||
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"},
|
||||
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"},
|
||||
{file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"},
|
||||
{file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"},
|
||||
{file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"},
|
||||
{file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"},
|
||||
{file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"},
|
||||
{file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"},
|
||||
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"},
|
||||
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"},
|
||||
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"},
|
||||
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"},
|
||||
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"},
|
||||
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"},
|
||||
{file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"},
|
||||
{file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"},
|
||||
{file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"},
|
||||
{file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"},
|
||||
{file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"},
|
||||
{file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"},
|
||||
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"},
|
||||
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"},
|
||||
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"},
|
||||
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"},
|
||||
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"},
|
||||
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"},
|
||||
{file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"},
|
||||
{file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"},
|
||||
{file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"},
|
||||
{file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"},
|
||||
{file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"},
|
||||
{file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"},
|
||||
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"},
|
||||
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"},
|
||||
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"},
|
||||
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"},
|
||||
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"},
|
||||
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"},
|
||||
{file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"},
|
||||
{file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"},
|
||||
{file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"},
|
||||
{file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"},
|
||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"},
|
||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"},
|
||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"},
|
||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"},
|
||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"},
|
||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"},
|
||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"},
|
||||
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"},
|
||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"},
|
||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"},
|
||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"},
|
||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"},
|
||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"},
|
||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"},
|
||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"},
|
||||
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"},
|
||||
{file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"},
|
||||
{file = "pydantic_core-2.23.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7f10a5d1b9281392f1bf507d16ac720e78285dfd635b05737c3911637601bae6"},
|
||||
{file = "pydantic_core-2.23.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c09a7885dd33ee8c65266e5aa7fb7e2f23d49d8043f089989726391dd7350c5"},
|
||||
{file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6470b5a1ec4d1c2e9afe928c6cb37eb33381cab99292a708b8cb9aa89e62429b"},
|
||||
{file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9172d2088e27d9a185ea0a6c8cebe227a9139fd90295221d7d495944d2367700"},
|
||||
{file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86fc6c762ca7ac8fbbdff80d61b2c59fb6b7d144aa46e2d54d9e1b7b0e780e01"},
|
||||
{file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0cb80fd5c2df4898693aa841425ea1727b1b6d2167448253077d2a49003e0ed"},
|
||||
{file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03667cec5daf43ac4995cefa8aaf58f99de036204a37b889c24a80927b629cec"},
|
||||
{file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:047531242f8e9c2db733599f1c612925de095e93c9cc0e599e96cf536aaf56ba"},
|
||||
{file = "pydantic_core-2.23.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5499798317fff7f25dbef9347f4451b91ac2a4330c6669821c8202fd354c7bee"},
|
||||
{file = "pydantic_core-2.23.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bbb5e45eab7624440516ee3722a3044b83fff4c0372efe183fd6ba678ff681fe"},
|
||||
{file = "pydantic_core-2.23.3-cp310-none-win32.whl", hash = "sha256:8b5b3ed73abb147704a6e9f556d8c5cb078f8c095be4588e669d315e0d11893b"},
|
||||
{file = "pydantic_core-2.23.3-cp310-none-win_amd64.whl", hash = "sha256:2b603cde285322758a0279995b5796d64b63060bfbe214b50a3ca23b5cee3e83"},
|
||||
{file = "pydantic_core-2.23.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c889fd87e1f1bbeb877c2ee56b63bb297de4636661cc9bbfcf4b34e5e925bc27"},
|
||||
{file = "pydantic_core-2.23.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea85bda3189fb27503af4c45273735bcde3dd31c1ab17d11f37b04877859ef45"},
|
||||
{file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7f7f72f721223f33d3dc98a791666ebc6a91fa023ce63733709f4894a7dc611"},
|
||||
{file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b2b55b0448e9da68f56b696f313949cda1039e8ec7b5d294285335b53104b61"},
|
||||
{file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c24574c7e92e2c56379706b9a3f07c1e0c7f2f87a41b6ee86653100c4ce343e5"},
|
||||
{file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2b05e6ccbee333a8f4b8f4d7c244fdb7a979e90977ad9c51ea31261e2085ce0"},
|
||||
{file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2c409ce1c219c091e47cb03feb3c4ed8c2b8e004efc940da0166aaee8f9d6c8"},
|
||||
{file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d965e8b325f443ed3196db890d85dfebbb09f7384486a77461347f4adb1fa7f8"},
|
||||
{file = "pydantic_core-2.23.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f56af3a420fb1ffaf43ece3ea09c2d27c444e7c40dcb7c6e7cf57aae764f2b48"},
|
||||
{file = "pydantic_core-2.23.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5b01a078dd4f9a52494370af21aa52964e0a96d4862ac64ff7cea06e0f12d2c5"},
|
||||
{file = "pydantic_core-2.23.3-cp311-none-win32.whl", hash = "sha256:560e32f0df04ac69b3dd818f71339983f6d1f70eb99d4d1f8e9705fb6c34a5c1"},
|
||||
{file = "pydantic_core-2.23.3-cp311-none-win_amd64.whl", hash = "sha256:c744fa100fdea0d000d8bcddee95213d2de2e95b9c12be083370b2072333a0fa"},
|
||||
{file = "pydantic_core-2.23.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e0ec50663feedf64d21bad0809f5857bac1ce91deded203efc4a84b31b2e4305"},
|
||||
{file = "pydantic_core-2.23.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db6e6afcb95edbe6b357786684b71008499836e91f2a4a1e55b840955b341dbb"},
|
||||
{file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98ccd69edcf49f0875d86942f4418a4e83eb3047f20eb897bffa62a5d419c8fa"},
|
||||
{file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a678c1ac5c5ec5685af0133262103defb427114e62eafeda12f1357a12140162"},
|
||||
{file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01491d8b4d8db9f3391d93b0df60701e644ff0894352947f31fff3e52bd5c801"},
|
||||
{file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fcf31facf2796a2d3b7fe338fe8640aa0166e4e55b4cb108dbfd1058049bf4cb"},
|
||||
{file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7200fd561fb3be06827340da066df4311d0b6b8eb0c2116a110be5245dceb326"},
|
||||
{file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc1636770a809dee2bd44dd74b89cc80eb41172bcad8af75dd0bc182c2666d4c"},
|
||||
{file = "pydantic_core-2.23.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:67a5def279309f2e23014b608c4150b0c2d323bd7bccd27ff07b001c12c2415c"},
|
||||
{file = "pydantic_core-2.23.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:748bdf985014c6dd3e1e4cc3db90f1c3ecc7246ff5a3cd4ddab20c768b2f1dab"},
|
||||
{file = "pydantic_core-2.23.3-cp312-none-win32.whl", hash = "sha256:255ec6dcb899c115f1e2a64bc9ebc24cc0e3ab097775755244f77360d1f3c06c"},
|
||||
{file = "pydantic_core-2.23.3-cp312-none-win_amd64.whl", hash = "sha256:40b8441be16c1e940abebed83cd006ddb9e3737a279e339dbd6d31578b802f7b"},
|
||||
{file = "pydantic_core-2.23.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6daaf5b1ba1369a22c8b050b643250e3e5efc6a78366d323294aee54953a4d5f"},
|
||||
{file = "pydantic_core-2.23.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d015e63b985a78a3d4ccffd3bdf22b7c20b3bbd4b8227809b3e8e75bc37f9cb2"},
|
||||
{file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3fc572d9b5b5cfe13f8e8a6e26271d5d13f80173724b738557a8c7f3a8a3791"},
|
||||
{file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f6bd91345b5163ee7448bee201ed7dd601ca24f43f439109b0212e296eb5b423"},
|
||||
{file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc379c73fd66606628b866f661e8785088afe2adaba78e6bbe80796baf708a63"},
|
||||
{file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbdce4b47592f9e296e19ac31667daed8753c8367ebb34b9a9bd89dacaa299c9"},
|
||||
{file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3cf31edf405a161a0adad83246568647c54404739b614b1ff43dad2b02e6d5"},
|
||||
{file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e22b477bf90db71c156f89a55bfe4d25177b81fce4aa09294d9e805eec13855"},
|
||||
{file = "pydantic_core-2.23.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0a0137ddf462575d9bce863c4c95bac3493ba8e22f8c28ca94634b4a1d3e2bb4"},
|
||||
{file = "pydantic_core-2.23.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:203171e48946c3164fe7691fc349c79241ff8f28306abd4cad5f4f75ed80bc8d"},
|
||||
{file = "pydantic_core-2.23.3-cp313-none-win32.whl", hash = "sha256:76bdab0de4acb3f119c2a4bff740e0c7dc2e6de7692774620f7452ce11ca76c8"},
|
||||
{file = "pydantic_core-2.23.3-cp313-none-win_amd64.whl", hash = "sha256:37ba321ac2a46100c578a92e9a6aa33afe9ec99ffa084424291d84e456f490c1"},
|
||||
{file = "pydantic_core-2.23.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d063c6b9fed7d992bcbebfc9133f4c24b7a7f215d6b102f3e082b1117cddb72c"},
|
||||
{file = "pydantic_core-2.23.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6cb968da9a0746a0cf521b2b5ef25fc5a0bee9b9a1a8214e0a1cfaea5be7e8a4"},
|
||||
{file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edbefe079a520c5984e30e1f1f29325054b59534729c25b874a16a5048028d16"},
|
||||
{file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbaaf2ef20d282659093913da9d402108203f7cb5955020bd8d1ae5a2325d1c4"},
|
||||
{file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb539d7e5dc4aac345846f290cf504d2fd3c1be26ac4e8b5e4c2b688069ff4cf"},
|
||||
{file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e6f33503c5495059148cc486867e1d24ca35df5fc064686e631e314d959ad5b"},
|
||||
{file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04b07490bc2f6f2717b10c3969e1b830f5720b632f8ae2f3b8b1542394c47a8e"},
|
||||
{file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03795b9e8a5d7fda05f3873efc3f59105e2dcff14231680296b87b80bb327295"},
|
||||
{file = "pydantic_core-2.23.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c483dab0f14b8d3f0df0c6c18d70b21b086f74c87ab03c59250dbf6d3c89baba"},
|
||||
{file = "pydantic_core-2.23.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b2682038e255e94baf2c473dca914a7460069171ff5cdd4080be18ab8a7fd6e"},
|
||||
{file = "pydantic_core-2.23.3-cp38-none-win32.whl", hash = "sha256:f4a57db8966b3a1d1a350012839c6a0099f0898c56512dfade8a1fe5fb278710"},
|
||||
{file = "pydantic_core-2.23.3-cp38-none-win_amd64.whl", hash = "sha256:13dd45ba2561603681a2676ca56006d6dee94493f03d5cadc055d2055615c3ea"},
|
||||
{file = "pydantic_core-2.23.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:82da2f4703894134a9f000e24965df73cc103e31e8c31906cc1ee89fde72cbd8"},
|
||||
{file = "pydantic_core-2.23.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dd9be0a42de08f4b58a3cc73a123f124f65c24698b95a54c1543065baca8cf0e"},
|
||||
{file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89b731f25c80830c76fdb13705c68fef6a2b6dc494402987c7ea9584fe189f5d"},
|
||||
{file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6de1ec30c4bb94f3a69c9f5f2182baeda5b809f806676675e9ef6b8dc936f28"},
|
||||
{file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb68b41c3fa64587412b104294b9cbb027509dc2f6958446c502638d481525ef"},
|
||||
{file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c3980f2843de5184656aab58698011b42763ccba11c4a8c35936c8dd6c7068c"},
|
||||
{file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94f85614f2cba13f62c3c6481716e4adeae48e1eaa7e8bac379b9d177d93947a"},
|
||||
{file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:510b7fb0a86dc8f10a8bb43bd2f97beb63cffad1203071dc434dac26453955cd"},
|
||||
{file = "pydantic_core-2.23.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1eba2f7ce3e30ee2170410e2171867ea73dbd692433b81a93758ab2de6c64835"},
|
||||
{file = "pydantic_core-2.23.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b259fd8409ab84b4041b7b3f24dcc41e4696f180b775961ca8142b5b21d0e70"},
|
||||
{file = "pydantic_core-2.23.3-cp39-none-win32.whl", hash = "sha256:40d9bd259538dba2f40963286009bf7caf18b5112b19d2b55b09c14dde6db6a7"},
|
||||
{file = "pydantic_core-2.23.3-cp39-none-win_amd64.whl", hash = "sha256:5a8cd3074a98ee70173a8633ad3c10e00dcb991ecec57263aacb4095c5efb958"},
|
||||
{file = "pydantic_core-2.23.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f399e8657c67313476a121a6944311fab377085ca7f490648c9af97fc732732d"},
|
||||
{file = "pydantic_core-2.23.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:6b5547d098c76e1694ba85f05b595720d7c60d342f24d5aad32c3049131fa5c4"},
|
||||
{file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dda0290a6f608504882d9f7650975b4651ff91c85673341789a476b1159f211"},
|
||||
{file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b6e5da855e9c55a0c67f4db8a492bf13d8d3316a59999cfbaf98cc6e401961"},
|
||||
{file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:09e926397f392059ce0afdcac920df29d9c833256354d0c55f1584b0b70cf07e"},
|
||||
{file = "pydantic_core-2.23.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:87cfa0ed6b8c5bd6ae8b66de941cece179281239d482f363814d2b986b79cedc"},
|
||||
{file = "pydantic_core-2.23.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e61328920154b6a44d98cabcb709f10e8b74276bc709c9a513a8c37a18786cc4"},
|
||||
{file = "pydantic_core-2.23.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce3317d155628301d649fe5e16a99528d5680af4ec7aa70b90b8dacd2d725c9b"},
|
||||
{file = "pydantic_core-2.23.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e89513f014c6be0d17b00a9a7c81b1c426f4eb9224b15433f3d98c1a071f8433"},
|
||||
{file = "pydantic_core-2.23.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4f62c1c953d7ee375df5eb2e44ad50ce2f5aff931723b398b8bc6f0ac159791a"},
|
||||
{file = "pydantic_core-2.23.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2718443bc671c7ac331de4eef9b673063b10af32a0bb385019ad61dcf2cc8f6c"},
|
||||
{file = "pydantic_core-2.23.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d90e08b2727c5d01af1b5ef4121d2f0c99fbee692c762f4d9d0409c9da6541"},
|
||||
{file = "pydantic_core-2.23.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b676583fc459c64146debea14ba3af54e540b61762dfc0613dc4e98c3f66eeb"},
|
||||
{file = "pydantic_core-2.23.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:50e4661f3337977740fdbfbae084ae5693e505ca2b3130a6d4eb0f2281dc43b8"},
|
||||
{file = "pydantic_core-2.23.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:68f4cf373f0de6abfe599a38307f4417c1c867ca381c03df27c873a9069cda25"},
|
||||
{file = "pydantic_core-2.23.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:59d52cf01854cb26c46958552a21acb10dd78a52aa34c86f284e66b209db8cab"},
|
||||
{file = "pydantic_core-2.23.3.tar.gz", hash = "sha256:3cb0f65d8b4121c1b015c60104a685feb929a29d7cf204387c7f2688c7974690"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
|
@ -851,13 +878,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
|||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.4.0"
|
||||
version = "2.5.2"
|
||||
description = "Settings management using Pydantic"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pydantic_settings-2.4.0-py3-none-any.whl", hash = "sha256:bb6849dc067f1687574c12a639e231f3a6feeed0a12d710c1382045c5db1c315"},
|
||||
{file = "pydantic_settings-2.4.0.tar.gz", hash = "sha256:ed81c3a0f46392b4d7c0a565c05884e6e54b3456e6f0fe4d8814981172dc9a88"},
|
||||
{file = "pydantic_settings-2.5.2-py3-none-any.whl", hash = "sha256:2c912e55fd5794a59bf8c832b9de832dcfdf4778d79ff79b708744eed499a907"},
|
||||
{file = "pydantic_settings-2.5.2.tar.gz", hash = "sha256:f90b139682bee4d2065273d5185d71d37ea46cfe57e1b5ae184fc6a0b2484ca0"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
|
@ -902,13 +929,13 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
|
|||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.1.2"
|
||||
version = "3.1.4"
|
||||
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
|
||||
optional = false
|
||||
python-versions = ">=3.6.8"
|
||||
files = [
|
||||
{file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"},
|
||||
{file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"},
|
||||
{file = "pyparsing-3.1.4-py3-none-any.whl", hash = "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c"},
|
||||
{file = "pyparsing-3.1.4.tar.gz", hash = "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
|
|
@ -969,13 +996,13 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
|
|||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "13.7.1"
|
||||
version = "13.8.1"
|
||||
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||
optional = false
|
||||
python-versions = ">=3.7.0"
|
||||
files = [
|
||||
{file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"},
|
||||
{file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"},
|
||||
{file = "rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06"},
|
||||
{file = "rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
|
@ -1001,17 +1028,17 @@ pyasn1 = ">=0.1.3"
|
|||
|
||||
[[package]]
|
||||
name = "slack-sdk"
|
||||
version = "3.31.0"
|
||||
version = "3.32.0"
|
||||
description = "The Slack API Platform SDK for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "slack_sdk-3.31.0-py2.py3-none-any.whl", hash = "sha256:a120cc461e8ebb7d9175f171dbe0ded37a6878d9f7b96b28e4bad1227399047b"},
|
||||
{file = "slack_sdk-3.31.0.tar.gz", hash = "sha256:740d2f9c49cbfcbd46fca56b4be9d527934c225312aac18fd2c0fca0ef6bc935"},
|
||||
{file = "slack_sdk-3.32.0-py2.py3-none-any.whl", hash = "sha256:f35e85f2847e6c25cf7c2d1df206ca0ad75556263fb592457bf03cca68ef64bb"},
|
||||
{file = "slack_sdk-3.32.0.tar.gz", hash = "sha256:af8fc4ef1d1cbcecd28d01acf6955a3bb5b13d56f0a43a1b1c7e3b212cc5ec5b"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
optional = ["SQLAlchemy (>=1.4,<3)", "aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "websocket-client (>=1,<2)", "websockets (>=9.1,<13)"]
|
||||
optional = ["SQLAlchemy (>=1.4,<3)", "aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "websocket-client (>=1,<2)", "websockets (>=9.1,<14)"]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
|
|
@ -1024,15 +1051,26 @@ files = [
|
|||
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "soupsieve"
|
||||
version = "2.6"
|
||||
description = "A modern CSS selector implementation for Beautiful Soup."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"},
|
||||
{file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.38.2"
|
||||
version = "0.38.5"
|
||||
description = "The little ASGI library that shines."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "starlette-0.38.2-py3-none-any.whl", hash = "sha256:4ec6a59df6bbafdab5f567754481657f7ed90dc9d69b0c9ff017907dd54faeff"},
|
||||
{file = "starlette-0.38.2.tar.gz", hash = "sha256:c7c0441065252160993a1a37cf2a73bb64d271b17303e0b0c1eb7191cfb12d75"},
|
||||
{file = "starlette-0.38.5-py3-none-any.whl", hash = "sha256:632f420a9d13e3ee2a6f18f437b0a9f1faecb0bc42e1942aa2ea0e379a4c4206"},
|
||||
{file = "starlette-0.38.5.tar.gz", hash = "sha256:04a92830a9b6eb1442c766199d62260c3d4dc9c4f9188360626b1e0273cb7077"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
|
@ -1128,13 +1166,13 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.2.2"
|
||||
version = "2.2.3"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
|
||||
{file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
|
||||
{file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"},
|
||||
{file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
|
|
@ -1146,4 +1184,4 @@ zstd = ["zstandard (>=0.18.0)"]
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "b40d9fea27174b3ac18d83a936744819ed484e1baa2aaba5ab54ae9e4681c416"
|
||||
content-hash = "20a8af8467a60345749d6b0e2c516314d3392b6e304ab9d5da9edcf684aa004e"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ python = "^3.10"
|
|||
fastapi = "^0.112.0"
|
||||
arcade-ai = {path = "../../arcade", develop = true}
|
||||
arcade_arithmetic = {path = "../../toolkits/math", develop = true}
|
||||
arcade_gmail = {path = "../../toolkits/gmail", develop = true}
|
||||
arcade_google = {path = "../../toolkits/google", develop = true}
|
||||
arcade_github = {path = "../../toolkits/github", develop = true}
|
||||
arcade_slack = {path = "../../toolkits/slack", develop = true}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,214 +0,0 @@
|
|||
import base64
|
||||
import datetime
|
||||
from enum import Enum
|
||||
import json
|
||||
from email.mime.text import MIMEText
|
||||
from typing import Annotated, Optional
|
||||
from arcade.core.errors import ToolExecutionError
|
||||
from googleapiclient.errors import HttpError
|
||||
|
||||
from arcade_gmail.tools.utils import parse_email
|
||||
from google.oauth2.credentials import Credentials
|
||||
from googleapiclient.discovery import build
|
||||
|
||||
from arcade.core.schema import ToolContext
|
||||
from arcade.sdk import tool
|
||||
from arcade.sdk.auth import Google
|
||||
|
||||
|
||||
@tool(
|
||||
requires_auth=Google(
|
||||
scopes=["https://www.googleapis.com/auth/gmail.compose"],
|
||||
)
|
||||
)
|
||||
async def write_draft(
|
||||
context: ToolContext,
|
||||
subject: Annotated[str, "The subject of the email"],
|
||||
body: Annotated[str, "The body of the email"],
|
||||
recipient: Annotated[str, "The recipient of the email"],
|
||||
) -> Annotated[str, "The URL of the draft"]:
|
||||
"""Compose a new email draft."""
|
||||
|
||||
# Set up the Gmail API client
|
||||
service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
|
||||
|
||||
message = MIMEText(body)
|
||||
message["to"] = recipient
|
||||
message["subject"] = subject
|
||||
|
||||
# Encode the message in base64
|
||||
raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
|
||||
|
||||
# Create the draft
|
||||
draft = {"message": {"raw": raw_message}}
|
||||
|
||||
draft_message = service.users().drafts().create(userId="me", body=draft).execute()
|
||||
return f"Draft created: {get_draft_url(draft_message['id'])}"
|
||||
|
||||
|
||||
def get_draft_url(draft_id):
|
||||
return f"https://mail.google.com/mail/u/0/#drafts/{draft_id}"
|
||||
|
||||
|
||||
class DateRange(Enum):
|
||||
TODAY = "today"
|
||||
YESTERDAY = "yesterday"
|
||||
LAST_7_DAYS = "last_7_days"
|
||||
LAST_30_DAYS = "last_30_days"
|
||||
THIS_MONTH = "this_month"
|
||||
LAST_MONTH = "last_month"
|
||||
THIS_YEAR = "this_year"
|
||||
|
||||
def to_date_query(self):
|
||||
today = datetime.datetime.now()
|
||||
result = "after:"
|
||||
comparison_date = today
|
||||
|
||||
if self == DateRange.YESTERDAY:
|
||||
comparison_date = today - datetime.timedelta(days=1)
|
||||
elif self == DateRange.LAST_7_DAYS:
|
||||
comparison_date = today - datetime.timedelta(days=7)
|
||||
elif self == DateRange.LAST_30_DAYS:
|
||||
comparison_date = today - datetime.timedelta(days=30)
|
||||
elif self == DateRange.THIS_MONTH:
|
||||
comparison_date = today.replace(day=1)
|
||||
elif self == DateRange.LAST_MONTH:
|
||||
comparison_date = (
|
||||
today.replace(day=1) - datetime.timedelta(days=1)
|
||||
).replace(day=1)
|
||||
elif self == DateRange.THIS_YEAR:
|
||||
comparison_date = today.replace(month=1, day=1)
|
||||
elif self == DateRange.LAST_MONTH:
|
||||
comparison_date = (
|
||||
today.replace(month=1, day=1) - datetime.timedelta(days=1)
|
||||
).replace(month=1, day=1)
|
||||
|
||||
return result + comparison_date.strftime("%Y/%m/%d")
|
||||
|
||||
|
||||
@tool(
|
||||
requires_auth=Google(
|
||||
scopes=["https://www.googleapis.com/auth/gmail.readonly"],
|
||||
)
|
||||
)
|
||||
async def search_emails_by_header(
|
||||
context: ToolContext,
|
||||
sender: Annotated[
|
||||
Optional[str], "The name or email address of the sender of the email"
|
||||
] = None,
|
||||
recipient: Annotated[
|
||||
Optional[str], "The name or email address of the recipient"
|
||||
] = None,
|
||||
subject: Annotated[
|
||||
Optional[str], "Words to find in the subject of the email"
|
||||
] = None,
|
||||
body: Annotated[Optional[str], "Words to find in the body of the email"] = None,
|
||||
date_range: Annotated[Optional[DateRange], "The date range of the email"] = None,
|
||||
limit: Annotated[Optional[int], "The maximum number of emails to return"] = 25,
|
||||
) -> Annotated[str, "A list of email details in JSON format"]:
|
||||
"""Search for emails by header.
|
||||
One of the following MUST be provided: sender, recipient, subject, body."""
|
||||
|
||||
if not any([sender, recipient, subject, body]):
|
||||
raise ValueError(
|
||||
"At least one of sender, recipient, subject, or body must be provided."
|
||||
)
|
||||
|
||||
# Set up the Gmail API client
|
||||
service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
|
||||
|
||||
# Build the query string
|
||||
query = []
|
||||
if sender:
|
||||
query.append(f"from:{sender}")
|
||||
if recipient:
|
||||
query.append(f"to:{recipient}")
|
||||
if subject:
|
||||
query.append(f"subject:{subject}")
|
||||
if body:
|
||||
query.append(body)
|
||||
if date_range:
|
||||
query.append(date_range.to_date_query())
|
||||
|
||||
query_string = " ".join(query)
|
||||
|
||||
try:
|
||||
# Perform the search
|
||||
response = (
|
||||
service.users()
|
||||
.messages()
|
||||
.list(userId="me", q=query_string, maxResults=limit or 100)
|
||||
.execute()
|
||||
)
|
||||
messages = response.get("messages", [])
|
||||
|
||||
if not messages:
|
||||
return json.dumps({"emails": []})
|
||||
|
||||
emails = []
|
||||
for msg in messages:
|
||||
try:
|
||||
email_data = (
|
||||
service.users().messages().get(userId="me", id=msg["id"]).execute()
|
||||
)
|
||||
email_details = parse_email(email_data)
|
||||
if email_details:
|
||||
emails.append(email_details)
|
||||
except HttpError as e:
|
||||
print(f"Error reading email {msg['id']}: {e}")
|
||||
|
||||
return json.dumps({"emails": emails})
|
||||
|
||||
except HttpError as e:
|
||||
raise ToolExecutionError(
|
||||
"Error searching emails",
|
||||
developer_message=f"Gmail API Error: {e}",
|
||||
)
|
||||
|
||||
|
||||
@tool(
|
||||
requires_auth=Google(
|
||||
scopes=["https://www.googleapis.com/auth/gmail.readonly"],
|
||||
)
|
||||
)
|
||||
async def get_emails(
|
||||
context: ToolContext,
|
||||
n_emails: Annotated[int, "Number of emails to read"] = 5,
|
||||
) -> Annotated[str, "A list of email details in JSON format"]:
|
||||
"""
|
||||
Read emails from a Gmail account and extract plain text content.
|
||||
|
||||
Args:
|
||||
context (ToolContext): The context containing authorization information.
|
||||
n_emails (int): Number of emails to read (default: 5).
|
||||
|
||||
Returns:
|
||||
Dict[str, List[Dict[str, str]]]: A dictionary containing a list of email details.
|
||||
"""
|
||||
service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
|
||||
|
||||
try:
|
||||
messages = (
|
||||
service.users().messages().list(userId="me").execute().get("messages", [])
|
||||
)
|
||||
|
||||
if not messages:
|
||||
return {"emails": []}
|
||||
|
||||
emails = []
|
||||
for msg in messages[:n_emails]:
|
||||
try:
|
||||
email_data = (
|
||||
service.users().messages().get(userId="me", id=msg["id"]).execute()
|
||||
)
|
||||
email_details = parse_email(email_data)
|
||||
if email_details:
|
||||
emails.append(email_details)
|
||||
except Exception as e:
|
||||
print(f"Error reading email {msg['id']}: {e}")
|
||||
|
||||
return json.dumps({"emails": emails})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error reading emails: {e}")
|
||||
return "Error reading emails"
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
from base64 import urlsafe_b64decode
|
||||
import re
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
def parse_email(email_data: Dict[str, Any]) -> Optional[Dict[str, str]]:
|
||||
"""
|
||||
Parse email data and extract relevant information.
|
||||
|
||||
Args:
|
||||
email_data (Dict[str, Any]): Raw email data from Gmail API.
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, str]]: Parsed email details or None if parsing fails.
|
||||
"""
|
||||
try:
|
||||
payload = email_data["payload"]
|
||||
headers = {d["name"].lower(): d["value"] for d in payload["headers"]}
|
||||
|
||||
body_data = _get_email_body(payload)
|
||||
|
||||
return {
|
||||
"from": headers.get("from", ""),
|
||||
"date": headers.get("date", ""),
|
||||
"subject": headers.get("subject", "No subject"),
|
||||
"body": _clean_email_body(body_data) if body_data else "",
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error parsing email {email_data.get('id', 'unknown')}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _get_email_body(payload: Dict[str, Any]) -> Optional[str]:
|
||||
"""
|
||||
Extract email body from payload.
|
||||
|
||||
Args:
|
||||
payload (Dict[str, Any]): Email payload data.
|
||||
|
||||
Returns:
|
||||
Optional[str]: Decoded email body or None if not found.
|
||||
"""
|
||||
if "body" in payload and payload["body"].get("data"):
|
||||
return urlsafe_b64decode(payload["body"]["data"]).decode()
|
||||
|
||||
for part in payload.get("parts", []):
|
||||
if part.get("mimeType") == "text/plain" and "data" in part["body"]:
|
||||
return urlsafe_b64decode(part["body"]["data"]).decode()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _clean_email_body(body: str) -> str:
|
||||
"""
|
||||
Remove HTML tags and clean up email body text while preserving most content.
|
||||
|
||||
Args:
|
||||
body (str): The raw email body text.
|
||||
|
||||
Returns:
|
||||
str: Cleaned email body text.
|
||||
"""
|
||||
try:
|
||||
# Remove HTML tags using BeautifulSoup
|
||||
soup = BeautifulSoup(body, "html.parser")
|
||||
text = soup.get_text(separator=" ")
|
||||
|
||||
# Clean up the text
|
||||
text = _clean_text(text)
|
||||
|
||||
return text.strip()
|
||||
except Exception as e:
|
||||
print(f"Error cleaning email body: {e}")
|
||||
return body
|
||||
|
||||
|
||||
def _clean_text(text: str) -> str:
|
||||
"""
|
||||
Clean up the text while preserving most content.
|
||||
|
||||
Args:
|
||||
text (str): The input text.
|
||||
|
||||
Returns:
|
||||
str: Cleaned text.
|
||||
"""
|
||||
# Replace multiple newlines with a single newline
|
||||
text = re.sub(r"\n+", "\n", text)
|
||||
|
||||
# Replace multiple spaces with a single space
|
||||
text = re.sub(r"\s+", " ", text)
|
||||
|
||||
# Remove leading/trailing whitespace from each line
|
||||
text = "\n".join(line.strip() for line in text.split("\n"))
|
||||
|
||||
return text
|
||||
484
toolkits/google/arcade_google/tools/gmail.py
Normal file
484
toolkits/google/arcade_google/tools/gmail.py
Normal file
|
|
@ -0,0 +1,484 @@
|
|||
import base64
|
||||
import json
|
||||
from email.message import EmailMessage
|
||||
from email.mime.text import MIMEText
|
||||
from typing import Annotated, Optional
|
||||
from arcade.core.errors import ToolExecutionError, ToolInputError
|
||||
from googleapiclient.errors import HttpError
|
||||
|
||||
from arcade_google.tools.utils import (
|
||||
DateRange,
|
||||
parse_email,
|
||||
get_draft_url,
|
||||
get_sent_email_url,
|
||||
get_email_in_trash_url,
|
||||
parse_draft_email,
|
||||
)
|
||||
from google.oauth2.credentials import Credentials
|
||||
from googleapiclient.discovery import build
|
||||
|
||||
from arcade.core.schema import ToolContext
|
||||
from arcade.sdk import tool
|
||||
from arcade.sdk.auth import Google
|
||||
|
||||
|
||||
# Email sending tools
|
||||
@tool(
|
||||
requires_auth=Google(
|
||||
scopes=["https://www.googleapis.com/auth/gmail.send"],
|
||||
)
|
||||
)
|
||||
async def send_email(
|
||||
context: ToolContext,
|
||||
subject: Annotated[str, "The subject of the email"],
|
||||
body: Annotated[str, "The body of the email"],
|
||||
recipient: Annotated[str, "The recipient of the email"],
|
||||
cc: Annotated[Optional[list[str]], "CC recipients of the email"] = None,
|
||||
bcc: Annotated[Optional[list[str]], "BCC recipients of the email"] = None,
|
||||
) -> Annotated[str, "A confirmation message with the sent email ID and URL"]:
|
||||
"""
|
||||
Send an email using the Gmail API.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Set up the Gmail API client
|
||||
service = build(
|
||||
"gmail", "v1", credentials=Credentials(context.authorization.token)
|
||||
)
|
||||
|
||||
message = EmailMessage()
|
||||
message.set_content(body)
|
||||
message["To"] = recipient
|
||||
message["Subject"] = subject
|
||||
if cc:
|
||||
message["Cc"] = ", ".join(cc)
|
||||
if bcc:
|
||||
message["Bcc"] = ", ".join(bcc)
|
||||
|
||||
# Encode the message in base64
|
||||
encoded_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
|
||||
|
||||
# Create the email
|
||||
email = {"raw": encoded_message}
|
||||
|
||||
# Send the email
|
||||
sent_message = (
|
||||
service.users().messages().send(userId="me", body=email).execute()
|
||||
)
|
||||
return f"Email with ID {sent_message['id']} sent: {get_sent_email_url(sent_message['id'])}"
|
||||
except HttpError as e:
|
||||
raise ToolExecutionError(
|
||||
f"HttpError during execution of '{send_email.__name__}' tool.", str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
raise ToolExecutionError(
|
||||
f"Unexpected Error encountered during execution of '{send_email.__name__}' tool.",
|
||||
str(e),
|
||||
)
|
||||
|
||||
|
||||
@tool(
|
||||
requires_auth=Google(
|
||||
scopes=["https://www.googleapis.com/auth/gmail.send"],
|
||||
)
|
||||
)
|
||||
async def send_draft_email(
|
||||
context: ToolContext, id: Annotated[str, "The ID of the draft to send"]
|
||||
) -> Annotated[str, "A confirmation message with the sent email ID and URL"]:
|
||||
"""
|
||||
Send a draft email using the Gmail API.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Set up the Gmail API client
|
||||
service = build(
|
||||
"gmail", "v1", credentials=Credentials(context.authorization.token)
|
||||
)
|
||||
|
||||
# Send the draft email
|
||||
sent_message = (
|
||||
service.users().drafts().send(userId="me", body={"id": id}).execute()
|
||||
)
|
||||
|
||||
# Construct the URL to the sent email
|
||||
return f"Draft email with ID {sent_message['id']} sent: {get_sent_email_url(sent_message['id'])}"
|
||||
except HttpError as e:
|
||||
raise ToolExecutionError(
|
||||
f"HttpError during execution of '{send_draft_email.__name__}' tool.", str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
raise ToolExecutionError(
|
||||
f"Unexpected Error encountered during execution of '{send_draft_email.__name__}' tool.",
|
||||
str(e),
|
||||
)
|
||||
|
||||
|
||||
# Draft Management Tools
|
||||
@tool(
|
||||
requires_auth=Google(
|
||||
scopes=["https://www.googleapis.com/auth/gmail.compose"],
|
||||
)
|
||||
)
|
||||
async def write_draft_email(
|
||||
context: ToolContext,
|
||||
subject: Annotated[str, "The subject of the draft email"],
|
||||
body: Annotated[str, "The body of the draft email"],
|
||||
recipient: Annotated[str, "The recipient of the draft email"],
|
||||
cc: Annotated[Optional[list[str]], "CC recipients of the draft email"] = None,
|
||||
bcc: Annotated[Optional[list[str]], "BCC recipients of the draft email"] = None,
|
||||
) -> Annotated[str, "A confirmation message with the draft email ID and URL"]:
|
||||
"""
|
||||
Compose a new email draft using the Gmail API.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Set up the Gmail API client
|
||||
service = build(
|
||||
"gmail", "v1", credentials=Credentials(context.authorization.token)
|
||||
)
|
||||
|
||||
message = MIMEText(body)
|
||||
message["to"] = recipient
|
||||
message["subject"] = subject
|
||||
if cc:
|
||||
message["Cc"] = ", ".join(cc)
|
||||
if bcc:
|
||||
message["Bcc"] = ", ".join(bcc)
|
||||
|
||||
# Encode the message in base64
|
||||
raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
|
||||
|
||||
# Create the draft
|
||||
draft = {"message": {"raw": raw_message}}
|
||||
|
||||
draft_message = (
|
||||
service.users().drafts().create(userId="me", body=draft).execute()
|
||||
)
|
||||
return f"Draft email with ID {draft_message['id']} created: {get_draft_url(draft_message['id'])}"
|
||||
except HttpError as e:
|
||||
raise ToolExecutionError(
|
||||
f"HttpError during execution of '{write_draft_email.__name__}' tool.",
|
||||
str(e),
|
||||
)
|
||||
except Exception as e:
|
||||
raise ToolExecutionError(
|
||||
f"Unexpected Error encountered during execution of '{write_draft_email.__name__}' tool.",
|
||||
str(e),
|
||||
)
|
||||
|
||||
|
||||
@tool(
|
||||
requires_auth=Google(
|
||||
scopes=["https://www.googleapis.com/auth/gmail.compose"],
|
||||
)
|
||||
)
|
||||
async def update_draft_email(
|
||||
context: ToolContext,
|
||||
id: Annotated[str, "The ID of the draft email to update."],
|
||||
subject: Annotated[str, "The subject of the draft email"],
|
||||
body: Annotated[str, "The body of the draft email"],
|
||||
recipient: Annotated[str, "The recipient of the draft email"],
|
||||
cc: Annotated[Optional[list[str]], "CC recipients of the draft email"] = None,
|
||||
bcc: Annotated[Optional[list[str]], "BCC recipients of the draft email"] = None,
|
||||
) -> Annotated[str, "A confirmation message with the updated draft email ID and URL"]:
|
||||
"""
|
||||
Update an existing email draft using the Gmail API.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Set up the Gmail API client
|
||||
service = build(
|
||||
"gmail", "v1", credentials=Credentials(context.authorization.token)
|
||||
)
|
||||
|
||||
message = MIMEText(body)
|
||||
message["to"] = recipient
|
||||
message["subject"] = subject
|
||||
if cc:
|
||||
message["Cc"] = ", ".join(cc)
|
||||
if bcc:
|
||||
message["Bcc"] = ", ".join(bcc)
|
||||
|
||||
# Encode the message in base64
|
||||
raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
|
||||
|
||||
# Update the draft
|
||||
draft = {"id": id, "message": {"raw": raw_message}}
|
||||
|
||||
updated_draft_message = (
|
||||
service.users().drafts().update(userId="me", id=id, body=draft).execute()
|
||||
)
|
||||
return f"Draft email with ID {updated_draft_message['id']} updated: {get_draft_url(updated_draft_message['id'])}"
|
||||
except HttpError as e:
|
||||
raise ToolExecutionError(
|
||||
f"HttpError during execution of '{update_draft_email.__name__}' tool.",
|
||||
str(e),
|
||||
)
|
||||
except Exception as e:
|
||||
raise ToolExecutionError(
|
||||
f"Unexpected Error encountered during execution of '{update_draft_email.__name__}' tool.",
|
||||
str(e),
|
||||
)
|
||||
|
||||
|
||||
@tool(
|
||||
requires_auth=Google(
|
||||
scopes=["https://www.googleapis.com/auth/gmail.compose"],
|
||||
)
|
||||
)
|
||||
async def delete_draft_email(
|
||||
context: ToolContext,
|
||||
id: Annotated[str, "The ID of the draft email to delete"],
|
||||
) -> Annotated[str, "A confirmation message indicating successful deletion"]:
|
||||
"""
|
||||
Delete a draft email using the Gmail API.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Set up the Gmail API client
|
||||
service = build(
|
||||
"gmail", "v1", credentials=Credentials(context.authorization.token)
|
||||
)
|
||||
|
||||
# Delete the draft
|
||||
service.users().drafts().delete(userId="me", id=id).execute()
|
||||
return f"Draft email with ID {id} deleted successfully."
|
||||
except HttpError as e:
|
||||
raise ToolExecutionError(
|
||||
f"HttpError during execution of '{delete_draft_email.__name__}' tool.",
|
||||
str(e),
|
||||
)
|
||||
except Exception as e:
|
||||
raise ToolExecutionError(
|
||||
f"Unexpected Error encountered during execution of '{delete_draft_email.__name__}' tool.",
|
||||
str(e),
|
||||
)
|
||||
|
||||
|
||||
# Email Management Tools
|
||||
@tool(
|
||||
requires_auth=Google(
|
||||
scopes=["https://www.googleapis.com/auth/gmail.modify"],
|
||||
)
|
||||
)
|
||||
async def trash_email(
|
||||
context: ToolContext, id: Annotated[str, "The ID of the email to trash"]
|
||||
) -> Annotated[str, "A confirmation message with the trashed email ID and URL"]:
|
||||
"""
|
||||
Move an email to the trash folder using the Gmail API.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Set up the Gmail API client
|
||||
service = build(
|
||||
"gmail", "v1", credentials=Credentials(context.authorization.token)
|
||||
)
|
||||
|
||||
# Trash the email
|
||||
service.users().messages().trash(userId="me", id=id).execute()
|
||||
|
||||
return f"Email with ID {id} trashed successfully: {get_email_in_trash_url(id)}"
|
||||
except HttpError as e:
|
||||
raise ToolExecutionError(
|
||||
f"HttpError during execution of '{trash_email.__name__}' tool.", str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
raise ToolExecutionError(
|
||||
f"Unexpected Error encountered during execution of '{trash_email.__name__}' tool.",
|
||||
str(e),
|
||||
)
|
||||
|
||||
|
||||
# Draft Search Tools
|
||||
@tool(
|
||||
requires_auth=Google(
|
||||
scopes=["https://www.googleapis.com/auth/gmail.readonly"],
|
||||
)
|
||||
)
|
||||
async def list_draft_emails(
|
||||
context: ToolContext,
|
||||
n_drafts: Annotated[int, "Number of draft emails to read"] = 5,
|
||||
) -> Annotated[
|
||||
str, "A JSON string containing a list of draft email details and their IDs"
|
||||
]:
|
||||
"""
|
||||
Lists draft emails in the user's draft mailbox using the Gmail API.
|
||||
"""
|
||||
try:
|
||||
# Set up the Gmail API client
|
||||
service = build(
|
||||
"gmail", "v1", credentials=Credentials(context.authorization.token)
|
||||
)
|
||||
|
||||
listed_drafts = service.users().drafts().list(userId="me").execute()
|
||||
|
||||
if not listed_drafts:
|
||||
return {"emails": []}
|
||||
|
||||
draft_ids = [draft["id"] for draft in listed_drafts.get("drafts", [])][
|
||||
:n_drafts
|
||||
]
|
||||
|
||||
emails = []
|
||||
for draft_id in draft_ids:
|
||||
try:
|
||||
draft_data = (
|
||||
service.users().drafts().get(userId="me", id=draft_id).execute()
|
||||
)
|
||||
draft_details = parse_draft_email(draft_data)
|
||||
if draft_details:
|
||||
emails.append(draft_details)
|
||||
except Exception as e:
|
||||
print(f"Error reading draft email {draft_id}: {e}")
|
||||
|
||||
return json.dumps({"emails": emails})
|
||||
except HttpError as e:
|
||||
raise ToolExecutionError(
|
||||
f"HttpError during execution of '{list_draft_emails.__name__}' tool.",
|
||||
str(e),
|
||||
)
|
||||
except Exception as e:
|
||||
raise ToolExecutionError(
|
||||
f"Unexpected Error encountered during execution of '{list_draft_emails.__name__}' tool.",
|
||||
str(e),
|
||||
)
|
||||
|
||||
|
||||
# Email Search Tools
|
||||
@tool(
|
||||
requires_auth=Google(
|
||||
scopes=["https://www.googleapis.com/auth/gmail.readonly"],
|
||||
)
|
||||
)
|
||||
async def list_emails_by_header(
|
||||
context: ToolContext,
|
||||
sender: Annotated[
|
||||
Optional[str], "The name or email address of the sender of the email"
|
||||
] = None,
|
||||
recipient: Annotated[
|
||||
Optional[str], "The name or email address of the recipient"
|
||||
] = None,
|
||||
subject: Annotated[
|
||||
Optional[str], "Words to find in the subject of the email"
|
||||
] = None,
|
||||
body: Annotated[Optional[str], "Words to find in the body of the email"] = None,
|
||||
date_range: Annotated[Optional[DateRange], "The date range of the email"] = None,
|
||||
limit: Annotated[Optional[int], "The maximum number of emails to return"] = 25,
|
||||
) -> Annotated[
|
||||
str, "A JSON string containing a list of email details matching the search criteria"
|
||||
]:
|
||||
"""
|
||||
Search for emails by header using the Gmail API.
|
||||
At least one of the following parametersMUST be provided: sender, recipient, subject, body.
|
||||
"""
|
||||
|
||||
if not any([sender, recipient, subject, body]):
|
||||
raise ToolInputError(
|
||||
"At least one of sender, recipient, subject, or body must be provided."
|
||||
)
|
||||
|
||||
# Build the query string
|
||||
query = []
|
||||
if sender:
|
||||
query.append(f"from:{sender}")
|
||||
if recipient:
|
||||
query.append(f"to:{recipient}")
|
||||
if subject:
|
||||
query.append(f"subject:{subject}")
|
||||
if body:
|
||||
query.append(body)
|
||||
if date_range:
|
||||
query.append(date_range.to_date_query())
|
||||
|
||||
query_string = " ".join(query)
|
||||
|
||||
try:
|
||||
# Set up the Gmail API client
|
||||
service = build(
|
||||
"gmail", "v1", credentials=Credentials(context.authorization.token)
|
||||
)
|
||||
|
||||
# Perform the search
|
||||
response = (
|
||||
service.users()
|
||||
.messages()
|
||||
.list(userId="me", q=query_string, maxResults=limit or 100)
|
||||
.execute()
|
||||
)
|
||||
messages = response.get("messages", [])
|
||||
|
||||
if not messages:
|
||||
return json.dumps({"emails": []})
|
||||
|
||||
emails = []
|
||||
for msg in messages:
|
||||
try:
|
||||
email_data = (
|
||||
service.users().messages().get(userId="me", id=msg["id"]).execute()
|
||||
)
|
||||
email_details = parse_email(email_data)
|
||||
if email_details:
|
||||
emails.append(email_details)
|
||||
except HttpError as e:
|
||||
print(f"Error reading email {msg['id']}: {e}")
|
||||
|
||||
return json.dumps({"emails": emails})
|
||||
except HttpError as e:
|
||||
raise ToolExecutionError(
|
||||
f"HttpError during execution of '{list_emails_by_header.__name__}' tool.",
|
||||
str(e),
|
||||
)
|
||||
except Exception as e:
|
||||
raise ToolExecutionError(
|
||||
f"Unexpected Error encountered during execution of '{list_emails_by_header.__name__}' tool.",
|
||||
str(e),
|
||||
)
|
||||
|
||||
|
||||
@tool(
|
||||
requires_auth=Google(
|
||||
scopes=["https://www.googleapis.com/auth/gmail.readonly"],
|
||||
)
|
||||
)
|
||||
async def list_emails(
|
||||
context: ToolContext,
|
||||
n_emails: Annotated[int, "Number of emails to read"] = 5,
|
||||
) -> Annotated[str, "A JSON string containing a list of email details"]:
|
||||
"""
|
||||
Read emails from a Gmail account and extract plain text content.
|
||||
"""
|
||||
try:
|
||||
# Set up the Gmail API client
|
||||
service = build(
|
||||
"gmail", "v1", credentials=Credentials(context.authorization.token)
|
||||
)
|
||||
|
||||
messages = (
|
||||
service.users().messages().list(userId="me").execute().get("messages", [])
|
||||
)
|
||||
|
||||
if not messages:
|
||||
return {"emails": []}
|
||||
|
||||
emails = []
|
||||
for msg in messages[:n_emails]:
|
||||
try:
|
||||
email_data = (
|
||||
service.users().messages().get(userId="me", id=msg["id"]).execute()
|
||||
)
|
||||
email_details = parse_email(email_data)
|
||||
if email_details:
|
||||
emails.append(email_details)
|
||||
except Exception as e:
|
||||
print(f"Error reading email {msg['id']}: {e}")
|
||||
|
||||
return json.dumps({"emails": emails})
|
||||
except HttpError as e:
|
||||
raise ToolExecutionError(
|
||||
f"HttpError during execution of '{list_emails.__name__}' tool.", str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
raise ToolExecutionError(
|
||||
f"Unexpected Error encountered during execution of '{list_emails.__name__}' tool.",
|
||||
str(e),
|
||||
)
|
||||
178
toolkits/google/arcade_google/tools/utils.py
Normal file
178
toolkits/google/arcade_google/tools/utils.py
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
from base64 import urlsafe_b64decode
|
||||
import datetime
|
||||
from enum import Enum
|
||||
import re
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
class DateRange(Enum):
|
||||
TODAY = "today"
|
||||
YESTERDAY = "yesterday"
|
||||
LAST_7_DAYS = "last_7_days"
|
||||
LAST_30_DAYS = "last_30_days"
|
||||
THIS_MONTH = "this_month"
|
||||
LAST_MONTH = "last_month"
|
||||
THIS_YEAR = "this_year"
|
||||
|
||||
def to_date_query(self):
|
||||
today = datetime.datetime.now()
|
||||
result = "after:"
|
||||
comparison_date = today
|
||||
|
||||
if self == DateRange.YESTERDAY:
|
||||
comparison_date = today - datetime.timedelta(days=1)
|
||||
elif self == DateRange.LAST_7_DAYS:
|
||||
comparison_date = today - datetime.timedelta(days=7)
|
||||
elif self == DateRange.LAST_30_DAYS:
|
||||
comparison_date = today - datetime.timedelta(days=30)
|
||||
elif self == DateRange.THIS_MONTH:
|
||||
comparison_date = today.replace(day=1)
|
||||
elif self == DateRange.LAST_MONTH:
|
||||
comparison_date = (
|
||||
today.replace(day=1) - datetime.timedelta(days=1)
|
||||
).replace(day=1)
|
||||
elif self == DateRange.THIS_YEAR:
|
||||
comparison_date = today.replace(month=1, day=1)
|
||||
elif self == DateRange.LAST_MONTH:
|
||||
comparison_date = (
|
||||
today.replace(month=1, day=1) - datetime.timedelta(days=1)
|
||||
).replace(month=1, day=1)
|
||||
|
||||
return result + comparison_date.strftime("%Y/%m/%d")
|
||||
|
||||
|
||||
def parse_email(email_data: Dict[str, Any]) -> Optional[Dict[str, str]]:
|
||||
"""
|
||||
Parse email data and extract relevant information.
|
||||
|
||||
Args:
|
||||
email_data (Dict[str, Any]): Raw email data from Gmail API.
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, str]]: Parsed email details or None if parsing fails.
|
||||
"""
|
||||
try:
|
||||
payload = email_data["payload"]
|
||||
headers = {d["name"].lower(): d["value"] for d in payload["headers"]}
|
||||
|
||||
body_data = _get_email_body(payload)
|
||||
|
||||
return {
|
||||
"id": email_data.get("id", ""),
|
||||
"from": headers.get("from", ""),
|
||||
"date": headers.get("date", ""),
|
||||
"subject": headers.get("subject", "No subject"),
|
||||
"body": _clean_email_body(body_data) if body_data else "",
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error parsing email {email_data.get('id', 'unknown')}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def parse_draft_email(draft_email_data: Dict[str, Any]) -> Optional[Dict[str, str]]:
|
||||
"""
|
||||
Parse draft email data and extract relevant information.
|
||||
|
||||
Args:
|
||||
draft_email_data (Dict[str, Any]): Raw draft email data from Gmail API.
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, str]]: Parsed draft email details or None if parsing fails.
|
||||
"""
|
||||
try:
|
||||
message = draft_email_data["message"]
|
||||
payload = message["payload"]
|
||||
headers = {d["name"].lower(): d["value"] for d in payload["headers"]}
|
||||
|
||||
body_data = _get_email_body(payload)
|
||||
|
||||
return {
|
||||
"id": draft_email_data.get("id", ""),
|
||||
"from": headers.get("from", ""),
|
||||
"date": headers.get("internaldate", ""),
|
||||
"subject": headers.get("subject", "No subject"),
|
||||
"body": _clean_email_body(body_data) if body_data else "",
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error parsing draft email {draft_email_data.get('id', 'unknown')}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def get_draft_url(draft_id):
|
||||
return f"https://mail.google.com/mail/u/0/#drafts/{draft_id}"
|
||||
|
||||
|
||||
def get_sent_email_url(sent_email_id):
|
||||
return f"https://mail.google.com/mail/u/0/#sent/{sent_email_id}"
|
||||
|
||||
|
||||
def get_email_in_trash_url(email_id):
|
||||
return f"https://mail.google.com/mail/u/0/#trash/{email_id}"
|
||||
|
||||
|
||||
def _get_email_body(payload: Dict[str, Any]) -> Optional[str]:
|
||||
"""
|
||||
Extract email body from payload.
|
||||
|
||||
Args:
|
||||
payload (Dict[str, Any]): Email payload data.
|
||||
|
||||
Returns:
|
||||
Optional[str]: Decoded email body or None if not found.
|
||||
"""
|
||||
if "body" in payload and payload["body"].get("data"):
|
||||
return urlsafe_b64decode(payload["body"]["data"]).decode()
|
||||
|
||||
for part in payload.get("parts", []):
|
||||
if part.get("mimeType") == "text/plain" and "data" in part["body"]:
|
||||
return urlsafe_b64decode(part["body"]["data"]).decode()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _clean_email_body(body: str) -> str:
|
||||
"""
|
||||
Remove HTML tags and clean up email body text while preserving most content.
|
||||
|
||||
Args:
|
||||
body (str): The raw email body text.
|
||||
|
||||
Returns:
|
||||
str: Cleaned email body text.
|
||||
"""
|
||||
try:
|
||||
# Remove HTML tags using BeautifulSoup
|
||||
soup = BeautifulSoup(body, "html.parser")
|
||||
text = soup.get_text(separator=" ")
|
||||
|
||||
# Clean up the text
|
||||
text = _clean_text(text)
|
||||
|
||||
return text.strip()
|
||||
except Exception as e:
|
||||
print(f"Error cleaning email body: {e}")
|
||||
return body
|
||||
|
||||
|
||||
def _clean_text(text: str) -> str:
|
||||
"""
|
||||
Clean up the text while preserving most content.
|
||||
|
||||
Args:
|
||||
text (str): The input text.
|
||||
|
||||
Returns:
|
||||
str: Cleaned text.
|
||||
"""
|
||||
# Replace multiple newlines with a single newline
|
||||
text = re.sub(r"\n+", "\n", text)
|
||||
|
||||
# Replace multiple spaces with a single space
|
||||
text = re.sub(r"\s+", " ", text)
|
||||
|
||||
# Remove leading/trailing whitespace from each line
|
||||
text = "\n".join(line.strip() for line in text.split("\n"))
|
||||
|
||||
return text
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
[tool.poetry]
|
||||
name = "arcade_gmail"
|
||||
name = "arcade_google"
|
||||
version = "0.1.0"
|
||||
description = "LLM tools for interating with gmail"
|
||||
authors = ["Sam Partee <sam@arcade-ai.com>"]
|
||||
description = "Arcade tools for the entire google suite"
|
||||
authors = ["Sam Partee <sam@arcade-ai.com>", "Eric Gustin <eric@arcade-ai.com>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
0
toolkits/google/tests/__init__.py
Normal file
0
toolkits/google/tests/__init__.py
Normal file
434
toolkits/google/tests/test_gmail.py
Normal file
434
toolkits/google/tests/test_gmail.py
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
import json
|
||||
from arcade.core.errors import ToolExecutionError
|
||||
from arcade_google.tools.utils import parse_draft_email, parse_email
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from arcade_google.tools.gmail import (
|
||||
send_email,
|
||||
write_draft_email,
|
||||
update_draft_email,
|
||||
send_draft_email,
|
||||
delete_draft_email,
|
||||
list_draft_emails,
|
||||
list_emails_by_header,
|
||||
list_emails,
|
||||
trash_email,
|
||||
)
|
||||
|
||||
from arcade.core.schema import ToolContext, ToolAuthorizationContext
|
||||
from googleapiclient.errors import HttpError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_context():
|
||||
mock_auth = ToolAuthorizationContext(token="fake-token")
|
||||
return ToolContext(authorization=mock_auth)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("arcade_google.tools.gmail.build")
|
||||
async def test_send_email(mock_build, mock_context):
|
||||
mock_service = MagicMock()
|
||||
mock_build.return_value = mock_service
|
||||
|
||||
# Test happy path
|
||||
result = await send_email(
|
||||
context=mock_context,
|
||||
subject="Test Subject",
|
||||
body="Test Body",
|
||||
recipient="test@example.com",
|
||||
)
|
||||
|
||||
assert "Email with ID" in result
|
||||
assert "sent" in result
|
||||
|
||||
# Test http error
|
||||
mock_service.users().messages().send().execute.side_effect = HttpError(
|
||||
resp=MagicMock(status=400),
|
||||
content=b'{"error": {"message": "Invalid recipient"}}',
|
||||
)
|
||||
|
||||
with pytest.raises(ToolExecutionError):
|
||||
await send_email(
|
||||
context=mock_context,
|
||||
subject="Test Subject",
|
||||
body="Test Body",
|
||||
recipient="invalid@example.com",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("arcade_google.tools.gmail.build")
|
||||
async def test_write_draft_email(mock_build, mock_context):
|
||||
mock_service = MagicMock()
|
||||
mock_build.return_value = mock_service
|
||||
|
||||
# Test happy path
|
||||
result = await write_draft_email(
|
||||
context=mock_context,
|
||||
subject="Test Draft Subject",
|
||||
body="Test Draft Body",
|
||||
recipient="draft@example.com",
|
||||
)
|
||||
|
||||
assert "Draft email with ID" in result
|
||||
assert "created" in result
|
||||
|
||||
# Test http error
|
||||
mock_service.users().drafts().create().execute.side_effect = HttpError(
|
||||
resp=MagicMock(status=400),
|
||||
content=b'{"error": {"message": "Invalid request"}}',
|
||||
)
|
||||
|
||||
with pytest.raises(ToolExecutionError):
|
||||
await write_draft_email(
|
||||
context=mock_context,
|
||||
subject="Test Draft Subject",
|
||||
body="Test Draft Body",
|
||||
recipient="draft@example.com",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("arcade_google.tools.gmail.build")
|
||||
async def test_update_draft_email(mock_build, mock_context):
|
||||
mock_service = MagicMock()
|
||||
mock_build.return_value = mock_service
|
||||
|
||||
# Test happy path
|
||||
result = await update_draft_email(
|
||||
context=mock_context,
|
||||
id="draft123",
|
||||
subject="Updated Subject",
|
||||
body="Updated Body",
|
||||
recipient="updated@example.com",
|
||||
)
|
||||
|
||||
assert "Draft email with ID" in result
|
||||
assert "updated" in result
|
||||
|
||||
# Test http error
|
||||
mock_service.users().drafts().update().execute.side_effect = HttpError(
|
||||
resp=MagicMock(status=400),
|
||||
content=b'{"error": {"message": "Draft not found"}}',
|
||||
)
|
||||
|
||||
with pytest.raises(ToolExecutionError):
|
||||
await update_draft_email(
|
||||
context=mock_context,
|
||||
id="nonexistent_draft",
|
||||
subject="Updated Subject",
|
||||
body="Updated Body",
|
||||
recipient="updated@example.com",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("arcade_google.tools.gmail.build")
|
||||
async def test_send_draft_email(mock_build, mock_context):
|
||||
mock_service = MagicMock()
|
||||
mock_build.return_value = mock_service
|
||||
|
||||
# Test happy path
|
||||
result = await send_draft_email(context=mock_context, id="draft456")
|
||||
|
||||
assert "Draft email with ID" in result
|
||||
assert "sent" in result
|
||||
|
||||
# Test http error
|
||||
mock_service.users().drafts().send().execute.side_effect = HttpError(
|
||||
resp=MagicMock(status=400),
|
||||
content=b'{"error": {"message": "Draft not found"}}',
|
||||
)
|
||||
|
||||
with pytest.raises(ToolExecutionError):
|
||||
await send_draft_email(context=mock_context, id="nonexistent_draft")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("arcade_google.tools.gmail.build")
|
||||
async def test_delete_draft_email(mock_build, mock_context):
|
||||
mock_service = MagicMock()
|
||||
mock_build.return_value = mock_service
|
||||
|
||||
# Test happy path
|
||||
result = await delete_draft_email(context=mock_context, id="draft789")
|
||||
|
||||
assert "Draft email with ID" in result
|
||||
assert "deleted successfully" in result
|
||||
|
||||
# Test http error
|
||||
mock_service.users().drafts().delete().execute.side_effect = HttpError(
|
||||
resp=MagicMock(status=400),
|
||||
content=b'{"error": {"message": "Draft not found"}}',
|
||||
)
|
||||
|
||||
with pytest.raises(ToolExecutionError):
|
||||
await delete_draft_email(context=mock_context, id="nonexistent_draft")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("arcade_google.tools.gmail.build")
|
||||
@patch("arcade_google.tools.gmail.parse_draft_email")
|
||||
async def test_get_draft_emails(mock_parse_draft_email, mock_build, mock_context):
|
||||
# Setup test data
|
||||
mock_drafts_list_response = {
|
||||
"drafts": [
|
||||
{
|
||||
"id": "r9999999999999999999",
|
||||
"message": {"id": "0000000000000000", "threadId": "0000000000000000"},
|
||||
}
|
||||
],
|
||||
"resultSizeEstimate": 1,
|
||||
}
|
||||
mock_drafts_get_response = {
|
||||
"id": "r9999999999999999999",
|
||||
"message": {
|
||||
"id": "0000000000000000",
|
||||
"threadId": "0000000000000000",
|
||||
"labelIds": ["DRAFT"],
|
||||
"snippet": "Hello! This is a test. Best regards, John",
|
||||
"payload": {
|
||||
"partId": "",
|
||||
"mimeType": "text/plain",
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{"name": "to", "value": "test@arcade-ai.com"},
|
||||
{"name": "subject", "value": "New Draft"},
|
||||
{"name": "Date", "value": "Mon, 16 Sep 2024 13:02:10 -0400"},
|
||||
{"name": "From", "value": "john-doe@arcade-ai.com"},
|
||||
],
|
||||
"body": {
|
||||
"size": 41,
|
||||
"data": "SGVsbG8hIFRoaXMgaXMgYSB0ZXN0LgoKQmVzdCByZWdhcmRzLApCb2I=",
|
||||
},
|
||||
},
|
||||
"sizeEstimate": 453,
|
||||
"historyId": "7061",
|
||||
"internalDate": "1726506130000",
|
||||
},
|
||||
}
|
||||
|
||||
# Setup mocking
|
||||
mock_service = MagicMock()
|
||||
mock_build.return_value = mock_service
|
||||
|
||||
# Mock the response from the Gmail list drafts API
|
||||
mock_service.users().drafts().list().execute.return_value = (
|
||||
mock_drafts_list_response
|
||||
)
|
||||
|
||||
# Mock the response from the Gmail get drafts API
|
||||
mock_service.users().drafts().get().execute.return_value = mock_drafts_get_response
|
||||
|
||||
# Mock the parse_email function since parse_email doesn't accept object of type MagicMock
|
||||
mock_parse_draft_email.return_value = parse_draft_email(mock_drafts_get_response)
|
||||
|
||||
# Test happy path
|
||||
result = await list_draft_emails(context=mock_context, n_drafts=2)
|
||||
|
||||
assert isinstance(result, str)
|
||||
result_json = json.loads(result)
|
||||
assert isinstance(result_json, dict)
|
||||
assert "emails" in result_json
|
||||
assert len(result_json["emails"]) == 1
|
||||
assert all("id" in draft and "subject" in draft for draft in result_json["emails"])
|
||||
|
||||
# Test http error
|
||||
mock_service.users().drafts().list().execute.side_effect = HttpError(
|
||||
resp=MagicMock(status=400),
|
||||
content=b'{"error": {"message": "Invalid request"}}',
|
||||
)
|
||||
|
||||
with pytest.raises(ToolExecutionError):
|
||||
await list_draft_emails(context=mock_context, n_drafts=2)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("arcade_google.tools.gmail.build")
|
||||
@patch("arcade_google.tools.gmail.parse_email")
|
||||
async def test_search_emails_by_header(mock_parse_email, mock_build, mock_context):
|
||||
# Setup test data
|
||||
mock_messages_list_response = {
|
||||
"messages": [
|
||||
{"id": "191fbc8ddce0f433", "threadId": "191fbc8ddce0f433"},
|
||||
{"id": "191fbc0ea11efa90", "threadId": "191fbc0ea11efa90"},
|
||||
],
|
||||
"nextPageToken": "00755945214480102915",
|
||||
"resultSizeEstimate": 201,
|
||||
}
|
||||
mock_messages_get_response = {
|
||||
"id": "191f2cf4d24bf23d",
|
||||
"threadId": "191f2cf4d24bf23d",
|
||||
"labelIds": ["UNREAD", "IMPORTANT", "CATEGORY_UPDATES", "INBOX"],
|
||||
"snippet": "Hey User, Your personal access token (classic) "ArcadeAI" with admin:enterprise, admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, admin:ssh_signing_key,",
|
||||
"payload": {
|
||||
"partId": "",
|
||||
"mimeType": "text/plain",
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{"name": "Delivered-To", "value": "example@arcade-ai.com"},
|
||||
{"name": "Date", "value": "Sat, 14 Sep 2024 16:12:37 -0700"},
|
||||
{"name": "From", "value": "GitHub \u003cnoreply@github.com\u003e"},
|
||||
{"name": "To", "value": "example@arcade-ai.com"},
|
||||
{
|
||||
"name": "Subject",
|
||||
"value": "[GitHub] Your personal access token (classic) has expired",
|
||||
},
|
||||
],
|
||||
"body": {
|
||||
"size": 605,
|
||||
"data": "SGV5IEBFcmljR3VzdGluLA0KDQpZb3VyIHBlcnNvbmFsIGFjY2VzcyB0b2tlbiAoY2xhc3NpYykgIkFyY2FkZUFJIiB3aXRoIGFkbWluOmVudGVycHJpc2UsIGFkbWluOmdwZ19rZXksIGFkbWluOm9yZywgYWRtaW46b3JnX2hvb2ssIGFkbWluOnB1YmxpY19rZXksIGFkbWluOnJlcG9faG9vaywgYWRtaW46c3NoX3NpZ25pbmdfa2V5LCBhdWRpdF9sb2csIGNvZGVzcGFjZSwgY29waWxvdCwgZGVsZXRlOnBhY2thZ2VzLCBkZWxldGVfcmVwbywgZ2lzdCwgbm90aWZpY2F0aW9ucywgcHJvamVjdCwgcmVwbywgdXNlciwgd29ya2Zsb3csIHdyaXRlOmRpc2N1c3Npb24sIGFuZCB3cml0ZTpwYWNrYWdlcyBzY29wZXMgaGFzIGV4cGlyZWQuDQoNCklmIHRoaXMgdG9rZW4gaXMgc3RpbGwgbmVlZGVkLCB2aXNpdCBodHRwczovL2dpdGh1Yi5jb20vc2V0dGluZ3MvdG9rZW5zLzE3MTM2OTg2MTMvcmVnZW5lcmF0ZSB0byBnZW5lcmF0ZSBhbiBlcXVpdmFsZW50Lg0KDQpJZiB5b3UgcnVuIGludG8gcHJvYmxlbXMsIHBsZWFzZSBjb250YWN0IHN1cHBvcnQgYnkgdmlzaXRpbmcgaHR0cHM6Ly9naXRodWIuY29tL2NvbnRhY3QNCg0KVGhhbmtzLA0KVGhlIEdpdEh1YiBUZWFtDQo=",
|
||||
},
|
||||
},
|
||||
"sizeEstimate": 4512,
|
||||
"historyId": "5508",
|
||||
"internalDate": "1726355557000",
|
||||
}
|
||||
|
||||
# Setup mocking
|
||||
mock_service = MagicMock()
|
||||
mock_build.return_value = mock_service
|
||||
|
||||
# Mock the response from the Gmail list messages API
|
||||
mock_service.users().messages().list().execute.return_value = (
|
||||
mock_messages_list_response
|
||||
)
|
||||
|
||||
# Mock the response from the Gmail get messages API
|
||||
mock_service.users().messages().get().execute.return_value = (
|
||||
mock_messages_get_response
|
||||
)
|
||||
|
||||
# Mock the parse_email function since parse_email doesn't accept object of type MagicMock
|
||||
mock_parse_email.return_value = parse_email(mock_messages_get_response)
|
||||
|
||||
# Test happy path
|
||||
result = await list_emails_by_header(
|
||||
context=mock_context, sender="noreply@github.com", limit=2
|
||||
)
|
||||
|
||||
assert isinstance(result, str)
|
||||
result_json = json.loads(result)
|
||||
assert isinstance(result_json, dict)
|
||||
assert "emails" in result_json
|
||||
assert len(result_json["emails"]) == 2
|
||||
assert all("id" in email and "subject" in email for email in result_json["emails"])
|
||||
|
||||
# Test http error
|
||||
mock_service.users().messages().list().execute.side_effect = HttpError(
|
||||
resp=MagicMock(status=400),
|
||||
content=b'{"error": {"message": "Invalid request"}}',
|
||||
)
|
||||
|
||||
with pytest.raises(ToolExecutionError):
|
||||
await list_emails_by_header(
|
||||
context=mock_context, sender="noreply@github.com", limit=2
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("arcade_google.tools.gmail.build")
|
||||
@patch("arcade_google.tools.gmail.parse_email")
|
||||
async def test_get_emails(mock_parse_email, mock_build, mock_context):
|
||||
# Setup test data
|
||||
mock_messages_list_response = {
|
||||
"messages": [
|
||||
{"id": "191fbc8ddce0f433", "threadId": "191fbc8ddce0f433"},
|
||||
],
|
||||
"nextPageToken": "00755945214480102915",
|
||||
"resultSizeEstimate": 1,
|
||||
}
|
||||
mock_messages_get_response = {
|
||||
"id": "191f2cf4d24bf23d",
|
||||
"threadId": "191f2cf4d24bf23d",
|
||||
"labelIds": ["UNREAD", "IMPORTANT", "CATEGORY_UPDATES", "INBOX"],
|
||||
"snippet": "Hey User, Your personal access token (classic) "ArcadeAI" with admin:enterprise, admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, admin:ssh_signing_key,",
|
||||
"payload": {
|
||||
"partId": "",
|
||||
"mimeType": "text/plain",
|
||||
"filename": "",
|
||||
"headers": [
|
||||
{"name": "Delivered-To", "value": "example@arcade-ai.com"},
|
||||
{"name": "Date", "value": "Sat, 14 Sep 2024 16:12:37 -0700"},
|
||||
{"name": "From", "value": "GitHub \u003cnoreply@github.com\u003e"},
|
||||
{"name": "To", "value": "example@arcade-ai.com"},
|
||||
{
|
||||
"name": "Subject",
|
||||
"value": "[GitHub] Your personal access token (classic) has expired",
|
||||
},
|
||||
],
|
||||
"body": {
|
||||
"size": 605,
|
||||
"data": "SGV5IEBFcmljR3VzdGluLA0KDQpZb3VyIHBlcnNvbmFsIGFjY2VzcyB0b2tlbiAoY2xhc3NpYykgIkFyY2FkZUFJIiB3aXRoIGFkbWluOmVudGVycHJpc2UsIGFkbWluOmdwZ19rZXksIGFkbWluOm9yZywgYWRtaW46b3JnX2hvb2ssIGFkbWluOnB1YmxpY19rZXksIGFkbWluOnJlcG9faG9vaywgYWRtaW46c3NoX3NpZ25pbmdfa2V5LCBhdWRpdF9sb2csIGNvZGVzcGFjZSwgY29waWxvdCwgZGVsZXRlOnBhY2thZ2VzLCBkZWxldGVfcmVwbywgZ2lzdCwgbm90aWZpY2F0aW9ucywgcHJvamVjdCwgcmVwbywgdXNlciwgd29ya2Zsb3csIHdyaXRlOmRpc2N1c3Npb24sIGFuZCB3cml0ZTpwYWNrYWdlcyBzY29wZXMgaGFzIGV4cGlyZWQuDQoNCklmIHRoaXMgdG9rZW4gaXMgc3RpbGwgbmVlZGVkLCB2aXNpdCBodHRwczovL2dpdGh1Yi5jb20vc2V0dGluZ3MvdG9rZW5zLzE3MTM2OTg2MTMvcmVnZW5lcmF0ZSB0byBnZW5lcmF0ZSBhbiBlcXVpdmFsZW50Lg0KDQpJZiB5b3UgcnVuIGludG8gcHJvYmxlbXMsIHBsZWFzZSBjb250YWN0IHN1cHBvcnQgYnkgdmlzaXRpbmcgaHR0cHM6Ly9naXRodWIuY29tL2NvbnRhY3QNCg0KVGhhbmtzLA0KVGhlIEdpdEh1YiBUZWFtDQo=",
|
||||
},
|
||||
},
|
||||
"sizeEstimate": 4512,
|
||||
"historyId": "5508",
|
||||
"internalDate": "1726355557000",
|
||||
}
|
||||
|
||||
# Setup mocking
|
||||
mock_service = MagicMock()
|
||||
mock_build.return_value = mock_service
|
||||
|
||||
# Mock the response from the Gmail list messages API
|
||||
mock_service.users().messages().list().execute.return_value = (
|
||||
mock_messages_list_response
|
||||
)
|
||||
|
||||
# Mock the Gmail get messages API
|
||||
mock_service.users().messages().get().execute.return_value = (
|
||||
mock_messages_get_response
|
||||
)
|
||||
|
||||
# Mock the parse_email function since parse_email doesn't accept object of type MagicMock
|
||||
mock_parse_email.return_value = parse_email(mock_messages_get_response)
|
||||
|
||||
# Test happy path
|
||||
result = await list_emails(context=mock_context, n_emails=1)
|
||||
|
||||
# Assert the result
|
||||
assert isinstance(result, str)
|
||||
result_json = json.loads(result)
|
||||
assert isinstance(result_json, dict)
|
||||
assert "emails" in result_json
|
||||
assert len(result_json["emails"]) == 1
|
||||
assert "id" in result_json["emails"][0]
|
||||
assert "subject" in result_json["emails"][0]
|
||||
assert "date" in result_json["emails"][0]
|
||||
assert "body" in result_json["emails"][0]
|
||||
|
||||
# Test http error
|
||||
mock_service.users().messages().list().execute.side_effect = HttpError(
|
||||
resp=MagicMock(status=400),
|
||||
content=b'{"error": {"message": "Invalid request"}}',
|
||||
)
|
||||
|
||||
with pytest.raises(ToolExecutionError):
|
||||
await list_emails(context=mock_context, n_emails=1)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("arcade_google.tools.gmail.build")
|
||||
async def test_trash_email(mock_build, mock_context):
|
||||
mock_service = MagicMock()
|
||||
mock_build.return_value = mock_service
|
||||
|
||||
# Test happy path
|
||||
email_id = "123456"
|
||||
result = await trash_email(context=mock_context, id=email_id)
|
||||
|
||||
assert (
|
||||
f"Email with ID {email_id} trashed successfully: https://mail.google.com/mail/u/0/#trash/{email_id}"
|
||||
== result
|
||||
)
|
||||
|
||||
# Test http error
|
||||
mock_service.users().messages().trash().execute.side_effect = HttpError(
|
||||
resp=MagicMock(status=400),
|
||||
content=b'{"error": {"message": "Email not found"}}',
|
||||
)
|
||||
|
||||
with pytest.raises(ToolExecutionError):
|
||||
await trash_email(context=mock_context, id="nonexistent_email")
|
||||
|
|
@ -66,7 +66,7 @@ def sum_list(
|
|||
|
||||
@tool
|
||||
def sum_range(
|
||||
start: Annotated[int, "The start of the range to sum"],
|
||||
start: Annotated[int, "The start of the range to sum"],
|
||||
end: Annotated[int, "The end of the range to sum"],
|
||||
) -> Annotated[int, "The sum of the numbers in the list"]:
|
||||
"""
|
||||
|
|
|
|||
0
toolkits/math/tests/__init__.py
Normal file
0
toolkits/math/tests/__init__.py
Normal file
48
toolkits/math/tests/test_arithmetic.py
Normal file
48
toolkits/math/tests/test_arithmetic.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import pytest
|
||||
from arcade_arithmetic.tools.arithmetic import (
|
||||
add,
|
||||
subtract,
|
||||
multiply,
|
||||
divide,
|
||||
sqrt,
|
||||
sum_list,
|
||||
sum_range,
|
||||
)
|
||||
|
||||
|
||||
def test_add():
|
||||
assert add(1, 2) == 3
|
||||
assert add(-1, 1) == 0
|
||||
assert add(0.5, 10.9) == 11.4
|
||||
|
||||
|
||||
def test_subtract():
|
||||
assert subtract(2, 1) == 1
|
||||
assert subtract(2, 3.5) == -1.5
|
||||
|
||||
|
||||
def test_multiply():
|
||||
assert multiply(2, 3) == 6
|
||||
assert multiply(-1, 1.5) == -1.5
|
||||
|
||||
|
||||
def test_divide():
|
||||
assert divide(6, 3) == 2.0
|
||||
assert divide(5, 2) == 2.5
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
divide(1, 0)
|
||||
|
||||
|
||||
def test_sqrt():
|
||||
assert sqrt(4) == 2.0
|
||||
assert sqrt(9) == 3.0
|
||||
|
||||
|
||||
def test_sum_list():
|
||||
assert sum_list([1, 2, 3]) == 6
|
||||
assert sum_list([0, -1.5, 1]) == -0.5
|
||||
|
||||
|
||||
def test_sum_range():
|
||||
assert sum_range(1, 3) == 6
|
||||
assert sum_range(0, 10) == 55
|
||||
Loading…
Reference in a new issue