From 43198a3a9b98e97926058324029ab1d1b8681b93 Mon Sep 17 00:00:00 2001 From: Eric Gustin <34000337+EricGustin@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:18:49 -0700 Subject: [PATCH] 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. --- .vscode/launch.json | 14 +- Makefile | 7 + arcade/run_cli.py | 8 +- docker/toolkits.txt | 2 +- .../fastapi/arcade_example_fastapi/main.py | 8 +- examples/fastapi/poetry.lock | 352 +++++++------ examples/fastapi/pyproject.toml | 2 +- toolkits/gmail/arcade_gmail/tools/gmail.py | 214 -------- toolkits/gmail/arcade_gmail/tools/utils.py | 98 ---- .../arcade_google}/__init__.py | 0 .../arcade_google}/tools/__init__.py | 0 toolkits/google/arcade_google/tools/gmail.py | 484 ++++++++++++++++++ toolkits/google/arcade_google/tools/utils.py | 178 +++++++ toolkits/{gmail => google}/pyproject.toml | 6 +- toolkits/google/tests/__init__.py | 0 toolkits/google/tests/test_gmail.py | 434 ++++++++++++++++ .../arcade_arithmetic/tools/arithmetic.py | 2 +- toolkits/math/tests/__init__.py | 0 toolkits/math/tests/test_arithmetic.py | 48 ++ 19 files changed, 1374 insertions(+), 483 deletions(-) delete mode 100644 toolkits/gmail/arcade_gmail/tools/gmail.py delete mode 100644 toolkits/gmail/arcade_gmail/tools/utils.py rename toolkits/{gmail/arcade_gmail => google/arcade_google}/__init__.py (100%) rename toolkits/{gmail/arcade_gmail => google/arcade_google}/tools/__init__.py (100%) create mode 100644 toolkits/google/arcade_google/tools/gmail.py create mode 100644 toolkits/google/arcade_google/tools/utils.py rename toolkits/{gmail => google}/pyproject.toml (73%) create mode 100644 toolkits/google/tests/__init__.py create mode 100644 toolkits/google/tests/test_gmail.py create mode 100644 toolkits/math/tests/__init__.py create mode 100644 toolkits/math/tests/test_arithmetic.py diff --git a/.vscode/launch.json b/.vscode/launch.json index b52ce41d..14466dd6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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}" } ] } diff --git a/Makefile b/Makefile index 0e90047d..e5adf6c2 100644 --- a/Makefile +++ b/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" diff --git a/arcade/run_cli.py b/arcade/run_cli.py index ccf215f3..ce814d27 100644 --- a/arcade/run_cli.py +++ b/arcade/run_cli.py @@ -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) diff --git a/docker/toolkits.txt b/docker/toolkits.txt index 70572bc8..ea8a5035 100644 --- a/docker/toolkits.txt +++ b/docker/toolkits.txt @@ -1,4 +1,4 @@ -gmail +google slack github websearch diff --git a/examples/fastapi/arcade_example_fastapi/main.py b/examples/fastapi/arcade_example_fastapi/main.py index c4fb3ee0..069dadc9 100644 --- a/examples/fastapi/arcade_example_fastapi/main.py +++ b/examples/fastapi/arcade_example_fastapi/main.py @@ -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) diff --git a/examples/fastapi/poetry.lock b/examples/fastapi/poetry.lock index 5a54fd52..975f8c91 100644 --- a/examples/fastapi/poetry.lock +++ b/examples/fastapi/poetry.lock @@ -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" diff --git a/examples/fastapi/pyproject.toml b/examples/fastapi/pyproject.toml index 014d7fdd..51b07ed3 100644 --- a/examples/fastapi/pyproject.toml +++ b/examples/fastapi/pyproject.toml @@ -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} diff --git a/toolkits/gmail/arcade_gmail/tools/gmail.py b/toolkits/gmail/arcade_gmail/tools/gmail.py deleted file mode 100644 index fd6f3054..00000000 --- a/toolkits/gmail/arcade_gmail/tools/gmail.py +++ /dev/null @@ -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" diff --git a/toolkits/gmail/arcade_gmail/tools/utils.py b/toolkits/gmail/arcade_gmail/tools/utils.py deleted file mode 100644 index 9768b3a9..00000000 --- a/toolkits/gmail/arcade_gmail/tools/utils.py +++ /dev/null @@ -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 diff --git a/toolkits/gmail/arcade_gmail/__init__.py b/toolkits/google/arcade_google/__init__.py similarity index 100% rename from toolkits/gmail/arcade_gmail/__init__.py rename to toolkits/google/arcade_google/__init__.py diff --git a/toolkits/gmail/arcade_gmail/tools/__init__.py b/toolkits/google/arcade_google/tools/__init__.py similarity index 100% rename from toolkits/gmail/arcade_gmail/tools/__init__.py rename to toolkits/google/arcade_google/tools/__init__.py diff --git a/toolkits/google/arcade_google/tools/gmail.py b/toolkits/google/arcade_google/tools/gmail.py new file mode 100644 index 00000000..17441db6 --- /dev/null +++ b/toolkits/google/arcade_google/tools/gmail.py @@ -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), + ) diff --git a/toolkits/google/arcade_google/tools/utils.py b/toolkits/google/arcade_google/tools/utils.py new file mode 100644 index 00000000..097e469f --- /dev/null +++ b/toolkits/google/arcade_google/tools/utils.py @@ -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 diff --git a/toolkits/gmail/pyproject.toml b/toolkits/google/pyproject.toml similarity index 73% rename from toolkits/gmail/pyproject.toml rename to toolkits/google/pyproject.toml index 10c101e6..9b3ff4b3 100644 --- a/toolkits/gmail/pyproject.toml +++ b/toolkits/google/pyproject.toml @@ -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 "] +description = "Arcade tools for the entire google suite" +authors = ["Sam Partee ", "Eric Gustin "] [tool.poetry.dependencies] python = "^3.10" diff --git a/toolkits/google/tests/__init__.py b/toolkits/google/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/toolkits/google/tests/test_gmail.py b/toolkits/google/tests/test_gmail.py new file mode 100644 index 00000000..7cbe72e5 --- /dev/null +++ b/toolkits/google/tests/test_gmail.py @@ -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") diff --git a/toolkits/math/arcade_arithmetic/tools/arithmetic.py b/toolkits/math/arcade_arithmetic/tools/arithmetic.py index d9cb792b..f513ed0e 100644 --- a/toolkits/math/arcade_arithmetic/tools/arithmetic.py +++ b/toolkits/math/arcade_arithmetic/tools/arithmetic.py @@ -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"]: """ diff --git a/toolkits/math/tests/__init__.py b/toolkits/math/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/toolkits/math/tests/test_arithmetic.py b/toolkits/math/tests/test_arithmetic.py new file mode 100644 index 00000000..e931a552 --- /dev/null +++ b/toolkits/math/tests/test_arithmetic.py @@ -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