diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 3701a4e..2c88877 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -9,7 +9,7 @@ on: workflow_dispatch: env: - WORKFLOWAI_TEST_API_URL: https://api.workflowai.dev + WORKFLOWAI_TEST_API_URL: https://run.workflowai.dev jobs: e2e-tests: diff --git a/.vscode/settings.json b/.vscode/settings.json index 6441154..af511e2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,49 +1,43 @@ { - "[python]": { - "editor.codeActionsOnSave": { - "source.fixAll": "explicit", - "source.organizeImports": "explicit" - }, - "editor.defaultFormatter": "charliermarsh.ruff", - }, - "python.analysis.typeCheckingMode": "strict", - "python.analysis.ignore": [ - "**/*_pb2.py", - "**/*_pb2.pyi", - "**/*_pb2_grpc.py", - "*.proto" - ], - "ruff.lint.args": [ - "--extend-ignore=E501", - "--extend-exclude=/**/3rdparty_repositories/**/*.py", - "--extend-exclude=/**/site-packages/**/*.py", - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true, - "python.testing.pytestArgs": [ - "-vv", - ], - "python.languageServer": "Pylance", - "python.analysis.autoImportCompletions": true, - "editor.suggestSelection": "first", - "editor.quickSuggestions": { - "other": true, - "comments": false, - "strings": false - }, - "editor.acceptSuggestionOnEnter": "on", - "editor.tabCompletion": "on", - "python.analysis.completeFunctionParens": true, - "files.exclude": { - "**/.git": true, - "**/.svn": true, - "**/.hg": true, - "**/CVS": true, - "**/.DS_Store": true, - "**/Thumbs.db": true, - "ai_model_cache": true, - ".mypy_cache": true, - ".venv": true, - ".ruff_cache": true, + "[python]": { + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" }, + "editor.defaultFormatter": "charliermarsh.ruff" + }, + "python.analysis.typeCheckingMode": "strict", + "python.analysis.ignore": [ + "**/*_pb2.py", + "**/*_pb2.pyi", + "**/*_pb2_grpc.py", + "*.proto" + ], + + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "python.testing.pytestArgs": ["-vv"], + "python.languageServer": "Pylance", + "python.analysis.autoImportCompletions": true, + "editor.suggestSelection": "first", + "editor.quickSuggestions": { + "other": true, + "comments": false, + "strings": false + }, + "editor.acceptSuggestionOnEnter": "on", + "editor.tabCompletion": "on", + "python.analysis.completeFunctionParens": true, + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + "ai_model_cache": true, + ".mypy_cache": true, + ".venv": true, + ".ruff_cache": true + } } diff --git a/conftest.py b/conftest.py index a455099..24ed9ce 100644 --- a/conftest.py +++ b/conftest.py @@ -2,7 +2,7 @@ from freezegun import freeze_time -@pytest.fixture() +@pytest.fixture def frozen_time(): with freeze_time("2024-01-01T00:00:00Z") as frozen_time: yield frozen_time diff --git a/examples/city_to_capital_task.py b/examples/city_to_capital_task.py index b24fa3f..45880a0 100644 --- a/examples/city_to_capital_task.py +++ b/examples/city_to_capital_task.py @@ -1,6 +1,6 @@ -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field # pyright: ignore [reportUnknownVariableType] -from workflowai import Task, TaskVersionReference +from workflowai import Task, VersionReference class CityToCapitalTaskInput(BaseModel): @@ -12,7 +12,8 @@ class CityToCapitalTaskInput(BaseModel): class CityToCapitalTaskOutput(BaseModel): capital: str = Field( - description="The capital of the specified city", examples=["Tokyo"], + description="The capital of the specified city", + examples=["Tokyo"], ) @@ -22,6 +23,4 @@ class CityToCapitalTask(Task[CityToCapitalTaskInput, CityToCapitalTaskOutput]): input_class: type[CityToCapitalTaskInput] = CityToCapitalTaskInput output_class: type[CityToCapitalTaskOutput] = CityToCapitalTaskOutput - version: TaskVersionReference = TaskVersionReference( - iteration=4, - ) + version: VersionReference = 4 diff --git a/examples/import_example.py b/examples/import_example.py deleted file mode 100644 index 91eeb1b..0000000 --- a/examples/import_example.py +++ /dev/null @@ -1,33 +0,0 @@ -from asyncio import run as aiorun - -import typer -from rich import print as rprint - -import workflowai -from examples.city_to_capital_task import ( - CityToCapitalTask, - CityToCapitalTaskInput, - CityToCapitalTaskOutput, -) -from workflowai import TaskExample - - -def main(city: str, capital: str): - client = workflowai.start() - task = CityToCapitalTask() - - async def _inner(): - task_example = TaskExample( - task=task, - task_input=CityToCapitalTaskInput(city=city), - task_output=CityToCapitalTaskOutput(capital=capital), - ) - imported = await client.import_example(task_example) - - rprint(imported) - - aiorun(_inner()) - - -if __name__ == "__main__": - typer.run(main) diff --git a/examples/import_run.py b/examples/import_run.py deleted file mode 100644 index 624e74e..0000000 --- a/examples/import_run.py +++ /dev/null @@ -1,34 +0,0 @@ -from asyncio import run as aiorun - -import typer -from rich import print as rprint - -import workflowai -from examples.city_to_capital_task import ( - CityToCapitalTask, - CityToCapitalTaskInput, - CityToCapitalTaskOutput, -) -from workflowai import TaskRun, TaskVersion - - -def main(city: str, capital: str): - client = workflowai.start() - task = CityToCapitalTask() - - async def _inner(): - task_run = TaskRun( - task=task, - task_input=CityToCapitalTaskInput(city=city), - task_output=CityToCapitalTaskOutput(capital=capital), - version=TaskVersion(iteration=4), - ) - imported = await client.import_run(task_run) - - rprint(imported) - - aiorun(_inner()) - - -if __name__ == "__main__": - typer.run(main) diff --git a/poetry.lock b/poetry.lock index 4cc765a..564cdb1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,13 +13,13 @@ files = [ [[package]] name = "anyio" -version = "4.4.0" +version = "4.6.2.post1" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, - {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, + {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, + {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, ] [package.dependencies] @@ -29,9 +29,9 @@ sniffio = ">=1.1" typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.23)"] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +trio = ["trio (>=0.26.1)"] [[package]] name = "certifi" @@ -59,7 +59,7 @@ files = [ name = "click" version = "8.1.7" description = "Composable command line interface toolkit" -optional = true +optional = false python-versions = ">=3.7" files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, @@ -82,13 +82,13 @@ files = [ [[package]] name = "distlib" -version = "0.3.8" +version = "0.3.9" description = "Distribution utilities" optional = false python-versions = "*" files = [ - {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, - {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, ] [[package]] @@ -148,13 +148,13 @@ files = [ [[package]] name = "httpcore" -version = "1.0.5" +version = "1.0.7" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, - {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, + {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, + {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, ] [package.dependencies] @@ -165,7 +165,7 @@ h11 = ">=0.13,<0.15" asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<0.26.0)"] +trio = ["trio (>=0.22.0,<1.0)"] [[package]] name = "httpx" @@ -194,13 +194,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "identify" -version = "2.6.1" +version = "2.6.2" description = "File identification library for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, - {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, + {file = "identify-2.6.2-py2.py3-none-any.whl", hash = "sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3"}, + {file = "identify-2.6.2.tar.gz", hash = "sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd"}, ] [package.extras] @@ -235,7 +235,7 @@ files = [ name = "markdown-it-py" version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" -optional = true +optional = false python-versions = ">=3.8" files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, @@ -259,7 +259,7 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] name = "mdurl" version = "0.1.2" description = "Markdown URL utilities" -optional = true +optional = false python-versions = ">=3.7" files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, @@ -279,13 +279,13 @@ files = [ [[package]] name = "packaging" -version = "24.1" +version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] @@ -321,13 +321,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "3.8.0" +version = "4.0.1" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ - {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, - {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, + {file = "pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878"}, + {file = "pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2"}, ] [package.dependencies] @@ -339,22 +339,19 @@ virtualenv = ">=20.10.0" [[package]] name = "pydantic" -version = "2.9.2" +version = "2.10.0" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, - {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, + {file = "pydantic-2.10.0-py3-none-any.whl", hash = "sha256:5e7807ba9201bdf61b1b58aa6eb690916c40a47acfb114b1b4fef3e7fd5b30fc"}, + {file = "pydantic-2.10.0.tar.gz", hash = "sha256:0aca0f045ff6e2f097f1fe89521115335f15049eeb8a7bef3dafe4b19a74e289"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.23.4" -typing-extensions = [ - {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, - {version = ">=4.6.1", markers = "python_version < \"3.13\""}, -] +pydantic-core = "2.27.0" +typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] @@ -362,100 +359,111 @@ timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.23.4" +version = "2.27.0" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, - {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, - {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, - {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, - {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, - {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, - {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, - {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, - {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, - {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, - {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, - {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, - {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, - {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, - {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, - {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, - {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, - {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, - {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, - {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, - {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, - {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, - {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, - {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, - {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, - {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, - {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, - {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, - {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, - {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, - {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, - {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, - {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, - {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, - {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, - {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, - {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, + {file = "pydantic_core-2.27.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2ac6b919f7fed71b17fe0b4603c092a4c9b5bae414817c9c81d3c22d1e1bcc"}, + {file = "pydantic_core-2.27.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e015833384ca3e1a0565a79f5d953b0629d9138021c27ad37c92a9fa1af7623c"}, + {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db72e40628967f6dc572020d04b5f800d71264e0531c6da35097e73bdf38b003"}, + {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df45c4073bed486ea2f18757057953afed8dd77add7276ff01bccb79982cf46c"}, + {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:836a4bfe0cc6d36dc9a9cc1a7b391265bf6ce9d1eb1eac62ac5139f5d8d9a6fa"}, + {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bf1340ae507f6da6360b24179c2083857c8ca7644aab65807023cf35404ea8d"}, + {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ab325fc86fbc077284c8d7f996d904d30e97904a87d6fb303dce6b3de7ebba9"}, + {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1da0c98a85a6c6ed702d5556db3b09c91f9b0b78de37b7593e2de8d03238807a"}, + {file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7b0202ebf2268954090209a84f9897345719e46a57c5f2c9b7b250ca0a9d3e63"}, + {file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:35380671c3c921fe8adf31ad349dc6f7588b7e928dbe44e1093789734f607399"}, + {file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b4c19525c3538fbc0bbda6229f9682fb8199ce9ac37395880e6952798e00373"}, + {file = "pydantic_core-2.27.0-cp310-none-win32.whl", hash = "sha256:333c840a1303d1474f491e7be0b718226c730a39ead0f7dab2c7e6a2f3855555"}, + {file = "pydantic_core-2.27.0-cp310-none-win_amd64.whl", hash = "sha256:99b2863c1365f43f74199c980a3d40f18a218fbe683dd64e470199db426c4d6a"}, + {file = "pydantic_core-2.27.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4523c4009c3f39d948e01962223c9f5538602e7087a628479b723c939fab262d"}, + {file = "pydantic_core-2.27.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84af1cf7bfdcbc6fcf5a5f70cc9896205e0350306e4dd73d54b6a18894f79386"}, + {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e65466b31be1070b4a5b7dbfbd14b247884cb8e8b79c64fb0f36b472912dbaea"}, + {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a5c022bb0d453192426221605efc865373dde43b17822a264671c53b068ac20c"}, + {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bb69bf3b6500f195c3deb69c1205ba8fc3cb21d1915f1f158a10d6b1ef29b6a"}, + {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aa4d1b2eba9a325897308b3124014a142cdccb9f3e016f31d3ebee6b5ea5e75"}, + {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e96ca781e0c01e32115912ebdf7b3fb0780ce748b80d7d28a0802fa9fbaf44e"}, + {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b872c86d8d71827235c7077461c502feb2db3f87d9d6d5a9daa64287d75e4fa0"}, + {file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:82e1ad4ca170e8af4c928b67cff731b6296e6a0a0981b97b2eb7c275cc4e15bd"}, + {file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:eb40f828bc2f73f777d1eb8fee2e86cd9692a4518b63b6b5aa8af915dfd3207b"}, + {file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9a8fbf506fde1529a1e3698198fe64bfbe2e0c09557bc6a7dcf872e7c01fec40"}, + {file = "pydantic_core-2.27.0-cp311-none-win32.whl", hash = "sha256:24f984fc7762ed5f806d9e8c4c77ea69fdb2afd987b4fd319ef06c87595a8c55"}, + {file = "pydantic_core-2.27.0-cp311-none-win_amd64.whl", hash = "sha256:68950bc08f9735306322bfc16a18391fcaac99ded2509e1cc41d03ccb6013cfe"}, + {file = "pydantic_core-2.27.0-cp311-none-win_arm64.whl", hash = "sha256:3eb8849445c26b41c5a474061032c53e14fe92a11a5db969f722a2716cd12206"}, + {file = "pydantic_core-2.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8117839a9bdbba86e7f9df57018fe3b96cec934c3940b591b0fd3fbfb485864a"}, + {file = "pydantic_core-2.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a291d0b4243a259c8ea7e2b84eb9ccb76370e569298875a7c5e3e71baf49057a"}, + {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84e35afd9e10b2698e6f2f32256678cb23ca6c1568d02628033a837638b3ed12"}, + {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58ab0d979c969983cdb97374698d847a4acffb217d543e172838864636ef10d9"}, + {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d06b667e53320332be2bf6f9461f4a9b78092a079b8ce8634c9afaa7e10cd9f"}, + {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78f841523729e43e3928a364ec46e2e3f80e6625a4f62aca5c345f3f626c6e8a"}, + {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:400bf470e4327e920883b51e255617dfe4496d4e80c3fea0b5a5d0bf2c404dd4"}, + {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:951e71da6c89d354572098bada5ba5b5dc3a9390c933af8a614e37755d3d1840"}, + {file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a51ce96224eadd1845150b204389623c8e129fde5a67a84b972bd83a85c6c40"}, + {file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:483c2213a609e7db2c592bbc015da58b6c75af7360ca3c981f178110d9787bcf"}, + {file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:359e7951f04ad35111b5ddce184db3391442345d0ab073aa63a95eb8af25a5ef"}, + {file = "pydantic_core-2.27.0-cp312-none-win32.whl", hash = "sha256:ee7d9d5537daf6d5c74a83b38a638cc001b648096c1cae8ef695b0c919d9d379"}, + {file = "pydantic_core-2.27.0-cp312-none-win_amd64.whl", hash = "sha256:2be0ad541bb9f059954ccf8877a49ed73877f862529575ff3d54bf4223e4dd61"}, + {file = "pydantic_core-2.27.0-cp312-none-win_arm64.whl", hash = "sha256:6e19401742ed7b69e51d8e4df3c03ad5ec65a83b36244479fd70edde2828a5d9"}, + {file = "pydantic_core-2.27.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5f2b19b8d6fca432cb3acf48cf5243a7bf512988029b6e6fd27e9e8c0a204d85"}, + {file = "pydantic_core-2.27.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c86679f443e7085ea55a7376462553996c688395d18ef3f0d3dbad7838f857a2"}, + {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:510b11e9c3b1a852876d1ccd8d5903684336d635214148637ceb27366c75a467"}, + {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb704155e73b833801c247f39d562229c0303f54770ca14fb1c053acb376cf10"}, + {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ce048deb1e033e7a865ca384770bccc11d44179cf09e5193a535c4c2f497bdc"}, + {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58560828ee0951bb125c6f2862fbc37f039996d19ceb6d8ff1905abf7da0bf3d"}, + {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb4785894936d7682635726613c44578c420a096729f1978cd061a7e72d5275"}, + {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2883b260f7a93235488699d39cbbd94fa7b175d3a8063fbfddd3e81ad9988cb2"}, + {file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c6fcb3fa3855d583aa57b94cf146f7781d5d5bc06cb95cb3afece33d31aac39b"}, + {file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e851a051f7260e6d688267eb039c81f05f23a19431bd7dfa4bf5e3cb34c108cd"}, + {file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edb1bfd45227dec8d50bc7c7d86463cd8728bcc574f9b07de7369880de4626a3"}, + {file = "pydantic_core-2.27.0-cp313-none-win32.whl", hash = "sha256:678f66462058dd978702db17eb6a3633d634f7aa0deaea61e0a674152766d3fc"}, + {file = "pydantic_core-2.27.0-cp313-none-win_amd64.whl", hash = "sha256:d28ca7066d6cdd347a50d8b725dc10d9a1d6a1cce09836cf071ea6a2d4908be0"}, + {file = "pydantic_core-2.27.0-cp313-none-win_arm64.whl", hash = "sha256:6f4a53af9e81d757756508b57cae1cf28293f0f31b9fa2bfcb416cc7fb230f9d"}, + {file = "pydantic_core-2.27.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:e9f9feee7f334b72ceae46313333d002b56f325b5f04271b4ae2aadd9e993ae4"}, + {file = "pydantic_core-2.27.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:225bfff5d425c34e1fd562cef52d673579d59b967d9de06178850c4802af9039"}, + {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921ad596ff1a82f9c692b0758c944355abc9f0de97a4c13ca60ffc6d8dc15d4"}, + {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6354e18a9be37bfa124d6b288a87fb30c673745806c92956f1a25e3ae6e76b96"}, + {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ee4c2a75af9fe21269a4a0898c5425afb01af1f5d276063f57e2ae1bc64e191"}, + {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c91e3c04f5191fd3fb68764bddeaf02025492d5d9f23343b283870f6ace69708"}, + {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a6ebfac28fd51890a61df36ef202adbd77d00ee5aca4a3dadb3d9ed49cfb929"}, + {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:36aa167f69d8807ba7e341d67ea93e50fcaaf6bc433bb04939430fa3dab06f31"}, + {file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e8d89c276234579cd3d095d5fa2a44eb10db9a218664a17b56363cddf226ff3"}, + {file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:5cc822ab90a70ea3a91e6aed3afac570b276b1278c6909b1d384f745bd09c714"}, + {file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e15315691fe2253eb447503153acef4d7223dfe7e7702f9ed66539fcd0c43801"}, + {file = "pydantic_core-2.27.0-cp38-none-win32.whl", hash = "sha256:dfa5f5c0a4c8fced1422dc2ca7eefd872d5d13eb33cf324361dbf1dbfba0a9fe"}, + {file = "pydantic_core-2.27.0-cp38-none-win_amd64.whl", hash = "sha256:513cb14c0cc31a4dfd849a4674b20c46d87b364f997bbcb02282306f5e187abf"}, + {file = "pydantic_core-2.27.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:4148dc9184ab79e356dc00a4199dc0ee8647973332cb385fc29a7cced49b9f9c"}, + {file = "pydantic_core-2.27.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5fc72fbfebbf42c0856a824b8b0dc2b5cd2e4a896050281a21cfa6fed8879cb1"}, + {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:185ef205256cd8b38431205698531026979db89a79587725c1e55c59101d64e9"}, + {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:395e3e1148fa7809016231f8065f30bb0dc285a97b4dc4360cd86e17bab58af7"}, + {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33d14369739c5d07e2e7102cdb0081a1fa46ed03215e07f097b34e020b83b1ae"}, + {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7820bb0d65e3ce1e3e70b6708c2f66143f55912fa02f4b618d0f08b61575f12"}, + {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43b61989068de9ce62296cde02beffabcadb65672207fc51e7af76dca75e6636"}, + {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15e350efb67b855cd014c218716feea4986a149ed1f42a539edd271ee074a196"}, + {file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:433689845288f9a1ee5714444e65957be26d30915f7745091ede4a83cfb2d7bb"}, + {file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:3fd8bc2690e7c39eecdf9071b6a889ce7b22b72073863940edc2a0a23750ca90"}, + {file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:884f1806609c2c66564082540cffc96868c5571c7c3cf3a783f63f2fb49bd3cd"}, + {file = "pydantic_core-2.27.0-cp39-none-win32.whl", hash = "sha256:bf37b72834e7239cf84d4a0b2c050e7f9e48bced97bad9bdf98d26b8eb72e846"}, + {file = "pydantic_core-2.27.0-cp39-none-win_amd64.whl", hash = "sha256:31a2cae5f059329f9cfe3d8d266d3da1543b60b60130d186d9b6a3c20a346361"}, + {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4fb49cfdb53af5041aba909be00cccfb2c0d0a2e09281bf542371c5fd36ad04c"}, + {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:49633583eb7dc5cba61aaf7cdb2e9e662323ad394e543ee77af265736bcd3eaa"}, + {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:153017e3d6cd3ce979de06d84343ca424bb6092727375eba1968c8b4693c6ecb"}, + {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff63a92f6e249514ef35bc795de10745be0226eaea06eb48b4bbeaa0c8850a4a"}, + {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5982048129f40b082c2654de10c0f37c67a14f5ff9d37cf35be028ae982f26df"}, + {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:91bc66f878557313c2a6bcf396e7befcffe5ab4354cfe4427318968af31143c3"}, + {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:68ef5377eb582fa4343c9d0b57a5b094046d447b4c73dd9fbd9ffb216f829e7d"}, + {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c5726eec789ee38f2c53b10b1821457b82274f81f4f746bb1e666d8741fcfadb"}, + {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c0c431e4be5c1a0c6654e0c31c661cd89e0ca956ef65305c3c3fd96f4e72ca39"}, + {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8e21d927469d04b39386255bf00d0feedead16f6253dcc85e9e10ddebc334084"}, + {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b51f964fcbb02949fc546022e56cdb16cda457af485e9a3e8b78ac2ecf5d77e"}, + {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a7fd4de38f7ff99a37e18fa0098c3140286451bc823d1746ba80cec5b433a1"}, + {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fda87808429c520a002a85d6e7cdadbf58231d60e96260976c5b8f9a12a8e13"}, + {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a150392102c402c538190730fda06f3bce654fc498865579a9f2c1d2b425833"}, + {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c9ed88b398ba7e3bad7bd64d66cc01dcde9cfcb7ec629a6fd78a82fa0b559d78"}, + {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:9fe94d9d2a2b4edd7a4b22adcd45814b1b59b03feb00e56deb2e89747aec7bfe"}, + {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d8b5ee4ae9170e2775d495b81f414cc20268041c42571530513496ba61e94ba3"}, + {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d29e235ce13c91902ef3efc3d883a677655b3908b1cbc73dee816e5e1f8f7739"}, + {file = "pydantic_core-2.27.0.tar.gz", hash = "sha256:f57783fbaf648205ac50ae7d646f27582fc706be3977e87c3c124e7a92407b10"}, ] [package.dependencies] @@ -465,7 +473,7 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" name = "pygments" version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." -optional = true +optional = false python-versions = ">=3.8" files = [ {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, @@ -477,13 +485,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyright" -version = "1.1.383" +version = "1.1.389" description = "Command line wrapper for pyright" optional = false python-versions = ">=3.7" files = [ - {file = "pyright-1.1.383-py3-none-any.whl", hash = "sha256:d864d1182a313f45aaf99e9bfc7d2668eeabc99b29a556b5344894fd73cb1959"}, - {file = "pyright-1.1.383.tar.gz", hash = "sha256:1df7f12407f3710c9c6df938d98ec53f70053e6c6bbf71ce7bcb038d42f10070"}, + {file = "pyright-1.1.389-py3-none-any.whl", hash = "sha256:41e9620bba9254406dc1f621a88ceab5a88af4c826feb4f614d95691ed243a60"}, + {file = "pyright-1.1.389.tar.gz", hash = "sha256:716bf8cc174ab8b4dcf6828c3298cac05c5ed775dda9910106a5dcfe4c7fe220"}, ] [package.dependencies] @@ -519,17 +527,17 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments [[package]] name = "pytest-asyncio" -version = "0.23.8" +version = "0.24.0" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" files = [ - {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, - {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, + {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, + {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, ] [package.dependencies] -pytest = ">=7.0.0,<9" +pytest = ">=8.2,<9" [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] @@ -537,21 +545,21 @@ testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-httpx" -version = "0.30.0" +version = "0.34.0" description = "Send responses to httpx." optional = false python-versions = ">=3.9" files = [ - {file = "pytest-httpx-0.30.0.tar.gz", hash = "sha256:755b8edca87c974dd4f3605c374fda11db84631de3d163b99c0df5807023a19a"}, - {file = "pytest_httpx-0.30.0-py3-none-any.whl", hash = "sha256:6d47849691faf11d2532565d0c8e0e02b9f4ee730da31687feae315581d7520c"}, + {file = "pytest_httpx-0.34.0-py3-none-any.whl", hash = "sha256:42cf0a66f7b71b9111db2897e8b38a903abd33a27b11c48aff4a3c7650313af2"}, + {file = "pytest_httpx-0.34.0.tar.gz", hash = "sha256:3ca4b0975c0f93b985f17df19e76430c1086b5b0cce32b1af082d8901296a735"}, ] [package.dependencies] httpx = "==0.27.*" -pytest = ">=7,<9" +pytest = "==8.*" [package.extras] -testing = ["pytest-asyncio (==0.23.*)", "pytest-cov (==4.*)"] +testing = ["pytest-asyncio (==0.24.*)", "pytest-cov (==5.*)"] [[package]] name = "python-dateutil" @@ -645,54 +653,55 @@ files = [ [[package]] name = "rich" -version = "13.8.1" +version = "13.9.4" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = true -python-versions = ">=3.7.0" +optional = false +python-versions = ">=3.8.0" files = [ - {file = "rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06"}, - {file = "rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a"}, + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, ] [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.5.7" +version = "0.7.4" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"}, - {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"}, - {file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"}, - {file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"}, - {file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"}, - {file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"}, - {file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"}, + {file = "ruff-0.7.4-py3-none-linux_armv6l.whl", hash = "sha256:a4919925e7684a3f18e18243cd6bea7cfb8e968a6eaa8437971f681b7ec51478"}, + {file = "ruff-0.7.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfb365c135b830778dda8c04fb7d4280ed0b984e1aec27f574445231e20d6c63"}, + {file = "ruff-0.7.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:63a569b36bc66fbadec5beaa539dd81e0527cb258b94e29e0531ce41bacc1f20"}, + {file = "ruff-0.7.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d06218747d361d06fd2fdac734e7fa92df36df93035db3dc2ad7aa9852cb109"}, + {file = "ruff-0.7.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0cea28d0944f74ebc33e9f934238f15c758841f9f5edd180b5315c203293452"}, + {file = "ruff-0.7.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80094ecd4793c68b2571b128f91754d60f692d64bc0d7272ec9197fdd09bf9ea"}, + {file = "ruff-0.7.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:997512325c6620d1c4c2b15db49ef59543ef9cd0f4aa8065ec2ae5103cedc7e7"}, + {file = "ruff-0.7.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00b4cf3a6b5fad6d1a66e7574d78956bbd09abfd6c8a997798f01f5da3d46a05"}, + {file = "ruff-0.7.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7dbdc7d8274e1422722933d1edddfdc65b4336abf0b16dfcb9dedd6e6a517d06"}, + {file = "ruff-0.7.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e92dfb5f00eaedb1501b2f906ccabfd67b2355bdf117fea9719fc99ac2145bc"}, + {file = "ruff-0.7.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3bd726099f277d735dc38900b6a8d6cf070f80828877941983a57bca1cd92172"}, + {file = "ruff-0.7.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2e32829c429dd081ee5ba39aef436603e5b22335c3d3fff013cd585806a6486a"}, + {file = "ruff-0.7.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:662a63b4971807623f6f90c1fb664613f67cc182dc4d991471c23c541fee62dd"}, + {file = "ruff-0.7.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:876f5e09eaae3eb76814c1d3b68879891d6fde4824c015d48e7a7da4cf066a3a"}, + {file = "ruff-0.7.4-py3-none-win32.whl", hash = "sha256:75c53f54904be42dd52a548728a5b572344b50d9b2873d13a3f8c5e3b91f5cac"}, + {file = "ruff-0.7.4-py3-none-win_amd64.whl", hash = "sha256:745775c7b39f914238ed1f1b0bebed0b9155a17cd8bc0b08d3c87e4703b990d6"}, + {file = "ruff-0.7.4-py3-none-win_arm64.whl", hash = "sha256:11bff065102c3ae9d3ea4dc9ecdfe5a5171349cdd0787c1fc64761212fc9cf1f"}, + {file = "ruff-0.7.4.tar.gz", hash = "sha256:cd12e35031f5af6b9b93715d8c4f40360070b2041f81273d0527683d5708fce2"}, ] [[package]] name = "shellingham" version = "1.5.4" description = "Tool to Detect Surrounding Shell" -optional = true +optional = false python-versions = ">=3.7" files = [ {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, @@ -723,24 +732,24 @@ files = [ [[package]] name = "tomli" -version = "2.0.1" +version = "2.1.0" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, + {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, ] [[package]] name = "typer" -version = "0.12.5" +version = "0.13.1" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -optional = true +optional = false python-versions = ">=3.7" files = [ - {file = "typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b"}, - {file = "typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722"}, + {file = "typer-0.13.1-py3-none-any.whl", hash = "sha256:5b59580fd925e89463a29d363e0a43245ec02765bde9fb77d39e5d0f29dd7157"}, + {file = "typer-0.13.1.tar.gz", hash = "sha256:9d444cb96cc268ce6f8b94e13b4335084cef4c079998a9f4851a90229a3bd25c"}, ] [package.dependencies] @@ -762,13 +771,13 @@ files = [ [[package]] name = "virtualenv" -version = "20.26.5" +version = "20.27.1" description = "Virtual Python Environment builder" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "virtualenv-20.26.5-py3-none-any.whl", hash = "sha256:4f3ac17b81fba3ce3bd6f4ead2749a72da5929c01774948e243db9ba41df4ff6"}, - {file = "virtualenv-20.26.5.tar.gz", hash = "sha256:ce489cac131aa58f4b25e321d6d186171f78e6cb13fafbf32a840cee67733ff4"}, + {file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"}, + {file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"}, ] [package.dependencies] @@ -780,10 +789,7 @@ platformdirs = ">=3.9.1,<5" docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] -[extras] -cli = ["rich", "typer"] - [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "97ef914bc4530d0271923f6e79a413fe6e0543775e628374bdac7018e7dade1f" +content-hash = "81f3624451cc3d6628d5bc836a01afbc8b79bc64ff4700e824fa92a16a64ffb5" diff --git a/pyproject.toml b/pyproject.toml index f73edd5..458dae7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "workflowai" -version = "0.3.4" +version = "0.4.0-alpha.1" description = "" authors = ["Guillaume Aquilina "] readme = "README.md" @@ -10,21 +10,20 @@ exclude = ["**/*_test.py", "tests/**/*"] python = "^3.9" pydantic = "^2.7.4" httpx = "^0.27.0" -typer = { version = "^0.12.3", optional = true } -rich = { version = "^13.7.1", optional = true } + [tool.poetry.group.dev.dependencies] -pyright = "^1.1.381" +pyright = "^1.1.389" pytest = "^8.2.2" -pytest-asyncio = "^0.23.7" -ruff = "^0.5.0" +pytest-asyncio = "^0.24.0" +ruff = "^0.7.4" freezegun = "^1.5.1" -pre-commit = "^3.7.1" -pytest-httpx = "^0.30.0" +pre-commit = "^4.0.1" +pytest-httpx = "^0.34.0" python-dotenv = "^1.0.1" +typer = "^0.13.1" +rich = "^13.7.1" -[tool.poetry.extras] -cli = ["typer", "rich"] [tool.poetry.scripts] workflowai = "workflowai.cli.main:main" diff --git a/tests/e2e/deploy_test.py b/tests/e2e/deploy_test.py deleted file mode 100644 index 8ba62f8..0000000 --- a/tests/e2e/deploy_test.py +++ /dev/null @@ -1,45 +0,0 @@ -import uuid - -import workflowai -from examples.city_to_capital_task import CityToCapitalTask, CityToCapitalTaskInput -from workflowai.core.domain.task_version_reference import TaskVersionReference - - -async def test_deploy_task(wai: workflowai.Client): - task = CityToCapitalTask( - id=f"citytocapital-{uuid.uuid4()}", - schema_id=0, - version=TaskVersionReference.with_properties(model=""), - ) - - await wai.register(task) - - assert task.schema_id == 1 - - # Run task with input - task_run = await wai.run(task, task_input=CityToCapitalTaskInput(city="Osaka")) - assert task_run.task_output.capital == "Tokyo" - assert task_run.version.iteration == 1 - - # Deploy group to dev environnment - version = await wai.deploy_version(task, iteration=1, environment="dev") - assert version.iteration == 1 - assert version.aliases == {"environment=dev"} - - # Run using the environment and the same input - task_run2 = await wai.run( - task, - task_input=CityToCapitalTaskInput(city="Osaka"), - environment="dev", - ) - # IDs will match since we are using cache - assert task_run.id == task_run2.id - - # Run using the environment and a different input - task_run3 = await wai.run( - task, - task_input=CityToCapitalTaskInput(city="Toulouse"), - environment="dev", - ) - assert task_run3.task_output.capital == "Paris" - assert task_run3.id != task_run2.id diff --git a/tests/e2e/run_test.py b/tests/e2e/run_test.py new file mode 100644 index 0000000..90b2002 --- /dev/null +++ b/tests/e2e/run_test.py @@ -0,0 +1,52 @@ +from enum import Enum +from typing import Optional + +from pydantic import BaseModel + +import workflowai +from workflowai.core.domain.task import Task + + +class ExtractProductReviewSentimentTaskInput(BaseModel): + review_text: Optional[str] = None + + +class Sentiment(Enum): + POSITIVE = "POSITIVE" + NEGATIVE = "NEGATIVE" + NEUTRAL = "NEUTRAL" + MIXED = "MIXED" + UNKNOWN = "UNKNOWN" + + +class ExtractProductReviewSentimentTaskOutput(BaseModel): + sentiment: Optional[Sentiment] = None + + +class ExtractProductReviewSentimentTask( + Task[ExtractProductReviewSentimentTaskInput, ExtractProductReviewSentimentTaskOutput], +): + id: str = "extract-product-review-sentiment" + schema_id: int = 1 + input_class: type[ExtractProductReviewSentimentTaskInput] = ExtractProductReviewSentimentTaskInput + output_class: type[ExtractProductReviewSentimentTaskOutput] = ExtractProductReviewSentimentTaskOutput + + +async def test_run_task(wai: workflowai.Client): + task = ExtractProductReviewSentimentTask() + task_input = ExtractProductReviewSentimentTaskInput(review_text="This product is amazing!") + run = await wai.run(task, task_input=task_input, use_cache="never") + assert run.task_output.sentiment == Sentiment.POSITIVE + + +async def test_stream_task(wai: workflowai.Client): + task = ExtractProductReviewSentimentTask() + + task_input = ExtractProductReviewSentimentTaskInput( + review_text="This product is amazing!", + ) + + streamed = await wai.run(task, task_input=task_input, stream=True, use_cache="never") + chunks = [chunk async for chunk in streamed] + + assert len(chunks) > 1 diff --git a/tests/e2e/stream_test.py b/tests/e2e/stream_test.py deleted file mode 100644 index 3b235ec..0000000 --- a/tests/e2e/stream_test.py +++ /dev/null @@ -1,43 +0,0 @@ -from typing import Optional - -import pytest -from pydantic import BaseModel - -import workflowai -from workflowai.core.domain.task import Task - - -class ImprovePromptTaskInput(BaseModel): - original_prompt: Optional[str] = None - prompt_input: Optional[str] = None - prompt_output: Optional[str] = None - user_evaluation: Optional[str] = None - - -class ImprovePromptTaskOutput(BaseModel): - improved_prompt: Optional[str] = None - changelog: Optional[str] = None - - -class ImprovePromptTask(Task[ImprovePromptTaskInput, ImprovePromptTaskOutput]): - id: str = "improve-prompt" - schema_id: int = 3 - input_class: type[ImprovePromptTaskInput] = ImprovePromptTaskInput - output_class: type[ImprovePromptTaskOutput] = ImprovePromptTaskOutput - - -@pytest.mark.skip("This hits the API") -async def test_stream_task(wai: workflowai.Client): - task = ImprovePromptTask() - - task_input = ImprovePromptTaskInput( - original_prompt="Say hello to the guest", - prompt_input='{"guest": "John", "language": "French"}', - prompt_output='{"greeting": "Hello John"}', - user_evaluation="Not in the right language", - ) - - streamed = await wai.run(task, task_input=task_input, stream=True, use_cache="never") - chunks = [chunk async for chunk in streamed] - - assert len(chunks) > 1 diff --git a/tests/fixtures/task_run.json b/tests/fixtures/task_run.json index cf2a08f..74f0957 100644 --- a/tests/fixtures/task_run.json +++ b/tests/fixtures/task_run.json @@ -1,68 +1,22 @@ { - "id": "8f635b73-f403-47ee-bff9-18320616c6cc", - "task_id": "citytocapital", - "task_schema_id": 1, - "task_input": { - "name": "Houston" - }, - "task_input_hash": "403c3739ab1c20643336dde3ad2950bb", - "task_input_preview": "city: \"Houston\"", - "task_output": { - "message": "Austin" - }, - "task_output_hash": "41a00820c2d20f738de5cc6bcb02b550", - "task_output_preview": "capital: \"Austin\"", - "group": { - "id": "a037be0c5f716bcf55dce3176985026d", - "iteration": 4, - "properties": { - "model": "gpt-4o-2024-05-13", - "provider": "openai", - "temperature": 0, - "instructions": "", - "max_tokens": null, - "runner_name": "WorkflowAI", - "runner_version": "e7fe9d4850d8b8189ad38f51b1cf30e5", - "few_shot": null, - "task_schema_id": 1, - "task_variant_id": "4cfd201057b7fe56c00a8de5cbf42f1f" - }, - "tags": [ - "model=gpt-4o-2024-05-13", - "provider=openai", - "temperature=0" - ], - "aliases": null, - "is_external": null - }, - "start_time": "2024-05-31T01:38:47.887000Z", - "end_time": "2024-05-31T01:38:48.688000Z", - "duration_seconds": 0.800467, - "cost_usd": 0.0014550000000000001, - "created_at": "2024-05-31T01:38:48.688000Z", - "example_id": "663bc35f4957be930844dc35", - "corrections": null, - "parent_task_ids": null, - "scores": null, - "labels": [ - "bid=66592a1f4c1b7ce40bac3c59" - ], - "metadata": null, - "llm_completions": [ - { - "messages": [ - { - "role": "system", - "content": "Your goal is to generate a valid output based on the input data.\n\nThe task instructions are:\n\n\nThe input that will be passed in the user message is a json following the schema:\n```json\n{\n \"properties\": {\n \"city\": {\n \"description\": \"The name of the city for which the capital is to be found\",\n \"examples\": [\n \"Tokyo\"\n ],\n \"title\": \"City\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"city\"\n ],\n \"title\": \"CityToCapitalTaskInput\",\n \"type\": \"object\"\n}\n```\nThe output that you will return must be a JSON object equivalent to the following json shema:\n```json\n{\n \"properties\": {\n \"capital\": {\n \"description\": \"The capital of the specified city\",\n \"examples\": [\n \"Tokyo\"\n ],\n \"title\": \"Capital\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"capital\"\n ],\n \"title\": \"CityToCapitalTaskOutput\",\n \"type\": \"object\"\n}\n```" - }, - { - "role": "user", - "content": "Input data:\n```json\n{\n \"city\": \"Houston\"\n}\n```" - } - ], - "response": "{\n \"capital\": \"Austin\"\n}", - "usage": null - } - ], - "config_id": null + "id": "8f635b73-f403-47ee-bff9-18320616c6cc", + "task_output": { + "message": "Austin" + }, + "version": { + "properties": { + "model": "gpt-4o-2024-05-13", + "provider": "openai", + "temperature": 0, + "instructions": "", + "max_tokens": null, + "runner_name": "WorkflowAI", + "runner_version": "e7fe9d4850d8b8189ad38f51b1cf30e5", + "few_shot": null, + "task_schema_id": 1, + "task_variant_id": "4cfd201057b7fe56c00a8de5cbf42f1f" + } + }, + "duration_seconds": 0.800467, + "cost_usd": 0.0014550000000000001 } diff --git a/tests/fixtures/task_run_float_usage.json b/tests/fixtures/task_run_float_usage.json deleted file mode 100644 index 18d0899..0000000 --- a/tests/fixtures/task_run_float_usage.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "id": "e3683ced-efd2-4b15-9ab4-aefdf17c4c19", - "task_id": "generate-changelog-from-properties", - "task_schema_id": 1, - "task_input": { - "temperature": 1 - }, - "task_input_hash": "6faebdc55135f380b00e7ada53f5cccc", - "task_input_preview": "old_task_group: {properties: {temperature: 1, instructions: \"Add 5 to the n", - "task_output": { - "changes": [ - "Temperature decreased from Creative to 0.73" - ] - }, - "task_output_hash": "d057c1f26fd8447c11cda7300e0f3717", - "task_output_preview": "changes: [\"Temperature decreased from Creative to 0.73\"]", - "group": { - "id": "68eead780d01791ff2e09d39055ae6e8", - "iteration": 36, - "properties": { - "model": "gemini-1.5-flash-002", - "provider": "google", - "temperature": 0, - "instructions": "", - "max_tokens": null, - "runner_name": "WorkflowAI", - "runner_version": "v0.1.0", - "few_shot": null, - "template_name": "v1", - "task_variant_id": "fa546275ed8f6c801d6c6f174828d615" - }, - "tags": [ - "model=gemini-1.5-flash-002", - "provider=google", - "temperature=0" - ], - "aliases": null, - "is_external": null, - "is_favorite": null, - "notes": null, - "similarity_hash": "", - "benchmark_for_datasets": null - }, - "status": "success", - "error": null, - "start_time": "2024-10-01T17:55:06.241000Z", - "end_time": "2024-10-01T17:55:07.879000Z", - "duration_seconds": 1.638103, - "cost_usd": 0.00004651875, - "created_at": "2024-10-01T17:55:07.879000Z", - "updated_at": "2024-10-01T17:55:07.879000Z", - "example_id": null, - "corrections": null, - "parent_task_ids": null, - "scores": null, - "labels": null, - "metadata": { - "used_alias": "environment=production" - }, - "llm_completions": [ - { - "messages": [ - { - "role": "system", - "content": "" - }, - { - "role": "user", - "content": "" - } - ], - "response": "{\"changes\": [\"Temperature decreased from Creative to 0.73\"]}", - "usage": { - "completion_token_count": 13.5, - "completion_cost_usd": 0.00000405, - "prompt_token_count": 566.25, - "prompt_cost_usd": 0.00004246875 - } - } - ], - "config_id": null, - "dataset_benchmark_ids": null, - "is_free": null, - "author_tenant": null -} diff --git a/workflowai/__init__.py b/workflowai/__init__.py index e313a85..9d96059 100644 --- a/workflowai/__init__.py +++ b/workflowai/__init__.py @@ -3,14 +3,10 @@ from workflowai.core.client import Client as Client from workflowai.core.domain.cache_usage import CacheUsage as CacheUsage from workflowai.core.domain.errors import WorkflowAIError as WorkflowAIError -from workflowai.core.domain.llm_completion import LLMCompletion as LLMCompletion from workflowai.core.domain.task import Task as Task -from workflowai.core.domain.task_evaluation import TaskEvaluation as TaskEvaluation -from workflowai.core.domain.task_example import TaskExample as TaskExample -from workflowai.core.domain.task_run import TaskRun as TaskRun from workflowai.core.domain.task_version import TaskVersion as TaskVersion from workflowai.core.domain.task_version_reference import ( - TaskVersionReference as TaskVersionReference, + VersionReference as VersionReference, ) diff --git a/workflowai/cli/__init__.py b/workflowai/cli/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/workflowai/cli/__pycache__/cli.cpython-312.pyc b/workflowai/cli/__pycache__/cli.cpython-312.pyc deleted file mode 100644 index 7d32226..0000000 Binary files a/workflowai/cli/__pycache__/cli.cpython-312.pyc and /dev/null differ diff --git a/workflowai/cli/__pycache__/main.cpython-312.pyc b/workflowai/cli/__pycache__/main.cpython-312.pyc deleted file mode 100644 index 4d5f4be..0000000 Binary files a/workflowai/cli/__pycache__/main.cpython-312.pyc and /dev/null differ diff --git a/workflowai/cli/main.py b/workflowai/cli/main.py deleted file mode 100644 index 336e825..0000000 --- a/workflowai/cli/main.py +++ /dev/null @@ -1,2 +0,0 @@ -def main(): - pass diff --git a/workflowai/core/client/__init__.py b/workflowai/core/client/__init__.py index a5feed5..9865f9f 100644 --- a/workflowai/core/client/__init__.py +++ b/workflowai/core/client/__init__.py @@ -1,75 +1,51 @@ from typing import Any, AsyncIterator, Literal, Optional, Protocol, Union, overload -from workflowai.core.domain import ( - cache_usage, - task, - task_example, - task_run, - task_version, - task_version_reference, -) +from workflowai.core.domain.cache_usage import CacheUsage +from workflowai.core.domain.task import Task, TaskInput, TaskOutput +from workflowai.core.domain.task_run import Run, RunChunk +from workflowai.core.domain.task_version_reference import VersionReference class Client(Protocol): """A client to interact with the WorkflowAI API""" - async def register(self, task: "task.Task[task.TaskInput, task.TaskOutput]"): - """Register a task, creating a new task if needed and setting the task schema id - - Args: - task (task.Task[task.TaskInput, task.TaskOutput]): a task - """ - ... - @overload async def run( self, - task: "task.Task[task.TaskInput, task.TaskOutput]", - task_input: "task.TaskInput", - version: Optional["task_version_reference.TaskVersionReference"] = None, - environment: Optional[str] = None, - iteration: Optional[int] = None, + task: Task[TaskInput, TaskOutput], + task_input: TaskInput, stream: Literal[False] = False, - use_cache: "cache_usage.CacheUsage" = "when_available", - labels: Optional[set[str]] = None, + version: Optional[VersionReference] = None, + use_cache: CacheUsage = "when_available", metadata: Optional[dict[str, Any]] = None, max_retry_delay: float = 60, max_retry_count: float = 1, - ) -> "task_run.TaskRun[task.TaskInput, task.TaskOutput]": ... + ) -> Run[TaskOutput]: ... @overload async def run( self, - task: "task.Task[task.TaskInput, task.TaskOutput]", - task_input: "task.TaskInput", - version: Optional["task_version_reference.TaskVersionReference"] = None, - environment: Optional[str] = None, - iteration: Optional[int] = None, + task: Task[TaskInput, TaskOutput], + task_input: TaskInput, stream: Literal[True] = True, - use_cache: "cache_usage.CacheUsage" = "when_available", - labels: Optional[set[str]] = None, + version: Optional[VersionReference] = None, + use_cache: CacheUsage = "when_available", metadata: Optional[dict[str, Any]] = None, max_retry_delay: float = 60, max_retry_count: float = 1, - ) -> AsyncIterator["task.TaskOutput"]: ... + ) -> AsyncIterator[Union[RunChunk[TaskOutput], Run[TaskOutput]]]: ... async def run( self, - task: "task.Task[task.TaskInput, task.TaskOutput]", - task_input: "task.TaskInput", - version: Optional["task_version_reference.TaskVersionReference"] = None, - environment: Optional[str] = None, - iteration: Optional[int] = None, + task: Task[TaskInput, TaskOutput], + task_input: TaskInput, stream: bool = False, - use_cache: "cache_usage.CacheUsage" = "when_available", - labels: Optional[set[str]] = None, + version: Optional[VersionReference] = None, + use_cache: CacheUsage = "when_available", metadata: Optional[dict[str, Any]] = None, max_retry_delay: float = 60, max_retry_count: float = 1, - ) -> Union[ - "task_run.TaskRun[task.TaskInput, task.TaskOutput]", - AsyncIterator["task.TaskOutput"], - ]: + ) -> Union[Run[TaskOutput], AsyncIterator[Union[RunChunk[TaskOutput], Run[TaskOutput]]]]: """Run a task Args: @@ -97,49 +73,3 @@ async def run( or an async iterator of output objects """ ... - - async def import_run( - self, - run: "task_run.TaskRun[task.TaskInput, task.TaskOutput]", - ) -> "task_run.TaskRun[task.TaskInput, task.TaskOutput]": - """Import a task run - - Args: - run (task_run.TaskRun[task.TaskInput, task.TaskOutput]): a task run - - Returns: - task_run.TaskRun[task.TaskInput, task.TaskOutput]: the task run as stored in our database - """ - ... - - async def import_example( - self, - example: "task_example.TaskExample[task.TaskInput, task.TaskOutput]", - ) -> "task_example.TaskExample[task.TaskInput, task.TaskOutput]": - """Import a task example - - Args: - example (task_example.TaskExample[task.TaskInput, task.TaskOutput]): a task example - - Returns: - task_example.TaskExample[task.TaskInput, task.TaskOutput]: the task example as stored in our database - """ - ... - - async def deploy_version( - self, - task: "task.Task[task.TaskInput, task.TaskOutput]", - iteration: int, - environment: str, - ) -> "task_version.TaskVersion": - """Deploy a version to an environemnt. Version becomes usable using TaskVersionReference(environment=...) - - Args: - task (task.Task[task.TaskInput, task.TaskOutput]): the task to deploy - reference (task_version_reference.TaskVersionReference): the version to deploy - environment (str): the environment to deploy to - - Returns: - task_version.TaskVersion: the deployed version - """ - ... diff --git a/workflowai/core/client/client.py b/workflowai/core/client/client.py index fc76619..4bdb912 100644 --- a/workflowai/core/client/client.py +++ b/workflowai/core/client/client.py @@ -10,29 +10,22 @@ overload, ) +from workflowai.core.client import Client from workflowai.core.client.api import APIClient from workflowai.core.client.models import ( - CreateTaskRequest, - CreateTaskResponse, - ExampleResponse, - ImportExampleRequest, - ImportRunRequest, - PatchGroupRequest, RunRequest, - RunTaskStreamChunk, - TaskRunResponse, + RunResponse, + RunStreamChunk, ) from workflowai.core.client.utils import build_retryable_wait from workflowai.core.domain.cache_usage import CacheUsage from workflowai.core.domain.errors import BaseError, WorkflowAIError from workflowai.core.domain.task import Task, TaskInput, TaskOutput -from workflowai.core.domain.task_example import TaskExample -from workflowai.core.domain.task_run import TaskRun -from workflowai.core.domain.task_version import TaskVersion -from workflowai.core.domain.task_version_reference import TaskVersionReference +from workflowai.core.domain.task_run import Run, RunChunk +from workflowai.core.domain.task_version_reference import VersionReference -class WorkflowAIClient: +class WorkflowAIClient(Client): def __init__(self, endpoint: Optional[str] = None, api_key: Optional[str] = None): self.additional_headers = { "x-workflowai-source": "sdk", @@ -40,96 +33,57 @@ def __init__(self, endpoint: Optional[str] = None, api_key: Optional[str] = None "x-workflowai-version": importlib.metadata.version("workflowai"), } self.api = APIClient( - endpoint or os.getenv("WORKFLOWAI_API_URL", "https://api.workflowai.com"), + endpoint or os.getenv("WORKFLOWAI_API_URL", "https://run.workflowai.com"), api_key or os.getenv("WORKFLOWAI_API_KEY", ""), self.additional_headers, ) - async def register(self, task: Task[TaskInput, TaskOutput]): - request = CreateTaskRequest( - task_id=task.id or None, - name=task.__class__.__name__.removesuffix("Task"), - input_schema=task.input_class.model_json_schema(), - output_schema=task.output_class.model_json_schema(), - ) - - res = await self.api.post("/tasks", request, returns=CreateTaskResponse) - - task.id = res.task_id - task.schema_id = res.task_schema_id - task.created_at = res.created_at - - async def _auto_register(self, task: Task[TaskInput, TaskOutput]): - if not task.id or not task.schema_id: - await self.register(task) - @overload async def run( self, task: Task[TaskInput, TaskOutput], task_input: TaskInput, - version: Optional[TaskVersionReference] = None, - environment: Optional[str] = None, - iteration: Optional[int] = None, stream: Literal[False] = False, + version: Optional[VersionReference] = None, use_cache: CacheUsage = "when_available", - labels: Optional[set[str]] = None, metadata: Optional[dict[str, Any]] = None, max_retry_delay: float = 60, max_retry_count: float = 1, - ) -> TaskRun[TaskInput, TaskOutput]: ... + ) -> Run[TaskOutput]: ... @overload async def run( self, task: Task[TaskInput, TaskOutput], task_input: TaskInput, - version: Optional[TaskVersionReference] = None, - environment: Optional[str] = None, - iteration: Optional[int] = None, stream: Literal[True] = True, + version: Optional[VersionReference] = None, use_cache: CacheUsage = "when_available", - labels: Optional[set[str]] = None, metadata: Optional[dict[str, Any]] = None, max_retry_delay: float = 60, max_retry_count: float = 1, - ) -> AsyncIterator[TaskOutput]: ... + ) -> AsyncIterator[Union[RunChunk[TaskOutput], Run[TaskOutput]]]: ... async def run( self, task: Task[TaskInput, TaskOutput], task_input: TaskInput, - version: Optional[TaskVersionReference] = None, - environment: Optional[str] = None, - iteration: Optional[int] = None, stream: bool = False, + version: Optional[VersionReference] = None, use_cache: CacheUsage = "when_available", - labels: Optional[set[str]] = None, metadata: Optional[dict[str, Any]] = None, max_retry_delay: float = 60, max_retry_count: float = 1, - ) -> Union[TaskRun[TaskInput, TaskOutput], AsyncIterator[TaskOutput]]: - await self._auto_register(task) - - if version: - group_ref = version - elif environment: - group_ref = TaskVersionReference(alias=f"environment={environment}") - elif iteration: - group_ref = TaskVersionReference(iteration=iteration) - else: - group_ref = task.version - + ) -> Union[Run[TaskOutput], AsyncIterator[Union[RunChunk[TaskOutput], Run[TaskOutput]]]]: request = RunRequest( - task_input=task_input.model_dump(), - group=group_ref, + task_input=task_input.model_dump(by_alias=True), + version=version or task.version, stream=stream, use_cache=use_cache, - labels=labels, metadata=metadata, ) - route = f"/tasks/{task.id}/schemas/{task.schema_id}/run" + route = f"/v1/_/tasks/{task.id}/schemas/{task.schema_id}/run" should_retry, wait_for_exception = build_retryable_wait(max_retry_delay, max_retry_count) if not stream: @@ -160,7 +114,7 @@ async def _retriable_run( last_error = None while should_retry(): try: - res = await self.api.post(route, request, returns=TaskRunResponse) + res = await self.api.post(route, request, returns=RunResponse) return res.to_domain(task) except WorkflowAIError as e: # noqa: PERF203 last_error = e @@ -182,44 +136,9 @@ async def _retriable_stream( method="POST", path=route, data=request, - returns=RunTaskStreamChunk, + returns=RunStreamChunk, ): - yield task.output_class.model_construct(None, **chunk.task_output) + yield chunk.to_domain(task) return except WorkflowAIError as e: # noqa: PERF203 await wait_for_exception(e) - - async def import_run( - self, - run: TaskRun[TaskInput, TaskOutput], - ) -> TaskRun[TaskInput, TaskOutput]: - await self._auto_register(run.task) - - request = ImportRunRequest.from_domain(run) - route = f"/tasks/{run.task.id}/schemas/{run.task.schema_id}/runs" - res = await self.api.post(route, request, returns=TaskRunResponse) - return res.to_domain(run.task) - - async def import_example( - self, - example: TaskExample[TaskInput, TaskOutput], - ) -> TaskExample[TaskInput, TaskOutput]: - await self._auto_register(example.task) - - request = ImportExampleRequest.from_domain(example) - route = f"/tasks/{example.task.id}/schemas/{example.task.schema_id}/examples" - res = await self.api.post(route, request, returns=ExampleResponse) - return res.to_domain(example.task) - - async def deploy_version( - self, - task: Task[TaskInput, TaskOutput], - iteration: int, - environment: str, - ) -> TaskVersion: - await self._auto_register(task) - - route = f"/tasks/{task.id}/schemas/{task.schema_id}/groups/{iteration}" - req = PatchGroupRequest(add_alias=f"environment={environment}") - res = await self.api.patch(route, req, returns=TaskVersion) - return res diff --git a/workflowai/core/client/client_test.py b/workflowai/core/client/client_test.py index fea22d4..d5b1ca6 100644 --- a/workflowai/core/client/client_test.py +++ b/workflowai/core/client/client_test.py @@ -8,39 +8,14 @@ from tests.utils import fixtures_json from workflowai.core.client import Client from workflowai.core.client.client import WorkflowAIClient -from workflowai.core.domain.llm_completion import LLMCompletion -from workflowai.core.domain.task_example import TaskExample -from workflowai.core.domain.task_run import TaskRun -from workflowai.core.domain.task_version import TaskVersion +from workflowai.core.domain.task_run import Run -@pytest.fixture() +@pytest.fixture def client(): return WorkflowAIClient(endpoint="http://localhost:8000", api_key="test") -class TestRegister: - async def test_success(self, httpx_mock: HTTPXMock, client: WorkflowAIClient): - httpx_mock.add_response( - json={ - "task_id": "123", - "task_schema_id": 1, - "created_at": "2022-01-01T00:00:00Z", - "name": "Hello", - "input_schema": {"version": "1", "json_schema": {}}, - "output_schema": {"version": "1", "json_schema": {}}, - }, - ) - task = HelloTask() - assert task.id == "", "sanity" - assert task.schema_id == 0, "sanity" - - await client.register(task) - - assert task.id == "123" - assert task.schema_id == 1 - - class TestRun: async def test_success(self, httpx_mock: HTTPXMock, client: Client): httpx_mock.add_response(json=fixtures_json("task_run.json")) @@ -52,12 +27,12 @@ async def test_success(self, httpx_mock: HTTPXMock, client: Client): reqs = httpx_mock.get_requests() assert len(reqs) == 1 - assert reqs[0].url == "http://localhost:8000/tasks/123/schemas/1/run" + assert reqs[0].url == "http://localhost:8000/v1/_/tasks/123/schemas/1/run" body = json.loads(reqs[0].content) assert body == { "task_input": {"name": "Alice"}, - "group": {"properties": {}}, + "version": "production", "stream": False, "use_cache": "when_available", } @@ -66,9 +41,9 @@ async def test_stream(self, httpx_mock: HTTPXMock, client: Client): httpx_mock.add_response( stream=IteratorStream( [ - b'data: {"run_id":"1","task_output":{"message":""}}', - b'data: {"run_id":"1","task_output":{"message":"hel"}}', - b'data: {"run_id":"1","task_output":{"message":"hello"}}', + b'data: {"id":"1","task_output":{"message":""}}\n\n', + b'data: {"id":"1","task_output":{"message":"hel"}}\n\ndata: {"id":"1","task_output":{"message":"hello"}}\n\n', # noqa: E501 + b'data: {"id":"1","task_output":{"message":"hello"},"version":{"properties":{"model":"gpt-4o","temperature":0.5}},"cost_usd":0.01,"duration_seconds":10.1}\n\n', # noqa: E501 ], ), ) @@ -81,11 +56,19 @@ async def test_stream(self, httpx_mock: HTTPXMock, client: Client): ) chunks = [chunk async for chunk in streamed] - assert chunks == [ + outputs = [chunk.task_output for chunk in chunks] + assert outputs == [ HelloTaskOutput(message=""), HelloTaskOutput(message="hel"), HelloTaskOutput(message="hello"), + HelloTaskOutput(message="hello"), ] + last_message = chunks[-1] + assert isinstance(last_message, Run) + assert last_message.version.properties.model == "gpt-4o" + assert last_message.version.properties.temperature == 0.5 + assert last_message.cost_usd == 0.01 + assert last_message.duration_seconds == 10.1 async def test_run_with_env(self, httpx_mock: HTTPXMock, client: Client): httpx_mock.add_response(json=fixtures_json("task_run.json")) @@ -94,17 +77,17 @@ async def test_run_with_env(self, httpx_mock: HTTPXMock, client: Client): await client.run( task, task_input=HelloTaskInput(name="Alice"), - environment="dev", + version="dev", ) reqs = httpx_mock.get_requests() assert len(reqs) == 1 - assert reqs[0].url == "http://localhost:8000/tasks/123/schemas/1/run" + assert reqs[0].url == "http://localhost:8000/v1/_/tasks/123/schemas/1/run" body = json.loads(reqs[0].content) assert body == { "task_input": {"name": "Alice"}, - "group": {"alias": "environment=dev"}, + "version": "dev", "stream": False, "use_cache": "when_available", } @@ -119,7 +102,7 @@ async def test_success_with_headers(self, httpx_mock: HTTPXMock, client: Client) reqs = httpx_mock.get_requests() assert len(reqs) == 1 - assert reqs[0].url == "http://localhost:8000/tasks/123/schemas/1/run" + assert reqs[0].url == "http://localhost:8000/v1/_/tasks/123/schemas/1/run" headers = { "x-workflowai-source": "sdk", "x-workflowai-language": "python", @@ -129,7 +112,7 @@ async def test_success_with_headers(self, httpx_mock: HTTPXMock, client: Client) body = json.loads(reqs[0].content) assert body == { "task_input": {"name": "Alice"}, - "group": {"properties": {}}, + "version": "production", "stream": False, "use_cache": "when_available", } @@ -149,89 +132,5 @@ async def test_run_retries_on_too_many_requests(self, httpx_mock: HTTPXMock, cli reqs = httpx_mock.get_requests() assert len(reqs) == 2 - assert reqs[0].url == "http://localhost:8000/tasks/123/schemas/1/run" - assert reqs[1].url == "http://localhost:8000/tasks/123/schemas/1/run" - - -class TestImportRun: - async def test_success(self, httpx_mock: HTTPXMock, client: Client): - httpx_mock.add_response(json=fixtures_json("task_run.json")) - task = HelloTask(id="123", schema_id=1) - - run = TaskRun( - task=task, - task_input=HelloTaskInput(name="Alice"), - task_output=HelloTaskOutput(message="hello"), - version=TaskVersion(iteration=1), - llm_completions=[ - LLMCompletion(messages=[{"content": "hello"}], response="world"), - ], - ) - - imported = await client.import_run(run) - assert imported.id == "8f635b73-f403-47ee-bff9-18320616c6cc" - - reqs = httpx_mock.get_requests() - assert len(reqs) == 1 - assert reqs[0].url == "http://localhost:8000/tasks/123/schemas/1/runs" - - body = json.loads(reqs[0].content) - assert body == { - "id": run.id, - "task_input": {"name": "Alice"}, - "task_output": {"message": "hello"}, - "group": {"iteration": 1}, - "llm_completions": [ - { - "messages": [ - { - "content": "hello", - }, - ], - "response": "world", - }, - ], - } - - -class TestImportExample: - async def test_success(self, httpx_mock: HTTPXMock, client: Client): - httpx_mock.add_response(json=fixtures_json("task_example.json")) - task = HelloTask(id="123", schema_id=1) - - example = TaskExample( - task=task, - task_input=HelloTaskInput(name="Alice"), - task_output=HelloTaskOutput(message="hello"), - ) - - imported = await client.import_example(example) - assert imported.id == "8f635b73-f403-47ee-bff9-18320616c6cc" - - reqs = httpx_mock.get_requests() - assert len(reqs) == 1 - assert reqs[0].url == "http://localhost:8000/tasks/123/schemas/1/examples" - - body = json.loads(reqs[0].content) - assert body == { - "task_input": {"name": "Alice"}, - "task_output": {"message": "hello"}, - } - - -class TestDeployVersion: - async def test_success(self, httpx_mock: HTTPXMock, client: Client): - httpx_mock.add_response(json=fixtures_json("task_version.json")) - task = HelloTask(id="123", schema_id=1) - - version = await client.deploy_version(task, iteration=1, environment="dev") - assert version.iteration == 1 - - reqs = httpx_mock.get_requests() - assert len(reqs) == 1 - assert reqs[0].url == "http://localhost:8000/tasks/123/schemas/1/groups/1" - - body = json.loads(reqs[0].content) - assert body == { - "add_alias": "environment=dev", - } + assert reqs[0].url == "http://localhost:8000/v1/_/tasks/123/schemas/1/run" + assert reqs[1].url == "http://localhost:8000/v1/_/tasks/123/schemas/1/run" diff --git a/workflowai/core/client/models.py b/workflowai/core/client/models.py index 4ca3b29..0be02d3 100644 --- a/workflowai/core/client/models.py +++ b/workflowai/core/client/models.py @@ -1,171 +1,88 @@ -from datetime import datetime -from typing import Any, Optional +from typing import Any, Optional, Union -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel +from typing_extensions import NotRequired, TypedDict from workflowai.core.domain.cache_usage import CacheUsage -from workflowai.core.domain.llm_completion import LLMCompletion from workflowai.core.domain.task import Task, TaskInput, TaskOutput -from workflowai.core.domain.task_evaluation import TaskEvaluation -from workflowai.core.domain.task_example import TaskExample -from workflowai.core.domain.task_run import TaskRun +from workflowai.core.domain.task_run import Run, RunChunk from workflowai.core.domain.task_version import TaskVersion -from workflowai.core.domain.task_version_reference import TaskVersionReference +from workflowai.core.domain.task_version_properties import TaskVersionProperties -class CreateTaskRequest(BaseModel): - name: str - input_schema: dict[str, Any] - output_schema: dict[str, Any] - task_id: Optional[str] = None - - -class CreateTaskResponse(BaseModel): - task_id: str = Field(description="the task id, stable accross all versions") - task_schema_id: int = Field( - description="""The task schema idx. The schema index only changes when the types - of the input / ouput objects change so all task versions with the same schema idx - have compatible input / output objects. Read only""", - ) - name: str = Field(description="the task display name") - - class VersionedSchema(BaseModel): - version: str - json_schema: dict[str, Any] - - input_schema: VersionedSchema - output_schema: VersionedSchema - - created_at: datetime - - -class _RunRequestCommon(BaseModel): +class RunRequest(BaseModel): task_input: dict[str, Any] - group: TaskVersionReference - - id: Optional[str] = None + version: Union[str, int] - labels: Optional[set[str]] + use_cache: Optional[CacheUsage] = None - metadata: Optional[dict[str, Any]] + metadata: Optional[dict[str, Any]] = None + private_fields: Optional[set[str]] = None -class RunRequest(_RunRequestCommon): - stream: bool = False + stream: Optional[bool] = None - use_cache: Optional[CacheUsage] +# Not using a base model to avoid validation +class VersionProperties(TypedDict): + model: NotRequired[Optional[str]] + provider: NotRequired[Optional[str]] + temperature: NotRequired[Optional[float]] + instructions: NotRequired[Optional[str]] -class ImportRunRequest(_RunRequestCommon): - task_output: dict[str, Any] - llm_completions: Optional[list[LLMCompletion]] = None - cost_usd: Optional[float] = None - start_time: Optional[datetime] = None - end_time: Optional[datetime] = None - - @classmethod - def from_domain(cls, task_run: TaskRun[TaskInput, TaskOutput]): - return cls( - id=task_run.id, - task_input=task_run.task_input.model_dump(mode="json"), - task_output=task_run.task_output.model_dump(mode="json"), - group=TaskVersionReference.from_version(task_run.version), - labels=task_run.labels, - metadata=task_run.metadata, - llm_completions=task_run.llm_completions, - cost_usd=task_run.cost_usd, - start_time=task_run.start_time, - end_time=task_run.end_time, - ) - -class RunTaskStreamChunk(BaseModel): - run_id: str - task_output: dict[str, Any] +class Version(BaseModel): + properties: VersionProperties -class TaskRunResponse(BaseModel): +class RunResponse(BaseModel): id: str - task_id: str - task_schema_id: int - task_input: dict[str, Any] task_output: dict[str, Any] - group: TaskVersion - start_time: Optional[datetime] = None - end_time: Optional[datetime] = None + version: Version duration_seconds: Optional[float] = None cost_usd: Optional[float] = None - created_at: datetime - example_id: Optional[str] - scores: Optional[list[TaskEvaluation]] = None - labels: Optional[set[str]] = None - metadata: Optional[dict[str, Any]] = None - llm_completions: Optional[list[LLMCompletion]] = None - def to_domain(self, task: Task[TaskInput, TaskOutput]): - return TaskRun[TaskInput, TaskOutput]( + def to_domain(self, task: Task[TaskInput, TaskOutput]) -> Run[TaskOutput]: + return Run( id=self.id, - task=task, - task_input=task.input_class.model_validate(self.task_input), task_output=task.output_class.model_validate(self.task_output), - version=self.group, - start_time=self.start_time, - end_time=self.end_time, + version=TaskVersion( + properties=TaskVersionProperties.model_construct( + None, + **self.version.properties, + ), + ), duration_seconds=self.duration_seconds, cost_usd=self.cost_usd, - created_at=self.created_at, - example_id=self.example_id, - scores=self.scores, - labels=self.labels, - metadata=self.metadata, - llm_completions=self.llm_completions, ) -class ImportExampleRequest(BaseModel): - task_input: dict[str, Any] +class RunStreamChunk(BaseModel): + id: str task_output: dict[str, Any] - @classmethod - def from_domain(cls, task_example: TaskExample[TaskInput, TaskOutput]): - return cls( - task_input=task_example.task_input.model_dump(mode="json"), - task_output=task_example.task_output.model_dump(mode="json"), - ) - + version: Optional[Version] = None + duration_seconds: Optional[float] = None + cost_usd: Optional[float] = None -class ExampleResponse(BaseModel): - id: str - task_input: dict[str, Any] - task_output: dict[str, Any] + def to_domain(self, task: Task[TaskInput, TaskOutput]) -> Union[Run[TaskOutput], RunChunk[TaskOutput]]: + if self.version is None: + return RunChunk( + id=self.id, + task_output=task.output_class.model_validate(self.task_output), + ) - def to_domain(self, task: Task[TaskInput, TaskOutput]): - return TaskExample[TaskInput, TaskOutput]( + return Run( id=self.id, - task=task, - task_input=task.input_class.model_validate(self.task_input), task_output=task.output_class.model_validate(self.task_output), + version=TaskVersion( + properties=TaskVersionProperties.model_construct( + None, + **self.version.properties, + ), + ), + duration_seconds=self.duration_seconds, + cost_usd=self.cost_usd, ) - - -class PatchGroupRequest(BaseModel): - add_alias: Optional[str] = Field( - default=None, - description="A new alias for the group. If the alias is already used in another group of the task schema" - "it will be removed from the other group.", - ) - - remove_alias: Optional[str] = Field( - default=None, - description="An alias to remove from the group. The request is a noop if the group does not have the alias", - ) - - @model_validator(mode="after") - def post_validate(self): - if not self.add_alias and not self.remove_alias: - raise ValueError("At least one of add_alias or remove_alias must be set") - if self.add_alias == self.remove_alias: - raise ValueError("Cannot add and remove the same alias") - return self diff --git a/workflowai/core/client/models_test.py b/workflowai/core/client/models_test.py index 8038cb6..192d0eb 100644 --- a/workflowai/core/client/models_test.py +++ b/workflowai/core/client/models_test.py @@ -1,17 +1,16 @@ import pytest from tests.utils import fixture_text -from workflowai.core.client.models import TaskRunResponse +from workflowai.core.client.models import RunResponse @pytest.mark.parametrize( "fixture", [ "task_run.json", - "task_run_float_usage.json", ], ) def test_task_run_response(fixture: str): txt = fixture_text(fixture) - task_run = TaskRunResponse.model_validate_json(txt) + task_run = RunResponse.model_validate_json(txt) assert task_run diff --git a/workflowai/core/client/utils_test.py b/workflowai/core/client/utils_test.py index e1cd4a8..61e06ee 100644 --- a/workflowai/core/client/utils_test.py +++ b/workflowai/core/client/utils_test.py @@ -37,7 +37,7 @@ def test_retry_after_to_delay_seconds(retry_after: Optional[str], expected: Opti class TestBuildRetryableWait: - @pytest.fixture() + @pytest.fixture def request_error(self): response = Mock() response.headers = {"Retry-After": "0.01"} diff --git a/workflowai/core/domain/llm_completion.py b/workflowai/core/domain/llm_completion.py deleted file mode 100644 index 966043d..0000000 --- a/workflowai/core/domain/llm_completion.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Any, Optional - -from pydantic import BaseModel, Field - -from workflowai.core.domain.llm_usage import LLMUsage - - -class LLMCompletion(BaseModel): - """A raw response from the LLM api""" - - messages: list[dict[str, Any]] = Field( - description="The raw messages sent to the LLM", - ) - response: Optional[str] = Field( - default=None, description="The raw response from the LLM", - ) - - usage: Optional[LLMUsage] = Field( - default=None, description="The usage of the LLM model", - ) diff --git a/workflowai/core/domain/llm_usage.py b/workflowai/core/domain/llm_usage.py deleted file mode 100644 index 4745224..0000000 --- a/workflowai/core/domain/llm_usage.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import Optional - -from pydantic import BaseModel, Field - - -class LLMUsage(BaseModel): - """An object to store usage information for the LLM provider""" - - prompt_token_count: Optional[float] = Field( - default=None, - description="The number of tokens in the prompt", - ) - prompt_cost_usd: Optional[float] = Field( - default=None, - description="The cost of the prompt in USD", - ) - - completion_token_count: Optional[float] = Field( - default=None, - description="The number of tokens in the completion", - ) - completion_cost_usd: Optional[float] = Field( - default=None, - description="The cost of the completion in USD", - ) - - @property - def cost_usd(self) -> Optional[float]: - if self.prompt_cost_usd and self.completion_cost_usd: - return self.prompt_cost_usd + self.completion_cost_usd - - # If either 'prompt_cost_usd' or 'completion_cost_usd' is missing, we consider there is a problem and prefer - # to return nothing rather than a False value. - return None diff --git a/workflowai/core/domain/task.py b/workflowai/core/domain/task.py index aa5be2c..97eacc8 100644 --- a/workflowai/core/domain/task.py +++ b/workflowai/core/domain/task.py @@ -1,9 +1,9 @@ from datetime import datetime from typing import Generic, Optional, TypeVar -from pydantic import BaseModel, Field +from pydantic import BaseModel -from workflowai.core.domain.task_version_reference import TaskVersionReference +from workflowai.core.domain.task_version_reference import VersionReference TaskInput = TypeVar("TaskInput", bound=BaseModel) TaskOutput = TypeVar("TaskOutput", bound=BaseModel) @@ -20,11 +20,9 @@ class Task(BaseModel, Generic[TaskInput, TaskOutput]): id: str = "" schema_id: int = 0 + version: VersionReference = "production" + input_class: type[TaskInput] = BaseModel # pyright: ignore [reportAssignmentType] output_class: type[TaskOutput] = BaseModel # pyright: ignore [reportAssignmentType] - version: TaskVersionReference = Field( - default_factory=lambda: TaskVersionReference.with_properties(), - ) - created_at: Optional[datetime] = None diff --git a/workflowai/core/domain/task_evaluation.py b/workflowai/core/domain/task_evaluation.py deleted file mode 100644 index c231a9a..0000000 --- a/workflowai/core/domain/task_evaluation.py +++ /dev/null @@ -1,53 +0,0 @@ -import datetime -from typing import Any, Literal, Optional, Union - -from pydantic import BaseModel, Field - -# A type alias for evaluation tags. Goal is to provide some basic commonly used values -EvaluationTags = Union[Literal["positive", "negative", "neutral"], str] - -EvaluatorMetric = Literal["correctness", "latency", "cost", "quality", "faithfulness"] - - -class TaskEvaluation(BaseModel): - score: float = Field(..., ge=0, le=1, description="The score of the evaluation") - tags: Optional[list[EvaluationTags]] = Field( - default=None, - description="Metadata added by the evaluator", - ) - - comment: Optional[str] = Field( - default=None, - description="An optional comment from the evaluation", - ) - - class Evaluator(BaseModel): - id: str = Field( - ..., - description="The id of the evaluator that computed the score. " - "Only one score per id can be attached to a task run.", - examples=["1.0", "user:1"], - ) - name: str = Field( - ..., - description="The name of the evaluator that computed the score e-g 'equality' or 'user'", - ) - properties: dict[str, Any] - metric: EvaluatorMetric = Field( - default="correctness", - description="The metric that was used to compute the score", - ) - - evaluator: Evaluator = Field( - ..., - description="Information about the evaluator that computed the score", - ) - - created_at: datetime.datetime = Field( - description="The time at which the score was created", - ) - - example_id: Optional[str] = Field( - default=None, - description="The id of the example that was used in the evaluation", - ) diff --git a/workflowai/core/domain/task_example.py b/workflowai/core/domain/task_example.py deleted file mode 100644 index 830e020..0000000 --- a/workflowai/core/domain/task_example.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import Generic - -from pydantic import BaseModel, Field - -from workflowai.core.domain.task import Task, TaskInput, TaskOutput - - -class TaskExample(BaseModel, Generic[TaskInput, TaskOutput]): - id: str = Field(default="", description="A unique identifier") - - task: Task[TaskInput, TaskOutput] - - task_input: TaskInput - task_output: TaskOutput diff --git a/workflowai/core/domain/task_run.py b/workflowai/core/domain/task_run.py index d6bab04..d7ed71d 100644 --- a/workflowai/core/domain/task_run.py +++ b/workflowai/core/domain/task_run.py @@ -1,16 +1,21 @@ import uuid -from datetime import datetime from typing import Any, Generic, Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field # pyright: ignore [reportUnknownVariableType] -from workflowai.core.domain.llm_completion import LLMCompletion -from workflowai.core.domain.task import Task, TaskInput, TaskOutput -from workflowai.core.domain.task_evaluation import TaskEvaluation +from workflowai.core.domain.task import TaskOutput from workflowai.core.domain.task_version import TaskVersion -class TaskRun(BaseModel, Generic[TaskInput, TaskOutput]): +class RunChunk(BaseModel, Generic[TaskOutput]): + id: str = Field( + default_factory=lambda: str(uuid.uuid4()), + description="The unique identifier of the task run", + ) + task_output: TaskOutput + + +class Run(RunChunk[TaskOutput]): """ A task run is an instance of a task with a specific input and output. @@ -18,45 +23,9 @@ class TaskRun(BaseModel, Generic[TaskInput, TaskOutput]): been evaluated """ - id: str = Field( - default_factory=lambda: str(uuid.uuid4()), - description="The unique identifier of the task run", - ) - start_time: Optional[datetime] = None - end_time: Optional[datetime] = None duration_seconds: Optional[float] = None cost_usd: Optional[float] = None - task: Task[TaskInput, TaskOutput] - - task_input: TaskInput - - task_output: TaskOutput - version: TaskVersion - from_cache: bool = Field( - default=False, description="Whether the task run was loaded from the cache", - ) - - # if available, the id of an example that match the task run input - example_id: Optional[str] = None - - scores: Optional[list[TaskEvaluation]] = Field( - default=None, - description="A list of scores computed for the task run. A run can be evaluated in multiple ways.", - ) - - labels: Optional[set[str]] = Field( - default=None, - description="A list of labels ", - ) - metadata: Optional[dict[str, Any]] = None - - llm_completions: Optional[list[LLMCompletion]] = Field( - default=None, - description="A list of raw completions used to generate the task output", - ) - - created_at: Optional[datetime] = None diff --git a/workflowai/core/domain/task_run_test.py b/workflowai/core/domain/task_run_test.py deleted file mode 100644 index 079354d..0000000 --- a/workflowai/core/domain/task_run_test.py +++ /dev/null @@ -1,110 +0,0 @@ -from typing import Optional - -from pydantic import BaseModel - -from workflowai.core.client.models import TaskRunResponse -from workflowai.core.domain.task import Task - - -class GenerateChangelogFromPropertiesTaskInput(BaseModel): - temperature: Optional[float] = None - - -class GenerateChangelogFromPropertiesTaskOutput(BaseModel): - changes: Optional[list[str]] = None - - -def test_task_run_model_validate(): - json_str = """{ - "id": "e3683ced-efd2-4b15-9ab4-aefdf17c4c19", - "task_id": "generate-changelog-from-properties", - "task_schema_id": 1, - "task_input": { - "temperature": 1 - }, - "task_input_hash": "6faebdc55135f380b00e7ada53f5cccc", - "task_input_preview": "old_task_group: {properties: {temperature: 1, instructions: \\"Add 5 to the n", - "task_output": { - "changes": [ - "Temperature decreased from Creative to 0.73" - ] - }, - "task_output_hash": "d057c1f26fd8447c11cda7300e0f3717", - "task_output_preview": "changes: [\\"Temperature decreased from Creative to 0.73\\"]", - "group": { - "id": "68eead780d01791ff2e09d39055ae6e8", - "iteration": 36, - "properties": { - "model": "gemini-1.5-flash-002", - "provider": "google", - "temperature": 0, - "instructions": "", - "max_tokens": null, - "runner_name": "WorkflowAI", - "runner_version": "v0.1.0", - "few_shot": null, - "template_name": "v1", - "task_variant_id": "fa546275ed8f6c801d6c6f174828d615" - }, - "tags": [ - "model=gemini-1.5-flash-002", - "provider=google", - "temperature=0" - ], - "aliases": null, - "is_external": null, - "is_favorite": null, - "notes": null, - "similarity_hash": "", - "benchmark_for_datasets": null - }, - "status": "success", - "error": null, - "start_time": "2024-10-01T17:55:06.241000Z", - "end_time": "2024-10-01T17:55:07.879000Z", - "duration_seconds": 1.638103, - "cost_usd": 0.00004651875, - "created_at": "2024-10-01T17:55:07.879000Z", - "updated_at": "2024-10-01T17:55:07.879000Z", - "example_id": null, - "corrections": null, - "parent_task_ids": null, - "scores": null, - "labels": null, - "metadata": { - "used_alias": "environment=production" - }, - "llm_completions": [ - { - "messages": [ - { - "role": "system", - "content": "" - }, - { - "role": "user", - "content": "" - } - ], - "response": "{\\"changes\\": [\\"Temperature decreased from Creative to 0.73\\"]}", - "usage": { - "completion_token_count": 13.5, - "completion_cost_usd": 0.00000405, - "prompt_token_count": 566.25, - "prompt_cost_usd": 0.00004246875 - } - } - ], - "config_id": null, - "dataset_benchmark_ids": null, - "is_free": null, - "author_tenant": null -}""" - - task_run = TaskRunResponse.model_validate_json(json_str).to_domain( - Task( - input_class=GenerateChangelogFromPropertiesTaskInput, - output_class=GenerateChangelogFromPropertiesTaskOutput, - ), - ) - assert task_run.id == "e3683ced-efd2-4b15-9ab4-aefdf17c4c19" diff --git a/workflowai/core/domain/task_version.py b/workflowai/core/domain/task_version.py index 7d8ada2..3be4b01 100644 --- a/workflowai/core/domain/task_version.py +++ b/workflowai/core/domain/task_version.py @@ -1,35 +1,10 @@ -from typing import Optional - from pydantic import BaseModel, Field from workflowai.core.domain.task_version_properties import TaskVersionProperties class TaskVersion(BaseModel): - id: str = Field( - default="", - description="The group id either client provided or generated, stable for given set of properties", - ) - iteration: int = Field( - default=0, - description="The iteration of the group, incremented for each new group", - ) properties: TaskVersionProperties = Field( default_factory=TaskVersionProperties, description="The properties used for executing the run.", ) - tags: list[str] = Field( - default_factory=list, - description="A list of tags associated with the group. When empty, tags are computed from the properties.", - ) - - aliases: Optional[set[str]] = Field( - default=None, - description="A list of aliases to use in place of iteration or id. " - "An alias can be used to uniquely identify a group for a given task. ", - ) - - is_external: Optional[bool] = Field( - default=None, - description="Whether the group is external, i-e not creating by internal runners", - ) diff --git a/workflowai/core/domain/task_version_properties.py b/workflowai/core/domain/task_version_properties.py index e8fb8cf..85a37ef 100644 --- a/workflowai/core/domain/task_version_properties.py +++ b/workflowai/core/domain/task_version_properties.py @@ -1,4 +1,4 @@ -from typing import Any, Literal, Optional, Union +from typing import Optional from pydantic import BaseModel, ConfigDict, Field @@ -11,13 +11,16 @@ class TaskVersionProperties(BaseModel): model_config = ConfigDict(extra="allow") model: Optional[str] = Field( - default=None, description="The LLM model used for the run", + default=None, + description="The LLM model used for the run", ) provider: Optional[str] = Field( - default=None, description="The LLM provider used for the run", + default=None, + description="The LLM provider used for the run", ) temperature: Optional[float] = Field( - default=None, description="The temperature for generation", + default=None, + description="The temperature for generation", ) instructions: Optional[str] = Field( default=None, @@ -29,37 +32,11 @@ class TaskVersionProperties(BaseModel): ) runner_name: Optional[str] = Field( - default=None, description="The name of the runner used", - ) - - runner_version: Optional[str] = Field( - default=None, description="The version of the runner used", - ) - - few_shot: "Optional[FewShotConfiguration]" = Field( - default=None, description="Few shot configuration", - ) - - -class FewShotConfiguration(BaseModel): - count: Optional[int] = Field( - default=None, - description="The number of few-shot examples to use for the task", - ) - - selection: Union[Literal["latest", "manual"], str, None] = Field( default=None, - description="The selection method to use for few-shot examples", + description="The name of the runner used", ) - examples: Optional[list["FewShotExample"]] = Field( + runner_version: Optional[str] = Field( default=None, - description="The few-shot examples used for the task. If provided, count and selection are ignored. " - "If not provided, count and selection are used to select examples and the examples list will be set " - "in the final group.", + description="The version of the runner used", ) - - -class FewShotExample(BaseModel): - task_input: dict[str, Any] - task_output: dict[str, Any] diff --git a/workflowai/core/domain/task_version_reference.py b/workflowai/core/domain/task_version_reference.py index eb6baaa..8faf26b 100644 --- a/workflowai/core/domain/task_version_reference.py +++ b/workflowai/core/domain/task_version_reference.py @@ -1,77 +1,5 @@ -from typing import Any, Optional +from typing import Literal, Union -from pydantic import BaseModel, Field, model_validator -from typing_extensions import Self +VersionEnvironment = Literal["dev", "staging", "production"] -from workflowai.core.domain.task_version import TaskVersion -from workflowai.core.domain.task_version_properties import TaskVersionProperties - - -class TaskVersionReference(BaseModel): - """Refer to an existing group or create a new one with the given properties. - Only one of id, iteration or properties must be provided""" - - id: Optional[str] = Field(description="The id of an existing group", default=None) - iteration: Optional[int] = Field( - description="An iteration for an existing group.", default=None, - ) - properties: Optional[TaskVersionProperties] = Field( - description="The properties to evaluate the task schema with. A group will be created if needed", - default=None, - ) - alias: Optional[str] = Field(description="An alias for the group", default=None) - - is_external: Optional[bool] = Field( - description="Whether the group is external, i-e not created by internal runners", - default=None, - ) - - @model_validator(mode="after") - def post_validate(self) -> Self: - count = sum( - 1 - for x in [ - self.id, - self.iteration, - self.properties, - self.alias, - ] - if x - ) - if count != 1: - raise ValueError( - "Exactly one of id, iteration or properties must be provided", - ) - return self - - @classmethod - def with_properties( - cls, - model: Optional[str] = None, - provider: Optional[str] = None, - temperature: Optional[float] = None, - instructions: Optional[str] = None, - max_tokens: Optional[int] = None, - **kwargs: Any, - ) -> "TaskVersionReference": - """Short hand for creating a TaskVersionReference with properties.""" - return TaskVersionReference( - properties=TaskVersionProperties( - model=model, - provider=provider, - temperature=temperature, - instructions=instructions, - max_tokens=max_tokens, - **kwargs, - ), - ) - - @classmethod - def from_version(cls, version: TaskVersion): - if version.iteration: - return cls(iteration=version.iteration) - if version.id: - return cls(id=version.id) - if version.aliases: - return cls(alias=next(iter(version.aliases))) - return cls(properties=version.properties) +VersionReference = Union[int, VersionEnvironment]