From 7f3abfd1f9dbfdc9fd98fea5c7d24469dd89c22e Mon Sep 17 00:00:00 2001 From: Sam Partee Date: Sun, 14 Jul 2024 23:37:46 -0700 Subject: [PATCH] Tool SDK, Schemas (#2) Co-authored-by: Nate Barbettini --- .editorconfig | 5 + .gitignore | 183 +- .pre-commit-config.yaml | 22 + CONTRIBUTING.md | 133 ++ LICENSE | 21 + README.md | 12 +- arcade/Makefile | 56 + {examples/tools => arcade/arcade}/__init__.py | 0 .../tests => arcade/arcade/actor}/__init__.py | 0 .../arcade/actor/common}/__init__.py | 0 .../arcade/actor/common/exception/__init__.py | 1 + .../arcade/actor}/common/exception/errors.py | 59 +- .../common/exception/exception_handler.py | 129 +- .../arcade/actor}/common/log.py | 28 +- .../arcade/actor}/common/response.py | 18 +- .../arcade/actor}/common/response_code.py | 40 +- arcade/arcade/actor/common/serializers.py | 13 + .../arcade/actor/core}/__init__.py | 0 arcade/arcade/actor/core/conf.py | 71 + .../arcade/actor}/core/depends.py | 3 +- .../arcade/actor}/core/generate.py | 44 +- .../arcade/actor}/core/registrar.py | 34 +- arcade/arcade/actor/main.py | 21 + arcade/arcade/actor/routes/__init__.py | 7 + arcade/arcade/actor/routes/tool.py | 59 + .../arcade/actor/schemas}/__init__.py | 0 arcade/arcade/actor/schemas/base.py | 145 ++ .../arcade/actor/utils}/__init__.py | 0 .../arcade/actor}/utils/timezone.py | 4 +- .../common => arcade/arcade/apm}/__init__.py | 0 .../toolserve => arcade/arcade}/apm/base.py | 26 +- .../toolserve => arcade/arcade}/apm/pack.py | 49 +- .../toolserve => arcade/arcade}/apm/parse.py | 26 +- .../core => arcade/arcade/cli}/__init__.py | 0 .../toolserve => arcade/arcade}/cli/main.py | 38 +- .../crud => arcade/arcade/sdk}/__init__.py | 0 arcade/arcade/sdk/annotations.py | 8 + arcade/arcade/sdk/errors.py | 14 + arcade/arcade/sdk/schemas.py | 71 + arcade/arcade/sdk/tool.py | 34 + .../arcade/tool}/__init__.py | 0 arcade/arcade/tool/catalog.py | 407 ++++ .../arcade/tool/openai.py | 36 +- arcade/arcade/utils/__init__.py | 59 + arcade/codecov.yaml | 9 + arcade/docs/index.md | 8 + arcade/docs/modules.md | 1 + arcade/mkdocs.yml | 54 + arcade/poetry.lock | 1826 +++++++++++++++++ arcade/poetry.toml | 2 + arcade/pyproject.toml | 127 ++ .../services => arcade/tests}/__init__.py | 0 arcade/tests/sdk/test_tool_decorator.py | 52 + .../tests/tool/test_create_tool_definition.py | 456 ++++ .../test_create_tool_definition_errors.py | 55 + .../test_create_tool_definition_pydantic.py | 57 + arcade/tests/utils/test_utils_casing.py | 26 + arcade/tox.ini | 18 + cspell.config.yaml | 12 + examples/data/make_sqlite_db.py | 21 +- examples/data/test_prompt.py | 24 - examples/{ => generic}/pack.lock.toml | 7 +- examples/{ => generic}/pack.toml | 4 +- examples/generic/tools/BM25.py | 158 ++ .../generic/tools}/__init__.py | 0 examples/generic/tools/gmail.py | 257 +++ examples/generic/tools/products.py | 76 + examples/generic/tools/read_sqlite.py | 38 + examples/tools/gmail.py | 119 -- examples/tools/gmail_oauth.py | 119 -- examples/tools/llm.py | 83 - examples/tools/read_sqlite.py | 43 - examples/tools/search.py | 35 - examples/tools/sql.py | 0 schemas/preview/tool_definition.schema.jsonc | 198 ++ schemas/preview/tool_request.schema.jsonc | 80 + schemas/preview/tool_response.schema.jsonc | 135 ++ toolserve/poetry.lock | 663 ------ toolserve/pyproject.toml | 29 - toolserve/toolserve/sdk/__init__.py | 6 - toolserve/toolserve/sdk/client.py | 201 -- toolserve/toolserve/sdk/dataframe.py | 39 - toolserve/toolserve/sdk/tool.py | 35 - .../server/common/exception/__init__.py | 2 - .../toolserve/server/common/serializers.py | 68 - toolserve/toolserve/server/core/catalog.py | 220 -- toolserve/toolserve/server/core/conf.py | 126 -- toolserve/toolserve/server/crud/base.py | 102 - .../toolserve/server/crud/crud_artifact.py | 47 - toolserve/toolserve/server/crud/crud_data.py | 51 - toolserve/toolserve/server/crud/crud_log.py | 45 - .../toolserve/server/database/__init__.py | 2 - .../toolserve/server/database/db_sqlite.py | 68 - toolserve/toolserve/server/main.py | 24 - toolserve/toolserve/server/models/__init__.py | 7 - toolserve/toolserve/server/models/base.py | 83 - .../toolserve/server/models/sys_artifact.py | 13 - toolserve/toolserve/server/models/sys_data.py | 14 - toolserve/toolserve/server/models/sys_log.py | 15 - toolserve/toolserve/server/routes/__init__.py | 16 - toolserve/toolserve/server/routes/artifact.py | 45 - toolserve/toolserve/server/routes/chat.py | 124 -- toolserve/toolserve/server/routes/data.py | 79 - toolserve/toolserve/server/routes/log.py | 40 - toolserve/toolserve/server/routes/tool.py | 44 - .../toolserve/server/schemas/artifact.py | 29 - toolserve/toolserve/server/schemas/base.py | 148 -- toolserve/toolserve/server/schemas/data.py | 46 - toolserve/toolserve/server/schemas/log.py | 22 - .../server/services/artifact_service.py | 47 - .../toolserve/server/services/data_service.py | 60 - .../toolserve/server/services/log_service.py | 47 - toolserve/toolserve/server/test.py | 27 - toolserve/toolserve/utils/__init__.py | 8 - 114 files changed, 5276 insertions(+), 3342 deletions(-) create mode 100644 .editorconfig create mode 100644 .pre-commit-config.yaml create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 arcade/Makefile rename {examples/tools => arcade/arcade}/__init__.py (100%) rename {toolserve/tests => arcade/arcade/actor}/__init__.py (100%) rename {toolserve/toolserve => arcade/arcade/actor/common}/__init__.py (100%) create mode 100644 arcade/arcade/actor/common/exception/__init__.py rename {toolserve/toolserve/server => arcade/arcade/actor}/common/exception/errors.py (55%) rename {toolserve/toolserve/server => arcade/arcade/actor}/common/exception/exception_handler.py (61%) rename {toolserve/toolserve/server => arcade/arcade/actor}/common/log.py (59%) rename {toolserve/toolserve/server => arcade/arcade/actor}/common/response.py (83%) rename {toolserve/toolserve/server => arcade/arcade/actor}/common/response_code.py (82%) create mode 100644 arcade/arcade/actor/common/serializers.py rename {toolserve/toolserve/apm => arcade/arcade/actor/core}/__init__.py (100%) create mode 100644 arcade/arcade/actor/core/conf.py rename {toolserve/toolserve/server => arcade/arcade/actor}/core/depends.py (66%) rename {toolserve/toolserve/server => arcade/arcade/actor}/core/generate.py (67%) rename {toolserve/toolserve/server => arcade/arcade/actor}/core/registrar.py (69%) create mode 100644 arcade/arcade/actor/main.py create mode 100644 arcade/arcade/actor/routes/__init__.py create mode 100644 arcade/arcade/actor/routes/tool.py rename {toolserve/toolserve/cli => arcade/arcade/actor/schemas}/__init__.py (100%) create mode 100644 arcade/arcade/actor/schemas/base.py rename {toolserve/toolserve/server => arcade/arcade/actor/utils}/__init__.py (100%) rename {toolserve/toolserve/server => arcade/arcade/actor}/utils/timezone.py (92%) rename {toolserve/toolserve/server/common => arcade/arcade/apm}/__init__.py (100%) rename {toolserve/toolserve => arcade/arcade}/apm/base.py (69%) rename {toolserve/toolserve => arcade/arcade}/apm/pack.py (64%) rename {toolserve/toolserve => arcade/arcade}/apm/parse.py (84%) rename {toolserve/toolserve/server/core => arcade/arcade/cli}/__init__.py (100%) rename {toolserve/toolserve => arcade/arcade}/cli/main.py (50%) rename {toolserve/toolserve/server/crud => arcade/arcade/sdk}/__init__.py (100%) create mode 100644 arcade/arcade/sdk/annotations.py create mode 100644 arcade/arcade/sdk/errors.py create mode 100644 arcade/arcade/sdk/schemas.py create mode 100644 arcade/arcade/sdk/tool.py rename {toolserve/toolserve/server/schemas => arcade/arcade/tool}/__init__.py (100%) create mode 100644 arcade/arcade/tool/catalog.py rename toolserve/toolserve/utils/openai_tool.py => arcade/arcade/tool/openai.py (80%) create mode 100644 arcade/arcade/utils/__init__.py create mode 100644 arcade/codecov.yaml create mode 100644 arcade/docs/index.md create mode 100644 arcade/docs/modules.md create mode 100644 arcade/mkdocs.yml create mode 100644 arcade/poetry.lock create mode 100644 arcade/poetry.toml create mode 100644 arcade/pyproject.toml rename {toolserve/toolserve/server/services => arcade/tests}/__init__.py (100%) create mode 100644 arcade/tests/sdk/test_tool_decorator.py create mode 100644 arcade/tests/tool/test_create_tool_definition.py create mode 100644 arcade/tests/tool/test_create_tool_definition_errors.py create mode 100644 arcade/tests/tool/test_create_tool_definition_pydantic.py create mode 100644 arcade/tests/utils/test_utils_casing.py create mode 100644 arcade/tox.ini create mode 100644 cspell.config.yaml delete mode 100644 examples/data/test_prompt.py rename examples/{ => generic}/pack.lock.toml (50%) rename examples/{ => generic}/pack.toml (75%) create mode 100644 examples/generic/tools/BM25.py rename {toolserve/toolserve/server/utils => examples/generic/tools}/__init__.py (100%) create mode 100644 examples/generic/tools/gmail.py create mode 100644 examples/generic/tools/products.py create mode 100644 examples/generic/tools/read_sqlite.py delete mode 100644 examples/tools/gmail.py delete mode 100644 examples/tools/gmail_oauth.py delete mode 100644 examples/tools/llm.py delete mode 100644 examples/tools/read_sqlite.py delete mode 100644 examples/tools/search.py delete mode 100644 examples/tools/sql.py create mode 100644 schemas/preview/tool_definition.schema.jsonc create mode 100644 schemas/preview/tool_request.schema.jsonc create mode 100644 schemas/preview/tool_response.schema.jsonc delete mode 100644 toolserve/poetry.lock delete mode 100644 toolserve/pyproject.toml delete mode 100644 toolserve/toolserve/sdk/__init__.py delete mode 100644 toolserve/toolserve/sdk/client.py delete mode 100644 toolserve/toolserve/sdk/dataframe.py delete mode 100644 toolserve/toolserve/sdk/tool.py delete mode 100644 toolserve/toolserve/server/common/exception/__init__.py delete mode 100644 toolserve/toolserve/server/common/serializers.py delete mode 100644 toolserve/toolserve/server/core/catalog.py delete mode 100644 toolserve/toolserve/server/core/conf.py delete mode 100644 toolserve/toolserve/server/crud/base.py delete mode 100644 toolserve/toolserve/server/crud/crud_artifact.py delete mode 100644 toolserve/toolserve/server/crud/crud_data.py delete mode 100644 toolserve/toolserve/server/crud/crud_log.py delete mode 100644 toolserve/toolserve/server/database/__init__.py delete mode 100644 toolserve/toolserve/server/database/db_sqlite.py delete mode 100644 toolserve/toolserve/server/main.py delete mode 100644 toolserve/toolserve/server/models/__init__.py delete mode 100644 toolserve/toolserve/server/models/base.py delete mode 100644 toolserve/toolserve/server/models/sys_artifact.py delete mode 100644 toolserve/toolserve/server/models/sys_data.py delete mode 100644 toolserve/toolserve/server/models/sys_log.py delete mode 100644 toolserve/toolserve/server/routes/__init__.py delete mode 100644 toolserve/toolserve/server/routes/artifact.py delete mode 100644 toolserve/toolserve/server/routes/chat.py delete mode 100644 toolserve/toolserve/server/routes/data.py delete mode 100644 toolserve/toolserve/server/routes/log.py delete mode 100644 toolserve/toolserve/server/routes/tool.py delete mode 100644 toolserve/toolserve/server/schemas/artifact.py delete mode 100644 toolserve/toolserve/server/schemas/base.py delete mode 100644 toolserve/toolserve/server/schemas/data.py delete mode 100644 toolserve/toolserve/server/schemas/log.py delete mode 100644 toolserve/toolserve/server/services/artifact_service.py delete mode 100644 toolserve/toolserve/server/services/data_service.py delete mode 100644 toolserve/toolserve/server/services/log_service.py delete mode 100644 toolserve/toolserve/server/test.py delete mode 100644 toolserve/toolserve/utils/__init__.py diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..9395b543 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,5 @@ +max_line_length = 120 + +[*.json] +indent_style = space +indent_size = 4 diff --git a/.gitignore b/.gitignore index 73bce9fd..7240ce5b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,172 @@ -__pycache__/ -.idea/ -.env -venv/ -.mypy_cache/ -backend/app/log/ -backend/app/alembic/versions/ -backend/app/static/media/ -.ruff_cache/ -.pytest_cache/ +# example data examples/data -examples/set_secrets.sh -scratch \ No newline at end of file +scratch + + +docs/source + +# From https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# Vscode config files +.vscode/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..5e3015bf --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,22 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: "v4.4.0" + hooks: + - id: check-case-conflict + - id: check-merge-conflict + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.1.6" + hooks: + - id: ruff + args: [--exit-non-zero-on-fix] + - id: ruff-format + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: "v3.0.3" + hooks: + - id: prettier diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..38fc02bc --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,133 @@ +# Contributing to `arcade-ai` + +Contributions are welcome, and they are greatly appreciated! +Every little bit helps, and credit will always be given. + +You can contribute in many ways: + +# Types of Contributions + +## Report Bugs + +Report bugs at https://github.com/spartee/arcade-ai/issues + +If you are reporting a bug, please include: + +- Your operating system name and version. +- Any details about your local setup that might be helpful in troubleshooting. +- Detailed steps to reproduce the bug. + +## Fix Bugs + +Look through the GitHub issues for bugs. +Anything tagged with "bug" and "help wanted" is open to whoever wants to implement a fix for it. + +## Implement Features + +Look through the GitHub issues for features. +Anything tagged with "enhancement" and "help wanted" is open to whoever wants to implement it. + +## Write Documentation + +Cookiecutter PyPackage could always use more documentation, whether as part of the official docs, in docstrings, or even on the web in blog posts, articles, and such. + +## Submit Feedback + +The best way to send feedback is to file an issue at https://github.com/spartee/arcade-ai/issues. + +If you are proposing a new feature: + +- Explain in detail how it would work. +- Keep the scope as narrow as possible, to make it easier to implement. +- Remember that this is a volunteer-driven project, and that contributions + are welcome :) + +# Get Started! + +Ready to contribute? Here's how to set up `arcade-ai` for local development. +Please note this documentation assumes you already have `poetry` and `Git` installed and ready to go. + +1. Fork the `arcade-ai` repo on GitHub. + +2. Clone your fork locally: + +```bash +cd +git clone git@github.com:YOUR_NAME/arcade-ai.git +``` + +3. Now we need to install the environment. Navigate into the directory + +```bash +cd arcade-ai +``` + +If you are using `pyenv`, select a version to use locally. (See installed versions with `pyenv versions`) + +```bash +pyenv local +``` + +Then, install and activate the environment with: + +```bash +poetry install +poetry shell +``` + +4. Install pre-commit to run linters/formatters at commit time: + +```bash +poetry run pre-commit install +``` + +5. Create a branch for local development: + +```bash +git checkout -b name-of-your-bugfix-or-feature +``` + +Now you can make your changes locally. + +6. Don't forget to add test cases for your added functionality to the `tests` directory. + +7. When you're done making changes, check that your changes pass the formatting tests. + +```bash +make check +``` + +Now, validate that all unit tests are passing: + +```bash +make test +``` + +9. Before raising a pull request you should also run tox. + This will run the tests across different versions of Python: + +```bash +tox +``` + +This requires you to have multiple versions of python installed. +This step is also triggered in the CI/CD pipeline, so you could also choose to skip this step locally. + +10. Commit your changes and push your branch to GitHub: + +```bash +git add . +git commit -m "Your detailed description of your changes." +git push origin name-of-your-bugfix-or-feature +``` + +11. Submit a pull request through the GitHub website. + +# Pull Request Guidelines + +Before you submit a pull request, check that it meets these guidelines: + +1. The pull request should include tests. + +2. If the pull request adds functionality, the docs should be updated. + Put your new functionality into a function with a docstring, and add the feature to the list in `README.md`. diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..76cd1385 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024, Arcade AI + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index fc11e3aa..d9cb7753 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ -# ToolServe +[![Release](https://img.shields.io/github/v/release/spartee/arcade-ai)](https://img.shields.io/github/v/release/spartee/arcade-ai) +[![Build status](https://img.shields.io/github/actions/workflow/status/spartee/arcade-ai/main.yml?branch=main)](https://github.com/spartee/arcade-ai/actions/workflows/main.yml?query=branch%3Amain) +[![codecov](https://codecov.io/gh/spartee/arcade-ai/branch/main/graph/badge.svg)](https://codecov.io/gh/spartee/arcade-ai) +[![Commit activity](https://img.shields.io/github/commit-activity/m/spartee/arcade-ai)](https://img.shields.io/github/commit-activity/m/spartee/arcade-ai) +[![License](https://img.shields.io/github/license/spartee/arcade-ai)](https://img.shields.io/github/license/spartee/arcade-ai) + +# Arcade-AI ToolServe is a framework specifically designed to manage and orchestrate Language Learning Models (LLMs) or "tools" with high efficiency. It distinctively separates the tools from the orchestration framework to improve dependency management, packaging, and execution. @@ -9,15 +15,19 @@ This functionality is especially beneficial for agents tasked with performing ac ## Components ### 1. Command Line Interface (CLI) + The CLI component, located at `toolserve/cli/main.py`, offers commands to package, serve, and inspect LLM "tools". It utilizes the Typer library to manage command-line arguments and options. ### 2. Server + The server component, which manages the storage of artifacts, data, and logs generated by the tools, is implemented using FastAPI and can be found at `toolserve/server/main.py`. The server configuration includes routes, middleware, and database connections. ### 3. SDK + Located at `toolserve/sdk`, the SDK streamlines the development of tools by providing decorators and helper functions that abstract routine tasks, allowing developers to concentrate on the logic of the tool rather than on repetitive code. ### 4. Builtins + Built-in tools for common tasks such as SQL queries are available at `toolserve/builtin/default`. These tools are ready to use and require no additional setup. ## Installation diff --git a/arcade/Makefile b/arcade/Makefile new file mode 100644 index 00000000..fcd6e24f --- /dev/null +++ b/arcade/Makefile @@ -0,0 +1,56 @@ +.PHONY: install +install: ## Install the poetry environment and install the pre-commit hooks + @echo "🚀 Creating virtual environment using pyenv and poetry" + @poetry install + @ poetry run pre-commit install + @poetry shell + +.PHONY: check +check: ## Run code quality tools. + @echo "🚀 Checking Poetry lock file consistency with 'pyproject.toml': Running poetry check --lock" + @poetry check --lock + @echo "🚀 Linting code: Running pre-commit" + @poetry run pre-commit run -a + @echo "🚀 Static type checking: Running mypy" + @poetry run mypy + @echo "🚀 Checking for obsolete dependencies: Running deptry" + @poetry run deptry . + +.PHONY: test +test: ## Test the code with pytest + @echo "🚀 Testing code: Running pytest" + @poetry run pytest -v --cov --cov-config=pyproject.toml --cov-report=xml + +.PHONY: build +build: clean-build ## Build wheel file using poetry + @echo "🚀 Creating wheel file" + @poetry build + +.PHONY: clean +clean: ## clean build artifacts + @rm -rf dist + +.PHONY: publish +publish: ## publish a release to pypi. + @echo "🚀 Publishing: Dry run." + @poetry config pypi-token.pypi $(PYPI_TOKEN) + @poetry publish --dry-run + @echo "🚀 Publishing." + @poetry publish + +.PHONY: build-and-publish +build-and-publish: build publish ## Build and publish. + +.PHONY: docs-test +docs-test: ## Test if documentation can be built without warnings or errors + @poetry run mkdocs build -s + +.PHONY: docs +docs: ## Build and serve the documentation + @poetry run mkdocs serve + +.PHONY: help +help: + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' + +.DEFAULT_GOAL := help diff --git a/examples/tools/__init__.py b/arcade/arcade/__init__.py similarity index 100% rename from examples/tools/__init__.py rename to arcade/arcade/__init__.py diff --git a/toolserve/tests/__init__.py b/arcade/arcade/actor/__init__.py similarity index 100% rename from toolserve/tests/__init__.py rename to arcade/arcade/actor/__init__.py diff --git a/toolserve/toolserve/__init__.py b/arcade/arcade/actor/common/__init__.py similarity index 100% rename from toolserve/toolserve/__init__.py rename to arcade/arcade/actor/common/__init__.py diff --git a/arcade/arcade/actor/common/exception/__init__.py b/arcade/arcade/actor/common/exception/__init__.py new file mode 100644 index 00000000..e5a0d9b4 --- /dev/null +++ b/arcade/arcade/actor/common/exception/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python3 diff --git a/toolserve/toolserve/server/common/exception/errors.py b/arcade/arcade/actor/common/exception/errors.py similarity index 55% rename from toolserve/toolserve/server/common/exception/errors.py rename to arcade/arcade/actor/common/exception/errors.py index cd3072ac..50d3b71b 100644 --- a/toolserve/toolserve/server/common/exception/errors.py +++ b/arcade/arcade/actor/common/exception/errors.py @@ -1,16 +1,21 @@ - -from typing import Any +from typing import Any, Optional from fastapi import HTTPException from starlette.background import BackgroundTask -from toolserve.server.common.response_code import CustomErrorCode, StandardResponseCode +from arcade.actor.common.response_code import CustomErrorCode, StandardResponseCode class BaseExceptionMixin(Exception): code: int - def __init__(self, *, msg: str = None, data: Any = None, background: BackgroundTask | None = None): + def __init__( + self, + *, + msg: Optional[str] = None, + data: Any = None, + background: BackgroundTask | None = None, + ): self.msg = msg self.data = data # The original background task: https://www.starlette.io/background/ @@ -23,7 +28,9 @@ class HTTPError(HTTPException): class CustomError(BaseExceptionMixin): - def __init__(self, *, error: CustomErrorCode, data: Any = None, background: BackgroundTask | None = None): + def __init__( + self, *, error: CustomErrorCode, data: Any = None, background: BackgroundTask | None = None + ): self.code = error.code super().__init__(msg=error.msg, data=data, background=background) @@ -31,21 +38,31 @@ class CustomError(BaseExceptionMixin): class RequestError(BaseExceptionMixin): code = StandardResponseCode.HTTP_400 - def __init__(self, *, msg: str = 'Bad Request', data: Any = None, background: BackgroundTask | None = None): + def __init__( + self, + *, + msg: str = "Bad Request", + data: Any = None, + background: BackgroundTask | None = None, + ): super().__init__(msg=msg, data=data, background=background) class ForbiddenError(BaseExceptionMixin): code = StandardResponseCode.HTTP_403 - def __init__(self, *, msg: str = 'Forbidden', data: Any = None, background: BackgroundTask | None = None): + def __init__( + self, *, msg: str = "Forbidden", data: Any = None, background: BackgroundTask | None = None + ): super().__init__(msg=msg, data=data, background=background) class NotFoundError(BaseExceptionMixin): code = StandardResponseCode.HTTP_404 - def __init__(self, *, msg: str = 'Not Found', data: Any = None, background: BackgroundTask | None = None): + def __init__( + self, *, msg: str = "Not Found", data: Any = None, background: BackgroundTask | None = None + ): super().__init__(msg=msg, data=data, background=background) @@ -53,7 +70,11 @@ class ServerError(BaseExceptionMixin): code = StandardResponseCode.HTTP_500 def __init__( - self, *, msg: str = 'Internal Server Error', data: Any = None, background: BackgroundTask | None = None + self, + *, + msg: str = "Internal Server Error", + data: Any = None, + background: BackgroundTask | None = None, ): super().__init__(msg=msg, data=data, background=background) @@ -61,19 +82,31 @@ class ServerError(BaseExceptionMixin): class GatewayError(BaseExceptionMixin): code = StandardResponseCode.HTTP_502 - def __init__(self, *, msg: str = 'Bad Gateway', data: Any = None, background: BackgroundTask | None = None): + def __init__( + self, + *, + msg: str = "Bad Gateway", + data: Any = None, + background: BackgroundTask | None = None, + ): super().__init__(msg=msg, data=data, background=background) class AuthorizationError(BaseExceptionMixin): code = StandardResponseCode.HTTP_401 - def __init__(self, *, msg: str = 'Permission Denied', data: Any = None, background: BackgroundTask | None = None): + def __init__( + self, + *, + msg: str = "Permission Denied", + data: Any = None, + background: BackgroundTask | None = None, + ): super().__init__(msg=msg, data=data, background=background) class TokenError(HTTPError): code = StandardResponseCode.HTTP_401 - def __init__(self, *, msg: str = 'Not Authenticated', headers: dict[str, Any] | None = None): - super().__init__(code=self.code, msg=msg, headers=headers or {'WWW-Authenticate': 'Bearer'}) + def __init__(self, *, msg: str = "Not Authenticated", headers: dict[str, Any] | None = None): + super().__init__(code=self.code, msg=msg, headers=headers or {"WWW-Authenticate": "Bearer"}) diff --git a/toolserve/toolserve/server/common/exception/exception_handler.py b/arcade/arcade/actor/common/exception/exception_handler.py similarity index 61% rename from toolserve/toolserve/server/common/exception/exception_handler.py rename to arcade/arcade/actor/common/exception/exception_handler.py index d2a0c38c..b1ce71e4 100644 --- a/toolserve/toolserve/server/common/exception/exception_handler.py +++ b/arcade/arcade/actor/common/exception/exception_handler.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- from fastapi import FastAPI, Request from fastapi.exceptions import RequestValidationError from pydantic import ValidationError @@ -7,19 +6,23 @@ from pydantic.errors import PydanticUserError from starlette.exceptions import HTTPException from starlette.middleware.cors import CORSMiddleware -from toolserve.server.common.exception.errors import BaseExceptionMixin -from toolserve.server.common.log import log -from toolserve.server.common.response_code import CustomResponseCode, StandardResponseCode -from toolserve.server.common.response_code import response_base -from toolserve.server.core.conf import settings -from toolserve.server.schemas.base import ( - CUSTOM_USAGE_ERROR_MESSAGES, - CUSTOM_VALIDATION_ERROR_MESSAGES, +from arcade.actor.common.exception.errors import BaseExceptionMixin +from arcade.actor.common.log import log +from arcade.actor.common.response_code import ( + CustomResponseCode, + StandardResponseCode, + response_base, ) -from toolserve.server.utils.serializers import MsgSpecJSONResponse +from arcade.actor.core.conf import settings +from arcade.actor.schemas.base import ( + CUSTOM_USAGE_ERROR_MESSAGES, +) +from arcade.actor.utils.serializers import MsgSpecJSONResponse -async def _validation_exception_handler(request: Request, e: RequestValidationError | ValidationError): +async def _validation_exception_handler( + request: Request, e: RequestValidationError | ValidationError +): """ Data validation exception handling @@ -27,25 +30,27 @@ async def _validation_exception_handler(request: Request, e: RequestValidationEr :return: """ error = e.errors()[0] - if error.get('type') == 'json_invalid': - message = 'JSON parsing failed' + if error.get("type") == "json_invalid": + message = "JSON parsing failed" else: - error_input = error.get('input') - field = str(error.get('loc')[-1]) - error_msg = error.get('msg') - message = f'{field} {error_msg}, input: {error_input}' - msg = f'Invalid request parameters: {message}' - data = {'errors': error} if settings.ENVIRONMENT == 'dev' else None + error_input = error.get("input") + field = str(error.get("loc")[-1]) + error_msg = error.get("msg") + message = f"{field} {error_msg}, input: {error_input}" + msg = f"Invalid request parameters: {message}" + data = {"errors": error} if settings.ENVIRONMENT == "dev" else None content = { - 'code': StandardResponseCode.HTTP_422, - 'msg': msg, - 'data': data, + "code": StandardResponseCode.HTTP_422, + "msg": msg, + "data": data, } - request.state.__request_validation_exception__ = content # For obtaining exception information in middleware + request.state.__request_validation_exception__ = ( + content # For obtaining exception information in middleware + ) return MsgSpecJSONResponse(status_code=422, content=content) -def register_exception(app: FastAPI): +def register_exception(app: FastAPI): # noqa: C901 @app.exception_handler(HTTPException) async def http_exception_handler(request: Request, exc: HTTPException): """ @@ -55,16 +60,18 @@ def register_exception(app: FastAPI): :param exc: :return: """ - if settings.ENVIRONMENT == 'dev': + if settings.ENVIRONMENT == "dev": content = { - 'code': exc.status_code, - 'msg': exc.detail, - 'data': None, + "code": exc.status_code, + "msg": exc.detail, + "data": None, } else: res = await response_base.fail(res=CustomResponseCode.HTTP_400) content = res.model_dump() - request.state.__request_http_exception__ = content # For obtaining exception information in middleware + request.state.__request_http_exception__ = ( + content # For obtaining exception information in middleware + ) return MsgSpecJSONResponse( status_code=StandardResponseCode.HTTP_400, content=content, @@ -105,9 +112,9 @@ def register_exception(app: FastAPI): return MsgSpecJSONResponse( status_code=StandardResponseCode.HTTP_500, content={ - 'code': StandardResponseCode.HTTP_500, - 'msg': CUSTOM_USAGE_ERROR_MESSAGES.get(exc.code), - 'data': None, + "code": StandardResponseCode.HTTP_500, + "msg": CUSTOM_USAGE_ERROR_MESSAGES.get(exc.code), + "data": None, }, ) @@ -120,11 +127,11 @@ def register_exception(app: FastAPI): :param exc: :return: """ - if settings.ENVIRONMENT == 'dev': + if settings.ENVIRONMENT == "dev": content = { - 'code': StandardResponseCode.HTTP_500, - 'msg': str(''.join(exc.args) if exc.args else exc.__doc__), - 'data': None, + "code": StandardResponseCode.HTTP_500, + "msg": str("".join(exc.args) if exc.args else exc.__doc__), + "data": None, } else: res = await response_base.fail(res=CustomResponseCode.HTTP_500) @@ -147,22 +154,22 @@ def register_exception(app: FastAPI): return MsgSpecJSONResponse( status_code=StandardResponseCode.HTTP_400, content={ - 'code': exc.code, - 'msg': str(exc.msg), - 'data': exc.data if exc.data else None, + "code": exc.code, + "msg": str(exc.msg), + "data": exc.data if exc.data else None, }, background=exc.background, ) else: import traceback - log.error(f'Unknown exception: {exc}') + log.error(f"Unknown exception: {exc}") log.error(traceback.format_exc()) - if settings.ENVIRONMENT == 'dev': + if settings.ENVIRONMENT == "dev": content = { - 'code': 500, - 'msg': str(exc), - 'data': None, + "code": 500, + "msg": str(exc), + "data": None, } else: res = await response_base.fail(res=CustomResponseCode.HTTP_500) @@ -184,39 +191,41 @@ def register_exception(app: FastAPI): """ if isinstance(exc, BaseExceptionMixin): content = { - 'code': exc.code, - 'msg': exc.msg, - 'data': exc.data, + "code": exc.code, + "msg": exc.msg, + "data": exc.data, } else: - if settings.ENVIRONMENT == 'dev': + if settings.ENVIRONMENT == "dev": content = { - 'code': StandardResponseCode.HTTP_500, - 'msg': str(exc), - 'data': None, + "code": StandardResponseCode.HTTP_500, + "msg": str(exc), + "data": None, } else: res = await response_base.fail(res=CustomResponseCode.HTTP_500) content = res.model_dump() response = MsgSpecJSONResponse( - status_code=exc.code if isinstance(exc, BaseExceptionMixin) else StandardResponseCode.HTTP_500, + status_code=exc.code + if isinstance(exc, BaseExceptionMixin) + else StandardResponseCode.HTTP_500, content=content, background=exc.background if isinstance(exc, BaseExceptionMixin) else None, ) - origin = request.headers.get('origin') + origin = request.headers.get("origin") if origin: cors = CORSMiddleware( app=app, - allow_origins=['*'], + allow_origins=["*"], allow_credentials=True, - allow_methods=['*'], - allow_headers=['*'], + allow_methods=["*"], + allow_headers=["*"], ) response.headers.update(cors.simple_headers) - has_cookie = 'cookie' in request.headers + has_cookie = "cookie" in request.headers if cors.allow_all_origins and has_cookie: - response.headers['Access-Control-Allow-Origin'] = origin + response.headers["Access-Control-Allow-Origin"] = origin elif not cors.allow_all_origins and cors.is_allowed_origin(origin=origin): - response.headers['Access-Control-Allow-Origin'] = origin - response.headers.add_vary_header('Origin') + response.headers["Access-Control-Allow-Origin"] = origin + response.headers.add_vary_header("Origin") return response diff --git a/toolserve/toolserve/server/common/log.py b/arcade/arcade/actor/common/log.py similarity index 59% rename from toolserve/toolserve/server/common/log.py rename to arcade/arcade/actor/common/log.py index f6912a15..d8dbf199 100644 --- a/toolserve/toolserve/server/common/log.py +++ b/arcade/arcade/actor/common/log.py @@ -1,18 +1,14 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- from __future__ import annotations import os - from typing import TYPE_CHECKING from loguru import logger -from toolserve.server.core.conf import settings +from arcade.actor.core.conf import settings -from pathlib import Path - -server_log_path = os.path.join(settings.WORK_DIR, 'server_logs') +actor_log_path = os.path.join(settings.WORK_DIR, "actor_logs") if TYPE_CHECKING: @@ -21,7 +17,7 @@ if TYPE_CHECKING: class Logger: def __init__(self): - self.log_path = server_log_path + self.log_path = actor_log_path def log(self) -> loguru.Logger: if not os.path.exists(self.log_path): @@ -30,12 +26,17 @@ class Logger: log_stdout_file = os.path.join(self.log_path, settings.LOG_STDOUT_FILENAME) log_stderr_file = os.path.join(self.log_path, settings.LOG_STDERR_FILENAME) - log_config = dict(rotation='10 MB', retention='15 days', compression='tar.gz', enqueue=True) + log_config = { + "rotation": "10 MB", + "retention": "15 days", + "compression": "tar.gz", + "enqueue": True, + } # stdout logger.add( log_stdout_file, - level='INFO', - filter=lambda record: record['level'].name == 'INFO' or record['level'].no <= 25, + level="INFO", + filter=lambda record: record["level"].name == "INFO" or record["level"].no <= 25, **log_config, backtrace=False, diagnose=False, @@ -43,8 +44,8 @@ class Logger: # stderr logger.add( log_stderr_file, - level='ERROR', - filter=lambda record: record['level'].name == 'ERROR' or record['level'].no >= 30, + level="ERROR", + filter=lambda record: record["level"].name == "ERROR" or record["level"].no >= 30, **log_config, backtrace=True, diagnose=True, @@ -52,4 +53,5 @@ class Logger: return logger -log = Logger().log() \ No newline at end of file + +log = Logger().log() diff --git a/toolserve/toolserve/server/common/response.py b/arcade/arcade/actor/common/response.py similarity index 83% rename from toolserve/toolserve/server/common/response.py rename to arcade/arcade/actor/common/response.py index 642129df..ad5857e3 100644 --- a/toolserve/toolserve/server/common/response.py +++ b/arcade/arcade/actor/common/response.py @@ -1,16 +1,15 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- from datetime import datetime from typing import Any from pydantic import BaseModel, ConfigDict -from toolserve.server.common.response_code import CustomResponse, CustomResponseCode -from toolserve.server.core.conf import settings +from arcade.actor.common.response_code import CustomResponse, CustomResponseCode +from arcade.actor.core.conf import settings _ExcludeData = set[int | str] | dict[int | str, Any] -__all__ = ['ResponseModel', 'response_base'] +__all__ = ["ResponseModel", "response_base"] class ResponseModel(BaseModel): @@ -33,7 +32,9 @@ class ResponseModel(BaseModel): """ # TODO: json_encoders: https://github.com/tiangolo/fastapi/discussions/10252 - model_config = ConfigDict(json_encoders={datetime: lambda x: x.strftime(settings.DATETIME_FORMAT)}) + model_config = ConfigDict( + json_encoders={datetime: lambda x: x.strftime(settings.DATETIME_FORMAT)} + ) code: int = CustomResponseCode.HTTP_200.code msg: str = CustomResponseCode.HTTP_200.msg @@ -56,7 +57,12 @@ class ResponseBase: """ @staticmethod - async def __response(*, res: CustomResponseCode | CustomResponse = None, msg: str | None = None, data: Any | None = None) -> ResponseModel: + async def __response( + *, + res: CustomResponseCode | CustomResponse = None, + msg: str | None = None, + data: Any | None = None, + ) -> ResponseModel: """ General method for successful response diff --git a/toolserve/toolserve/server/common/response_code.py b/arcade/arcade/actor/common/response_code.py similarity index 82% rename from toolserve/toolserve/server/common/response_code.py rename to arcade/arcade/actor/common/response_code.py index 2d97a21a..c8b1a2c9 100644 --- a/toolserve/toolserve/server/common/response_code.py +++ b/arcade/arcade/actor/common/response_code.py @@ -1,7 +1,5 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- import dataclasses - from enum import Enum @@ -25,27 +23,30 @@ class CustomCodeBase(Enum): class CustomResponseCode(CustomCodeBase): """自定义响应状态码""" - HTTP_200 = (200, 'Request Successful') - HTTP_201 = (201, 'Created Successfully') - HTTP_202 = (202, 'Request Accepted, but Processing Not Yet Complete') - HTTP_204 = (204, 'Request Successful, but No Content Returned') - HTTP_400 = (400, 'Bad Request') - HTTP_401 = (401, 'Unauthorized') - HTTP_403 = (403, 'Forbidden Access') - HTTP_404 = (404, 'Requested Resource Not Found') - HTTP_410 = (410, 'Requested Resource Permanently Deleted') - HTTP_422 = (422, 'Invalid Request Parameters') - HTTP_425 = (425, 'Request Unexecutable, as Server Cannot Meet Requirements') - HTTP_429 = (429, 'Too Many Requests, Server Limiting') - HTTP_500 = (500, 'Internal Server Error') - HTTP_502 = (502, 'Gateway Error') - HTTP_503 = (503, 'Server Temporarily Unable to Process Request') - HTTP_504 = (504, 'Gateway Timeout') + + HTTP_200 = (200, "Request Successful") + HTTP_201 = (201, "Created Successfully") + HTTP_202 = (202, "Request Accepted, but Processing Not Yet Complete") + HTTP_204 = (204, "Request Successful, but No Content Returned") + HTTP_400 = (400, "Bad Request") + HTTP_401 = (401, "Unauthorized") + HTTP_403 = (403, "Forbidden Access") + HTTP_404 = (404, "Requested Resource Not Found") + HTTP_410 = (410, "Requested Resource Permanently Deleted") + HTTP_422 = (422, "Invalid Request Parameters") + HTTP_425 = (425, "Request Unexecutable, as Server Cannot Meet Requirements") + HTTP_429 = (429, "Too Many Requests, Server Limiting") + HTTP_500 = (500, "Internal Server Error") + HTTP_502 = (502, "Gateway Error") + HTTP_503 = (503, "Server Temporarily Unable to Process Request") + HTTP_504 = (504, "Gateway Timeout") + class CustomErrorCode(CustomCodeBase): """自定义错误状态码""" - CAPTCHA_ERROR = (40001, 'CAPTCHA Error') + CAPTCHA_ERROR = (40001, "CAPTCHA Error") + @dataclasses.dataclass class CustomResponse: @@ -56,6 +57,7 @@ class CustomResponse: code: int msg: str + class StandardResponseCode: """Standard response status codes""" diff --git a/arcade/arcade/actor/common/serializers.py b/arcade/arcade/actor/common/serializers.py new file mode 100644 index 00000000..e4bea3d3 --- /dev/null +++ b/arcade/arcade/actor/common/serializers.py @@ -0,0 +1,13 @@ +from typing import Any + +import msgspec +from starlette.responses import JSONResponse + + +class MsgSpecJSONResponse(JSONResponse): + """ + JSON response using the high-performance msgspec library to serialize data to JSON. + """ + + def render(self, content: Any) -> bytes: + return msgspec.json.encode(content) diff --git a/toolserve/toolserve/apm/__init__.py b/arcade/arcade/actor/core/__init__.py similarity index 100% rename from toolserve/toolserve/apm/__init__.py rename to arcade/arcade/actor/core/__init__.py diff --git a/arcade/arcade/actor/core/conf.py b/arcade/arcade/actor/core/conf.py new file mode 100644 index 00000000..56eda9ba --- /dev/null +++ b/arcade/arcade/actor/core/conf.py @@ -0,0 +1,71 @@ +import os +from functools import lru_cache +from pathlib import Path +from typing import Literal + +from pydantic_settings import BaseSettings, SettingsConfigDict + + +# https://docs.pydantic.dev/latest/concepts/pydantic_settings/ +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_file=".env") + + WORK_DIR: Path = Path.home() / ".arcade" + TOOLS_DIR: Path = os.getcwd() + + # Env Config + ENVIRONMENT: Literal["dev", "pro"] = "dev" + + # FastAPI + API_V1_STR: str = "/v1" + API_ACTION_STR: str = "/tool" + TITLE: str = "Arcade AI Toolserver" + VERSION: str = "0.1.0" + DESCRIPTION: str = "Arcade AI Toolserver API" + DOCS_URL: str | None = f"{API_V1_STR}/docs" + REDOCS_URL: str | None = f"{API_V1_STR}/redocs" + OPENAPI_URL: str | None = f"{API_V1_STR}/openapi" + + # @model_validator(mode='before') + # @classmethod + # def validate_openapi_url(cls, values): + # if values['ENVIRONMENT'] == 'pro': + # values['OPENAPI_URL'] = None + # return values + + # Uvicorn + UVICORN_HOST: str = "127.0.0.1" + UVICORN_PORT: int = 8000 + UVICORN_RELOAD: bool = True + + # Static Server + STATIC_FILES: bool = False + + # Logs + LOG_STDOUT_FILENAME: str = "actor.log" + LOG_STDERR_FILENAME: str = "actor.err" + + # DateTime + DATETIME_TIMEZONE: str = "US/Pacific" + DATETIME_FORMAT: str = "%Y-%m-%d %H:%M:%S" + + # Middleware + MIDDLEWARE_CORS: bool = True + MIDDLEWARE_GZIP: bool = True + MIDDLEWARE_ACCESS: bool = False + + # these should be set in .env + TOKEN_SECRET_KEY: str = "secret" + OPERA_LOG_ENCRYPT_SECRET_KEY: str = "secret" + + +@lru_cache +def get_settings(): + try: + env_path = Path(os.environ["TOOLSERVE_ENV"]) + except KeyError: + env_path = Path(__file__).parent.parent / ".env" + return Settings(_env_file=env_path) + + +settings = get_settings() diff --git a/toolserve/toolserve/server/core/depends.py b/arcade/arcade/actor/core/depends.py similarity index 66% rename from toolserve/toolserve/server/core/depends.py rename to arcade/arcade/actor/core/depends.py index 74e7bd1a..40a0f7a5 100644 --- a/toolserve/toolserve/server/core/depends.py +++ b/arcade/arcade/actor/core/depends.py @@ -1,4 +1,5 @@ from starlette.requests import Request + def get_catalog(request: Request): - return request.app.state.catalog \ No newline at end of file + return request.app.state.catalog diff --git a/toolserve/toolserve/server/core/generate.py b/arcade/arcade/actor/core/generate.py similarity index 67% rename from toolserve/toolserve/server/core/generate.py rename to arcade/arcade/actor/core/generate.py index 7efe7f04..73aeb0fb 100644 --- a/toolserve/toolserve/server/core/generate.py +++ b/arcade/arcade/actor/core/generate.py @@ -1,19 +1,14 @@ -import os -import sys import traceback -import inspect from textwrap import dedent -from typing import List, Optional, Type, Annotated, Dict -from pathlib import Path -from fastapi import APIRouter, Body, Depends, Path, HTTPException -from pydantic import BaseModel, ValidationError, create_model -from importlib import import_module +from fastapi import APIRouter +from pydantic import BaseModel, ValidationError -from toolserve.server.core.catalog import ToolSchema -from toolserve.server.core.conf import settings -from toolserve.server.common.response_code import CustomResponseCode -from toolserve.server.common.response import ResponseModel, response_base +from arcade.actor.common.response import response_base +from arcade.actor.common.response_code import CustomResponseCode +from arcade.actor.core.conf import settings +from arcade.tool.catalog import ToolDefinition +from arcade.utils import snake_to_pascal_case def create_endpoint_function(name, description, func, input_model, output_model): @@ -38,35 +33,35 @@ def create_endpoint_function(name, description, func, input_model, output_model) return run - -def generate_endpoint(schemas: List[ToolSchema]) -> APIRouter: +def generate_endpoint(schemas: list[ToolDefinition]) -> APIRouter: routers = [] top_level_router = APIRouter(prefix=settings.API_ACTION_STR) for schema in schemas: router = APIRouter(prefix="/" + schema.meta.module) + define = schema.definition # Create the endpoint function run = create_endpoint_function( - name=schema.name, - description=schema.description, + name=snake_to_pascal_case(define.name), + description=define.description, func=schema.tool, input_model=schema.input_model, - output_model=schema.output_model + output_model=schema.output_model, ) # Add the endpoint to the FastAPI app router.post( - f"/{schema.name}", - name=schema.name, - summary=schema.description, + f"/{snake_to_pascal_case(define.name)}", + name=snake_to_pascal_case(define.name), + summary=define.description, tags=[schema.meta.module], response_model=schema.output_model, response_model_exclude_unset=True, response_model_exclude_none=True, - response_description=create_output_description(schema.output_model) - )(run) + response_description=create_output_description(schema.output_model), + )(run) routers.append(router) for router in routers: @@ -74,8 +69,7 @@ def generate_endpoint(schemas: List[ToolSchema]) -> APIRouter: return top_level_router - -def create_output_description(output_model: Type[BaseModel]) -> str: +def create_output_description(output_model: type[BaseModel]) -> str: """ Create a description string for the output model. """ @@ -88,4 +82,4 @@ def create_output_description(output_model: Type[BaseModel]) -> str: for name, field in output_model.model_fields.items(): output_description += f"- **{name}** ({field.annotation.__name__})\n" - return output_description \ No newline at end of file + return output_description diff --git a/toolserve/toolserve/server/core/registrar.py b/arcade/arcade/actor/core/registrar.py similarity index 69% rename from toolserve/toolserve/server/core/registrar.py rename to arcade/arcade/actor/core/registrar.py index 73e834dd..6b894591 100644 --- a/toolserve/toolserve/server/core/registrar.py +++ b/arcade/arcade/actor/core/registrar.py @@ -1,13 +1,11 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- from contextlib import asynccontextmanager -from fastapi import Depends, FastAPI +from fastapi import FastAPI -from toolserve.server.routes import v1 -from toolserve.server.database.db_sqlite import create_table -from toolserve.server.core.conf import settings -from toolserve.server.common.serializers import MsgSpecJSONResponse +from arcade.actor.common.serializers import MsgSpecJSONResponse +from arcade.actor.core.conf import settings +from arcade.actor.routes import v1 @asynccontextmanager @@ -16,9 +14,7 @@ async def register_init(app: FastAPI): :return: """ - # create database tables - await create_table() - + # eventually lifecycle hooks will be added here yield @@ -41,7 +37,7 @@ def register_app(): register_router(app) - #register_exception(app) + # register_exception(app) generate_actions_routers(app) @@ -59,9 +55,9 @@ def register_static_file(app: FastAPI): from fastapi.staticfiles import StaticFiles - if not os.path.exists('./static'): - os.mkdir('./static') - app.mount('/static', StaticFiles(directory='static'), name='static') + if not os.path.exists("./static"): + os.mkdir("./static") + app.mount("/static", StaticFiles(directory="static"), name="static") def register_middleware(app: FastAPI): @@ -82,10 +78,10 @@ def register_middleware(app: FastAPI): app.add_middleware( CORSMiddleware, - allow_origins=['*'], + allow_origins=["*"], allow_credentials=True, - allow_methods=['*'], - allow_headers=['*'], + allow_methods=["*"], + allow_headers=["*"], ) @@ -108,10 +104,10 @@ def generate_actions_routers(app: FastAPI): :param app: FastAPI :return: """ - from toolserve.server.core.generate import generate_endpoint - from toolserve.server.core.catalog import ToolCatalog + from arcade.actor.core.generate import generate_endpoint + from arcade.tool.catalog import ToolCatalog catalog = ToolCatalog() router = generate_endpoint(catalog.tools.values()) app.include_router(router) - app.state.catalog = catalog \ No newline at end of file + app.state.catalog = catalog diff --git a/arcade/arcade/actor/main.py b/arcade/arcade/actor/main.py new file mode 100644 index 00000000..4e30ea7a --- /dev/null +++ b/arcade/arcade/actor/main.py @@ -0,0 +1,21 @@ +from pathlib import Path + +import uvicorn + +from arcade.actor.common.log import log +from arcade.actor.core.conf import settings +from arcade.actor.core.registrar import register_app + +app = register_app() + +if __name__ == "__main__": + try: + log.info("Arcade AI Toolserve is starting...") + uvicorn.run( + app=f"{Path(__file__).stem}:app", + host=settings.UVICORN_HOST, + port=settings.UVICORN_PORT, + reload=settings.UVICORN_RELOAD, + ) + except Exception as e: + log.error(f"FastAPI start filed: {e}") diff --git a/arcade/arcade/actor/routes/__init__.py b/arcade/arcade/actor/routes/__init__.py new file mode 100644 index 00000000..bca6b037 --- /dev/null +++ b/arcade/arcade/actor/routes/__init__.py @@ -0,0 +1,7 @@ +from fastapi import APIRouter + +from arcade.actor.core.conf import settings +from arcade.actor.routes.tool import router as tool_router + +v1 = APIRouter(prefix=settings.API_V1_STR) +v1.include_router(tool_router, prefix="/tools", tags=["Tool Catalog"]) diff --git a/arcade/arcade/actor/routes/tool.py b/arcade/arcade/actor/routes/tool.py new file mode 100644 index 00000000..8da7859c --- /dev/null +++ b/arcade/arcade/actor/routes/tool.py @@ -0,0 +1,59 @@ +from fastapi import APIRouter, Body, Depends, Query +from pydantic import ValidationError + +from arcade.actor.common.response import ResponseModel, response_base +from arcade.actor.common.response_code import CustomResponseCode +from arcade.actor.core.depends import get_catalog +from arcade.tool.openai import schema_to_openai_tool + +router = APIRouter() + + +@router.get( + "/list", + summary="List available tools", +) +async def list_tools(catalog=Depends(get_catalog)) -> ResponseModel: + """List all available tools""" + + tools = catalog.list_tools() + return await response_base.success(data=tools) + + +@router.get("/json", summary="Get the JSON (openai) format of a tool") +async def get_oai_function( + tool_name: str = Query(..., title="Tool Name", description="The name of the tool"), + catalog=Depends(get_catalog), +) -> ResponseModel: + """Get the OpenAI function format of an tool""" + + try: + # TODO handle keyerror + tool = catalog[tool_name] + json_data = schema_to_openai_tool(tool) + + return await response_base.success(data=json_data) + except ValidationError as e: + return await response_base.fail(res=CustomResponseCode.HTTP_400, data=str(e)) + except Exception as e: + return await response_base.fail(res=CustomResponseCode.HTTP_500, data=str(e)) + + +@router.post("/execute", summary="Execute a tool") +async def execute_tool( + tool_name: str = Query(..., title="Tool Name", description="The name of the tool"), + data: dict[str, str] = Body( + ..., title="Tool Data", description="The data to execute the tool with" + ), + catalog=Depends(get_catalog), +) -> ResponseModel: + """Execute a tool""" + + try: + tool = catalog.get_tool(tool_name) + result = await tool(**data) + return await response_base.success(data=result) + except ValidationError as e: + return await response_base.fail(res=CustomResponseCode.HTTP_400, data=str(e)) + except Exception as e: + return await response_base.fail(res=CustomResponseCode.HTTP_500, data=str(e)) diff --git a/toolserve/toolserve/cli/__init__.py b/arcade/arcade/actor/schemas/__init__.py similarity index 100% rename from toolserve/toolserve/cli/__init__.py rename to arcade/arcade/actor/schemas/__init__.py diff --git a/arcade/arcade/actor/schemas/base.py b/arcade/arcade/actor/schemas/base.py new file mode 100644 index 00000000..9972e67c --- /dev/null +++ b/arcade/arcade/actor/schemas/base.py @@ -0,0 +1,145 @@ +from pydantic import BaseModel, ConfigDict, EmailStr, validate_email + +# Custom validation error messages do not include the expected content of validation (i.e., input content). For supported expected content fields, refer to the following link: +# https://github.com/pydantic/pydantic-core/blob/a5cb7382643415b716b1a7a5392914e50f726528/tests/test_errors.py#L266 +# For replacing expected content fields, refer to the following link: +# https://github.com/pydantic/pydantic/blob/caa78016433ec9b16a973f92f187a7b6bfde6cb5/docs/errors/errors.md?plain=1#L232 +CUSTOM_VALIDATION_ERROR_MESSAGES = { + "arguments_type": "Incorrect argument type input", + "assertion_error": "Assertion execution error", + "bool_parsing": "Boolean value parsing error", + "bool_type": "Boolean type input error", + "bytes_too_long": "Byte length input too long", + "bytes_too_short": "Byte length input too short", + "bytes_type": "Byte type input error", + "callable_type": "Callable object type input error", + "dataclass_exact_type": "Dataclass instance type input error", + "dataclass_type": "Dataclass type input error", + "date_from_datetime_inexact": "Non-zero date component input", + "date_from_datetime_parsing": "Date input parsing error", + "date_future": "Date input is not in the future", + "date_parsing": "Date input validation error", + "date_past": "Date input is not in the past", + "date_type": "Date type input error", + "datetime_future": "Datetime input is not in the future", + "datetime_object_invalid": "Datetime input object invalid", + "datetime_parsing": "Datetime input parsing error", + "datetime_past": "Datetime input is not in the past", + "datetime_type": "Datetime type input error", + "decimal_max_digits": "Decimal input has too many digits", + "decimal_max_places": "Decimal places input error", + "decimal_parsing": "Decimal input parsing error", + "decimal_type": "Decimal type input error", + "decimal_whole_digits": "Decimal whole digits input error", + "dict_type": "Dictionary type input error", + "enum": "Enum member input error, allowed {expected}", + "extra_forbidden": "Extra fields input forbidden", + "finite_number": "Finite value input error", + "float_parsing": "Float parsing error", + "float_type": "Float type input error", + "frozen_field": "Frozen field input error", + "frozen_instance": "Modification of frozen instance forbidden", + "frozen_set_type": "Frozen set type input forbidden", + "get_attribute_error": "Attribute retrieval error", + "greater_than": "Input value too large", + "greater_than_equal": "Input value too large or equal", + "int_from_float": "Integer type input error", + "int_parsing": "Integer input parsing error", + "int_parsing_size": "Integer input parsing size error", + "int_type": "Integer type input error", + "invalid_key": "Invalid key input", + "is_instance_of": "Instance type input error", + "is_subclass_of": "Subclass type input error", + "iterable_type": "Iterable type input error", + "iteration_error": "Iteration value input error", + "json_invalid": "JSON string input error", + "json_type": "JSON type input error", + "less_than": "Input value too small", + "less_than_equal": "Input value too small or equal", + "list_type": "List type input error", + "literal_error": "Literal input error", + "mapping_type": "Mapping type input error", + "missing": "Missing required field", + "missing_argument": "Missing argument", + "missing_keyword_only_argument": "Missing keyword-only argument", + "missing_positional_only_argument": "Missing positional-only argument", + "model_attributes_type": "Model attributes type input error", + "model_type": "Model instance input error", + "multiple_argument_values": "Multiple argument values input", + "multiple_of": "Input value not a multiple", + "no_such_attribute": "Invalid attribute assignment", + "none_required": "Input value must be None", + "recursion_loop": "Recursion loop in input", + "set_type": "Set type input error", + "string_pattern_mismatch": "String pattern mismatch input", + "string_sub_type": "String subtype (non-strict instance) input error", + "string_too_long": "String input too long", + "string_too_short": "String input too short", + "string_type": "String type input error", + "string_unicode": "String input not Unicode", + "time_delta_parsing": "Time delta parsing error", + "time_delta_type": "Time delta type input error", + "time_parsing": "Time input parsing error", + "time_type": "Time type input error", + "timezone_aware": "Missing timezone input", + "timezone_naive": "Timezone input forbidden", + "too_long": "Input too long", + "too_short": "Input too short", + "tuple_type": "Tuple type input error", + "unexpected_keyword_argument": "Unexpected keyword argument input", + "unexpected_positional_argument": "Unexpected positional argument input", + "union_tag_invalid": "Union tag literal input error", + "union_tag_not_found": "Union tag argument not found", + "url_parsing": "URL input parsing error", + "url_scheme": "URL scheme input error", + "url_syntax_violation": "URL syntax violation", + "url_too_long": "URL input too long", + "url_type": "URL type input error", + "uuid_parsing": "UUID parsing error", + "uuid_type": "UUID type input error", + "uuid_version": "UUID version type input error", + "value_error": "Value input error", +} + +CUSTOM_USAGE_ERROR_MESSAGES = { + "class-not-fully-defined": "Class attributes type not fully defined", + "custom-json-schema": "__modify_schema__ method deprecated in V2", + "decorator-missing-field": "Invalid field validator defined", + "discriminator-no-field": "Discriminator field not fully defined", + "discriminator-alias-type": "Discriminator field defined using non-string type", + "discriminator-needs-literal": "Discriminator field requires literal definition", + "discriminator-alias": "Inconsistent discriminator field alias definition", + "discriminator-validator": "Field validator forbidden on discriminator field", + "model-field-overridden": "Typeless field override forbidden", + "model-field-missing-annotation": "Missing field type definition", + "config-both": "Duplicate configuration item defined", + "removed-kwargs": "Removed keyword configuration parameter called", + "invalid-for-json-schema": "Invalid JSON type present", + "base-model-instantiated": "Instantiation of base model forbidden", + "undefined-annotation": "Missing type definition", + "schema-for-unknown-type": "Unknown type definition", + "create-model-field-definitions": "Field definition error", + "create-model-config-base": "Configuration item definition error", + "validator-no-fields": "Field validator without specified fields", + "validator-invalid-fields": "Field validator fields definition error", + "validator-instance-method": "Field validator must be a class method", + "model-serializer-instance-method": "Serializer must be an instance method", + "validator-v1-signature": "V1 field validator error deprecated", + "validator-signature": "Field validator signature error", + "field-serializer-signature": "Field serializer signature unrecognized", + "model-serializer-signature": "Model serializer signature unrecognized", + "multiple-field-serializers": "Field serializers defined multiple times", + "invalid_annotated_type": "Invalid type definition", + "type-adapter-config-unused": "Type adapter configuration item definition error", + "root-model-extra": "Extra fields on root model forbidden", +} + + +class CustomEmailStr(EmailStr): + @classmethod + def _validate(cls, __input_value: str) -> str: + return None if __input_value == "" else validate_email(__input_value)[1] + + +class SchemaBase(BaseModel): + model_config = ConfigDict(use_enum_values=True) diff --git a/toolserve/toolserve/server/__init__.py b/arcade/arcade/actor/utils/__init__.py similarity index 100% rename from toolserve/toolserve/server/__init__.py rename to arcade/arcade/actor/utils/__init__.py diff --git a/toolserve/toolserve/server/utils/timezone.py b/arcade/arcade/actor/utils/timezone.py similarity index 92% rename from toolserve/toolserve/server/utils/timezone.py rename to arcade/arcade/actor/utils/timezone.py index 38a00b8f..601db95b 100644 --- a/toolserve/toolserve/server/utils/timezone.py +++ b/arcade/arcade/actor/utils/timezone.py @@ -1,10 +1,8 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- import zoneinfo - from datetime import datetime -from toolserve.server.core.conf import settings +from arcade.actor.core.conf import settings class TimeZone: diff --git a/toolserve/toolserve/server/common/__init__.py b/arcade/arcade/apm/__init__.py similarity index 100% rename from toolserve/toolserve/server/common/__init__.py rename to arcade/arcade/apm/__init__.py diff --git a/toolserve/toolserve/apm/base.py b/arcade/arcade/apm/base.py similarity index 69% rename from toolserve/toolserve/apm/base.py rename to arcade/arcade/apm/base.py index e5aca0a3..00f14633 100644 --- a/toolserve/toolserve/apm/base.py +++ b/arcade/arcade/apm/base.py @@ -1,12 +1,10 @@ - import os -import toml -import json -import tomlkit - from pathlib import Path -from pydantic import BaseModel, ValidationError, EmailStr, Field -from typing import Dict, List, Optional, TypeVar, Any, Tuple, Union +from typing import Optional, Union + +import toml +import tomlkit +from pydantic import BaseModel, EmailStr class PackInfo(BaseModel): @@ -19,11 +17,11 @@ class PackInfo(BaseModel): class ToolPack(BaseModel): pack: PackInfo - depends: Optional[Dict[str, str]] = None - tools: Optional[Dict[str, str]] = {} + depends: Optional[dict[str, str]] = None + tools: Optional[dict[str, str]] = {} def write_lock_file(self, pack_dir: Union[str, os.PathLike]): - lock_file = Path(pack_dir) / 'pack.lock.toml' + lock_file = Path(pack_dir) / "pack.lock.toml" pack_dict = self.dict(by_alias=True, exclude_none=True) pack_ordered_dict = { "pack": pack_dict.get("pack"), @@ -37,13 +35,13 @@ class ToolPack(BaseModel): doc[key] = value # Write the tomlkit document to file - with open(lock_file, 'w') as f: + with open(lock_file, "w") as f: f.write(tomlkit.dumps(doc)) @classmethod def from_lock_file(cls, pack_dir: Union[str, os.PathLike]): pack_dir = Path(pack_dir).resolve() - lock_file = pack_dir / 'pack.lock.toml' - with open(lock_file, 'r') as f: + lock_file = pack_dir / "pack.lock.toml" + with open(lock_file) as f: data = toml.load(f) - return cls(**data) \ No newline at end of file + return cls(**data) diff --git a/toolserve/toolserve/apm/pack.py b/arcade/arcade/apm/pack.py similarity index 64% rename from toolserve/toolserve/apm/pack.py rename to arcade/arcade/apm/pack.py index dd49cc68..8dc8f149 100644 --- a/toolserve/toolserve/apm/pack.py +++ b/arcade/arcade/apm/pack.py @@ -1,28 +1,27 @@ import os -import json -import toml import shutil - from pathlib import Path -from typing import List, Optional, Dict, Union -from pydantic import BaseModel, Field, ValidationError, EmailStr +from typing import Union + +import toml +from pydantic import ValidationError + +from arcade.apm.base import PackInfo, ToolPack +from arcade.apm.parse import get_tools_from_file +from arcade.utils import snake_to_pascal_case -from toolserve.apm.base import PackInfo, ToolPack -from toolserve.apm.parse import get_tools_from_file -from toolserve.utils import snake_to_camel class Packer: - def __init__(self, pack_dir: Union[str, os.PathLike]): self.pack_dir = Path(pack_dir).resolve() - self.tools_dir = self.pack_dir / 'tools' + self.tools_dir = self.pack_dir / "tools" # Load the action pack configuration from a TOML file try: - with open(self.pack_dir / 'pack.toml', 'r') as f: + with open(self.pack_dir / "pack.toml") as f: pack_data = toml.load(f) - self.pack = PackInfo(**pack_data['pack']) - self.modules = pack_data['modules'] + self.pack = PackInfo(**pack_data["pack"]) + self.modules = pack_data["modules"] except FileNotFoundError: raise FileNotFoundError(f"No 'pack.toml' found in {self.tools_dir}") @@ -30,14 +29,13 @@ class Packer: raise ValueError(f"Invalid 'pack.toml' format: {e}") self.tools = self.load_tools() - self.depends = {} # TODO - self.packs = [] # TODO + self.depends = {} # TODO + self.packs = [] # TODO - - def load_tools(self) -> Dict[str, str]: + def load_tools(self) -> dict[str, str]: tools = {} - for tool_file in self.tools_dir.rglob('*.py'): - if '__init__.py' in tool_file.name: + for tool_file in self.tools_dir.rglob("*.py"): + if "__init__.py" in tool_file.name: continue try: module = tool_file.stem @@ -46,15 +44,14 @@ class Packer: found_tools = get_tools_from_file(tool_file) for tool in found_tools: tool_name = module + "." + tool + "@" + version - tools[snake_to_camel(tool)] = tool_name + tools[snake_to_pascal_case(tool)] = tool_name except Exception as e: print(f"Error loading tool from {tool_file}: {e}") return tools - def _create_pack_dir(self, pack: ToolPack) -> Path: # Make "packs" directory if it doesn't exist - packs_dir = self.pack_dir / 'packs' + packs_dir = self.pack_dir / "packs" os.makedirs(packs_dir, exist_ok=True) # make the dir for the action pack and the version (making parent dirs if needed) top_pack_dir = packs_dir / pack.pack.name / pack.pack.version @@ -66,11 +63,7 @@ class Packer: def create_pack(self): # Create an ActionPack instance from the loaded data - pack = ToolPack( - pack=self.pack, - depends=self.depends, - tools=self.tools - ) - #pack_dir = self._create_pack_dir(pack) + pack = ToolPack(pack=self.pack, depends=self.depends, tools=self.tools) + # pack_dir = self._create_pack_dir(pack) # Write the action pack to a TOML file pack.write_lock_file(self.pack_dir) diff --git a/toolserve/toolserve/apm/parse.py b/arcade/arcade/apm/parse.py similarity index 84% rename from toolserve/toolserve/apm/parse.py rename to arcade/arcade/apm/parse.py index 83e793a4..0795defc 100644 --- a/toolserve/toolserve/apm/parse.py +++ b/arcade/arcade/apm/parse.py @@ -1,13 +1,12 @@ import ast -import sys -import os -import shutil -from typing import Dict, List, Optional, Tuple, Any import importlib.metadata import importlib.util -import toml +import sys +from typing import Optional + from stdlib_list import stdlib_list + def load_ast_tree(filepath: str) -> ast.AST: """ Load and parse the Abstract Syntax Tree (AST) from a Python file. @@ -16,11 +15,12 @@ def load_ast_tree(filepath: str) -> ast.AST: :return: AST of the Python file. """ try: - with open(filepath, "r") as file: + with open(filepath) as file: return ast.parse(file.read(), filename=filepath) except FileNotFoundError: raise FileNotFoundError(f"File {filepath} not found") + def get_python_version() -> str: """ Get the current Python version. @@ -29,7 +29,8 @@ def get_python_version() -> str: """ return f"{sys.version_info.major}.{sys.version_info.minor}" -def retrieve_imported_libraries(tree: ast.AST) -> Dict[str, Optional[str]]: + +def retrieve_imported_libraries(tree: ast.AST) -> dict[str, Optional[str]]: """ Retrieve non-standard libraries imported in the AST. @@ -42,8 +43,8 @@ def retrieve_imported_libraries(tree: ast.AST) -> Dict[str, Optional[str]]: for node in ast.walk(tree): if isinstance(node, ast.ImportFrom): - package_name = node.module.split('.')[0] if node.module else None - if package_name == 'dstar' or package_name in stdlib_modules: + package_name = node.module.split(".")[0] if node.module else None + if package_name == "dstar" or package_name in stdlib_modules: continue try: package_version = importlib.metadata.version(package_name) @@ -60,13 +61,14 @@ def get_function_name_if_decorated(node: ast.FunctionDef) -> Optional[str]: :param node: The function definition node from the AST. :return: The name of the function if it has the specified decorators, otherwise None. """ - decorator_ids = {'toolserve.tool', 'tool'} + decorator_ids = {"toolserve.tool", "tool"} for decorator in node.decorator_list: if isinstance(decorator, ast.Name) and decorator.id in decorator_ids: return node.name return None -def get_tools_from_file(filepath: str) -> List[str]: + +def get_tools_from_file(filepath: str) -> list[str]: """ Get the names of all functions in a Python file that are decorated with either "@toolserve.tool" or "@tool". @@ -80,4 +82,4 @@ def get_tools_from_file(filepath: str) -> List[str]: tool_name = get_function_name_if_decorated(node) if tool_name: tools.append(tool_name) - return tools \ No newline at end of file + return tools diff --git a/toolserve/toolserve/server/core/__init__.py b/arcade/arcade/cli/__init__.py similarity index 100% rename from toolserve/toolserve/server/core/__init__.py rename to arcade/arcade/cli/__init__.py diff --git a/toolserve/toolserve/cli/main.py b/arcade/arcade/cli/main.py similarity index 50% rename from toolserve/toolserve/cli/main.py rename to arcade/arcade/cli/main.py index 9794df67..10830bd4 100644 --- a/toolserve/toolserve/cli/main.py +++ b/arcade/arcade/cli/main.py @@ -1,35 +1,30 @@ import os + import typer import uvicorn - -from pathlib import Path from rich.console import Console from rich.markup import escape -from toolserve.server.core.conf import settings - +from arcade.actor.core.conf import settings cli = typer.Typer() console = Console() + @cli.command(help="Starts the ToolServer with specified configurations.") def serve( host: str = typer.Option( - settings.UVICORN_HOST, - help="Host for the app, from settings by default.", - show_default=True + settings.UVICORN_HOST, help="Host for the app, from settings by default.", show_default=True ), port: int = typer.Option( - settings.UVICORN_PORT, - help="Port for the app, settings default.", - show_default=True + settings.UVICORN_PORT, help="Port for the app, settings default.", show_default=True ), ): """ - Starts the server with host, port, and reload options. Uses - Uvicorn as ASGI server. Parameters allow runtime configuration. + Starts the actor with host, port, and reload options. Uses + Uvicorn as ASGI actor. Parameters allow runtime configuration. """ - from toolserve.server.main import app + from arcade.actor.main import app try: uvicorn.run( @@ -38,30 +33,27 @@ def serve( port=port, ) except KeyboardInterrupt: - console.print("Server stopped by user.", style="bold red") + console.print("actor stopped by user.", style="bold red") typer.Exit() except Exception as e: - error_message = f'❌ Failed to start Toolserver: {escape(str(e))}' + error_message = f"❌ Failed to start Toolserver: {escape(str(e))}" console.print(error_message, style="bold red") raise typer.Exit(code=1) + @cli.command(help="Build a new Tool Pack") def pack( - directory: str = typer.Option( - os.getcwd(), - "--dir", - help="tools directory path with pack.toml" - ), + directory: str = typer.Option(os.getcwd(), "--dir", help="tools directory path with pack.toml"), ): """ Creates a new tool pack with the given name, description, and result type. """ - from toolserve.apm.pack import Packer + from arcade.apm.pack import Packer try: pack = Packer(directory) pack.create_pack() except Exception as e: - error_message = f'❌ Failed to build Tool Pack: {escape(str(e))}' + error_message = f"❌ Failed to build Tool Pack: {escape(str(e))}" console.print(error_message, style="bold red") - raise typer.Exit(code=1) \ No newline at end of file + raise typer.Exit(code=1) diff --git a/toolserve/toolserve/server/crud/__init__.py b/arcade/arcade/sdk/__init__.py similarity index 100% rename from toolserve/toolserve/server/crud/__init__.py rename to arcade/arcade/sdk/__init__.py diff --git a/arcade/arcade/sdk/annotations.py b/arcade/arcade/sdk/annotations.py new file mode 100644 index 00000000..7ec7f07b --- /dev/null +++ b/arcade/arcade/sdk/annotations.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Inferrable: + """An annotation indicating that a parameter can be inferred by a model (default: True).""" + + value: bool = True diff --git a/arcade/arcade/sdk/errors.py b/arcade/arcade/sdk/errors.py new file mode 100644 index 00000000..ce0d08df --- /dev/null +++ b/arcade/arcade/sdk/errors.py @@ -0,0 +1,14 @@ +class ToolError(Exception): + """ + Base class for all errors related to tools. + """ + + pass + + +class ToolDefinitionError(ToolError): + """ + Raised when there is an error in the definition of a tool. + """ + + pass diff --git a/arcade/arcade/sdk/schemas.py b/arcade/arcade/sdk/schemas.py new file mode 100644 index 00000000..b6c36f87 --- /dev/null +++ b/arcade/arcade/sdk/schemas.py @@ -0,0 +1,71 @@ +from abc import ABC +from typing import Literal, Optional, Union + +from pydantic import AnyUrl, BaseModel, Field, conlist + + +class ValueSchema(BaseModel): + val_type: Literal["string", "integer", "float", "boolean", "json"] + enum: Optional[list[str]] = None + + +class InputParameter(BaseModel): + name: str = Field(..., description="The human-readable name of this parameter.") + required: bool = Field( + ..., + description="Whether this parameter is required (true) or optional (false).", + ) + description: Optional[str] = Field( + None, description="A descriptive, human-readable explanation of the parameter." + ) + value_schema: ValueSchema = Field( + ..., + description="The schema of the value of this parameter.", + ) + inferrable: bool = Field( + True, + description="Whether a value for this parameter can be inferred by a model. Defaults to `true`.", + ) + + +class ToolInputs(BaseModel): + parameters: conlist(InputParameter) + + +class ToolOutput(BaseModel): + description: Optional[str] = Field( + None, description="A descriptive, human-readable explanation of the output." + ) + available_modes: conlist( + Literal["value", "error", "null", "artifact", "requires_authorization"], + min_length=1, + ) = Field( + ..., + description="The available modes for the output.", + default_factory=lambda: ["value", "error", "null"], + ) + value_schema: Optional[ValueSchema] = Field( + None, description="The schema of the value of the output." + ) + + +class ToolAuthorizationRequirement(BaseModel, ABC): + pass + + +class OAuth2AuthorizationRequirement(ToolAuthorizationRequirement): + url: AnyUrl + scope: Optional[list[str]] = None + + +class ToolRequirements(BaseModel): + authorization: Union[ToolAuthorizationRequirement, None] = None + + +class ToolDefinition(BaseModel): + name: str + description: str + version: str + inputs: ToolInputs + output: ToolOutput + requirements: ToolRequirements diff --git a/arcade/arcade/sdk/tool.py b/arcade/arcade/sdk/tool.py new file mode 100644 index 00000000..8e104242 --- /dev/null +++ b/arcade/arcade/sdk/tool.py @@ -0,0 +1,34 @@ +import os +from typing import Any, Callable, Optional, TypeVar, Union + +from arcade.sdk.schemas import ToolAuthorizationRequirement +from arcade.utils import snake_to_pascal_case + +T = TypeVar("T") + + +def tool( + func: Callable | None = None, + desc: str | None = None, + name: str | None = None, + requires_auth: Union[ToolAuthorizationRequirement, None] = None, +) -> Callable: + def decorator(func: Callable) -> Callable: + func.__tool_name__ = name or snake_to_pascal_case(getattr(func, "__name__", None)) + func.__tool_description__ = desc or func.__doc__ + func.__tool_requires_auth__ = requires_auth + + return func + + if func: # This means the decorator is used without parameters + return decorator(func) + return decorator + + +def get_secret(name: str, default: Optional[Any] = None) -> str: + secret = os.getenv(name) + if secret is None: + if default is not None: + return default + raise ValueError(f"Secret {name} is not set.") + return secret diff --git a/toolserve/toolserve/server/schemas/__init__.py b/arcade/arcade/tool/__init__.py similarity index 100% rename from toolserve/toolserve/server/schemas/__init__.py rename to arcade/arcade/tool/__init__.py diff --git a/arcade/arcade/tool/catalog.py b/arcade/arcade/tool/catalog.py new file mode 100644 index 00000000..a03ec504 --- /dev/null +++ b/arcade/arcade/tool/catalog.py @@ -0,0 +1,407 @@ +import asyncio +import inspect +import sys +from datetime import datetime +from importlib import import_module +from pathlib import Path +from typing import ( + Annotated, + Callable, + Literal, + Optional, + Union, + cast, + get_args, + get_origin, +) + +from pydantic import BaseModel, Field, create_model + +from arcade.actor.common.response import ResponseModel +from arcade.actor.common.response_code import CustomResponseCode +from arcade.actor.core.conf import settings +from arcade.apm.base import ToolPack +from arcade.sdk.annotations import Inferrable +from arcade.sdk.errors import ToolDefinitionError +from arcade.sdk.schemas import ( + InputParameter, + ToolDefinition, + ToolInputs, + ToolOutput, + ToolRequirements, + ValueSchema, +) +from arcade.utils import ( + does_function_return_value, + first_or_none, + is_string_literal, + snake_to_pascal_case, +) + + +class ToolMeta(BaseModel): + module: str + path: Optional[str] = None + date_added: datetime = Field(default_factory=datetime.now) + date_updated: datetime = Field(default_factory=datetime.now) + + +class MaterializedTool(BaseModel): + tool: Callable + definition: ToolDefinition + meta: ToolMeta + + # Thought (Sam): Should generate create these from ToolDefinition? + input_model: type[BaseModel] + output_model: type[BaseModel] + + @property + def name(self) -> str: + return self.definition.name + + @property + def version(self) -> str: + return self.definition.version + + @property + def description(self) -> str: + return self.definition.description + + +class ToolCatalog: + def __init__(self, tools_dir: str = settings.TOOLS_DIR): + self.tools = self.read_tools(tools_dir) + + @staticmethod + def read_tools(directory: str) -> dict[str, MaterializedTool]: + toolpack = ToolPack.from_lock_file(directory) + sys.path.append(str(Path(directory).resolve() / "tools")) + + tools: dict[str, MaterializedTool] = {} + for name, tool_spec in toolpack.tools.items(): + module_name, versioned_tool = tool_spec.split(".", 1) + func_name, version = versioned_tool.split("@") + + module = import_module(module_name) + tool_func = getattr(module, func_name) + input_model, output_model = create_func_models(tool_func) + tool_name = snake_to_pascal_case( + name + ) # TODO make sure this follows create_tool_definition + tools[tool_name] = MaterializedTool( + definition=ToolCatalog.create_tool_definition(tool_func, version), + tool=tool_func, + meta=ToolMeta(module=module_name, path=module.__file__), + input_model=input_model, + output_model=output_model, + ) + + return tools + + @staticmethod + def create_tool_definition(tool: Callable, version: str) -> ToolDefinition: + tool_name = getattr(tool, "__tool_name__", tool.__name__) + + # Hard requirement: tools must have descriptions + tool_description = getattr(tool, "__tool_description__", None) + if tool_description is None: + raise ToolDefinitionError(f"Tool {tool_name} is missing a description") + + # If the function returns a value, it must have a type annotation + if does_function_return_value(tool) and tool.__annotations__.get("return") is None: + raise ToolDefinitionError(f"Tool {tool_name} must have a return type annotation") + + return ToolDefinition( + name=tool_name, + description=tool_description, + version=version, + inputs=create_input_definition(tool), + output=create_output_definition(tool), + requirements=ToolRequirements( + authorization=getattr(tool, "__tool_requires_auth__", None), + ), + ) + + def __getitem__(self, name: str) -> Optional[MaterializedTool]: + # TODO error handling + for tool_name, tool in self.tools.items(): + if tool_name == name: + return tool + return None + + def __iter__(self) -> MaterializedTool: + yield from self.tools.values() + + def get_tool(self, name: str) -> Optional[Callable]: + for _, tool in self: + if tool.definition.name == name: + return tool.tool + raise ValueError(f"Tool {name} not found.") + + def list_tools(self) -> list[dict[str, str]]: + def get_tool_endpoint(t: MaterializedTool) -> str: + return f"/tool/{t.meta.module}/{t.definition.name}" + + return [ + { + "name": t.definition.name, + "description": t.definition.description, + "version": t.version, + "endpoint": get_tool_endpoint(t), + } + for t in self.tools.values() + ] + + +def create_input_definition(func: Callable) -> ToolInputs: + """ + Create an input model for a function based on its parameters. + """ + input_parameters = [] + for _, param in inspect.signature(func, follow_wrapped=True).parameters.items(): + field_info = extract_field_info(param) + + # Hard requirement: params must be described + if field_info["field_params"]["description"] is None: + raise ToolDefinitionError( + f"Parameter {field_info['field_params']['name']} is missing a description" + ) + + is_enum = False + enum_values: list[str] = [] + + # Special case: Literal["string1", "string2"] can be enumerated on the wire + if is_string_literal(field_info["field_params"]["type"]): + is_enum = True + enum_values = [str(e) for e in get_args(field_info["field_params"]["type"])] + + input_parameters.append( + InputParameter( + name=field_info["field_params"]["name"], + description=field_info["field_params"]["description"], + required=field_info["field_params"]["default"] is None + and not field_info["field_params"]["optional"], + inferrable=field_info["field_params"]["inferrable"], + value_schema=ValueSchema( + val_type=field_info["field_params"]["wire_type"], + enum=enum_values if is_enum else None, + ), + ) + ) + + return ToolInputs(parameters=input_parameters) + + +def create_output_definition(func: Callable) -> ToolOutput: + """ + Create an output model for a function based on its return annotation. + """ + return_type = inspect.signature(func, follow_wrapped=True).return_annotation + description = "No description provided." + + if return_type is inspect.Signature.empty: + return ToolOutput( + value_schema=None, + description="No description provided.", + available_modes=["null"], + ) + + if hasattr(return_type, "__metadata__"): + description = return_type.__metadata__[0] if return_type.__metadata__ else None + return_type = return_type.__origin__ + + # Unwrap Optional types + is_optional = False + if get_origin(return_type) is Union and type(None) in get_args(return_type): + return_type = next(arg for arg in get_args(return_type) if arg is not type(None)) + is_optional = True + + wire_type = get_wire_type(return_type) + + available_modes = ["value", "error"] + + if is_optional: + available_modes.append("null") + + return ToolOutput( + description=description, + available_modes=available_modes, + value_schema=ValueSchema(val_type=wire_type), + ) + + +def extract_field_info(param: inspect.Parameter) -> dict: + """ + Extract type and field parameters from a function parameter. + + Args: + param (inspect.Parameter): The parameter to extract information from. + + Returns: + dict: A dictionary with 'type' and 'field_params'. + """ + annotation = param.annotation + if annotation == inspect.Parameter.empty: + raise TypeError(f"Parameter {param} has no type annotation.") + + metadata = getattr(annotation, "__metadata__", []) + + name = param.name + description = None + + str_annotations = [m for m in metadata if isinstance(m, str)] + if len(str_annotations) == 1: + description = str_annotations[0] + elif len(str_annotations) == 2: + name = str_annotations[0] + description = str_annotations[1] + else: + raise ToolDefinitionError(f"Parameter {param} has multiple descriptions") + + default = param.default if param.default is not inspect.Parameter.empty else None + + # If the param is Annotated[], unwrap the annotation + # Otherwise, use the literal type + original_type = annotation.__args__[0] if get_origin(annotation) is Annotated else annotation + field_type = original_type + + # Unwrap Optional types + is_optional = False + if get_origin(field_type) is Union and type(None) in get_args(field_type): + field_type = next(arg for arg in get_args(field_type) if arg is not type(None)) + is_optional = True + + wire_type = get_wire_type(str) if is_string_literal(field_type) else get_wire_type(field_type) + + # Get the Inferrable annotation, if it exists + inferrable_annotation = first_or_none(Inferrable, get_args(annotation)) + + field_params = { + "name": name, + "description": str(description) if description else None, + "default": default, + "optional": is_optional, + "inferrable": inferrable_annotation.value + if inferrable_annotation + else True, # Params are inferrable by default + "type": field_type, + "wire_type": wire_type, + "original_type": original_type, + } + + return {"type": field_type, "field_params": field_params} + + +def get_wire_type( + _type: type, +) -> Literal["string", "integer", "float", "boolean", "json"]: + type_mapping = { + str: "string", + bool: "boolean", + int: "integer", + float: "float", + dict: "json", + list: "json", + BaseModel: "json", + } + + wire_type = type_mapping.get(_type) + if wire_type: + return cast(Literal["string", "integer", "float", "boolean", "json"], wire_type) + elif hasattr(_type, "__origin__"): + # account for "list[str]" and "dict[str, int]" and "Optional[str]" and other typing types + origin = _type.__origin__ + if origin in [list, dict]: + return "json" + elif issubclass(_type, BaseModel): + return "json" + else: + raise TypeError(f"Unsupported parameter type: {_type}") + + +def create_func_models(func: Callable) -> tuple[type[BaseModel], type[BaseModel]]: + """ + Analyze a function to create corresponding Pydantic models for its input and output. + + Args: + func (Callable): The function to analyze. + + Returns: + Tuple[Type[BaseModel], Type[BaseModel]]: A tuple containing the input and output Pydantic models. + """ + input_fields = {} + # TODO figure this out (Sam) + if asyncio.iscoroutinefunction(func) and hasattr(func, "__wrapped__"): + func = func.__wrapped__ + for name, param in inspect.signature(func, follow_wrapped=True).parameters.items(): + # TODO make this cleaner + field_info = extract_field_info(param) + field_data = field_info["field_params"] + param_fields = { + "default": field_data["default"], + "description": field_data["description"], + # TODO more here? + } + input_fields[name] = (field_info["type"], Field(**param_fields)) + + input_model = create_model(f"{snake_to_pascal_case(func.__name__)}Input", **input_fields) + + output_model = determine_output_model(func) + + return input_model, output_model + + +def determine_output_model(func: Callable) -> type[BaseModel]: + """ + Determine the output model for a function based on its return annotation. + + Args: + func (Callable): The function to analyze. + + Returns: + Type[BaseModel]: A Pydantic model representing the output. + """ + return_annotation = inspect.signature(func).return_annotation + output_model_name = f"{snake_to_pascal_case(func.__name__)}Output" + if return_annotation is inspect.Signature.empty: + return create_model(output_model_name) + elif hasattr(return_annotation, "__origin__"): + if hasattr(return_annotation, "__metadata__"): + field_type = Optional[return_annotation.__args__[0]] + description = ( + return_annotation.__metadata__[0] if return_annotation.__metadata__ else "" + ) + if description: + return create_model( + output_model_name, + result=(field_type, Field(description=str(description))), + ) + else: + return create_model( + output_model_name, + result=( + return_annotation, + Field(description="No description provided."), + ), + ) + else: + # Handle simple return types (like str) + return create_model( + output_model_name, + result=(return_annotation, Field(description="No description provided.")), + ) + + +def create_response_model(name: str, output_model: type[BaseModel]) -> type[ResponseModel]: + """ + Create a response model for the given schema. + """ + # Create a new response model + response_model = create_model( + f"{snake_to_pascal_case(name)}Response", + code=(int, CustomResponseCode.HTTP_200.code), + msg=(str, CustomResponseCode.HTTP_200.msg), + data=(Optional[output_model], None), + ) + + return response_model diff --git a/toolserve/toolserve/utils/openai_tool.py b/arcade/arcade/tool/openai.py similarity index 80% rename from toolserve/toolserve/utils/openai_tool.py rename to arcade/arcade/tool/openai.py index d4d90184..3233aa8a 100644 --- a/toolserve/toolserve/utils/openai_tool.py +++ b/arcade/arcade/tool/openai.py @@ -1,11 +1,11 @@ import json -from typing import Any, Dict, Type +from enum import Enum +from typing import Any + from pydantic import BaseModel from pydantic_core import PydanticUndefined -from enum import Enum - -from toolserve.server.core.catalog import ToolSchema +from arcade.tool.catalog import MaterializedTool PYTHON_TO_JSON_TYPES = { str: "string", @@ -16,7 +16,8 @@ PYTHON_TO_JSON_TYPES = { dict: "object", } -def python_type_to_json_type(python_type: Type) -> Dict[str, Any]: + +def python_type_to_json_type(python_type: type) -> dict[str, Any]: """ Map Python types to JSON Schema types, including handling of complex types such as lists and dictionaries. @@ -26,23 +27,23 @@ def python_type_to_json_type(python_type: Type) -> Dict[str, Any]: Returns: Dict[str, Any]: A dictionary representing the JSON schema for the given Python type. """ - if hasattr(python_type, '__origin__'): + if hasattr(python_type, "__origin__"): origin = python_type.__origin__ - if origin is list: item_type = python_type_to_json_type(python_type.__args__[0]) - return {'type': 'array', 'items': item_type} + return {"type": "array", "items": item_type} elif origin is dict: value_type = python_type_to_json_type(python_type.__args__[1]) - return {'type': 'object', 'additionalProperties': value_type} + return {"type": "object", "additionalProperties": value_type} elif issubclass(python_type, BaseModel): return model_to_json_schema(python_type) return PYTHON_TO_JSON_TYPES.get(python_type, "string") -def model_to_json_schema(model: Type[BaseModel]) -> Dict[str, Any]: + +def model_to_json_schema(model: type[BaseModel]) -> dict[str, Any]: """ Convert a Pydantic model to a JSON schema. @@ -77,8 +78,9 @@ def model_to_json_schema(model: Type[BaseModel]) -> Dict[str, Any]: "required": required, } -def schema_to_openai_tool(tool_schema: 'ToolSchema') -> str: - """Convert an ToolSchema object to a JSON schema string in the specified function format. + +def schema_to_openai_tool(tool: "MaterializedTool") -> str: + """Convert an ToolDefinition object to a JSON schema string in the specified function format. Example output format: { @@ -104,18 +106,18 @@ def schema_to_openai_tool(tool_schema: 'ToolSchema') -> str: } Args: - tool_schema (ToolSchema): The tool schema to convert. + tool_schema (ToolDefinition): The tool schema to convert. Returns: str: A JSON schema string representing the tool in the specified format. """ - input_model_schema = model_to_json_schema(tool_schema.input_model) + input_model_schema = model_to_json_schema(tool.input_model) function_schema = { "type": "function", "function": { - "name": tool_schema.name, - "description": tool_schema.description, + "name": tool.definition.name, + "description": tool.definition.description, "parameters": input_model_schema, - } + }, } return json.dumps(function_schema, indent=2) diff --git a/arcade/arcade/utils/__init__.py b/arcade/arcade/utils/__init__.py new file mode 100644 index 00000000..5d06551d --- /dev/null +++ b/arcade/arcade/utils/__init__.py @@ -0,0 +1,59 @@ +import ast +import inspect +import re +from collections.abc import Iterable +from typing import Any, Callable, Literal, Optional, TypeVar, get_args, get_origin + +T = TypeVar("T") + + +def first_or_none(_type: type[T], iterable: Iterable[Any]) -> Optional[T]: + """ + Returns the first item in the iterable that is an instance of the given type, or None if no such item is found. + """ + for item in iterable: + if isinstance(item, _type): + return item + return None + + +def pascal_to_snake_case(name: str) -> str: + """ + Converts a PascalCase name to snake_case. + """ + name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) + return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower() + + +def snake_to_pascal_case(name: str) -> str: + """ + Converts a snake_case name to PascalCase. + """ + return "".join(x.capitalize() or "_" for x in name.split("_")) + + +def is_string_literal(_type: type) -> bool: + """ + Returns True if the given type is a string literal, i.e. a Literal[str] or Literal[str, str, ...] etc. + """ + return get_origin(_type) is Literal and all(isinstance(arg, str) for arg in get_args(_type)) + + +def does_function_return_value(func: Callable) -> bool: + """ + Returns True if the given function returns a value, i.e. if it has a return statement with a value. + """ + source = inspect.getsource(func) + tree = ast.parse(source) + + class ReturnVisitor(ast.NodeVisitor): + def __init__(self): + self.returns_value = False + + def visit_Return(self, node): + if node.value is not None: + self.returns_value = True + + visitor = ReturnVisitor() + visitor.visit(tree) + return visitor.returns_value diff --git a/arcade/codecov.yaml b/arcade/codecov.yaml new file mode 100644 index 00000000..058cfb76 --- /dev/null +++ b/arcade/codecov.yaml @@ -0,0 +1,9 @@ +coverage: + range: 70..100 + round: down + precision: 1 + status: + project: + default: + target: 90% + threshold: 0.5% diff --git a/arcade/docs/index.md b/arcade/docs/index.md new file mode 100644 index 00000000..e0739f69 --- /dev/null +++ b/arcade/docs/index.md @@ -0,0 +1,8 @@ +# arcade-ai + +[![Release](https://img.shields.io/github/v/release/spartee/arcade-ai)](https://img.shields.io/github/v/release/spartee/arcade-ai) +[![Build status](https://img.shields.io/github/actions/workflow/status/spartee/arcade-ai/main.yml?branch=main)](https://github.com/spartee/arcade-ai/actions/workflows/main.yml?query=branch%3Amain) +[![Commit activity](https://img.shields.io/github/commit-activity/m/spartee/arcade-ai)](https://img.shields.io/github/commit-activity/m/spartee/arcade-ai) +[![License](https://img.shields.io/github/license/spartee/arcade-ai)](https://img.shields.io/github/license/spartee/arcade-ai) + +Arcade AI python diff --git a/arcade/docs/modules.md b/arcade/docs/modules.md new file mode 100644 index 00000000..6f3b6b4a --- /dev/null +++ b/arcade/docs/modules.md @@ -0,0 +1 @@ +::: arcade.foo diff --git a/arcade/mkdocs.yml b/arcade/mkdocs.yml new file mode 100644 index 00000000..b53cfe41 --- /dev/null +++ b/arcade/mkdocs.yml @@ -0,0 +1,54 @@ +site_name: arcade-ai +repo_url: https://github.com/spartee/arcade-ai +site_url: https://spartee.github.io/arcade-ai +site_description: Arcade AI python +site_author: Arcade AI +edit_uri: edit/main/docs/ +repo_name: spartee/arcade-ai +copyright: Maintained by Florian. + +nav: + - Home: index.md + - Modules: modules.md +plugins: + - search + - mkdocstrings: + handlers: + python: + setup_commands: + - import sys + - sys.path.append('../') +theme: + name: material + feature: + tabs: true + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: white + accent: deep orange + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: black + accent: deep orange + toggle: + icon: material/brightness-4 + name: Switch to light mode + icon: + repo: fontawesome/brands/github + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/spartee/arcade-ai + - icon: fontawesome/brands/python + link: https://pypi.org/project/arcade-ai + +markdown_extensions: + - toc: + permalink: true + - pymdownx.arithmatex: + generic: true diff --git a/arcade/poetry.lock b/arcade/poetry.lock new file mode 100644 index 00000000..0d56d6b5 --- /dev/null +++ b/arcade/poetry.lock @@ -0,0 +1,1826 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.4.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, + {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +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)"] + +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + +[[package]] +name = "babel" +version = "2.15.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, + {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + +[[package]] +name = "cachetools" +version = "5.3.3" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"}, + {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"}, +] + +[[package]] +name = "certifi" +version = "2024.7.4" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "chardet" +version = "5.2.0" +description = "Universal encoding detector for Python 3" +optional = false +python-versions = ">=3.7" +files = [ + {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, + {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.5.4" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cfb5a4f556bb51aba274588200a46e4dd6b505fb1a5f8c5ae408222eb416f99"}, + {file = "coverage-7.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2174e7c23e0a454ffe12267a10732c273243b4f2d50d07544a91198f05c48f47"}, + {file = "coverage-7.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2214ee920787d85db1b6a0bd9da5f8503ccc8fcd5814d90796c2f2493a2f4d2e"}, + {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1137f46adb28e3813dec8c01fefadcb8c614f33576f672962e323b5128d9a68d"}, + {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b385d49609f8e9efc885790a5a0e89f2e3ae042cdf12958b6034cc442de428d3"}, + {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4a474f799456e0eb46d78ab07303286a84a3140e9700b9e154cfebc8f527016"}, + {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5cd64adedf3be66f8ccee418473c2916492d53cbafbfcff851cbec5a8454b136"}, + {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e564c2cf45d2f44a9da56f4e3a26b2236504a496eb4cb0ca7221cd4cc7a9aca9"}, + {file = "coverage-7.5.4-cp310-cp310-win32.whl", hash = "sha256:7076b4b3a5f6d2b5d7f1185fde25b1e54eb66e647a1dfef0e2c2bfaf9b4c88c8"}, + {file = "coverage-7.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:018a12985185038a5b2bcafab04ab833a9a0f2c59995b3cec07e10074c78635f"}, + {file = "coverage-7.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db14f552ac38f10758ad14dd7b983dbab424e731588d300c7db25b6f89e335b5"}, + {file = "coverage-7.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3257fdd8e574805f27bb5342b77bc65578e98cbc004a92232106344053f319ba"}, + {file = "coverage-7.5.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a6612c99081d8d6134005b1354191e103ec9705d7ba2754e848211ac8cacc6b"}, + {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d45d3cbd94159c468b9b8c5a556e3f6b81a8d1af2a92b77320e887c3e7a5d080"}, + {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed550e7442f278af76d9d65af48069f1fb84c9f745ae249c1a183c1e9d1b025c"}, + {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a892be37ca35eb5019ec85402c3371b0f7cda5ab5056023a7f13da0961e60da"}, + {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8192794d120167e2a64721d88dbd688584675e86e15d0569599257566dec9bf0"}, + {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:820bc841faa502e727a48311948e0461132a9c8baa42f6b2b84a29ced24cc078"}, + {file = "coverage-7.5.4-cp311-cp311-win32.whl", hash = "sha256:6aae5cce399a0f065da65c7bb1e8abd5c7a3043da9dceb429ebe1b289bc07806"}, + {file = "coverage-7.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:d2e344d6adc8ef81c5a233d3a57b3c7d5181f40e79e05e1c143da143ccb6377d"}, + {file = "coverage-7.5.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:54317c2b806354cbb2dc7ac27e2b93f97096912cc16b18289c5d4e44fc663233"}, + {file = "coverage-7.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:042183de01f8b6d531e10c197f7f0315a61e8d805ab29c5f7b51a01d62782747"}, + {file = "coverage-7.5.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bb74ed465d5fb204b2ec41d79bcd28afccf817de721e8a807d5141c3426638"}, + {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3d45ff86efb129c599a3b287ae2e44c1e281ae0f9a9bad0edc202179bcc3a2e"}, + {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5013ed890dc917cef2c9f765c4c6a8ae9df983cd60dbb635df8ed9f4ebc9f555"}, + {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1014fbf665fef86cdfd6cb5b7371496ce35e4d2a00cda501cf9f5b9e6fced69f"}, + {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3684bc2ff328f935981847082ba4fdc950d58906a40eafa93510d1b54c08a66c"}, + {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:581ea96f92bf71a5ec0974001f900db495488434a6928a2ca7f01eee20c23805"}, + {file = "coverage-7.5.4-cp312-cp312-win32.whl", hash = "sha256:73ca8fbc5bc622e54627314c1a6f1dfdd8db69788f3443e752c215f29fa87a0b"}, + {file = "coverage-7.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:cef4649ec906ea7ea5e9e796e68b987f83fa9a718514fe147f538cfeda76d7a7"}, + {file = "coverage-7.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdd31315fc20868c194130de9ee6bfd99755cc9565edff98ecc12585b90be882"}, + {file = "coverage-7.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:02ff6e898197cc1e9fa375581382b72498eb2e6d5fc0b53f03e496cfee3fac6d"}, + {file = "coverage-7.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d05c16cf4b4c2fc880cb12ba4c9b526e9e5d5bb1d81313d4d732a5b9fe2b9d53"}, + {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5986ee7ea0795a4095ac4d113cbb3448601efca7f158ec7f7087a6c705304e4"}, + {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df54843b88901fdc2f598ac06737f03d71168fd1175728054c8f5a2739ac3e4"}, + {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ab73b35e8d109bffbda9a3e91c64e29fe26e03e49addf5b43d85fc426dde11f9"}, + {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:aea072a941b033813f5e4814541fc265a5c12ed9720daef11ca516aeacd3bd7f"}, + {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:16852febd96acd953b0d55fc842ce2dac1710f26729b31c80b940b9afcd9896f"}, + {file = "coverage-7.5.4-cp38-cp38-win32.whl", hash = "sha256:8f894208794b164e6bd4bba61fc98bf6b06be4d390cf2daacfa6eca0a6d2bb4f"}, + {file = "coverage-7.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:e2afe743289273209c992075a5a4913e8d007d569a406ffed0bd080ea02b0633"}, + {file = "coverage-7.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b95c3a8cb0463ba9f77383d0fa8c9194cf91f64445a63fc26fb2327e1e1eb088"}, + {file = "coverage-7.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d7564cc09dd91b5a6001754a5b3c6ecc4aba6323baf33a12bd751036c998be4"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44da56a2589b684813f86d07597fdf8a9c6ce77f58976727329272f5a01f99f7"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e16f3d6b491c48c5ae726308e6ab1e18ee830b4cdd6913f2d7f77354b33f91c8"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbc5958cb471e5a5af41b0ddaea96a37e74ed289535e8deca404811f6cb0bc3d"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a04e990a2a41740b02d6182b498ee9796cf60eefe40cf859b016650147908029"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ddbd2f9713a79e8e7242d7c51f1929611e991d855f414ca9996c20e44a895f7c"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b1ccf5e728ccf83acd313c89f07c22d70d6c375a9c6f339233dcf792094bcbf7"}, + {file = "coverage-7.5.4-cp39-cp39-win32.whl", hash = "sha256:56b4eafa21c6c175b3ede004ca12c653a88b6f922494b023aeb1e836df953ace"}, + {file = "coverage-7.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:65e528e2e921ba8fd67d9055e6b9f9e34b21ebd6768ae1c1723f4ea6ace1234d"}, + {file = "coverage-7.5.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:79b356f3dd5b26f3ad23b35c75dbdaf1f9e2450b6bcefc6d0825ea0aa3f86ca5"}, + {file = "coverage-7.5.4.tar.gz", hash = "sha256:a44963520b069e12789d0faea4e9fdb1e410cdc4aab89d94f7f55cbb7fef0353"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "deptry" +version = "0.12.0" +description = "A command line utility to check for unused, missing and transitive dependencies in a Python project." +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "deptry-0.12.0-py3-none-any.whl", hash = "sha256:69c801a6ae1b39c7b8e0daf40dbe8b75f1f161277d206dd8f921f32cd22dad91"}, + {file = "deptry-0.12.0.tar.gz", hash = "sha256:ac3cd32d149c92a9af12f63cd9486ddd1760f0277ed0cf306c6ef0388f57ff0a"}, +] + +[package.dependencies] +chardet = ">=4.0.0" +click = ">=8.0.0,<9.0.0" +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} +pathspec = ">=0.9.0" +tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "distlib" +version = "0.3.8" +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"}, +] + +[[package]] +name = "dnspython" +version = "2.6.1" +description = "DNS toolkit" +optional = false +python-versions = ">=3.8" +files = [ + {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, + {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, +] + +[package.extras] +dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] +dnssec = ["cryptography (>=41)"] +doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] +doq = ["aioquic (>=0.9.25)"] +idna = ["idna (>=3.6)"] +trio = ["trio (>=0.23)"] +wmi = ["wmi (>=1.5.1)"] + +[[package]] +name = "email-validator" +version = "2.2.0" +description = "A robust email address syntax and deliverability validation library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, + {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, +] + +[package.dependencies] +dnspython = ">=2.0.0" +idna = ">=2.0.0" + +[[package]] +name = "exceptiongroup" +version = "1.2.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "fastapi" +version = "0.110.3" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi-0.110.3-py3-none-any.whl", hash = "sha256:fd7600612f755e4050beb74001310b5a7e1796d149c2ee363124abdfa0289d32"}, + {file = "fastapi-0.110.3.tar.gz", hash = "sha256:555700b0159379e94fdbfc6bb66a0f1c43f4cf7060f25239af3d84b63a656626"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.37.2,<0.38.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email_validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "filelock" +version = "3.15.4" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, + {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] +typing = ["typing-extensions (>=4.8)"] + +[[package]] +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." +optional = false +python-versions = "*" +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] + +[[package]] +name = "griffe" +version = "0.47.0" +description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +optional = false +python-versions = ">=3.8" +files = [ + {file = "griffe-0.47.0-py3-none-any.whl", hash = "sha256:07a2fd6a8c3d21d0bbb0decf701d62042ccc8a576645c7f8799fe1f10de2b2de"}, + {file = "griffe-0.47.0.tar.gz", hash = "sha256:95119a440a3c932b13293538bdbc405bee4c36428547553dc6b327e7e7d35e5a"}, +] + +[package.dependencies] +colorama = ">=0.4" + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "identify" +version = "2.6.0" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, + {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "loguru" +version = "0.7.2" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = ">=3.5" +files = [ + {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, + {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"] + +[[package]] +name = "markdown" +version = "3.6" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"}, + {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"}, +] + +[package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +optional = false +python-versions = ">=3.6" +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mkdocs" +version = "1.6.0" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs-1.6.0-py3-none-any.whl", hash = "sha256:1eb5cb7676b7d89323e62b56235010216319217d4af5ddc543a91beb8d125ea7"}, + {file = "mkdocs-1.6.0.tar.gz", hash = "sha256:a73f735824ef83a4f3bcb7a231dcab23f5a838f88b7efc54a0eef5fbdbc3c512"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +jinja2 = ">=2.11.1" +markdown = ">=3.3.6" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +mkdocs-get-deps = ">=0.2.0" +packaging = ">=20.5" +pathspec = ">=0.11.1" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-autorefs" +version = "1.0.1" +description = "Automatically link across pages in MkDocs." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_autorefs-1.0.1-py3-none-any.whl", hash = "sha256:aacdfae1ab197780fb7a2dac92ad8a3d8f7ca8049a9cbe56a4218cd52e8da570"}, + {file = "mkdocs_autorefs-1.0.1.tar.gz", hash = "sha256:f684edf847eced40b570b57846b15f0bf57fb93ac2c510450775dcf16accb971"}, +] + +[package.dependencies] +Markdown = ">=3.3" +markupsafe = ">=2.0.1" +mkdocs = ">=1.1" + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] + +[package.dependencies] +mergedeep = ">=1.3.4" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" + +[[package]] +name = "mkdocs-material" +version = "9.5.28" +description = "Documentation that simply works" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material-9.5.28-py3-none-any.whl", hash = "sha256:ff48b11b2a9f705dd210409ec3b418ab443dd36d96915bcba45a41f10ea27bfd"}, + {file = "mkdocs_material-9.5.28.tar.gz", hash = "sha256:9cba305283ad1600e3d0a67abe72d7a058b54793b47be39930911a588fe0336b"}, +] + +[package.dependencies] +babel = ">=2.10,<3.0" +colorama = ">=0.4,<1.0" +jinja2 = ">=3.0,<4.0" +markdown = ">=3.2,<4.0" +mkdocs = ">=1.6,<2.0" +mkdocs-material-extensions = ">=1.3,<2.0" +paginate = ">=0.5,<1.0" +pygments = ">=2.16,<3.0" +pymdown-extensions = ">=10.2,<11.0" +regex = ">=2022.4" +requests = ">=2.26,<3.0" + +[package.extras] +git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] +imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"] +recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +description = "Extension pack for Python Markdown and MkDocs Material." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, + {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, +] + +[[package]] +name = "mkdocstrings" +version = "0.23.0" +description = "Automatic documentation from sources, for MkDocs." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocstrings-0.23.0-py3-none-any.whl", hash = "sha256:051fa4014dfcd9ed90254ae91de2dbb4f24e166347dae7be9a997fe16316c65e"}, + {file = "mkdocstrings-0.23.0.tar.gz", hash = "sha256:d9c6a37ffbe7c14a7a54ef1258c70b8d394e6a33a1c80832bce40b9567138d1c"}, +] + +[package.dependencies] +Jinja2 = ">=2.11.1" +Markdown = ">=3.3" +MarkupSafe = ">=1.1" +mkdocs = ">=1.2" +mkdocs-autorefs = ">=0.3.1" +mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""} +pymdown-extensions = ">=6.3" + +[package.extras] +crystal = ["mkdocstrings-crystal (>=0.3.4)"] +python = ["mkdocstrings-python (>=0.5.2)"] +python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] + +[[package]] +name = "mkdocstrings-python" +version = "1.8.0" +description = "A Python handler for mkdocstrings." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocstrings_python-1.8.0-py3-none-any.whl", hash = "sha256:4209970cc90bec194568682a535848a8d8489516c6ed4adbe58bbc67b699ca9d"}, + {file = "mkdocstrings_python-1.8.0.tar.gz", hash = "sha256:1488bddf50ee42c07d9a488dddc197f8e8999c2899687043ec5dd1643d057192"}, +] + +[package.dependencies] +griffe = ">=0.37" +mkdocstrings = ">=0.20" + +[[package]] +name = "msgpack" +version = "1.0.8" +description = "MessagePack serializer" +optional = false +python-versions = ">=3.8" +files = [ + {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653"}, + {file = "msgpack-1.0.8-cp310-cp310-win32.whl", hash = "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693"}, + {file = "msgpack-1.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce"}, + {file = "msgpack-1.0.8-cp311-cp311-win32.whl", hash = "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305"}, + {file = "msgpack-1.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543"}, + {file = "msgpack-1.0.8-cp312-cp312-win32.whl", hash = "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c"}, + {file = "msgpack-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a"}, + {file = "msgpack-1.0.8-cp38-cp38-win32.whl", hash = "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c"}, + {file = "msgpack-1.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, + {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, + {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, + {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, +] + +[[package]] +name = "msgspec" +version = "0.18.6" +description = "A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML." +optional = false +python-versions = ">=3.8" +files = [ + {file = "msgspec-0.18.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77f30b0234eceeff0f651119b9821ce80949b4d667ad38f3bfed0d0ebf9d6d8f"}, + {file = "msgspec-0.18.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a76b60e501b3932782a9da039bd1cd552b7d8dec54ce38332b87136c64852dd"}, + {file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06acbd6edf175bee0e36295d6b0302c6de3aaf61246b46f9549ca0041a9d7177"}, + {file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40a4df891676d9c28a67c2cc39947c33de516335680d1316a89e8f7218660410"}, + {file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a6896f4cd5b4b7d688018805520769a8446df911eb93b421c6c68155cdf9dd5a"}, + {file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3ac4dd63fd5309dd42a8c8c36c1563531069152be7819518be0a9d03be9788e4"}, + {file = "msgspec-0.18.6-cp310-cp310-win_amd64.whl", hash = "sha256:fda4c357145cf0b760000c4ad597e19b53adf01382b711f281720a10a0fe72b7"}, + {file = "msgspec-0.18.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e77e56ffe2701e83a96e35770c6adb655ffc074d530018d1b584a8e635b4f36f"}, + {file = "msgspec-0.18.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5351afb216b743df4b6b147691523697ff3a2fc5f3d54f771e91219f5c23aaa"}, + {file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3232fabacef86fe8323cecbe99abbc5c02f7698e3f5f2e248e3480b66a3596b"}, + {file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b524df6ea9998bbc99ea6ee4d0276a101bcc1aa8d14887bb823914d9f60d07"}, + {file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:37f67c1d81272131895bb20d388dd8d341390acd0e192a55ab02d4d6468b434c"}, + {file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d0feb7a03d971c1c0353de1a8fe30bb6579c2dc5ccf29b5f7c7ab01172010492"}, + {file = "msgspec-0.18.6-cp311-cp311-win_amd64.whl", hash = "sha256:41cf758d3f40428c235c0f27bc6f322d43063bc32da7b9643e3f805c21ed57b4"}, + {file = "msgspec-0.18.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d86f5071fe33e19500920333c11e2267a31942d18fed4d9de5bc2fbab267d28c"}, + {file = "msgspec-0.18.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce13981bfa06f5eb126a3a5a38b1976bddb49a36e4f46d8e6edecf33ccf11df1"}, + {file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97dec6932ad5e3ee1e3c14718638ba333befc45e0661caa57033cd4cc489466"}, + {file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad237100393f637b297926cae1868b0d500f764ccd2f0623a380e2bcfb2809ca"}, + {file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db1d8626748fa5d29bbd15da58b2d73af25b10aa98abf85aab8028119188ed57"}, + {file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d70cb3d00d9f4de14d0b31d38dfe60c88ae16f3182988246a9861259c6722af6"}, + {file = "msgspec-0.18.6-cp312-cp312-win_amd64.whl", hash = "sha256:1003c20bfe9c6114cc16ea5db9c5466e49fae3d7f5e2e59cb70693190ad34da0"}, + {file = "msgspec-0.18.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f7d9faed6dfff654a9ca7d9b0068456517f63dbc3aa704a527f493b9200b210a"}, + {file = "msgspec-0.18.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9da21f804c1a1471f26d32b5d9bc0480450ea77fbb8d9db431463ab64aaac2cf"}, + {file = "msgspec-0.18.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46eb2f6b22b0e61c137e65795b97dc515860bf6ec761d8fb65fdb62aa094ba61"}, + {file = "msgspec-0.18.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8355b55c80ac3e04885d72db515817d9fbb0def3bab936bba104e99ad22cf46"}, + {file = "msgspec-0.18.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9080eb12b8f59e177bd1eb5c21e24dd2ba2fa88a1dbc9a98e05ad7779b54c681"}, + {file = "msgspec-0.18.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cc001cf39becf8d2dcd3f413a4797c55009b3a3cdbf78a8bf5a7ca8fdb76032c"}, + {file = "msgspec-0.18.6-cp38-cp38-win_amd64.whl", hash = "sha256:fac5834e14ac4da1fca373753e0c4ec9c8069d1fe5f534fa5208453b6065d5be"}, + {file = "msgspec-0.18.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:974d3520fcc6b824a6dedbdf2b411df31a73e6e7414301abac62e6b8d03791b4"}, + {file = "msgspec-0.18.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fd62e5818731a66aaa8e9b0a1e5543dc979a46278da01e85c3c9a1a4f047ef7e"}, + {file = "msgspec-0.18.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7481355a1adcf1f08dedd9311193c674ffb8bf7b79314b4314752b89a2cf7f1c"}, + {file = "msgspec-0.18.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6aa85198f8f154cf35d6f979998f6dadd3dc46a8a8c714632f53f5d65b315c07"}, + {file = "msgspec-0.18.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e24539b25c85c8f0597274f11061c102ad6b0c56af053373ba4629772b407be"}, + {file = "msgspec-0.18.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c61ee4d3be03ea9cd089f7c8e36158786cd06e51fbb62529276452bbf2d52ece"}, + {file = "msgspec-0.18.6-cp39-cp39-win_amd64.whl", hash = "sha256:b5c390b0b0b7da879520d4ae26044d74aeee5144f83087eb7842ba59c02bc090"}, + {file = "msgspec-0.18.6.tar.gz", hash = "sha256:a59fc3b4fcdb972d09138cb516dbde600c99d07c38fd9372a6ef500d2d031b4e"}, +] + +[package.extras] +dev = ["attrs", "coverage", "furo", "gcovr", "ipython", "msgpack", "mypy", "pre-commit", "pyright", "pytest", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "tomli", "tomli-w"] +doc = ["furo", "ipython", "sphinx", "sphinx-copybutton", "sphinx-design"] +test = ["attrs", "msgpack", "mypy", "pyright", "pytest", "pyyaml", "tomli", "tomli-w"] +toml = ["tomli", "tomli-w"] +yaml = ["pyyaml"] + +[[package]] +name = "mypy" +version = "1.10.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"}, + {file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"}, + {file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"}, + {file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"}, + {file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"}, + {file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"}, + {file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"}, + {file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"}, + {file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"}, + {file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"}, + {file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"}, + {file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"}, + {file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"}, + {file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"}, + {file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"}, + {file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"}, + {file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"}, + {file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"}, + {file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"}, + {file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"}, + {file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"}, + {file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"}, + {file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"}, + {file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"}, + {file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"}, + {file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"}, + {file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "packaging" +version = "24.1" +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"}, +] + +[[package]] +name = "paginate" +version = "0.5.6" +description = "Divides large result sets into pages for easier browsing" +optional = false +python-versions = "*" +files = [ + {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.2.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "3.7.1" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"}, + {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pydantic" +version = "2.8.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, + {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""} +pydantic-core = "2.20.1" +typing-extensions = [ + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, + {version = ">=4.6.1", markers = "python_version < \"3.13\""}, +] + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.20.1" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, + {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, + {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, + {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, + {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, + {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, + {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, + {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, + {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, + {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, + {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, + {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, + {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, + {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pydantic-settings" +version = "2.3.4" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_settings-2.3.4-py3-none-any.whl", hash = "sha256:11ad8bacb68a045f00e4f862c7a718c8a9ec766aa8fd4c32e39a0594b207b53a"}, + {file = "pydantic_settings-2.3.4.tar.gz", hash = "sha256:c5802e3d62b78e82522319bbc9b8f8ffb28ad1c988a99311d04f2a6051fca0a7"}, +] + +[package.dependencies] +pydantic = ">=2.7.0" +python-dotenv = ">=0.21.0" + +[package.extras] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pymdown-extensions" +version = "10.8.1" +description = "Extension pack for Python Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pymdown_extensions-10.8.1-py3-none-any.whl", hash = "sha256:f938326115884f48c6059c67377c46cf631c733ef3629b6eed1349989d1b30cb"}, + {file = "pymdown_extensions-10.8.1.tar.gz", hash = "sha256:3ab1db5c9e21728dabf75192d71471f8e50f216627e9a1fa9535ecb0231b9940"}, +] + +[package.dependencies] +markdown = ">=3.6" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.12)"] + +[[package]] +name = "pyproject-api" +version = "1.7.1" +description = "API to interact with the python pyproject.toml based projects" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyproject_api-1.7.1-py3-none-any.whl", hash = "sha256:2dc1654062c2b27733d8fd4cdda672b22fe8741ef1dde8e3a998a9547b071eeb"}, + {file = "pyproject_api-1.7.1.tar.gz", hash = "sha256:7ebc6cd10710f89f4cf2a2731710a98abce37ebff19427116ff2174c9236a827"}, +] + +[package.dependencies] +packaging = ">=24.1" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["furo (>=2024.5.6)", "sphinx-autodoc-typehints (>=2.2.1)"] +testing = ["covdefaults (>=2.3)", "pytest (>=8.2.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "setuptools (>=70.1)"] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.23.7" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"}, + {file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-dotenv" +version = "1.0.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +description = "A custom YAML tag for referencing environment variables in YAML files. " +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] + +[package.dependencies] +pyyaml = "*" + +[[package]] +name = "redis" +version = "5.0.7" +description = "Python client for Redis database and key-value store" +optional = false +python-versions = ">=3.7" +files = [ + {file = "redis-5.0.7-py3-none-any.whl", hash = "sha256:0e479e24da960c690be5d9b96d21f7b918a98c0cf49af3b6fafaa0753f93a0db"}, + {file = "redis-5.0.7.tar.gz", hash = "sha256:8f611490b93c8109b50adc317b31bfd84fff31def3475b92e7e80bf39f48175b"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} + +[package.extras] +hiredis = ["hiredis (>=1.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] + +[[package]] +name = "regex" +version = "2024.5.15" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.8" +files = [ + {file = "regex-2024.5.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a81e3cfbae20378d75185171587cbf756015ccb14840702944f014e0d93ea09f"}, + {file = "regex-2024.5.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7b59138b219ffa8979013be7bc85bb60c6f7b7575df3d56dc1e403a438c7a3f6"}, + {file = "regex-2024.5.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0bd000c6e266927cb7a1bc39d55be95c4b4f65c5be53e659537537e019232b1"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eaa7ddaf517aa095fa8da0b5015c44d03da83f5bd49c87961e3c997daed0de7"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba68168daedb2c0bab7fd7e00ced5ba90aebf91024dea3c88ad5063c2a562cca"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e8d717bca3a6e2064fc3a08df5cbe366369f4b052dcd21b7416e6d71620dca1"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1337b7dbef9b2f71121cdbf1e97e40de33ff114801263b275aafd75303bd62b5"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9ebd0a36102fcad2f03696e8af4ae682793a5d30b46c647eaf280d6cfb32796"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9efa1a32ad3a3ea112224897cdaeb6aa00381627f567179c0314f7b65d354c62"}, + {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1595f2d10dff3d805e054ebdc41c124753631b6a471b976963c7b28543cf13b0"}, + {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b802512f3e1f480f41ab5f2cfc0e2f761f08a1f41092d6718868082fc0d27143"}, + {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a0981022dccabca811e8171f913de05720590c915b033b7e601f35ce4ea7019f"}, + {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:19068a6a79cf99a19ccefa44610491e9ca02c2be3305c7760d3831d38a467a6f"}, + {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1b5269484f6126eee5e687785e83c6b60aad7663dafe842b34691157e5083e53"}, + {file = "regex-2024.5.15-cp310-cp310-win32.whl", hash = "sha256:ada150c5adfa8fbcbf321c30c751dc67d2f12f15bd183ffe4ec7cde351d945b3"}, + {file = "regex-2024.5.15-cp310-cp310-win_amd64.whl", hash = "sha256:ac394ff680fc46b97487941f5e6ae49a9f30ea41c6c6804832063f14b2a5a145"}, + {file = "regex-2024.5.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f5b1dff3ad008dccf18e652283f5e5339d70bf8ba7c98bf848ac33db10f7bc7a"}, + {file = "regex-2024.5.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c6a2b494a76983df8e3d3feea9b9ffdd558b247e60b92f877f93a1ff43d26656"}, + {file = "regex-2024.5.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a32b96f15c8ab2e7d27655969a23895eb799de3665fa94349f3b2fbfd547236f"}, + {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10002e86e6068d9e1c91eae8295ef690f02f913c57db120b58fdd35a6bb1af35"}, + {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec54d5afa89c19c6dd8541a133be51ee1017a38b412b1321ccb8d6ddbeb4cf7d"}, + {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10e4ce0dca9ae7a66e6089bb29355d4432caed736acae36fef0fdd7879f0b0cb"}, + {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e507ff1e74373c4d3038195fdd2af30d297b4f0950eeda6f515ae3d84a1770f"}, + {file = "regex-2024.5.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1f059a4d795e646e1c37665b9d06062c62d0e8cc3c511fe01315973a6542e40"}, + {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0721931ad5fe0dda45d07f9820b90b2148ccdd8e45bb9e9b42a146cb4f695649"}, + {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:833616ddc75ad595dee848ad984d067f2f31be645d603e4d158bba656bbf516c"}, + {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:287eb7f54fc81546346207c533ad3c2c51a8d61075127d7f6d79aaf96cdee890"}, + {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:19dfb1c504781a136a80ecd1fff9f16dddf5bb43cec6871778c8a907a085bb3d"}, + {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:119af6e56dce35e8dfb5222573b50c89e5508d94d55713c75126b753f834de68"}, + {file = "regex-2024.5.15-cp311-cp311-win32.whl", hash = "sha256:1c1c174d6ec38d6c8a7504087358ce9213d4332f6293a94fbf5249992ba54efa"}, + {file = "regex-2024.5.15-cp311-cp311-win_amd64.whl", hash = "sha256:9e717956dcfd656f5055cc70996ee2cc82ac5149517fc8e1b60261b907740201"}, + {file = "regex-2024.5.15-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:632b01153e5248c134007209b5c6348a544ce96c46005d8456de1d552455b014"}, + {file = "regex-2024.5.15-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e64198f6b856d48192bf921421fdd8ad8eb35e179086e99e99f711957ffedd6e"}, + {file = "regex-2024.5.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68811ab14087b2f6e0fc0c2bae9ad689ea3584cad6917fc57be6a48bbd012c49"}, + {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ec0c2fea1e886a19c3bee0cd19d862b3aa75dcdfb42ebe8ed30708df64687a"}, + {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0c0c0003c10f54a591d220997dd27d953cd9ccc1a7294b40a4be5312be8797b"}, + {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2431b9e263af1953c55abbd3e2efca67ca80a3de8a0437cb58e2421f8184717a"}, + {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a605586358893b483976cffc1723fb0f83e526e8f14c6e6614e75919d9862cf"}, + {file = "regex-2024.5.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391d7f7f1e409d192dba8bcd42d3e4cf9e598f3979cdaed6ab11288da88cb9f2"}, + {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9ff11639a8d98969c863d4617595eb5425fd12f7c5ef6621a4b74b71ed8726d5"}, + {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4eee78a04e6c67e8391edd4dad3279828dd66ac4b79570ec998e2155d2e59fd5"}, + {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8fe45aa3f4aa57faabbc9cb46a93363edd6197cbc43523daea044e9ff2fea83e"}, + {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d0a3d8d6acf0c78a1fff0e210d224b821081330b8524e3e2bc5a68ef6ab5803d"}, + {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c486b4106066d502495b3025a0a7251bf37ea9540433940a23419461ab9f2a80"}, + {file = "regex-2024.5.15-cp312-cp312-win32.whl", hash = "sha256:c49e15eac7c149f3670b3e27f1f28a2c1ddeccd3a2812cba953e01be2ab9b5fe"}, + {file = "regex-2024.5.15-cp312-cp312-win_amd64.whl", hash = "sha256:673b5a6da4557b975c6c90198588181029c60793835ce02f497ea817ff647cb2"}, + {file = "regex-2024.5.15-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:87e2a9c29e672fc65523fb47a90d429b70ef72b901b4e4b1bd42387caf0d6835"}, + {file = "regex-2024.5.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c3bea0ba8b73b71b37ac833a7f3fd53825924165da6a924aec78c13032f20850"}, + {file = "regex-2024.5.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bfc4f82cabe54f1e7f206fd3d30fda143f84a63fe7d64a81558d6e5f2e5aaba9"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5bb9425fe881d578aeca0b2b4b3d314ec88738706f66f219c194d67179337cb"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64c65783e96e563103d641760664125e91bd85d8e49566ee560ded4da0d3e704"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf2430df4148b08fb4324b848672514b1385ae3807651f3567871f130a728cc3"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5397de3219a8b08ae9540c48f602996aa6b0b65d5a61683e233af8605c42b0f2"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:455705d34b4154a80ead722f4f185b04c4237e8e8e33f265cd0798d0e44825fa"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b2b6f1b3bb6f640c1a92be3bbfbcb18657b125b99ecf141fb3310b5282c7d4ed"}, + {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3ad070b823ca5890cab606c940522d05d3d22395d432f4aaaf9d5b1653e47ced"}, + {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5b5467acbfc153847d5adb21e21e29847bcb5870e65c94c9206d20eb4e99a384"}, + {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e6662686aeb633ad65be2a42b4cb00178b3fbf7b91878f9446075c404ada552f"}, + {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:2b4c884767504c0e2401babe8b5b7aea9148680d2e157fa28f01529d1f7fcf67"}, + {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3cd7874d57f13bf70078f1ff02b8b0aa48d5b9ed25fc48547516c6aba36f5741"}, + {file = "regex-2024.5.15-cp38-cp38-win32.whl", hash = "sha256:e4682f5ba31f475d58884045c1a97a860a007d44938c4c0895f41d64481edbc9"}, + {file = "regex-2024.5.15-cp38-cp38-win_amd64.whl", hash = "sha256:d99ceffa25ac45d150e30bd9ed14ec6039f2aad0ffa6bb87a5936f5782fc1569"}, + {file = "regex-2024.5.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13cdaf31bed30a1e1c2453ef6015aa0983e1366fad2667657dbcac7b02f67133"}, + {file = "regex-2024.5.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cac27dcaa821ca271855a32188aa61d12decb6fe45ffe3e722401fe61e323cd1"}, + {file = "regex-2024.5.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7dbe2467273b875ea2de38ded4eba86cbcbc9a1a6d0aa11dcf7bd2e67859c435"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f18a9a3513a99c4bef0e3efd4c4a5b11228b48aa80743be822b71e132ae4f5"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d347a741ea871c2e278fde6c48f85136c96b8659b632fb57a7d1ce1872547600"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1878b8301ed011704aea4c806a3cadbd76f84dece1ec09cc9e4dc934cfa5d4da"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4babf07ad476aaf7830d77000874d7611704a7fcf68c9c2ad151f5d94ae4bfc4"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35cb514e137cb3488bce23352af3e12fb0dbedd1ee6e60da053c69fb1b29cc6c"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cdd09d47c0b2efee9378679f8510ee6955d329424c659ab3c5e3a6edea696294"}, + {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:72d7a99cd6b8f958e85fc6ca5b37c4303294954eac1376535b03c2a43eb72629"}, + {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a094801d379ab20c2135529948cb84d417a2169b9bdceda2a36f5f10977ebc16"}, + {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c0c18345010870e58238790a6779a1219b4d97bd2e77e1140e8ee5d14df071aa"}, + {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:16093f563098448ff6b1fa68170e4acbef94e6b6a4e25e10eae8598bb1694b5d"}, + {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e38a7d4e8f633a33b4c7350fbd8bad3b70bf81439ac67ac38916c4a86b465456"}, + {file = "regex-2024.5.15-cp39-cp39-win32.whl", hash = "sha256:71a455a3c584a88f654b64feccc1e25876066c4f5ef26cd6dd711308aa538694"}, + {file = "regex-2024.5.15-cp39-cp39-win_amd64.whl", hash = "sha256:cab12877a9bdafde5500206d1020a584355a97884dfd388af3699e9137bf7388"}, + {file = "regex-2024.5.15.tar.gz", hash = "sha256:d3ee02d9e5f482cc8309134a91eeaacbdd2261ba111b0fef3748eeb4913e6a2c"}, +] + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rich" +version = "13.7.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "starlette" +version = "0.37.2" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.8" +files = [ + {file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"}, + {file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] + +[[package]] +name = "stdlib-list" +version = "0.10.0" +description = "A list of Python Standard Libraries (2.7 through 3.12)." +optional = false +python-versions = ">=3.7" +files = [ + {file = "stdlib_list-0.10.0-py3-none-any.whl", hash = "sha256:b3a911bc441d03e0332dd1a9e7d0870ba3bb0a542a74d7524f54fb431256e214"}, + {file = "stdlib_list-0.10.0.tar.gz", hash = "sha256:6519c50d645513ed287657bfe856d527f277331540691ddeaf77b25459964a14"}, +] + +[package.extras] +dev = ["build", "stdlib-list[doc,lint,test]"] +doc = ["furo", "sphinx"] +lint = ["black", "mypy", "ruff"] +support = ["sphobjinv"] +test = ["coverage[toml]", "pytest", "pytest-cov"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "tomlkit" +version = "0.12.5" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomlkit-0.12.5-py3-none-any.whl", hash = "sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f"}, + {file = "tomlkit-0.12.5.tar.gz", hash = "sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c"}, +] + +[[package]] +name = "tox" +version = "4.16.0" +description = "tox is a generic virtualenv management and test command line tool" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tox-4.16.0-py3-none-any.whl", hash = "sha256:61e101061b977b46cf00093d4319438055290ad0009f84497a07bf2d2d7a06d0"}, + {file = "tox-4.16.0.tar.gz", hash = "sha256:43499656f9949edb681c0f907f86fbfee98677af9919d8b11ae5ad77cb800748"}, +] + +[package.dependencies] +cachetools = ">=5.3.3" +chardet = ">=5.2" +colorama = ">=0.4.6" +filelock = ">=3.15.4" +packaging = ">=24.1" +platformdirs = ">=4.2.2" +pluggy = ">=1.5" +pyproject-api = ">=1.7.1" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} +virtualenv = ">=20.26.3" + +[package.extras] +docs = ["furo (>=2024.5.6)", "sphinx (>=7.3.7)", "sphinx-argparse-cli (>=1.16)", "sphinx-autodoc-typehints (>=2.2.2)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.11)"] +testing = ["build[virtualenv] (>=1.2.1)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.2)", "devpi-process (>=1)", "diff-cover (>=9.1)", "distlib (>=0.3.8)", "flaky (>=3.8.1)", "hatch-vcs (>=0.4)", "hatchling (>=1.25)", "psutil (>=6)", "pytest (>=8.2.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-xdist (>=3.6.1)", "re-assert (>=1.1)", "setuptools (>=70.2)", "time-machine (>=2.14.2)", "wheel (>=0.43)"] + +[[package]] +name = "typer" +version = "0.9.4" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.6" +files = [ + {file = "typer-0.9.4-py3-none-any.whl", hash = "sha256:aa6c4a4e2329d868b80ecbaf16f807f2b54e192209d7ac9dd42691d63f7a54eb"}, + {file = "typer-0.9.4.tar.gz", hash = "sha256:f714c2d90afae3a7929fcd72a3abb08df305e1ff61719381384211c4070af57f"}, +] + +[package.dependencies] +click = ">=7.1.1,<9.0.0" +typing-extensions = ">=3.7.4.3" + +[package.extras] +all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] +dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] +doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] +test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.971)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "urllib3" +version = "2.2.2" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "uvicorn" +version = "0.28.1" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +files = [ + {file = "uvicorn-0.28.1-py3-none-any.whl", hash = "sha256:5162f6d652f545be91b1feeaee8180774af143965ca9dc8a47ff1dc6bafa4ad5"}, + {file = "uvicorn-0.28.1.tar.gz", hash = "sha256:08103e79d546b6cf20f67c7e5e434d2cf500a6e29b28773e407250c54fc4fa3c"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "virtualenv" +version = "20.26.3" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, + {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +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)"] + +[[package]] +name = "watchdog" +version = "4.0.1" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.8" +files = [ + {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645"}, + {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b"}, + {file = "watchdog-4.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ef0107bbb6a55f5be727cfc2ef945d5676b97bffb8425650dadbb184be9f9a2b"}, + {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5"}, + {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767"}, + {file = "watchdog-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459"}, + {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:093b23e6906a8b97051191a4a0c73a77ecc958121d42346274c6af6520dec175"}, + {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:611be3904f9843f0529c35a3ff3fd617449463cb4b73b1633950b3d97fa4bfb7"}, + {file = "watchdog-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:62c613ad689ddcb11707f030e722fa929f322ef7e4f18f5335d2b73c61a85c28"}, + {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d4925e4bf7b9bddd1c3de13c9b8a2cdb89a468f640e66fbfabaf735bd85b3e35"}, + {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cad0bbd66cd59fc474b4a4376bc5ac3fc698723510cbb64091c2a793b18654db"}, + {file = "watchdog-4.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a3c2c317a8fb53e5b3d25790553796105501a235343f5d2bf23bb8649c2c8709"}, + {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9904904b6564d4ee8a1ed820db76185a3c96e05560c776c79a6ce5ab71888ba"}, + {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:667f3c579e813fcbad1b784db7a1aaa96524bed53437e119f6a2f5de4db04235"}, + {file = "watchdog-4.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d10a681c9a1d5a77e75c48a3b8e1a9f2ae2928eda463e8d33660437705659682"}, + {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0144c0ea9997b92615af1d94afc0c217e07ce2c14912c7b1a5731776329fcfc7"}, + {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:998d2be6976a0ee3a81fb8e2777900c28641fb5bfbd0c84717d89bca0addcdc5"}, + {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e7921319fe4430b11278d924ef66d4daa469fafb1da679a2e48c935fa27af193"}, + {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f0de0f284248ab40188f23380b03b59126d1479cd59940f2a34f8852db710625"}, + {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bca36be5707e81b9e6ce3208d92d95540d4ca244c006b61511753583c81c70dd"}, + {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ab998f567ebdf6b1da7dc1e5accfaa7c6992244629c0fdaef062f43249bd8dee"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_i686.whl", hash = "sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84"}, + {file = "watchdog-4.0.1-py3-none-win32.whl", hash = "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429"}, + {file = "watchdog-4.0.1-py3-none-win_amd64.whl", hash = "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a"}, + {file = "watchdog-4.0.1-py3-none-win_ia64.whl", hash = "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d"}, + {file = "watchdog-4.0.1.tar.gz", hash = "sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +name = "win32-setctime" +version = "1.1.0" +description = "A small Python utility to set file creation time on Windows" +optional = false +python-versions = ">=3.5" +files = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, +] + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.10,<4.0" +content-hash = "367c05a3e4fb7f278181e0af623c722ef28a695efc72240e62739e71cd78b99c" diff --git a/arcade/poetry.toml b/arcade/poetry.toml new file mode 100644 index 00000000..ab1033bd --- /dev/null +++ b/arcade/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true diff --git a/arcade/pyproject.toml b/arcade/pyproject.toml new file mode 100644 index 00000000..753663f4 --- /dev/null +++ b/arcade/pyproject.toml @@ -0,0 +1,127 @@ +[tool.poetry] +name = "arcade-ai" +version = "0.1.0" +description = "" +packages = [ + {include="arcade", from="."} +] +authors = ["Arcade AI "] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + + +[tool.poetry.dependencies] +python = ">=3.10,<4.0" +pydantic = {extras = ["email"], version = "^2.7.0"} +fastapi = "^0.110.0" +redis = "^5.0.3" +uvicorn = "^0.28.0" +loguru = "^0.7.2" +pydantic-settings = "^2.2.1" +msgspec = "^0.18.6" +msgpack = "^1.0.8" +typer = "^0.9.0" +rich = "^13.7.1" +toml = "^0.10.2" +tomlkit = "^0.12.4" +stdlib-list = "^0.10.0" +requests = "^2.26.0" + +[tool.poetry.group.dev.dependencies] +pytest = "^7.2.0" +pytest-cov = "^4.0.0" +deptry = "^0.12.0" +mypy = "^1.5.1" +pre-commit = "^3.4.0" +tox = "^4.11.1" + +pytest-asyncio = "^0.23.7" +[tool.poetry.group.docs.dependencies] +mkdocs = "^1.4.2" +mkdocs-material = "^9.2.7" +mkdocstrings = {extras = ["python"], version = "^0.23.0"} + + +[tool.poetry.scripts] +arcade = "arcade.cli.main:cli" + + +[tool.mypy] +files = ["arcade"] +disallow_untyped_defs = "True" +disallow_any_unimported = "True" +no_implicit_optional = "True" +check_untyped_defs = "True" +warn_return_any = "True" +warn_unused_ignores = "True" +show_error_codes = "True" + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[tool.ruff] +target-version = "py39" +line-length = 100 +fix = true +select = [ + # flake8-2020 + "YTT", + # flake8-bandit + "S", + # flake8-bugbear + "B", + # flake8-builtins + "A", + # flake8-comprehensions + "C4", + # flake8-debugger + "T10", + # flake8-simplify + "SIM", + # isort + "I", + # mccabe + "C90", + # pycodestyle + "E", "W", + # pyflakes + "F", + # pygrep-hooks + "PGH", + # pyupgrade + "UP", + # ruff + "RUF", + # tryceratops + "TRY", +] +ignore = [ # TODO work to remove these + # LineTooLong + "E501", + # DoNotAssignLambda + "E731", + # raise from (cli specific) + "TRY200", + # Depends function in arg string + "B008", + # raise from (cli specific) + "B904", + # long message exceptions + "TRY003" +] + +[tool.ruff.format] +preview = true + +[tool.coverage.report] +skip_empty = true + +[tool.coverage.run] +branch = true +source = ["arcade"] + + +[tool.ruff.per-file-ignores] +"tests/*" = ["S101"] diff --git a/toolserve/toolserve/server/services/__init__.py b/arcade/tests/__init__.py similarity index 100% rename from toolserve/toolserve/server/services/__init__.py rename to arcade/tests/__init__.py diff --git a/arcade/tests/sdk/test_tool_decorator.py b/arcade/tests/sdk/test_tool_decorator.py new file mode 100644 index 00000000..e4cfd894 --- /dev/null +++ b/arcade/tests/sdk/test_tool_decorator.py @@ -0,0 +1,52 @@ +import asyncio + +import pytest + +from arcade.sdk.schemas import OAuth2AuthorizationRequirement +from arcade.sdk.tool import tool + + +def test_sync_function(): + """ + Ensures a function will run when decorated by @tool + """ + + @tool + def sync_func(x, y): + return x + y + + result = sync_func(1, 2) + assert result == 3 + + +@pytest.mark.asyncio +async def test_async_function(): + """ + Ensures an async function will run when decorated by @tool + """ + + @tool + async def async_func(x, y): + await asyncio.sleep(0) + return x + y + + result = await async_func(1, 2) + assert result == 3 + + +def test_tool_decorator_with_all_options(): + @tool( + name="TestTool", + desc="Test description", + requires_auth=OAuth2AuthorizationRequirement( + url="https://example.com/oauth2/auth", + scope=["test_scope", "another.scope"], + ), + ) + def test_tool(x, y): + return x + y + + assert test_tool.__tool_name__ == "TestTool" + assert test_tool.__tool_description__ == "Test description" + assert str(test_tool.__tool_requires_auth__.url) == "https://example.com/oauth2/auth" + assert test_tool.__tool_requires_auth__.scope == ["test_scope", "another.scope"] diff --git a/arcade/tests/tool/test_create_tool_definition.py b/arcade/tests/tool/test_create_tool_definition.py new file mode 100644 index 00000000..fc1c19b6 --- /dev/null +++ b/arcade/tests/tool/test_create_tool_definition.py @@ -0,0 +1,456 @@ +from typing import Annotated, Literal, Optional + +import pytest + +from arcade.sdk.annotations import Inferrable +from arcade.sdk.schemas import ( + InputParameter, + OAuth2AuthorizationRequirement, + ToolInputs, + ToolOutput, + ToolRequirements, + ValueSchema, +) +from arcade.sdk.tool import tool +from arcade.tool.catalog import ToolCatalog + + +### Tests on @tool decorator +@tool(desc="A function with a description") +def func_with_description(): + pass + + +@tool +def func_with_docstring_description(): + """Docstring description""" + pass + + +@tool(name="MyCustomTool", desc="A function with a very cool description") +def func_with_name_and_description(): + pass + + +@tool( + desc="A function that requires authentication", + requires_auth=OAuth2AuthorizationRequirement( + url="https://example.com/oauth2/auth", scope=["scope1", "scope2"] + ), +) +def func_with_auth_requirement(): + pass + + +### Tests on input params +@tool(desc="A function with an input parameter") +def func_with_param(param1: Annotated[str, "First param"]): + pass + + +@tool(desc="A function with a non-inferrable input parameter") +def func_with_non_inferrable_param(param1: Annotated[str, "First param", Inferrable(False)]): + pass + + +# Two string annotations on an input parameter is understood to be name, description +@tool(desc="A function with a renamed input parameter") +def func_with_renamed_param(param1: Annotated[str, "ParamOne", "First param"]): + pass + + +@tool(desc="A function with every possible input parameter") +def func_with_every_param( + param1: Annotated[str, "a string"], + param2: Annotated[int, "an integer"], + param3: Annotated[float, "a float"], + param4: Annotated[bool, "a boolean"], +): + pass + + +@tool(desc="A function that takes a dictionary") +def func_with_dict_param(param1: Annotated[dict, "a cool dictionary"]): + pass + + +@tool(desc="A function that takes a string enum") +def func_with_string_enum_param(param1: Annotated[Literal["value1", "value2"], "a few choices"]): + pass + + +@tool(desc="A function with an input parameter with a default value (considered optional)") +def func_with_param_with_default(param1: Annotated[str, "First param"] = "default"): + pass + + +@tool(desc="A function with an optional input parameter") +def func_with_optional_param(param1: Annotated[Optional[str], "First param"]): + pass + + +@tool(desc="A function with multiple parameters, some with default values") +def func_with_mixed_params( + param1: Annotated[str, "First param"], + param2: Annotated[int, "Second param"] = 42, +): + pass + + +@tool(desc="A function with a complex parameter type") +def func_with_complex_param(param1: Annotated[list[str], "A list of strings"]): + pass + + +### Tests on output/return values +@tool(desc="A function that performs an action without returning anything") +def func_with_no_return(): + pass + + +@tool(desc="A function that returns a value") +def func_with_value_return() -> str: + return "output" + + +@tool(desc="A function with an annotated return type") +def func_with_annotated_return() -> Annotated[str, "Annotated return description"]: + return "output" + + +@tool(desc="A function with an optional return type") +def func_with_optional_return() -> Optional[str]: + return "maybe output" + + +@tool(desc="A function with a complex return type") +def func_with_complex_return() -> list[dict[str, str]]: + return [{"key": "value"}] + + +@pytest.mark.parametrize( + "func_under_test, expected_tool_def_fields", + [ + # Tests on @tool decorator + pytest.param( + func_with_description, + { + "name": "FuncWithDescription", # Defaults to the camelCased function name + }, + id="func_with_default_name", + ), + pytest.param( + func_with_description, + {"description": "A function with a description"}, + id="func_with_description", + ), + pytest.param( + func_with_docstring_description, + {"description": "Docstring description"}, + id="func_with_docstring_description", + ), + pytest.param( + func_with_name_and_description, + {"name": "MyCustomTool", "description": "A function with a very cool description"}, + id="func_with_description_and_name", + ), + pytest.param( + func_with_name_and_description, + {"name": "MyCustomTool", "requirements": ToolRequirements(authorization=None)}, + id="func_with_no_auth_requirement", + ), + pytest.param( + func_with_auth_requirement, + { + "requirements": ToolRequirements( + authorization=OAuth2AuthorizationRequirement( + url="https://example.com/oauth2/auth", scope=["scope1", "scope2"] + ) + ) + }, + id="func_with_auth_requirement", + ), + # Tests on input params + pytest.param( + func_with_value_return, + { + "inputs": ToolInputs(parameters=[]), + }, + id="func_with_no_params", + ), + pytest.param( + func_with_param, + { + "inputs": ToolInputs( + parameters=[ + InputParameter( + name="param1", + description="First param", + inferrable=True, # Defaults to true + required=True, + value_schema=ValueSchema(val_type="string", enum=None), + ) + ] + ), + }, + id="func_with_param", + ), + pytest.param( + func_with_non_inferrable_param, + { + "inputs": ToolInputs( + parameters=[ + InputParameter( + name="param1", + description="First param", + inferrable=False, # Set using Inferrable(False) + required=True, + value_schema=ValueSchema(val_type="string", enum=None), + ) + ] + ), + }, + id="func_with_non_inferrable_param", + ), + pytest.param( + func_with_renamed_param, + { + "inputs": ToolInputs( + parameters=[ + InputParameter( + name="ParamOne", + description="First param", + inferrable=True, + required=True, + value_schema=ValueSchema(val_type="string", enum=None), + ) + ] + ), + }, + id="func_with_renamed_param", + ), + pytest.param( + func_with_every_param, + { + "inputs": ToolInputs( + parameters=[ + InputParameter( + name="param1", + description="a string", + inferrable=True, + required=True, + value_schema=ValueSchema(val_type="string", enum=None), + ), + InputParameter( + name="param2", + description="an integer", + inferrable=True, + required=True, + value_schema=ValueSchema(val_type="integer", enum=None), + ), + InputParameter( + name="param3", + description="a float", + inferrable=True, + required=True, + value_schema=ValueSchema(val_type="float", enum=None), + ), + InputParameter( + name="param4", + description="a boolean", + inferrable=True, + required=True, + value_schema=ValueSchema(val_type="boolean", enum=None), + ), + ] + ), + }, + id="func_with_every_param", + ), + pytest.param( + func_with_dict_param, + { + "inputs": ToolInputs( + parameters=[ + InputParameter( + name="param1", + description="a cool dictionary", + inferrable=True, + required=True, + value_schema=ValueSchema(val_type="json", enum=None), + ) + ] + ), + }, + id="func_with_dict_param", + ), + pytest.param( + func_with_string_enum_param, + { + "inputs": ToolInputs( + parameters=[ + InputParameter( + name="param1", + description="a few choices", + inferrable=True, + required=True, + value_schema=ValueSchema(val_type="string", enum=["value1", "value2"]), + ) + ] + ), + }, + id="func_with_string_enum_param", + ), + pytest.param( + func_with_param_with_default, + { + "inputs": ToolInputs( + parameters=[ + InputParameter( + name="param1", + description="First param", + inferrable=True, + required=False, # Because a default value is provided + value_schema=ValueSchema(val_type="string", enum=None), + ) + ] + ), + "output": ToolOutput( + available_modes=["null"], description="No description provided." + ), + }, + id="func_with_param_with_default", + ), + pytest.param( + func_with_optional_param, + { + "inputs": ToolInputs( + parameters=[ + InputParameter( + name="param1", + description="First param", + inferrable=True, + required=False, # Because of Optional[str] + value_schema=ValueSchema(val_type="string", enum=None), + ) + ] + ), + "output": ToolOutput( + available_modes=["null"], description="No description provided." + ), + }, + id="func_with_optional_param", + ), + pytest.param( + func_with_mixed_params, + { + "inputs": ToolInputs( + parameters=[ + InputParameter( + name="param1", + description="First param", + inferrable=True, + required=True, + value_schema=ValueSchema(val_type="string", enum=None), + ), + InputParameter( + name="param2", + description="Second param", + inferrable=True, + required=False, # Because a default value is provided + value_schema=ValueSchema(val_type="integer", enum=None), + ), + ] + ), + }, + id="func_with_mixed_params", + ), + pytest.param( + func_with_complex_param, + { + "inputs": ToolInputs( + parameters=[ + InputParameter( + name="param1", + description="A list of strings", + inferrable=True, + required=True, + value_schema=ValueSchema(val_type="json", enum=None), + ) + ] + ), + }, + id="func_with_complex_param", + ), + # Tests on output values + pytest.param( + func_with_no_return, + { + "output": ToolOutput( + available_modes=["null"], description="No description provided." + ), + }, + id="func_with_no_return", + ), + pytest.param( + func_with_value_return, + { + "inputs": ToolInputs(parameters=[]), + "output": ToolOutput( + value_schema=ValueSchema(val_type="string", enum=None), + available_modes=["value", "error"], + description="No description provided.", + ), + }, + id="func_with_value_return", + ), + pytest.param( + func_with_annotated_return, + { + "inputs": ToolInputs(parameters=[]), + "output": ToolOutput( + value_schema=ValueSchema(val_type="string", enum=None), + available_modes=["value", "error"], + description="Annotated return description", + ), + }, + id="func_with_annotated_return", + ), + pytest.param( + func_with_optional_return, + { + "inputs": ToolInputs(parameters=[]), + "output": ToolOutput( + value_schema=ValueSchema(val_type="string", enum=None), + available_modes=["value", "error", "null"], + description="No description provided.", + ), + }, + id="func_with_optional_return", + ), + pytest.param( + func_with_complex_return, + { + "inputs": ToolInputs(parameters=[]), + "output": ToolOutput( + value_schema=ValueSchema(val_type="json", enum=None), + available_modes=["value", "error"], + description="No description provided.", + ), + }, + id="func_with_complex_return", + ), + ], +) +def test_create_tool_def(func_under_test, expected_tool_def_fields): + tool_def = ToolCatalog.create_tool_definition(func_under_test, "1.0") + + assert tool_def.version == "1.0" + + for field, expected_value in expected_tool_def_fields.items(): + assert getattr(tool_def, field) == expected_value + + +def tool_version_is_set_correctly(): + tool_def = ToolCatalog.create_tool_definition(func_with_no_return, "abcd1236") + assert tool_def.version == "abcd1236" diff --git a/arcade/tests/tool/test_create_tool_definition_errors.py b/arcade/tests/tool/test_create_tool_definition_errors.py new file mode 100644 index 00000000..7a9b7023 --- /dev/null +++ b/arcade/tests/tool/test_create_tool_definition_errors.py @@ -0,0 +1,55 @@ +import pytest + +from arcade.sdk.errors import ToolDefinitionError +from arcade.sdk.tool import tool +from arcade.tool.catalog import ToolCatalog + + +@tool +def func_with_missing_description(): + pass + + +@tool(desc="Returning function with declared no return type (illegal)") +def func_with_missing_return_type(): + return "hello world" + + +@tool(desc="A function with a parameter missing a description (illegal)") +def func_with_missing_param_description(param1: str): + pass + + +@tool(desc="A function with an unsupported parameter type (illegal)") +def func_with_unsupported_param(param1: complex): + pass + + +@pytest.mark.parametrize( + "func_under_test, exception_type", + [ + pytest.param( + func_with_missing_description, + ToolDefinitionError, + id=func_with_missing_description.__name__, + ), + pytest.param( + func_with_missing_return_type, + ToolDefinitionError, + id=func_with_missing_return_type.__name__, + ), + pytest.param( + func_with_missing_param_description, + ToolDefinitionError, + id=func_with_missing_param_description.__name__, + ), + pytest.param( + func_with_unsupported_param, + ToolDefinitionError, + id=func_with_unsupported_param.__name__, + ), + ], +) +def test_missing_info_raises_error(func_under_test, exception_type): + with pytest.raises(exception_type): + ToolCatalog.create_tool_definition(func_under_test, "1.0") diff --git a/arcade/tests/tool/test_create_tool_definition_pydantic.py b/arcade/tests/tool/test_create_tool_definition_pydantic.py new file mode 100644 index 00000000..e4f9a3f4 --- /dev/null +++ b/arcade/tests/tool/test_create_tool_definition_pydantic.py @@ -0,0 +1,57 @@ +from typing import Annotated + +import pytest +from pydantic import BaseModel, Field + +from arcade.sdk.schemas import ( + ToolOutput, + ValueSchema, +) +from arcade.sdk.tool import tool +from arcade.tool.catalog import ToolCatalog + + +class ProductOutput(BaseModel): + product_name: str = Field(..., description="The name of the product") + price: int = Field(..., description="The price of the product") + stock_quantity: int = Field(..., description="The stock quantity of the product") + + +@tool(desc="A function that returns a Pydantic model") +def func_returns_pydantic_model() -> Annotated[ProductOutput, "The product, price, and quantity"]: + return ProductOutput( + product_name="Product 1", + price=100, + stock_quantity=1000, + ) + + +# TODO: Function that takes a Pydantic model as an argument: break it down into components? Look at OpenAPI, do they represent nested arguments? +# TODO: Function that takes a Pydantic Field as an argument +# TODO: Pydantic Field() properties: description, default, title, default_factory, nullable +# TODO: Pydantic Field() properties stretch goal: gt, ge, lt, le, multiple_of, range, regex, max_length, min_length, max_items, min_items, unique_items, exclusive_maximum, exclusive_minimum + + +@pytest.mark.parametrize( + "func_under_test, expected_tool_def_fields", + [ + pytest.param( + func_returns_pydantic_model, + { + "output": ToolOutput( + value_schema=ValueSchema(val_type="json", enum=None), + available_modes=["value", "error"], + description="The product, price, and quantity", + ) + }, + id="func_returns_pydantic_model", + ), + ], +) +def test_create_tool_def(func_under_test, expected_tool_def_fields): + tool_def = ToolCatalog.create_tool_definition(func_under_test, "1.0") + + assert tool_def.version == "1.0" + + for field, expected_value in expected_tool_def_fields.items(): + assert getattr(tool_def, field) == expected_value diff --git a/arcade/tests/utils/test_utils_casing.py b/arcade/tests/utils/test_utils_casing.py new file mode 100644 index 00000000..e4075017 --- /dev/null +++ b/arcade/tests/utils/test_utils_casing.py @@ -0,0 +1,26 @@ +import pytest + +from arcade.utils import pascal_to_snake_case, snake_to_pascal_case + + +@pytest.mark.parametrize( + "input_str, expected", + [ + ("SnakeCase", "snake_case"), + ("VeryLongSnake456", "very_long_snake456"), + ], +) +def test_pascal_to_snake_case(input_str: str, expected: str): + assert pascal_to_snake_case(input_str) == expected + + +@pytest.mark.parametrize( + "input_str, expected", + [ + ("snake_case", "SnakeCase"), + ("very_long_snake_456", "VeryLongSnake456"), + ("camelCase", "Camelcase"), # camelCase isn't explicitly supported + ], +) +def test_snake_to_pascal_case(input_str: str, expected: str): + assert snake_to_pascal_case(input_str) == expected diff --git a/arcade/tox.ini b/arcade/tox.ini new file mode 100644 index 00000000..a44a21bd --- /dev/null +++ b/arcade/tox.ini @@ -0,0 +1,18 @@ +[tox] +skipsdist = true +envlist = py38, py39, py310, py311 + +[gh-actions] +python = + 3.8: py38 + 3.9: py39 + 3.10: py310 + 3.11: py311 + +[testenv] +passenv = PYTHON_VERSION +allowlist_externals = poetry +commands = + poetry install -v + pytest --doctest-modules tests --cov --cov-config=pyproject.toml --cov-report=xml + mypy diff --git a/cspell.config.yaml b/cspell.config.yaml new file mode 100644 index 00000000..1c973ce8 --- /dev/null +++ b/cspell.config.yaml @@ -0,0 +1,12 @@ +version: "0.2" +ignorePaths: + - pyproject.toml +dictionaryDefinitions: [] +dictionaries: [] +words: + - conlist + - pydantic + - pyproject + - toolpack +ignoreWords: [] +import: [] diff --git a/examples/data/make_sqlite_db.py b/examples/data/make_sqlite_db.py index 5dc6b265..63cf78b5 100644 --- a/examples/data/make_sqlite_db.py +++ b/examples/data/make_sqlite_db.py @@ -2,14 +2,15 @@ import csv import sqlite3 # Path to the CSV file -csv_file_path = './synthetic_people_data.csv' +csv_file_path = "./synthetic_people_data.csv" # Connect to a SQLite database (will be created if it doesn't exist) -conn = sqlite3.connect('people.sqlite') +conn = sqlite3.connect("people.sqlite") cur = conn.cursor() # Create a table -cur.execute(''' +cur.execute( + """ CREATE TABLE IF NOT EXISTS people ( id INTEGER PRIMARY KEY, Name TEXT, @@ -18,21 +19,25 @@ CREATE TABLE IF NOT EXISTS people ( Occupation TEXT, Email TEXT ) -''') +""" +) # Read data from the CSV file -with open(csv_file_path, 'r') as csvfile: +with open(csv_file_path, "r") as csvfile: csvreader = csv.reader(csvfile) next(csvreader) # Skip the header row for row in csvreader: # Insert each row into the database - cur.execute(''' + cur.execute( + """ INSERT INTO people (Name, Age, Location, Occupation, Email) VALUES (?, ?, ?, ?, ?) - ''', row) + """, + row, + ) # Commit changes and close the connection conn.commit() conn.close() -print('Data imported into SQLite database successfully.') +print("Data imported into SQLite database successfully.") diff --git a/examples/data/test_prompt.py b/examples/data/test_prompt.py deleted file mode 100644 index e973e88d..00000000 --- a/examples/data/test_prompt.py +++ /dev/null @@ -1,24 +0,0 @@ -from sqlalchemy import create_engine, MetaData - -# Replace 'your_database.db' with your actual SQLite database file -database_path = 'sqlite:///people.sqlite' -engine = create_engine(database_path) -metadata = MetaData() - -# Reflect the tables in the database -metadata.reflect(bind=engine) - -# Iterate over all tables and print their descriptions -for table_name in metadata.tables: - print(f"Table: {table_name}") - table = metadata.tables[table_name] - - # Iterate over columns in the table and print details - for column in table.c: - print(f"Column: {column.name}") - print(f"Type: {column.type}") - print(f"Nullable: {column.nullable}") - print(f"Primary Key: {column.primary_key}") - print(f"---------------------") - - print(f"{'='*20}\n") \ No newline at end of file diff --git a/examples/pack.lock.toml b/examples/generic/pack.lock.toml similarity index 50% rename from examples/pack.lock.toml rename to examples/generic/pack.lock.toml index 6cd3b523..aae3a4de 100644 --- a/examples/pack.lock.toml +++ b/examples/generic/pack.lock.toml @@ -8,7 +8,10 @@ email = "sam@partee.io" [depends] [tools] -Summarize = "llm.summarize@0.0.1" -Respond = "llm.respond@0.0.1" +TextSearch = "BM25.text_search@0.0.1" +ReadProducts = "products.read_products@0.0.1" +ReadSqlite = "read_sqlite.read_sqlite@0.0.1" SendEmail = "gmail.send_email@0.0.1" ReadEmail = "gmail.read_email@0.0.1" +OauthReadEmail = "gmail.oauth_read_email@0.0.1" +ListDriveFiles = "gmail.list_drive_files@0.0.1" diff --git a/examples/pack.toml b/examples/generic/pack.toml similarity index 75% rename from examples/pack.toml rename to examples/generic/pack.toml index 7f0c91e2..08081482 100644 --- a/examples/pack.toml +++ b/examples/generic/pack.toml @@ -9,4 +9,6 @@ email = "sam@partee.io" [modules] gmail = "0.0.1" -llm = "0.0.1" +read_sqlite = "0.0.1" +BM25 = "0.0.1" +products = "0.0.1" \ No newline at end of file diff --git a/examples/generic/tools/BM25.py b/examples/generic/tools/BM25.py new file mode 100644 index 00000000..5a38958a --- /dev/null +++ b/examples/generic/tools/BM25.py @@ -0,0 +1,158 @@ +import math +import numpy as np + +from typing import Annotated +from multiprocessing import Pool, cpu_count + +from arcade.sdk.tool import tool + + +class BM25: + def __init__(self, corpus, tokenizer=None): + self.corpus_size = 0 + self.avgdl = 0 + self.doc_freqs = [] + self.idf = {} + self.doc_len = [] + self.tokenizer = tokenizer + + if tokenizer: + corpus = self._tokenize_corpus(corpus) + else: + corpus = self._tokenize(corpus) + + nd = self._initialize(corpus) + self._calc_idf(nd) + + @staticmethod + def _tokenize(texts: list[str]) -> list[list[str]]: + return [text.split() for text in texts] + + def _initialize(self, corpus): + nd = {} # word -> number of documents with word + num_doc = 0 + for document in corpus: + self.doc_len.append(len(document)) + num_doc += len(document) + + frequencies = {} + for word in document: + if word not in frequencies: + frequencies[word] = 0 + frequencies[word] += 1 + self.doc_freqs.append(frequencies) + + for word, freq in frequencies.items(): + try: + nd[word] += 1 + except KeyError: + nd[word] = 1 + + self.corpus_size += 1 + + self.avgdl = num_doc / self.corpus_size + return nd + + def _tokenize_corpus(self, corpus): + pool = Pool(cpu_count()) + tokenized_corpus = pool.map(self.tokenizer, corpus) + return tokenized_corpus + + def _calc_idf(self, nd): + raise NotImplementedError() + + def get_scores(self, query): + raise NotImplementedError() + + def get_batch_scores(self, query, doc_ids): + raise NotImplementedError() + + def get_top_n(self, query, documents, n=5): + assert self.corpus_size == len( + documents + ), "The documents given don't match the index corpus!" + query = self._tokenize([query])[0] # tokenize the query + scores = self.get_scores(query) + top_n = np.argsort(scores)[::-1][:n] + return [documents[i] for i in top_n] + + +class Okapi(BM25): + def __init__(self, corpus, tokenizer=None, k1=1.5, b=0.75, epsilon=0.25): + self.k1 = k1 + self.b = b + self.epsilon = epsilon + super().__init__(corpus, tokenizer) + + def _calc_idf(self, nd): + """ + Calculates frequencies of terms in documents and in corpus. + This algorithm sets a floor on the idf values to eps * average_idf + """ + # collect idf sum to calculate an average idf for epsilon value + idf_sum = 0 + # collect words with negative idf to set them a special epsilon value. + # idf can be negative if word is contained in more than half of documents + negative_idfs = [] + for word, freq in nd.items(): + idf = math.log(self.corpus_size - freq + 0.5) - math.log(freq + 0.5) + self.idf[word] = idf + idf_sum += idf + if idf < 0: + negative_idfs.append(word) + self.average_idf = idf_sum / len(self.idf) + + eps = self.epsilon * self.average_idf + for word in negative_idfs: + self.idf[word] = eps + + def get_scores(self, query): + """ + The ATIRE BM25 variant uses an idf function which uses a log(idf) score. To prevent negative idf scores, + this algorithm also adds a floor to the idf value of epsilon. + See [Trotman, A., X. Jia, M. Crane, Towards an Efficient and Effective Search Engine] for more info + :param query: + :return: + """ + score = np.zeros(self.corpus_size) + doc_len = np.array(self.doc_len) + for q in query: + q_freq = np.array([(doc.get(q) or 0) for doc in self.doc_freqs]) + score += (self.idf.get(q) or 0) * ( + q_freq + * (self.k1 + 1) + / (q_freq + self.k1 * (1 - self.b + self.b * doc_len / self.avgdl)) + ) + return score + + def get_batch_scores(self, query, doc_ids): + """ + Calculate bm25 scores between query and subset of all docs + """ + assert all(di < len(self.doc_freqs) for di in doc_ids) + score = np.zeros(len(doc_ids)) + doc_len = np.array(self.doc_len)[doc_ids] + for q in query: + q_freq = np.array([(self.doc_freqs[di].get(q) or 0) for di in doc_ids]) + score += (self.idf.get(q) or 0) * ( + q_freq + * (self.k1 + 1) + / (q_freq + self.k1 * (1 - self.b + self.b * doc_len / self.avgdl)) + ) + return score.tolist() + + +@tool +async def text_search( + query: Annotated[str, "The search query"], + texts: Annotated[list[str], "The texts through which to search"], + num_results: Annotated[int, "Number of texts to return"] = 5, +) -> Annotated[list[str], "Most similar texts"]: + """Use the BM25 algorithm to search through texts + + This should only be used for smaller datasets where the number + of texts is less than 100. + """ + + bm25 = Okapi(texts) + return bm25.get_top_n(query, texts, num_results) diff --git a/toolserve/toolserve/server/utils/__init__.py b/examples/generic/tools/__init__.py similarity index 100% rename from toolserve/toolserve/server/utils/__init__.py rename to examples/generic/tools/__init__.py diff --git a/examples/generic/tools/gmail.py b/examples/generic/tools/gmail.py new file mode 100644 index 00000000..712de6a6 --- /dev/null +++ b/examples/generic/tools/gmail.py @@ -0,0 +1,257 @@ +import os +import re +import email +import smtplib +import imaplib + +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from base64 import urlsafe_b64decode +from bs4 import BeautifulSoup +from google.auth.transport.requests import Request +from google.oauth2.credentials import Credentials +from google_auth_oauthlib.flow import InstalledAppFlow +from google.auth.exceptions import RefreshError +from googleapiclient.discovery import build +from typing import Dict, List, Annotated +from arcade.sdk.tool import tool, get_secret + + +@tool +async def send_email( + recipient_email: Annotated[str, "Email address of the recipient"], + subject: Annotated[str, "Subject of the email"], + body: Annotated[str, "Body of the email"], +): + """Send an email via gmail SMTP server""" + + sender_email = get_secret("gmail_email") + sender_password = get_secret("gmail_password") + server = get_secret("gmail_stmp_server", "smtp.gmail.com") + port = get_secret("gmail_stmp_port", 587) + + message = MIMEMultipart() + message["From"] = sender_email + message["To"] = recipient_email + message["Subject"] = subject + message.attach(MIMEText(body, "plain")) + + server = smtplib.SMTP(server, port) + server.starttls() + server.login(sender_email, sender_password) + print(f"Logged in to SMTP server at {':'.join((server, port))}", "DEBUG") + + server.send_message(message) + server.quit() + + print(f"Email sent from {sender_email} to {recipient_email}", "INFO") + + +@tool +async def read_email( + n_emails: Annotated[int, "Number of emails to read"] = 5, +) -> Annotated[str, "emails"]: + """Read emails from a Gmail account and extract plain text content, removing any HTML.""" + + email_address = get_secret("gmail_email") + password = get_secret("gmail_password") + server = get_secret("gmail_stmp_server", "smtp.gmail.com") + + # Connect to the Gmail IMAP server + mail = imaplib.IMAP4_SSL(server) + mail.login(email_address, password) + mail.select("inbox") # connect to inbox. + + result, data = mail.search(None, "ALL") + email_ids = data[0].split() + email_ids.reverse() # Reverse to get the most recent emails first + + emails = [] + + for email_id in email_ids[:n_emails]: + try: + result, data = mail.fetch(email_id, "(RFC822)") + raw_email = data[0][1] + msg = email.message_from_bytes(raw_email) + + email_details = {"from": msg["From"], "to": msg["To"], "date": msg["Date"]} + + if msg.is_multipart(): + for part in msg.walk(): + if part.get_content_type() == "text/plain": + body = part.get_payload(decode=True).decode("utf-8") + email_details["body"] = clean_email_body(body) + else: + body = msg.get_payload(decode=True).decode("utf-8") + email_details["body"] = clean_email_body(body) + except Exception as e: + print(f"Error reading email {email_id}: {e}", "ERROR") + continue + + emails.append(email_details) + + mail.close() + mail.logout() + data = "\n".join( + [f"{email['from']} - {email['date']}\n{email['body']}\n" for email in emails] + ) + return data + + +SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"] +SECRET_FILE = "/Users/spartee/Dropbox/Arcade/gcp/credentials.json" + + +@tool +async def oauth_read_email( + n_emails: Annotated[int, "Number of emails to read"] = 5, +) -> List[Dict[str, str]]: + """Read emails from a Gmail account and extract plain text content, removing any HTML.""" + + creds = None + # The file token.json stores the user's access and refresh tokens, and is + # created automatically when the authorization flow completes for the first time. + if os.path.exists("token.json"): + creds = Credentials.from_authorized_user_file("token.json") + # If there are no (valid) credentials available, let the user log in. + if not creds or not creds.valid: + if creds and creds.expired and creds.refresh_token: + try: + creds.refresh(Request()) + except RefreshError: + flow = InstalledAppFlow.from_client_secrets_file(SECRET_FILE, SCOPES) + creds = flow.run_local_server(port=0) + # Save the credentials for the next run + with open("token.json", "w") as token: + token.write(creds.to_json()) + else: + flow = InstalledAppFlow.from_client_secrets_file(SECRET_FILE, SCOPES) + creds = flow.run_local_server(port=0) + # Save the credentials for the next run + with open("token.json", "w") as token: + token.write(creds.to_json()) + + # Call the Gmail API + service = build("gmail", "v1", credentials=creds) + + # Request a list of all the messages + result = service.users().messages().list(userId="me").execute() + messages = result.get("messages") + + # If there are no messages, return an empty string + if not messages: + return "" + + emails = [] + + for msg in messages[:n_emails]: + txt = service.users().messages().get(userId="me", id=msg["id"]).execute() + + try: + payload = txt["payload"] + headers = payload["headers"] + + for d in headers: + if d["name"] == "From": + from_ = d["value"] + if d["name"] == "Date": + date = d["value"] + if d["name"] == "Subject": + subject = d["value"] + else: + subject = "No subject" + + data = None + parts = payload.get("parts") + if parts: + part = parts[0] + body = part.get("body") + if body: + data = body.get("data") + if data: + data = urlsafe_b64decode(data).decode() + + email_details = { + "from": from_, + "date": date, + "subject": subject, + "body": clean_email_body(data) if data else "", + } + emails.append(email_details) + + except Exception as e: + print(f"Error reading email {msg['id']}: {e}", "ERROR") + continue + + return emails + + +def clean_email_body(body: str) -> str: + """Remove HTML tags and non-sentence elements from email body text.""" + + # Remove HTML tags using BeautifulSoup + soup = BeautifulSoup(body, "html.parser") + text = soup.get_text(separator=" ") + + # Remove any non-sentence elements (e.g., URLs, email addresses, etc.) + text = re.sub(r"\S*@\S*\s?", "", text) # Remove emails + text = re.sub(r"http\S+", "", text) # Remove URLs + text = re.sub(r"[^.!?a-zA-Z0-9\s]", "", text) # Remove non-sentence characters + text = " ".join(text.split()) # Remove extra whitespace + + return text + + +DRIVE_SCOPES = ["https://www.googleapis.com/auth/drive.metadata.readonly"] + + +@tool +async def list_drive_files( + n_files: Annotated[int, "Number of files to search"] = 5, +) -> list[str]: + """List files from a Google Drive account and return their details.""" + + creds = None + # The file token.json stores the user's access and refresh tokens, and is + # created automatically when the authorization flow completes for the first time. + if os.path.exists("token.json"): + creds = Credentials.from_authorized_user_file("token.json") + # If there are no (valid) credentials available, let the user log in. + if not creds or not creds.valid: + if creds and creds.expired and creds.refresh_token: + try: + creds.refresh(Request()) + except RefreshError: + flow = InstalledAppFlow.from_client_secrets_file( + SECRET_FILE, DRIVE_SCOPES + ) + creds = flow.run_local_server(port=0) + # Save the credentials for the next run + with open("token.json", "w") as token: + token.write(creds.to_json()) + else: + flow = InstalledAppFlow.from_client_secrets_file(SECRET_FILE, DRIVE_SCOPES) + creds = flow.run_local_server(port=0) + # Save the credentials for the next run + with open("token.json", "w") as token: + token.write(creds.to_json()) + + # Call the Drive v3 API + service = build("drive", "v3", credentials=creds) + + # Request a list of all the files + results = ( + service.files() + .list(pageSize=n_files, fields="nextPageToken, files(id, name)") + .execute() + ) + items = results.get("files", []) + + if not items: + print("No files found.") + else: + print("Files:") + for item in items: + print("{0} ({1})".format(item["name"], item["id"])) + + return items diff --git a/examples/generic/tools/products.py b/examples/generic/tools/products.py new file mode 100644 index 00000000..3fd67c73 --- /dev/null +++ b/examples/generic/tools/products.py @@ -0,0 +1,76 @@ +from typing import Union +from arcade.sdk.tool import tool, get_secret +import pandas as pd +from pydantic import BaseModel, Field + + +class ProductFilter(BaseModel): + column: str = Field(..., description="The column to filter on") + + +class FilterRating(ProductFilter): + greater_than: int = Field( + ..., description="The rating to filter greater than", gt=0, lt=5 + ) + + +class FilterPriceGreaterThan(ProductFilter): + price: int = Field(..., description="The price to filter greater than", gt=0) + + +class FilterPriceLessThan(ProductFilter): + price: int = Field(..., description="The price to filter less than", gt=0) + + +class ProductSearch(BaseModel): + column: str = Field("Product Name", description="The column to search in") + query: str = Field(..., description="The query to search for") + filter_operation: Union[ + FilterRating, FilterPriceGreaterThan, FilterPriceLessThan + ] = None + + +class ProductOutput(BaseModel): + product_name: str = Field(..., description="The name of the product") + price: int = Field(..., description="The price of the product") + stock_quantity: int = Field(..., description="The stock quantity of the product") + + +@tool +def read_products( + action: ProductSearch, + cols: list[str] = [ + "Product Name", + "Price", + "Stock Quantity", + ], +) -> list[ProductOutput]: + """Used to search through products by name and filter by rating or price.""" + + file_path = get_secret( + "PRODUCTS_PATH", + "/Users/spartee/Dropbox/Arcade/platform/toolserver/examples/data/Sample_Products_Info.csv", + ) + try: + df = pd.read_csv(file_path) + df = df[cols] + + if action.filter_operation: + if isinstance(action.filter_operation, FilterRating): + df = df[ + df[action.filter_operation.column] + > action.filter_operation.greater_than + ] + elif isinstance(action.filter_operation, FilterPriceGreaterThan): + df = df[ + df[action.filter_operation.column] > action.filter_operation.price + ] + elif isinstance(action.filter_operation, FilterPriceLessThan): + df = df[ + df[action.filter_operation.column] < action.filter_operation.price + ] + + except Exception as e: + # TODO what to do here? + print(e) + return df.to_json() diff --git a/examples/generic/tools/read_sqlite.py b/examples/generic/tools/read_sqlite.py new file mode 100644 index 00000000..7058c864 --- /dev/null +++ b/examples/generic/tools/read_sqlite.py @@ -0,0 +1,38 @@ +from typing import Annotated + +from arcade.sdk.tool import tool +import pandas as pd + +from sqlite3 import connect + + +@tool +async def read_sqlite( + file_path: Annotated[str, "Path to the SQLite database file"], + table_name: Annotated[str, "Name of the table to read from"], + cols: Annotated[str, "Columns to read from the table"] = "*", +) -> str: + """Read data from a SQLite database table and save it as a DataFrame. + + Columns to choose from are: + - *: All columns + - column_name: Single column + - column_name1, column_name2, ...: Multiple columns + """ + # Connect to the SQLite database + conn = connect(file_path) + cursor = conn.cursor() + + # Read the data from the table + query = f"SELECT * FROM {table_name}" + cursor.execute(query) + rows = cursor.fetchall() + + # Get the column names + cursor.execute(f"PRAGMA table_info({table_name})") + columns = [col[1] for col in cursor.fetchall()] + + # Create a DataFrame from the data + df = pd.DataFrame(rows, columns=columns) + + return df.json() diff --git a/examples/tools/gmail.py b/examples/tools/gmail.py deleted file mode 100644 index 8af006b4..00000000 --- a/examples/tools/gmail.py +++ /dev/null @@ -1,119 +0,0 @@ -import re -import email -import smtplib -import imaplib -import pandas as pd -import plotly.express as px - -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText -from email.header import decode_header - -from pydantic import BaseModel -from bs4 import BeautifulSoup - -from toolserve.sdk import Param, tool, get_secret -from toolserve.sdk.client import log - - -@tool -async def send_email( - sender_email: Param(str, "Email address of the sender"), - recipient_email: Param(str, "Email address of the recipient"), - subject: Param(str, "Subject of the email"), - body: Param(str, "Body of the email"), - ): - """Send an email via gmail SMTP server""" - - email_address = get_secret("gmail_email") - sender_password = get_secret("gmail_password") - server = get_secret("gmail_stmp_server", "smtp.gmail.com") - port = get_secret("gmail_smtp_port", 587) - - message = MIMEMultipart() - message['From'] = sender_email - message['To'] = recipient_email - message['Subject'] = subject - message.attach(MIMEText(body, 'plain')) - - server = smtplib.SMTP(server, port) - server.starttls() - server.login(sender_email, sender_password) - log(f"Logged in to SMTP server at {':'.join((server, port))}", "DEBUG") - - server.send_message(message) - server.quit() - - log(f"Email sent from {sender_email} to {recipient_email}", "INFO") - - -@tool -async def read_email( - n_emails: Param(int, "Number of emails to read") = 5, - ) -> Param(str, "emails"): - """Read emails from a Gmail account and extract plain text content, removing any HTML.""" - - email_address = get_secret("gmail_email") - password = get_secret("gmail_password") - server = get_secret("gmail_stmp_server", "smtp.gmail.com") - port = get_secret("gmail_smtp_port", 587) - - # Connect to the Gmail IMAP server - mail = imaplib.IMAP4_SSL(server) - mail.login(email_address, password) - mail.select("inbox") # connect to inbox. - - result, data = mail.search(None, "ALL") - email_ids = data[0].split() - email_ids.reverse() # Reverse to get the most recent emails first - - emails = [] - - for email_id in email_ids[:n_emails]: - try: - result, data = mail.fetch(email_id, "(RFC822)") - raw_email = data[0][1] - msg = email.message_from_bytes(raw_email) - - email_details = { - "from": msg["From"], - "to": msg["To"], - "date": msg["Date"] - } - - if msg.is_multipart(): - for part in msg.walk(): - if part.get_content_type() == "text/plain": - body = part.get_payload(decode=True).decode('utf-8') - email_details["body"] = clean_email_body(body) - else: - body = msg.get_payload(decode=True).decode('utf-8') - email_details["body"] = clean_email_body(body) - except Exception as e: - log(f"Error reading email {email_id}: {e}", "ERROR") - continue - - emails.append(email_details) - - mail.close() - mail.logout() - data = "\n".join([f"{email['from']} - {email['date']}\n{email['body']}\n" for email in emails]) - return data - - - -def clean_email_body(body: str) -> str: - """Remove HTML tags and non-sentence elements from email body text.""" - - - # Remove HTML tags using BeautifulSoup - soup = BeautifulSoup(body, "html.parser") - text = soup.get_text(separator=' ') - - # Remove any non-sentence elements (e.g., URLs, email addresses, etc.) - text = re.sub(r'\S*@\S*\s?', '', text) # Remove emails - text = re.sub(r'http\S+', '', text) # Remove URLs - text = re.sub(r'[^.!?a-zA-Z0-9\s]', '', text) # Remove non-sentence characters - text = ' '.join(text.split()) # Remove extra whitespace - - return text \ No newline at end of file diff --git a/examples/tools/gmail_oauth.py b/examples/tools/gmail_oauth.py deleted file mode 100644 index 8af006b4..00000000 --- a/examples/tools/gmail_oauth.py +++ /dev/null @@ -1,119 +0,0 @@ -import re -import email -import smtplib -import imaplib -import pandas as pd -import plotly.express as px - -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText -from email.header import decode_header - -from pydantic import BaseModel -from bs4 import BeautifulSoup - -from toolserve.sdk import Param, tool, get_secret -from toolserve.sdk.client import log - - -@tool -async def send_email( - sender_email: Param(str, "Email address of the sender"), - recipient_email: Param(str, "Email address of the recipient"), - subject: Param(str, "Subject of the email"), - body: Param(str, "Body of the email"), - ): - """Send an email via gmail SMTP server""" - - email_address = get_secret("gmail_email") - sender_password = get_secret("gmail_password") - server = get_secret("gmail_stmp_server", "smtp.gmail.com") - port = get_secret("gmail_smtp_port", 587) - - message = MIMEMultipart() - message['From'] = sender_email - message['To'] = recipient_email - message['Subject'] = subject - message.attach(MIMEText(body, 'plain')) - - server = smtplib.SMTP(server, port) - server.starttls() - server.login(sender_email, sender_password) - log(f"Logged in to SMTP server at {':'.join((server, port))}", "DEBUG") - - server.send_message(message) - server.quit() - - log(f"Email sent from {sender_email} to {recipient_email}", "INFO") - - -@tool -async def read_email( - n_emails: Param(int, "Number of emails to read") = 5, - ) -> Param(str, "emails"): - """Read emails from a Gmail account and extract plain text content, removing any HTML.""" - - email_address = get_secret("gmail_email") - password = get_secret("gmail_password") - server = get_secret("gmail_stmp_server", "smtp.gmail.com") - port = get_secret("gmail_smtp_port", 587) - - # Connect to the Gmail IMAP server - mail = imaplib.IMAP4_SSL(server) - mail.login(email_address, password) - mail.select("inbox") # connect to inbox. - - result, data = mail.search(None, "ALL") - email_ids = data[0].split() - email_ids.reverse() # Reverse to get the most recent emails first - - emails = [] - - for email_id in email_ids[:n_emails]: - try: - result, data = mail.fetch(email_id, "(RFC822)") - raw_email = data[0][1] - msg = email.message_from_bytes(raw_email) - - email_details = { - "from": msg["From"], - "to": msg["To"], - "date": msg["Date"] - } - - if msg.is_multipart(): - for part in msg.walk(): - if part.get_content_type() == "text/plain": - body = part.get_payload(decode=True).decode('utf-8') - email_details["body"] = clean_email_body(body) - else: - body = msg.get_payload(decode=True).decode('utf-8') - email_details["body"] = clean_email_body(body) - except Exception as e: - log(f"Error reading email {email_id}: {e}", "ERROR") - continue - - emails.append(email_details) - - mail.close() - mail.logout() - data = "\n".join([f"{email['from']} - {email['date']}\n{email['body']}\n" for email in emails]) - return data - - - -def clean_email_body(body: str) -> str: - """Remove HTML tags and non-sentence elements from email body text.""" - - - # Remove HTML tags using BeautifulSoup - soup = BeautifulSoup(body, "html.parser") - text = soup.get_text(separator=' ') - - # Remove any non-sentence elements (e.g., URLs, email addresses, etc.) - text = re.sub(r'\S*@\S*\s?', '', text) # Remove emails - text = re.sub(r'http\S+', '', text) # Remove URLs - text = re.sub(r'[^.!?a-zA-Z0-9\s]', '', text) # Remove non-sentence characters - text = ' '.join(text.split()) # Remove extra whitespace - - return text \ No newline at end of file diff --git a/examples/tools/llm.py b/examples/tools/llm.py deleted file mode 100644 index f9599959..00000000 --- a/examples/tools/llm.py +++ /dev/null @@ -1,83 +0,0 @@ - - -from typing import ( - IO, - Union, - List, - Dict, - Optional, - Any, - Type, -) -import io -import requests -from os import PathLike -import base64 - -from toolserve.sdk import Param, tool, get_secret -from typing import List -import pandas as pd -import openai - - - -@tool -async def summarize( - text: Param(str, "Text to summarize"), - system_prompt: Param(str, "System prompt to use") = "Summarize the following text", - max_tokens: Param(int, "Maximum number of tokens to generate") = 1000, - ) -> Param(str, "Summarized text"): - """Summarize a piece of text using OpenAI Language models.""" - - api_key = get_secret("openai_api_key", None) - model = get_secret("openai_model_summarize", "gpt-4-turbo") - # Call the OpenAI model with the tools and messages - - if isinstance(text, list): - text = "\n".join(text) - - messages = [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": text}, - ] - - client = openai.AsyncClient(api_key=api_key) - completion = await openai.chat.completions.create( - model=model, - messages=messages, - ) - summary = completion.choices[0].message.content - return summary - - - -@tool -async def respond( - context: Param(str, "context of the conversation"), - system_prompt: Param(str, "System prompt to use") = "Given the following context, respond with a message in a friendly and helpful manner. Be informal and use a casual tone.", - max_tokens: Param(int, "Maximum number of tokens to generate") = 1000, - ) -> Param(str, "The response to the context provided"): - """Respond to a user given context using OpenAI Language models""" - - api_key = get_secret("openai_api_key", None) - model = get_secret("openai_model_summarize", "gpt-4-turbo") - # Call the OpenAI model with the tools and messages - - if isinstance(context, list): - context = "\n".join(context) - - messages = [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": context}, - ] - - client = openai.AsyncClient(api_key=api_key) - completion = await openai.chat.completions.create( - model=model, - messages=messages, - ) - response = completion.choices[0].message.content - return response - - - diff --git a/examples/tools/read_sqlite.py b/examples/tools/read_sqlite.py deleted file mode 100644 index 1fdde656..00000000 --- a/examples/tools/read_sqlite.py +++ /dev/null @@ -1,43 +0,0 @@ - -from toolserve.sdk import Param, tool, get_secret -from toolserve.sdk.dataframe import save_df -import pandas as pd - -from sqlite3 import connect - -@tool -async def read_sqlite( - file_path: Param(str, "Path to the SQLite database file"), - table_name: Param(str, "Name of the table to read from"), - output_name: Param(str, "Name of the output data to save"), - ) -> Param(str, "Output data name"): - """Read data from a SQLite database table and save it as a DataFrame. - - Args: - file_path (str): Path to the SQLite database file. - table_name (str): Name of the table to read from. - output_name (str): Name of the output data to save. - - Returns: - str: Name of the output data. - """ - # Connect to the SQLite database - conn = connect(file_path) - cursor = conn.cursor() - - # Read the data from the table - query = f"SELECT * FROM {table_name}" - cursor.execute(query) - rows = cursor.fetchall() - - # Get the column names - cursor.execute(f"PRAGMA table_info({table_name})") - columns = [col[1] for col in cursor.fetchall()] - - # Create a DataFrame from the data - df = pd.DataFrame(rows, columns=columns) - - # Save the DataFrame - await save_df(df, output_name) - - return output_name \ No newline at end of file diff --git a/examples/tools/search.py b/examples/tools/search.py deleted file mode 100644 index 99f38499..00000000 --- a/examples/tools/search.py +++ /dev/null @@ -1,35 +0,0 @@ -import asyncio -from serpapi import GoogleSearch -from typing import List, Dict -import json -from toolserve.sdk import Param, tool, get_secret - -async def google_search( - query: Param(str, "search query for google"), - num_results: Param(int, "number of results") - ) -> Param(str, "Json blob of Search results"): - """ - Perform a Google search using SerpAPI and retrieve a specified number of results. - - Args: - query (str): The search query. - num_results (int): The number of search results to retrieve. - - Returns: - List[Dict[str, str]]: A list of dictionaries containing the link and text of each result. - """ - serpapi_key = get_secret("serp_api_key", None) - params = { - "engine": "google", - "q": query, - "num": num_results, - "api_key": serpapi_key - } - - search = GoogleSearch(params) - results = search.get_dict() - - json_results = json.dumps(results.get("organic_results"), indent=2) - - return json_results - diff --git a/examples/tools/sql.py b/examples/tools/sql.py deleted file mode 100644 index e69de29b..00000000 diff --git a/schemas/preview/tool_definition.schema.jsonc b/schemas/preview/tool_definition.schema.jsonc new file mode 100644 index 00000000..5e6343c6 --- /dev/null +++ b/schemas/preview/tool_definition.schema.jsonc @@ -0,0 +1,198 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "primitives": { + // All supported primitive data types + "type": "string", + "enum": [ + "string", + "integer", + "float", + "boolean", + "json" + ] + }, + "value_schema": { + // Represents a value schema (e.g. function input parameter) + "type": "object", + "properties": { + "type": { + "$ref": "#/$defs/primitives" + } + }, + "required": [ + "type" + ], + "additionalProperties": false, + "if": { + "properties": { + "type": { + "const": "string" + } + } + }, + "then": { + // String values can optionally be constrained to a known list + "properties": { + "enum": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "type": "object", + "properties": { + "$schema": { + // Explicitly allow JSON-Schema to be referenced (due to additionalProperties: false) + "type": "string", + "format": "uri" + }, + "name": { + "type": "string", + "description": "The tool name" + }, + "description": { + "type": "string", + "description": "A human-readable description of the tool and when to use it" + }, + "version": { + "type": "string", + "pattern": "^[a-f0-9]{7,40}$", // SHA version pattern (7-40 hexadecimal characters) + "description": "An identifier for this version of the tool" + }, + "input": { + "type": "object", + "properties": { + "parameters": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "name": { + "description": "The human-readable name of this parameter.", + "type": "string" + }, + "required": { + "description": "Whether this parameter is required (true) or optional (false).", + "type": "boolean" + }, + "description": { + "description": "A descriptive, human-readable explanation of the parameter.", + "type": "string" + }, + "schema": { + "$ref": "#/$defs/value_schema" + }, + "inferrable": { + "type": "boolean", + "description": "Whether a value for this parameter can be inferred by a model. Defaults to `true`.", + "default": true + } + }, + "required": [ + "name", + "required", + "schema" + ], + "additionalProperties": false + } + } + }, + "required": [ + "parameters" + ], + "additionalProperties": false + }, + "output": { + "type": "object", + "properties": { + "available_modes": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "enum": [ + "value", + "error", + "null", + "artifact", + "requires_authorization" + ] + } + }, + "value": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "schema": { + "$ref": "#/$defs/value_schema" + } + }, + "required": [ + "schema" + ], + "additionalProperties": false + } + }, + "required": [ + "available_modes" + ], + "additionalProperties": false + }, + "requirements": { + "type": "object", + "properties": { + "authorization": { + "oneOf": [ + { + "type": "string", + "enum": [ + "none", + "token" + ] + }, + { + "type": "object", + "properties": { + "oauth2": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri" + }, + "scope": { + "type": "string" + } + }, + "required": [ + "url" + ], + "additionalProperties": false + } + }, + "required": [ + "oauth2" + ], + "additionalProperties": false + } + ] + } + } + } + }, + "required": [ + "name", + "version", + "input", + "output" + ], + "additionalProperties": false +} diff --git a/schemas/preview/tool_request.schema.jsonc b/schemas/preview/tool_request.schema.jsonc new file mode 100644 index 00000000..5c60550c --- /dev/null +++ b/schemas/preview/tool_request.schema.jsonc @@ -0,0 +1,80 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "$schema": { + // Explicitly allow JSON-Schema to be referenced (needed due to additionalProperties: false) + "type": "string", + "format": "uri" + }, + "run_id": { + "type": "string", + "description": "ID of the overall run" + }, + "invocation_id": { + "type": "string", + "description": "ID of this specific tool call" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "tool": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the tool to invoke" + }, + "version": { + "type": "string", + "description": "Version of the named tool to invoke" + } + }, + "required": ["name", "version"], + "additionalProperties": false + }, + "input": { + "type": "object", + "additionalProperties": true + }, + "context": { + "type": "object", + "properties": { + "authorization": { + "type": "object", + "properties": { + "token": { + "type": "string" + }, + "additionalProperties": false + } + }, + "user": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "A unique ID that identifies the user" + }, + "name": { + "type": "string", + "description": "The name of the user" + } + }, + "required": ["id"], + "additionalProperties": false + } + } + } + }, + "required": [ + "run_id", + "invocation_id", + "created_at", + "tool", + "input", + "context" + ], + "additionalProperties": false +} diff --git a/schemas/preview/tool_response.schema.jsonc b/schemas/preview/tool_response.schema.jsonc new file mode 100644 index 00000000..6dc44dd4 --- /dev/null +++ b/schemas/preview/tool_response.schema.jsonc @@ -0,0 +1,135 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "$schema": { + // Explicitly allow JSON-Schema to be referenced (needed due to additionalProperties: false) + "type": "string", + "format": "uri" + }, + "invocation_id": { + "type": "string", + "description": "ID of this specific tool call" + }, + "finished_at": { + "type": "string", + "format": "date-time" + }, + "success": { + "type": "boolean", + "description": "Whether the tool call was successful" + }, + "output": { + // Can be null/omitted, in the case of a null-returning (void) function + "type": "object", + "oneOf": [ + { + "properties": { + "value": { + "description": "The value returned from the function", + "oneOf": [ + { "type": "object", "additionalProperties": true }, // aka JSON + { "type": "number" }, + { "type": "string" }, + { "type": "boolean" } + ] + } + }, + "required": ["value"], + "additionalProperties": false + }, + { + "properties": { + "error": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "An error message that can be shown to the user or the AI model" + }, + "developer_message": { + "type": "string", + "description": "An internal message that will be logged but will not be shown to the user or the AI model" + } + }, + "required": ["message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false + }, + { + "properties": { + "requires_authorization": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "A message that can be shown to the user or AI model that explains the authorization requirement" + }, + "oauth2": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri" + }, + "scope": { + "type": "string" + } + }, + "required": ["url"], + "additionalProperties": false + } + }, + "required": ["message"], + "additionalProperties": false + } + }, + "required": ["requires_authorization"], + "additionalProperties": false + }, + { + "properties": { + "artifact": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "The location of the stored artifact" + }, + "content_type": { + "type": "string", + "description": "The MIME Media Type of the data inside the artifact (e.g. text/csv or application/json)" + }, + "size": { + "type": "integer", + "description": "The size of the artifact, in bytes" + }, + "meta": { + "type": "object", + "properties": { + "description": { + "type": "string", + "description": "A descriptive, human-readable explanation of the data inside the artifact" + } + }, + "required": ["description"], + "additionalProperties": false + } + }, + "required": ["url", "content_type", "size", "meta"], + "additionalProperties": false + } + }, + "required": ["artifact"], + "additionalProperties": false + } + ] + } + }, + "required": ["invocation_id", "finished_at", "success"], + "additionalProperties": false +} diff --git a/toolserve/poetry.lock b/toolserve/poetry.lock deleted file mode 100644 index cfdbf97e..00000000 --- a/toolserve/poetry.lock +++ /dev/null @@ -1,663 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. - -[[package]] -name = "annotated-types" -version = "0.6.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -files = [ - {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, - {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, -] - -[[package]] -name = "anyio" -version = "4.3.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -optional = false -python-versions = ">=3.8" -files = [ - {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, - {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, -] - -[package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} -idna = ">=2.8" -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)"] - -[[package]] -name = "async-timeout" -version = "4.0.3" -description = "Timeout context manager for asyncio programs" -optional = false -python-versions = ">=3.7" -files = [ - {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, - {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, -] - -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "dnspython" -version = "2.6.1" -description = "DNS toolkit" -optional = false -python-versions = ">=3.8" -files = [ - {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, - {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, -] - -[package.extras] -dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] -dnssec = ["cryptography (>=41)"] -doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] -doq = ["aioquic (>=0.9.25)"] -idna = ["idna (>=3.6)"] -trio = ["trio (>=0.23)"] -wmi = ["wmi (>=1.5.1)"] - -[[package]] -name = "email-validator" -version = "2.1.1" -description = "A robust email address syntax and deliverability validation library." -optional = false -python-versions = ">=3.8" -files = [ - {file = "email_validator-2.1.1-py3-none-any.whl", hash = "sha256:97d882d174e2a65732fb43bfce81a3a834cbc1bde8bf419e30ef5ea976370a05"}, - {file = "email_validator-2.1.1.tar.gz", hash = "sha256:200a70680ba08904be6d1eef729205cc0d687634399a5924d842533efb824b84"}, -] - -[package.dependencies] -dnspython = ">=2.0.0" -idna = ">=2.0.0" - -[[package]] -name = "exceptiongroup" -version = "1.2.0" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, - {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, -] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "fastapi" -version = "0.110.1" -description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -optional = false -python-versions = ">=3.8" -files = [ - {file = "fastapi-0.110.1-py3-none-any.whl", hash = "sha256:5df913203c482f820d31f48e635e022f8cbfe7350e4830ef05a3163925b1addc"}, - {file = "fastapi-0.110.1.tar.gz", hash = "sha256:6feac43ec359dfe4f45b2c18ec8c94edb8dc2dfc461d417d9e626590c071baad"}, -] - -[package.dependencies] -pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.37.2,<0.38.0" -typing-extensions = ">=4.8.0" - -[package.extras] -all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] - -[[package]] -name = "h11" -version = "0.14.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.7" -files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] - -[[package]] -name = "idna" -version = "3.7" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.5" -files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, -] - -[[package]] -name = "loguru" -version = "0.7.2" -description = "Python logging made (stupidly) simple" -optional = false -python-versions = ">=3.5" -files = [ - {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, - {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, -] - -[package.dependencies] -colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} -win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} - -[package.extras] -dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.8" -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "msgpack" -version = "1.0.8" -description = "MessagePack serializer" -optional = false -python-versions = ">=3.8" -files = [ - {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868"}, - {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c"}, - {file = "msgpack-1.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659"}, - {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2"}, - {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982"}, - {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa"}, - {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128"}, - {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d"}, - {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653"}, - {file = "msgpack-1.0.8-cp310-cp310-win32.whl", hash = "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693"}, - {file = "msgpack-1.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a"}, - {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836"}, - {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad"}, - {file = "msgpack-1.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce"}, - {file = "msgpack-1.0.8-cp311-cp311-win32.whl", hash = "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305"}, - {file = "msgpack-1.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543"}, - {file = "msgpack-1.0.8-cp312-cp312-win32.whl", hash = "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c"}, - {file = "msgpack-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd"}, - {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40"}, - {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151"}, - {file = "msgpack-1.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24"}, - {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d"}, - {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db"}, - {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77"}, - {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13"}, - {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2"}, - {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a"}, - {file = "msgpack-1.0.8-cp38-cp38-win32.whl", hash = "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c"}, - {file = "msgpack-1.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480"}, - {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a"}, - {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596"}, - {file = "msgpack-1.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d"}, - {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f"}, - {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228"}, - {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18"}, - {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8"}, - {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746"}, - {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, - {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, - {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, - {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, -] - -[[package]] -name = "msgspec" -version = "0.18.6" -description = "A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML." -optional = false -python-versions = ">=3.8" -files = [ - {file = "msgspec-0.18.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77f30b0234eceeff0f651119b9821ce80949b4d667ad38f3bfed0d0ebf9d6d8f"}, - {file = "msgspec-0.18.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a76b60e501b3932782a9da039bd1cd552b7d8dec54ce38332b87136c64852dd"}, - {file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06acbd6edf175bee0e36295d6b0302c6de3aaf61246b46f9549ca0041a9d7177"}, - {file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40a4df891676d9c28a67c2cc39947c33de516335680d1316a89e8f7218660410"}, - {file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a6896f4cd5b4b7d688018805520769a8446df911eb93b421c6c68155cdf9dd5a"}, - {file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3ac4dd63fd5309dd42a8c8c36c1563531069152be7819518be0a9d03be9788e4"}, - {file = "msgspec-0.18.6-cp310-cp310-win_amd64.whl", hash = "sha256:fda4c357145cf0b760000c4ad597e19b53adf01382b711f281720a10a0fe72b7"}, - {file = "msgspec-0.18.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e77e56ffe2701e83a96e35770c6adb655ffc074d530018d1b584a8e635b4f36f"}, - {file = "msgspec-0.18.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5351afb216b743df4b6b147691523697ff3a2fc5f3d54f771e91219f5c23aaa"}, - {file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3232fabacef86fe8323cecbe99abbc5c02f7698e3f5f2e248e3480b66a3596b"}, - {file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b524df6ea9998bbc99ea6ee4d0276a101bcc1aa8d14887bb823914d9f60d07"}, - {file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:37f67c1d81272131895bb20d388dd8d341390acd0e192a55ab02d4d6468b434c"}, - {file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d0feb7a03d971c1c0353de1a8fe30bb6579c2dc5ccf29b5f7c7ab01172010492"}, - {file = "msgspec-0.18.6-cp311-cp311-win_amd64.whl", hash = "sha256:41cf758d3f40428c235c0f27bc6f322d43063bc32da7b9643e3f805c21ed57b4"}, - {file = "msgspec-0.18.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d86f5071fe33e19500920333c11e2267a31942d18fed4d9de5bc2fbab267d28c"}, - {file = "msgspec-0.18.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce13981bfa06f5eb126a3a5a38b1976bddb49a36e4f46d8e6edecf33ccf11df1"}, - {file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97dec6932ad5e3ee1e3c14718638ba333befc45e0661caa57033cd4cc489466"}, - {file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad237100393f637b297926cae1868b0d500f764ccd2f0623a380e2bcfb2809ca"}, - {file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db1d8626748fa5d29bbd15da58b2d73af25b10aa98abf85aab8028119188ed57"}, - {file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d70cb3d00d9f4de14d0b31d38dfe60c88ae16f3182988246a9861259c6722af6"}, - {file = "msgspec-0.18.6-cp312-cp312-win_amd64.whl", hash = "sha256:1003c20bfe9c6114cc16ea5db9c5466e49fae3d7f5e2e59cb70693190ad34da0"}, - {file = "msgspec-0.18.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f7d9faed6dfff654a9ca7d9b0068456517f63dbc3aa704a527f493b9200b210a"}, - {file = "msgspec-0.18.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9da21f804c1a1471f26d32b5d9bc0480450ea77fbb8d9db431463ab64aaac2cf"}, - {file = "msgspec-0.18.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46eb2f6b22b0e61c137e65795b97dc515860bf6ec761d8fb65fdb62aa094ba61"}, - {file = "msgspec-0.18.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8355b55c80ac3e04885d72db515817d9fbb0def3bab936bba104e99ad22cf46"}, - {file = "msgspec-0.18.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9080eb12b8f59e177bd1eb5c21e24dd2ba2fa88a1dbc9a98e05ad7779b54c681"}, - {file = "msgspec-0.18.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cc001cf39becf8d2dcd3f413a4797c55009b3a3cdbf78a8bf5a7ca8fdb76032c"}, - {file = "msgspec-0.18.6-cp38-cp38-win_amd64.whl", hash = "sha256:fac5834e14ac4da1fca373753e0c4ec9c8069d1fe5f534fa5208453b6065d5be"}, - {file = "msgspec-0.18.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:974d3520fcc6b824a6dedbdf2b411df31a73e6e7414301abac62e6b8d03791b4"}, - {file = "msgspec-0.18.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fd62e5818731a66aaa8e9b0a1e5543dc979a46278da01e85c3c9a1a4f047ef7e"}, - {file = "msgspec-0.18.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7481355a1adcf1f08dedd9311193c674ffb8bf7b79314b4314752b89a2cf7f1c"}, - {file = "msgspec-0.18.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6aa85198f8f154cf35d6f979998f6dadd3dc46a8a8c714632f53f5d65b315c07"}, - {file = "msgspec-0.18.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e24539b25c85c8f0597274f11061c102ad6b0c56af053373ba4629772b407be"}, - {file = "msgspec-0.18.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c61ee4d3be03ea9cd089f7c8e36158786cd06e51fbb62529276452bbf2d52ece"}, - {file = "msgspec-0.18.6-cp39-cp39-win_amd64.whl", hash = "sha256:b5c390b0b0b7da879520d4ae26044d74aeee5144f83087eb7842ba59c02bc090"}, - {file = "msgspec-0.18.6.tar.gz", hash = "sha256:a59fc3b4fcdb972d09138cb516dbde600c99d07c38fd9372a6ef500d2d031b4e"}, -] - -[package.extras] -dev = ["attrs", "coverage", "furo", "gcovr", "ipython", "msgpack", "mypy", "pre-commit", "pyright", "pytest", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "tomli", "tomli-w"] -doc = ["furo", "ipython", "sphinx", "sphinx-copybutton", "sphinx-design"] -test = ["attrs", "msgpack", "mypy", "pyright", "pytest", "pyyaml", "tomli", "tomli-w"] -toml = ["tomli", "tomli-w"] -yaml = ["pyyaml"] - -[[package]] -name = "pydantic" -version = "2.7.0" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic-2.7.0-py3-none-any.whl", hash = "sha256:9dee74a271705f14f9a1567671d144a851c675b072736f0a7b2608fd9e495352"}, - {file = "pydantic-2.7.0.tar.gz", hash = "sha256:b5ecdd42262ca2462e2624793551e80911a1e989f462910bb81aef974b4bb383"}, -] - -[package.dependencies] -annotated-types = ">=0.4.0" -email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""} -pydantic-core = "2.18.1" -typing-extensions = ">=4.6.1" - -[package.extras] -email = ["email-validator (>=2.0.0)"] - -[[package]] -name = "pydantic-core" -version = "2.18.1" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic_core-2.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ee9cf33e7fe14243f5ca6977658eb7d1042caaa66847daacbd2117adb258b226"}, - {file = "pydantic_core-2.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b7bbb97d82659ac8b37450c60ff2e9f97e4eb0f8a8a3645a5568b9334b08b50"}, - {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df4249b579e75094f7e9bb4bd28231acf55e308bf686b952f43100a5a0be394c"}, - {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d0491006a6ad20507aec2be72e7831a42efc93193d2402018007ff827dc62926"}, - {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ae80f72bb7a3e397ab37b53a2b49c62cc5496412e71bc4f1277620a7ce3f52b"}, - {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58aca931bef83217fca7a390e0486ae327c4af9c3e941adb75f8772f8eeb03a1"}, - {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1be91ad664fc9245404a789d60cba1e91c26b1454ba136d2a1bf0c2ac0c0505a"}, - {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:667880321e916a8920ef49f5d50e7983792cf59f3b6079f3c9dac2b88a311d17"}, - {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f7054fdc556f5421f01e39cbb767d5ec5c1139ea98c3e5b350e02e62201740c7"}, - {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:030e4f9516f9947f38179249778709a460a3adb516bf39b5eb9066fcfe43d0e6"}, - {file = "pydantic_core-2.18.1-cp310-none-win32.whl", hash = "sha256:2e91711e36e229978d92642bfc3546333a9127ecebb3f2761372e096395fc649"}, - {file = "pydantic_core-2.18.1-cp310-none-win_amd64.whl", hash = "sha256:9a29726f91c6cb390b3c2338f0df5cd3e216ad7a938762d11c994bb37552edb0"}, - {file = "pydantic_core-2.18.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9ece8a49696669d483d206b4474c367852c44815fca23ac4e48b72b339807f80"}, - {file = "pydantic_core-2.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a5d83efc109ceddb99abd2c1316298ced2adb4570410defe766851a804fcd5b"}, - {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7973c381283783cd1043a8c8f61ea5ce7a3a58b0369f0ee0ee975eaf2f2a1b"}, - {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54c7375c62190a7845091f521add19b0f026bcf6ae674bdb89f296972272e86d"}, - {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd63cec4e26e790b70544ae5cc48d11b515b09e05fdd5eff12e3195f54b8a586"}, - {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:561cf62c8a3498406495cfc49eee086ed2bb186d08bcc65812b75fda42c38294"}, - {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68717c38a68e37af87c4da20e08f3e27d7e4212e99e96c3d875fbf3f4812abfc"}, - {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d5728e93d28a3c63ee513d9ffbac9c5989de8c76e049dbcb5bfe4b923a9739d"}, - {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f0f17814c505f07806e22b28856c59ac80cee7dd0fbb152aed273e116378f519"}, - {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d816f44a51ba5175394bc6c7879ca0bd2be560b2c9e9f3411ef3a4cbe644c2e9"}, - {file = "pydantic_core-2.18.1-cp311-none-win32.whl", hash = "sha256:09f03dfc0ef8c22622eaa8608caa4a1e189cfb83ce847045eca34f690895eccb"}, - {file = "pydantic_core-2.18.1-cp311-none-win_amd64.whl", hash = "sha256:27f1009dc292f3b7ca77feb3571c537276b9aad5dd4efb471ac88a8bd09024e9"}, - {file = "pydantic_core-2.18.1-cp311-none-win_arm64.whl", hash = "sha256:48dd883db92e92519201f2b01cafa881e5f7125666141a49ffba8b9facc072b0"}, - {file = "pydantic_core-2.18.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b6b0e4912030c6f28bcb72b9ebe4989d6dc2eebcd2a9cdc35fefc38052dd4fe8"}, - {file = "pydantic_core-2.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3202a429fe825b699c57892d4371c74cc3456d8d71b7f35d6028c96dfecad31"}, - {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3982b0a32d0a88b3907e4b0dc36809fda477f0757c59a505d4e9b455f384b8b"}, - {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25595ac311f20e5324d1941909b0d12933f1fd2171075fcff763e90f43e92a0d"}, - {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14fe73881cf8e4cbdaded8ca0aa671635b597e42447fec7060d0868b52d074e6"}, - {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca976884ce34070799e4dfc6fbd68cb1d181db1eefe4a3a94798ddfb34b8867f"}, - {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684d840d2c9ec5de9cb397fcb3f36d5ebb6fa0d94734f9886032dd796c1ead06"}, - {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:54764c083bbe0264f0f746cefcded6cb08fbbaaf1ad1d78fb8a4c30cff999a90"}, - {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:201713f2f462e5c015b343e86e68bd8a530a4f76609b33d8f0ec65d2b921712a"}, - {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd1a9edb9dd9d79fbeac1ea1f9a8dd527a6113b18d2e9bcc0d541d308dae639b"}, - {file = "pydantic_core-2.18.1-cp312-none-win32.whl", hash = "sha256:d5e6b7155b8197b329dc787356cfd2684c9d6a6b1a197f6bbf45f5555a98d411"}, - {file = "pydantic_core-2.18.1-cp312-none-win_amd64.whl", hash = "sha256:9376d83d686ec62e8b19c0ac3bf8d28d8a5981d0df290196fb6ef24d8a26f0d6"}, - {file = "pydantic_core-2.18.1-cp312-none-win_arm64.whl", hash = "sha256:c562b49c96906b4029b5685075fe1ebd3b5cc2601dfa0b9e16c2c09d6cbce048"}, - {file = "pydantic_core-2.18.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:3e352f0191d99fe617371096845070dee295444979efb8f27ad941227de6ad09"}, - {file = "pydantic_core-2.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0295d52b012cbe0d3059b1dba99159c3be55e632aae1999ab74ae2bd86a33d7"}, - {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56823a92075780582d1ffd4489a2e61d56fd3ebb4b40b713d63f96dd92d28144"}, - {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd3f79e17b56741b5177bcc36307750d50ea0698df6aa82f69c7db32d968c1c2"}, - {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38a5024de321d672a132b1834a66eeb7931959c59964b777e8f32dbe9523f6b1"}, - {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ce426ee691319d4767748c8e0895cfc56593d725594e415f274059bcf3cb76"}, - {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2adaeea59849ec0939af5c5d476935f2bab4b7f0335b0110f0f069a41024278e"}, - {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9b6431559676a1079eac0f52d6d0721fb8e3c5ba43c37bc537c8c83724031feb"}, - {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:85233abb44bc18d16e72dc05bf13848a36f363f83757541f1a97db2f8d58cfd9"}, - {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:641a018af4fe48be57a2b3d7a1f0f5dbca07c1d00951d3d7463f0ac9dac66622"}, - {file = "pydantic_core-2.18.1-cp38-none-win32.whl", hash = "sha256:63d7523cd95d2fde0d28dc42968ac731b5bb1e516cc56b93a50ab293f4daeaad"}, - {file = "pydantic_core-2.18.1-cp38-none-win_amd64.whl", hash = "sha256:907a4d7720abfcb1c81619863efd47c8a85d26a257a2dbebdb87c3b847df0278"}, - {file = "pydantic_core-2.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:aad17e462f42ddbef5984d70c40bfc4146c322a2da79715932cd8976317054de"}, - {file = "pydantic_core-2.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:94b9769ba435b598b547c762184bcfc4783d0d4c7771b04a3b45775c3589ca44"}, - {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80e0e57cc704a52fb1b48f16d5b2c8818da087dbee6f98d9bf19546930dc64b5"}, - {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76b86e24039c35280ceee6dce7e62945eb93a5175d43689ba98360ab31eebc4a"}, - {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a05db5013ec0ca4a32cc6433f53faa2a014ec364031408540ba858c2172bb0"}, - {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:250ae39445cb5475e483a36b1061af1bc233de3e9ad0f4f76a71b66231b07f88"}, - {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a32204489259786a923e02990249c65b0f17235073149d0033efcebe80095570"}, - {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6395a4435fa26519fd96fdccb77e9d00ddae9dd6c742309bd0b5610609ad7fb2"}, - {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2533ad2883f001efa72f3d0e733fb846710c3af6dcdd544fe5bf14fa5fe2d7db"}, - {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b560b72ed4816aee52783c66854d96157fd8175631f01ef58e894cc57c84f0f6"}, - {file = "pydantic_core-2.18.1-cp39-none-win32.whl", hash = "sha256:582cf2cead97c9e382a7f4d3b744cf0ef1a6e815e44d3aa81af3ad98762f5a9b"}, - {file = "pydantic_core-2.18.1-cp39-none-win_amd64.whl", hash = "sha256:ca71d501629d1fa50ea7fa3b08ba884fe10cefc559f5c6c8dfe9036c16e8ae89"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e178e5b66a06ec5bf51668ec0d4ac8cfb2bdcb553b2c207d58148340efd00143"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:72722ce529a76a4637a60be18bd789d8fb871e84472490ed7ddff62d5fed620d"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fe0c1ce5b129455e43f941f7a46f61f3d3861e571f2905d55cdbb8b5c6f5e2c"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4284c621f06a72ce2cb55f74ea3150113d926a6eb78ab38340c08f770eb9b4d"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0c3e718f4e064efde68092d9d974e39572c14e56726ecfaeebbe6544521f47"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2027493cc44c23b598cfaf200936110433d9caa84e2c6cf487a83999638a96ac"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:76909849d1a6bffa5a07742294f3fa1d357dc917cb1fe7b470afbc3a7579d539"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ee7ccc7fb7e921d767f853b47814c3048c7de536663e82fbc37f5eb0d532224b"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ee2794111c188548a4547eccc73a6a8527fe2af6cf25e1a4ebda2fd01cdd2e60"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a139fe9f298dc097349fb4f28c8b81cc7a202dbfba66af0e14be5cfca4ef7ce5"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d074b07a10c391fc5bbdcb37b2f16f20fcd9e51e10d01652ab298c0d07908ee2"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c69567ddbac186e8c0aadc1f324a60a564cfe25e43ef2ce81bcc4b8c3abffbae"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf1c7b78cddb5af00971ad5294a4583188bda1495b13760d9f03c9483bb6203"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2684a94fdfd1b146ff10689c6e4e815f6a01141781c493b97342cdc5b06f4d5d"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:73c1bc8a86a5c9e8721a088df234265317692d0b5cd9e86e975ce3bc3db62a59"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e60defc3c15defb70bb38dd605ff7e0fae5f6c9c7cbfe0ad7868582cb7e844a6"}, - {file = "pydantic_core-2.18.1.tar.gz", hash = "sha256:de9d3e8717560eb05e28739d1b35e4eac2e458553a52a301e51352a7ffc86a35"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" - -[[package]] -name = "pydantic-settings" -version = "2.2.1" -description = "Settings management using Pydantic" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic_settings-2.2.1-py3-none-any.whl", hash = "sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091"}, - {file = "pydantic_settings-2.2.1.tar.gz", hash = "sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed"}, -] - -[package.dependencies] -pydantic = ">=2.3.0" -python-dotenv = ">=0.21.0" - -[package.extras] -toml = ["tomli (>=2.0.1)"] -yaml = ["pyyaml (>=6.0.1)"] - -[[package]] -name = "pygments" -version = "2.17.2" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, - {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, -] - -[package.extras] -plugins = ["importlib-metadata"] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "python-dotenv" -version = "1.0.1" -description = "Read key-value pairs from a .env file and set them as environment variables" -optional = false -python-versions = ">=3.8" -files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, -] - -[package.extras] -cli = ["click (>=5.0)"] - -[[package]] -name = "redis" -version = "5.0.3" -description = "Python client for Redis database and key-value store" -optional = false -python-versions = ">=3.7" -files = [ - {file = "redis-5.0.3-py3-none-any.whl", hash = "sha256:5da9b8fe9e1254293756c16c008e8620b3d15fcc6dde6babde9541850e72a32d"}, - {file = "redis-5.0.3.tar.gz", hash = "sha256:4973bae7444c0fbed64a06b87446f79361cb7e4ec1538c022d696ed7a5015580"}, -] - -[package.dependencies] -async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} - -[package.extras] -hiredis = ["hiredis (>=1.0.0)"] -ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] - -[[package]] -name = "rich" -version = "13.7.1" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "starlette" -version = "0.37.2" -description = "The little ASGI library that shines." -optional = false -python-versions = ">=3.8" -files = [ - {file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"}, - {file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"}, -] - -[package.dependencies] -anyio = ">=3.4.0,<5" - -[package.extras] -full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] - -[[package]] -name = "stdlib-list" -version = "0.10.0" -description = "A list of Python Standard Libraries (2.7 through 3.12)." -optional = false -python-versions = ">=3.7" -files = [ - {file = "stdlib_list-0.10.0-py3-none-any.whl", hash = "sha256:b3a911bc441d03e0332dd1a9e7d0870ba3bb0a542a74d7524f54fb431256e214"}, - {file = "stdlib_list-0.10.0.tar.gz", hash = "sha256:6519c50d645513ed287657bfe856d527f277331540691ddeaf77b25459964a14"}, -] - -[package.extras] -dev = ["build", "stdlib-list[doc,lint,test]"] -doc = ["furo", "sphinx"] -lint = ["black", "mypy", "ruff"] -support = ["sphobjinv"] -test = ["coverage[toml]", "pytest", "pytest-cov"] - -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] - -[[package]] -name = "tomlkit" -version = "0.12.4" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"}, - {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, -] - -[[package]] -name = "typer" -version = "0.9.4" -description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -optional = false -python-versions = ">=3.6" -files = [ - {file = "typer-0.9.4-py3-none-any.whl", hash = "sha256:aa6c4a4e2329d868b80ecbaf16f807f2b54e192209d7ac9dd42691d63f7a54eb"}, - {file = "typer-0.9.4.tar.gz", hash = "sha256:f714c2d90afae3a7929fcd72a3abb08df305e1ff61719381384211c4070af57f"}, -] - -[package.dependencies] -click = ">=7.1.1,<9.0.0" -typing-extensions = ">=3.7.4.3" - -[package.extras] -all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] -dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] -doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] -test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.971)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] - -[[package]] -name = "typing-extensions" -version = "4.11.0" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, - {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, -] - -[[package]] -name = "uvicorn" -version = "0.28.1" -description = "The lightning-fast ASGI server." -optional = false -python-versions = ">=3.8" -files = [ - {file = "uvicorn-0.28.1-py3-none-any.whl", hash = "sha256:5162f6d652f545be91b1feeaee8180774af143965ca9dc8a47ff1dc6bafa4ad5"}, - {file = "uvicorn-0.28.1.tar.gz", hash = "sha256:08103e79d546b6cf20f67c7e5e434d2cf500a6e29b28773e407250c54fc4fa3c"}, -] - -[package.dependencies] -click = ">=7.0" -h11 = ">=0.8" -typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} - -[package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] - -[[package]] -name = "win32-setctime" -version = "1.1.0" -description = "A small Python utility to set file creation time on Windows" -optional = false -python-versions = ">=3.5" -files = [ - {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, - {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, -] - -[package.extras] -dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] - -[metadata] -lock-version = "2.0" -python-versions = "^3.10" -content-hash = "f034a070ec06e119c2f182dcfdcfeeb0c7c9bc4a5f9116e5c12f7f650f0678f0" diff --git a/toolserve/pyproject.toml b/toolserve/pyproject.toml deleted file mode 100644 index c335a95b..00000000 --- a/toolserve/pyproject.toml +++ /dev/null @@ -1,29 +0,0 @@ -[tool.poetry] -name = "toolserve" -version = "0.1.0" -description = "" -authors = ["Sam Partee "] - -[tool.poetry.dependencies] -python = "^3.10" -pydantic = {extras = ["email"], version = "^2.7.0"} -fastapi = "^0.110.0" -redis = "^5.0.3" -uvicorn = "^0.28.0" -loguru = "^0.7.2" -pydantic-settings = "^2.2.1" -msgspec = "^0.18.6" -msgpack = "^1.0.8" -typer = "^0.9.0" -rich = "^13.7.1" -toml = "^0.10.2" -tomlkit = "^0.12.4" -stdlib-list = "^0.10.0" - - -[tool.poetry.scripts] -tool = "toolserve.cli.main:cli" - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" diff --git a/toolserve/toolserve/sdk/__init__.py b/toolserve/toolserve/sdk/__init__.py deleted file mode 100644 index 9bbac016..00000000 --- a/toolserve/toolserve/sdk/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ - -from .tool import ( - Param, - tool, - get_secret -) \ No newline at end of file diff --git a/toolserve/toolserve/sdk/client.py b/toolserve/toolserve/sdk/client.py deleted file mode 100644 index e87f19ec..00000000 --- a/toolserve/toolserve/sdk/client.py +++ /dev/null @@ -1,201 +0,0 @@ - -import os -import json -import httpx - -from typing import Optional, Dict, Any, List -from contextlib import asynccontextmanager -from enum import Enum - - -class HttpClient: - """ - A simple HTTP client class to handle requests to a specified base URL with optional authentication. - """ - def __init__(self, base_url: str, auth_token: Optional[str] = None): - """ - Initializes the HttpClient with a base URL and an optional authentication token. - - Args: - base_url (str): The base URL for the HTTP requests. - auth_token (Optional[str]): Optional bearer token for authorization. - """ - self.base_url = base_url - self.auth_token = auth_token - self.client = httpx.AsyncClient() - - async def __aenter__(self): - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - await self.client.aclose() - - async def post(self, endpoint: str, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> Any: - """ - Sends a POST request to the specified endpoint with the provided data and files. - - Args: - endpoint (str): The endpoint to send the POST request to. - data (Dict[str, Any]): The data to send in the POST request. - files (Optional[Dict[str, Any]]): Optional files to send with the request. - - Returns: - Any: The JSON response from the server. - """ - headers = {"Authorization": f"Bearer {self.auth_token}"} if self.auth_token else {} - try: - if files: - response = await self.client.post(f"{self.base_url}{endpoint}", files=files, headers=headers) - else: - response = await self.client.post(f"{self.base_url}{endpoint}", json=data, headers=headers) - response.raise_for_status() - return response.json() - except httpx.HTTPStatusError as e: - raise RuntimeError(f"HTTP error occurred: {e.response.status_code}") - except httpx.RequestError as e: - raise RuntimeError(f"Request error occurred: {e}") - - async def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Any: - """ - Sends a GET request to the specified endpoint with optional parameters. - - Args: - endpoint (str): The endpoint to send the GET request to. - params (Optional[Dict[str, Any]]): Optional parameters to include in the request. - - Returns: - Any: The JSON response from the server. - """ - headers = {"Authorization": f"Bearer {self.auth_token}"} if self.auth_token else {} - try: - response = await self.client.get(f"{self.base_url}{endpoint}", params=params, headers=headers) - response.raise_for_status() - return response.json() - except httpx.HTTPStatusError as e: - raise RuntimeError(f"HTTP error occurred: {e.response.status_code}") - except httpx.RequestError as e: - raise RuntimeError(f"Request error occurred: {e}") - -@asynccontextmanager -async def managed_http_client(base_url: str, auth_token: Optional[str] = None): - """ - Context manager to handle the lifecycle of HttpClient instances. - - Args: - base_url (str): The base URL for the HTTP requests. - auth_token (Optional[str]): Optional bearer token for authorization. - """ - client = HttpClient(base_url, auth_token) - try: - yield client - finally: - await client.__aexit__(None, None, None) - -def get_base_url() -> str: - return os.getenv('TOOLSERVE_URL', 'http://localhost:8000') - - -# ----- SDK Functions ----- - - -class LogLevel(Enum): - DEBUG = "DEBUG" - INFO = "INFO" - WARNING = "WARNING" - ERROR = "ERROR" - CRITICAL = "CRITICAL" - -async def log(message: str = "", level: LogLevel = LogLevel.INFO, auth_token: Optional[str] = None, endpoint: str = "/api/v1/log"): - """ - Asynchronously sends a log message to a specified endpoint. - - This function constructs a log entry with a message and a log level, then sends it to the server using the provided endpoint. It uses an HTTP POST request within a managed HTTP client context. - - Args: - message (str): The log message to send. Defaults to an empty string. - level (LogLevel): The severity level of the log message. Defaults to LogLevel.INFO. - auth_token (Optional[str]): An optional authorization token for the request. - endpoint (str): The API endpoint to which the log message is sent. Defaults to "/api/v1/log". - - Returns: - Any: The response from the server as a result of the log message post request. - """ - base_url = get_base_url() - if isinstance(level, str): - level = LogLevel(level) - async with managed_http_client(base_url, auth_token) as client: - log_data = {"msg": message, "level": level.value} - return await client.post(endpoint, data=log_data) - - -async def list_data(auth_token: Optional[str] = None, endpoint: str = "/api/v1/data") -> List[Dict[str, Any]]: - """ - Retrieve a list of data objects from a specified endpoint. - - Args: - auth_token (Optional[str]): Optional authorization token. - endpoint (str): API endpoint to send the request to. Defaults to "/api/v1/data". - - Returns: - Dict[str, Any]: The deserialized JSON data retrieved from the server. - """ - base_url = get_base_url() - async with managed_http_client(base_url, auth_token) as client: - response = await client.get(endpoint) - return response["data"] - -async def get_data(data_id: int, auth_token: Optional[str] = None, endpoint: str = "/api/v1/data/object") -> Any: - """ - Retrieve data object by its primary key from a specified endpoint. - - Args: - data_id (int): The primary key of the data object to retrieve. - auth_token (Optional[str]): Optional authorization token. - endpoint (str): API endpoint to send the request to. Defaults to "/api/v1/data/object". - - Returns: - Any: The deserialized JSON data retrieved from the server. - """ - base_url = get_base_url() - endpoint = f"{endpoint}/{str(data_id)}" # Append the data ID to the endpoint URL - async with managed_http_client(base_url, auth_token) as client: - response = await client.get(endpoint) - json_blob = response["data"].get('json_blob', '{}') - return json.loads(json_blob) - - -async def send_data(name: str, data: Dict[str, Any], auth_token: Optional[str] = None, endpoint: str = "/api/v1/data") -> Dict[str, Any]: - """ - Send data to a specified endpoint, serializing the data into JSON under the key 'json_blob'. - - Args: - data (Dict[str, Any]): Data to be serialized and sent. - auth_token (Optional[str]): Optional authorization token. - endpoint (str): API endpoint to send the data to. - - Returns: - Dict[str, Any]: The response from the server after sending the data. - """ - base_url = get_base_url() - json_blob = json.dumps(data) - payload = {'file_name': name, 'json_blob': json_blob} - async with managed_http_client(base_url, auth_token) as client: - response = await client.post(endpoint, data=payload) - if response["code"] != 200: - raise RuntimeError(f"Failed to send data: {response['msg']}") - else: - return { - "id": response["data"]["id"], - "file_path": response["data"]["file_path"] - } - - -async def save_artifact_from_file(file_path: str = "", auth_token: Optional[str] = None, endpoint: str = "/api/v1/artifact"): - base_url = get_base_url() - async with managed_http_client(base_url, auth_token) as client: - with open(file_path, 'rb') as file: - files = {'file': file} - return await client.post(endpoint, data={}, files=files) - - - diff --git a/toolserve/toolserve/sdk/dataframe.py b/toolserve/toolserve/sdk/dataframe.py deleted file mode 100644 index 795be3be..00000000 --- a/toolserve/toolserve/sdk/dataframe.py +++ /dev/null @@ -1,39 +0,0 @@ - -try: - import pandas as pd -except ImportError: - raise ImportError("Pandas is required for this SDK component. Please install it using `pip install pandas`.") - -from typing import Any, Dict -from toolserve.sdk.client import get_data, send_data - - -async def save_df(df: pd.DataFrame, name: str) -> Dict[str, Any]: - """ - Asynchronously saves a DataFrame to the server by converting it to a dictionary and using the SDK's send_data function. - - Args: - df (pd.DataFrame): The DataFrame to save. - name (str): The name under which the DataFrame should be saved. - - Returns: - Dict[str, Any]: The server's response after saving the DataFrame. - """ - data_dict = df.to_dict(orient='records') - response = await send_data(name=name, data={"data": data_dict}) - return response - - -async def get_df(data_id: int) -> pd.DataFrame: - """ - Asynchronously retrieves a DataFrame from the server using its data ID. - - Args: - data_id (int): The unique identifier for the DataFrame to retrieve. - - Returns: - pd.DataFrame: The DataFrame retrieved from the server. - """ - response = await get_data(data_id=data_id) - df = pd.DataFrame(response['data']) - return df diff --git a/toolserve/toolserve/sdk/tool.py b/toolserve/toolserve/sdk/tool.py deleted file mode 100644 index 31831333..00000000 --- a/toolserve/toolserve/sdk/tool.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import Annotated, TypeVar, _AnnotatedAlias, Type, Callable, Any, Optional -import functools -import os -import asyncio - -T = TypeVar('T') - -class Description: - def __init__(self, description: str): - self.description = description - - def __str__(self): - return self.description - -def Param(type_: Type[T], description: str) -> Annotated[T, Description]: - return Annotated[type_, Description(description)] - -def tool(func: Callable) -> Callable: - @functools.wraps(func) - async def wrapper(*args, **kwargs) -> Any: - if asyncio.iscoroutinefunction(func): - return await func(*args, **kwargs) - else: - loop = asyncio.get_running_loop() - partial_func = functools.partial(func, *args, **kwargs) - return await loop.run_in_executor(None, partial_func) - return wrapper - -def get_secret(name: str, default: Optional[Any] = None) -> str: - secret = os.getenv(name) - if secret is None: - if default is not None: - return default - raise ValueError(f"Secret {name} is not set.") - return secret \ No newline at end of file diff --git a/toolserve/toolserve/server/common/exception/__init__.py b/toolserve/toolserve/server/common/exception/__init__.py deleted file mode 100644 index 56fafa58..00000000 --- a/toolserve/toolserve/server/common/exception/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- diff --git a/toolserve/toolserve/server/common/serializers.py b/toolserve/toolserve/server/common/serializers.py deleted file mode 100644 index 0f4b928c..00000000 --- a/toolserve/toolserve/server/common/serializers.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from decimal import Decimal -from typing import Any, Sequence, TypeVar - -import msgspec - -from asgiref.sync import sync_to_async -from sqlalchemy import Row, RowMapping -from starlette.responses import JSONResponse - -RowData = Row | RowMapping | Any - -R = TypeVar('R', bound=RowData) - - -@sync_to_async -def select_columns_serialize(row: R) -> dict: - """ - Serialize SQLAlchemy select table columns, does not contain relational columns - - :param row: - :return: - """ - obj_dict = {} - for column in row.__table__.columns.keys(): - val = getattr(row, column) - if isinstance(val, Decimal): - if val % 1 == 0: - val = int(val) - val = float(val) - obj_dict[column] = val - return obj_dict - - -async def select_list_serialize(row: Sequence[R]) -> list: - """ - Serialize SQLAlchemy select list - - :param row: - :return: - """ - ret_list = [await select_columns_serialize(_) for _ in row] - return ret_list - - -@sync_to_async -def select_as_dict(row: R) -> dict: - """ - Converting SQLAlchemy select to dict, which can contain relational data, - depends on the properties of the select object itself - - :param row: - :return: - """ - obj_dict = row.__dict__ - if '_sa_instance_state' in obj_dict: - del obj_dict['_sa_instance_state'] - return obj_dict - - -class MsgSpecJSONResponse(JSONResponse): - """ - JSON response using the high-performance msgspec library to serialize data to JSON. - """ - - def render(self, content: Any) -> bytes: - return msgspec.json.encode(content) diff --git a/toolserve/toolserve/server/core/catalog.py b/toolserve/toolserve/server/core/catalog.py deleted file mode 100644 index 17666665..00000000 --- a/toolserve/toolserve/server/core/catalog.py +++ /dev/null @@ -1,220 +0,0 @@ - - -import os -import sys -import inspect -from datetime import datetime -from typing import List, Optional, Type, Dict, Annotated, Any, Callable, Tuple -from pathlib import Path -import asyncio - -from fastapi import APIRouter -from pydantic import BaseModel, ValidationError, Field, create_model -from importlib import import_module - -from toolserve.server.core.conf import settings -from toolserve.server.common.response_code import CustomResponseCode -from toolserve.server.common.response import ResponseModel, response_base -from toolserve.apm.base import ToolPack -from toolserve.sdk import Param -from toolserve.utils import snake_to_camel - -class ToolMeta(BaseModel): - module: str - path: str - date_added: datetime = Field(default_factory=datetime.now) - date_updated: datetime = Field(default_factory=datetime.now) - - -class ToolSchema(BaseModel): - name: str - description: str - version: str - tool: Callable - - input_model: Type[BaseModel] - output_model: Type[BaseModel] - - meta: ToolMeta - - -class ToolCatalog: - def __init__(self, tools_dir: str = settings.TOOLS_DIR): - self.tools = self.read_tools(tools_dir) - #self.tools.update(self.__get_builitin_tools()) - - @staticmethod - def read_tools(directory: str) -> List[ToolSchema]: - toolpack = ToolPack.from_lock_file(directory) - sys.path.append(str(Path(directory).resolve() / 'tools')) - - tools = {} - for name, tool_spec in toolpack.tools.items(): - print(name, tool_spec) - module_name, versioned_tool = tool_spec.split('.', 1) - func_name, version = versioned_tool.split('@') - - module = import_module(module_name) - tool = getattr(module, func_name) - - tool_meta = ToolMeta( - module=module_name, - path=module.__file__ - ) - - input_model, output_model = create_func_models(tool) - response_model = create_response_model(name, output_model) - tool_schema = ToolSchema( - name=name, - description=tool.__doc__, - version=version, - tool=tool, - input_model=input_model, - output_model=response_model, - meta=tool_meta - ) - tools[name] = tool_schema - - return tools - - def __get_builitin_tools(self) -> Dict[str, ToolSchema]: - tools = {} - sys.path.append(str(settings.BUILTIN_TOOLS_DIR)) - - for tool_spec in settings.BUILTIN_TOOLS: - print(tool_spec) - - module_name, versioned_tool = tool_spec.split('.', 1) - func_name, version = versioned_tool.split('@') - - module = import_module(module_name) - tool = getattr(module, func_name) - - input_model, output_model = create_func_models(tool) - response_model = create_response_model(func_name, output_model) - tool_schema = ToolSchema( - name=func_name, - description=tool.__doc__, - version='builtin', - tool=tool, - input_model=input_model, - output_model=response_model, - meta=ToolMeta(module=module_name, path=module.__file__) - ) - tools[func_name] = tool_schema - - return tools - - - def __getitem__(self, name: str) -> Optional[ToolSchema]: - #TODO error handling - for tool_name, tool in self.tools.items(): - if tool_name == name: - return tool - return None - - def get_tool(self, name: str) -> Optional[Callable]: - for tool in self.tools: - if tool.name == name: - return tool.tool - return None - - def list_tools(self) -> List[Dict[str, str]]: - def get_tool_endpoint(t: ToolSchema) -> str: - return f"/tool/{t.meta.module}/{t.name}" - return [ - {'name': t.name, - 'description': t.description, - 'endpoint': get_tool_endpoint(t) - } for t in self.tools.values()] - - - - - - -def create_func_models(func: Callable) -> Tuple[Type[BaseModel], Type[BaseModel]]: - """ - Analyze a function to create corresponding Pydantic models for its input and output. - - Args: - func (Callable): The function to analyze. - - Returns: - Tuple[Type[BaseModel], Type[BaseModel]]: A tuple containing the input and output Pydantic models. - """ - input_fields = {} - if asyncio.iscoroutinefunction(func): - func = func.__wrapped__ - for name, param in inspect.signature(func, follow_wrapped=True).parameters.items(): - field_info = extract_field_info(param) - input_fields[name] = (field_info['type'], Field(**field_info['field_params'])) - - input_model = create_model(f"{snake_to_camel(func.__name__)}Input", **input_fields) - - output_model = determine_output_model(func) - - return input_model, output_model - -def extract_field_info(param: inspect.Parameter) -> dict: - """ - Extract type and field parameters from a function parameter. - - Args: - param (inspect.Parameter): The parameter to extract information from. - - Returns: - dict: A dictionary with 'type' and 'field_params'. - """ - annotation = param.annotation - default = param.default if param.default is not inspect.Parameter.empty else None - description = getattr(annotation, '__metadata__', [None])[0] if hasattr(annotation, '__metadata__') else None - - field_params = { - 'default': default, - 'description': str(description) if description else "No description provided." - } - - # Handle specific annotations like Param and Secret if needed - if hasattr(annotation, '__origin__') and annotation.__origin__ in [Param]: - field_type = annotation.__args__[0] - else: - field_type = annotation - - return {'type': field_type, 'field_params': field_params} - -def determine_output_model(func: Callable) -> Type[BaseModel]: - """ - Determine the output model for a function based on its return annotation. - - Args: - func (Callable): The function to analyze. - - Returns: - Type[BaseModel]: A Pydantic model representing the output. - """ - return_annotation = inspect.signature(func).return_annotation - if return_annotation is inspect.Signature.empty: - return create_model(f"{snake_to_camel(func.__name__)}Output") - elif hasattr(return_annotation, '__origin__'): - if hasattr(return_annotation, '__metadata__'): - field_type = Optional[return_annotation.__args__[0]] - description = return_annotation.__metadata__[0] if return_annotation.__metadata__ else "" - if description: - return create_model(f"{snake_to_camel(func.__name__)}Output", result=(field_type, Field(description=str(description)))) - else: - return create_model(f"{snake_to_camel(func.__name__)}Output", result=(return_annotation, Field(description="No description provided."))) - -def create_response_model(name: str, output_model: Type[BaseModel]) -> Type[ResponseModel]: - """ - Create a response model for the given schema. - """ - # Create a new response model - response_model = create_model( - f"{snake_to_camel(name)}Response", - code=(int, CustomResponseCode.HTTP_200.code), - msg=(str, CustomResponseCode.HTTP_200.msg), - data=(Optional[output_model], None) - ) - - return response_model \ No newline at end of file diff --git a/toolserve/toolserve/server/core/conf.py b/toolserve/toolserve/server/core/conf.py deleted file mode 100644 index a9142abe..00000000 --- a/toolserve/toolserve/server/core/conf.py +++ /dev/null @@ -1,126 +0,0 @@ -import os - -from functools import lru_cache -from typing import Literal - -from pathlib import Path - -from pydantic import model_validator -from pydantic_settings import BaseSettings, SettingsConfigDict - - - -# https://docs.pydantic.dev/latest/concepts/pydantic_settings/ -class Settings(BaseSettings): - model_config = SettingsConfigDict(env_file='.env') - - WORK_DIR: Path = Path.home() / '.arcade' - TOOLS_DIR: Path = os.getcwd() - ARTIFACTS_DIR: Path = WORK_DIR / 'artifacts' - DATA_DIR: Path = WORK_DIR / 'data' - - BUILTIN_TOOLS_DIR: Path = Path(__file__).parent.parent.parent / 'builtin' / 'default' - BUILTIN_TOOLS: list[str] = [ - "query.list_data_sources@builtin", - "query.get_data_schema@builtin", - "query.query_sql@builtin", - "data.get@builtin", - "data.select_columns@builtin", - "data.filter_rows@builtin", - "data.sort@builtin", - "data.group_by@builtin", - "data.join@builtin", - "data.search_text_columns@builtin", - "data.combine_results@builtin", - ] - - # Env Config - ENVIRONMENT: Literal['dev', 'pro'] = 'dev' - - # Env Redis - REDIS_HOST: str = 'localhost' - REDIS_PORT: int = 6379 - REDIS_PASSWORD: str = '' - REDIS_DATABASE: int = 0 - - # Env Token - TOKEN_SECRET_KEY: str # 密钥 secrets.token_urlsafe(32) - - # Env Opera Log - OPERA_LOG_ENCRYPT_SECRET_KEY: str # 密钥 os.urandom(32), 需使用 bytes.hex() 方法转换为 str - - # FastAPI - API_V1_STR: str = '/v1' - API_ACTION_STR: str = '/tool' - TITLE: str = 'Arcade AI Toolserver' - VERSION: str = '0.1.0' - DESCRIPTION: str = 'Arcade AI Toolserver API' - DOCS_URL: str | None = f'{API_V1_STR}/docs' - REDOCS_URL: str | None = f'{API_V1_STR}/redocs' - OPENAPI_URL: str | None = f'{API_V1_STR}/openapi' - -# @model_validator(mode='before') -# @classmethod -# def validate_openapi_url(cls, values): -# if values['ENVIRONMENT'] == 'pro': -# values['OPENAPI_URL'] = None -# return values - - # Uvicorn - UVICORN_HOST: str = '127.0.0.1' - UVICORN_PORT: int = 8000 - UVICORN_RELOAD: bool = True - - # Static Server - STATIC_FILES: bool = False - - # DateTime - DATETIME_TIMEZONE: str = 'US/Pacific' - DATETIME_FORMAT: str = '%Y-%m-%d %H:%M:%S' - - # Redis - REDIS_TIMEOUT: int = 5 - - # Token - TOKEN_ALGORITHM: str = 'HS256' # 算法 - TOKEN_EXPIRE_SECONDS: int = 60 * 60 * 24 * 1 # 过期时间,单位:秒 - TOKEN_REFRESH_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7 # 刷新过期时间,单位:秒 - TOKEN_URL_SWAGGER: str = f'{API_V1_STR}/auth/swagger_login' - TOKEN_REDIS_PREFIX: str = 'ts_token' - TOKEN_REFRESH_REDIS_PREFIX: str = 'ts_refresh_token' - TOKEN_EXCLUDE: list[str] = [ # JWT / RBAC 白名单 - f'{API_V1_STR}/auth/login', - ] - - # Log - LOG_STDOUT_FILENAME: str = 'ts_access.log' - LOG_STDERR_FILENAME: str = 'ts_error.log' - - # Middleware - MIDDLEWARE_CORS: bool = True - MIDDLEWARE_GZIP: bool = True - MIDDLEWARE_ACCESS: bool = False - - # these should be set in .env - TOKEN_SECRET_KEY: str = "secret" - OPERA_LOG_ENCRYPT_SECRET_KEY: str = "secret" - - # SQL Database - DB_HOST: str = "localhost" - DB_PORT: int = "3306" - DB_USER: str = "arcade" - DB_PASSWORD: str = "arcade" - - DB_ECHO: bool = False - DB_DATABASE: str = 'arcade' - DB_CHARSET: str = 'utf8mb4' - -@lru_cache -def get_settings(): - try: - env_path = Path(os.environ["TOOLSERVE_ENV"]) - except KeyError: - env_path = Path(__file__).parent.parent / '.env' - return Settings(_env_file=env_path) - -settings = get_settings() diff --git a/toolserve/toolserve/server/crud/base.py b/toolserve/toolserve/server/crud/base.py deleted file mode 100644 index 433bee96..00000000 --- a/toolserve/toolserve/server/crud/base.py +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from typing import Any, Dict, Generic, Type, TypeVar - -from pydantic import BaseModel -from sqlalchemy import and_, delete, select, update -from sqlalchemy.ext.asyncio import AsyncSession - -from toolserve.server.models.base import MappedBase - -ModelType = TypeVar('ModelType', bound=MappedBase) -CreateSchemaType = TypeVar('CreateSchemaType', bound=BaseModel) -UpdateSchemaType = TypeVar('UpdateSchemaType', bound=BaseModel) - - -class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): - def __init__(self, model: Type[ModelType]): - self.model = model - - async def get_( - self, - db: AsyncSession, - *, - pk: int | None = None, - name: str | None = None, - status: int | None = None, - del_flag: int | None = None, - ) -> ModelType | None: - """ - Get a record by primary key id or name - - :param db: - :param pk: - :param name: - :param status: - :param del_flag: - :return: - """ - assert pk is not None or name is not None, 'Query error, pk and name parameters cannot be empty at the same time' - assert pk is None or name is None, 'Query error, pk and name parameters cannot exist at the same time' - where_list = [self.model.id == pk] if pk is not None else [self.model.name == name] - if status is not None: - assert status in (0, 1), 'Query error, status parameter can only be 0 or 1' - where_list.append(self.model.status == status) - if del_flag is not None: - assert del_flag in (0, 1), 'Query error, del_flag parameter can only be 0 or 1' - where_list.append(self.model.del_flag == del_flag) - - result = await db.execute(select(self.model).where(and_(*where_list))) - return result.scalars().first() - - async def create_(self, db: AsyncSession, obj_in: CreateSchemaType, user_id: int | None = None) -> None: - """ - Add a new record - - :param db: - :param obj_in: Pydantic model class - :param user_id: - :return: - """ - if user_id: - create_data = self.model(**obj_in.model_dump(), create_user=user_id) - else: - create_data = self.model(**obj_in.model_dump()) - db.add(create_data) - - async def update_( - self, db: AsyncSession, pk: int, obj_in: UpdateSchemaType | Dict[str, Any], user_id: int | None = None - ) -> int: - """ - Update a record by primary key id - - :param db: - :param pk: - :param obj_in: Pydantic model class or dictionary corresponding to database fields - :param user_id: - :return: - """ - if isinstance(obj_in, dict): - update_data = obj_in - else: - update_data = obj_in.model_dump(exclude_unset=True) - if user_id: - update_data.update({'update_user': user_id}) - result = await db.execute(update(self.model).where(self.model.id == pk).values(**update_data)) - return result.rowcount - - async def delete_(self, db: AsyncSession, pk: int, *, del_flag: int | None = None) -> int: - """ - Delete a record by primary key id - - :param db: - :param pk: - :param del_flag: - :return: - """ - if del_flag is None: - result = await db.execute(delete(self.model).where(self.model.id == pk)) - else: - assert del_flag == 1, 'Delete error, del_flag parameter can only be 1' - result = await db.execute(update(self.model).where(self.model.id == pk).values(del_flag=del_flag)) - return result.rowcount diff --git a/toolserve/toolserve/server/crud/crud_artifact.py b/toolserve/toolserve/server/crud/crud_artifact.py deleted file mode 100644 index 021a6e46..00000000 --- a/toolserve/toolserve/server/crud/crud_artifact.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from typing import Sequence - -from sqlalchemy import Select, and_, delete, desc, select -from sqlalchemy.ext.asyncio import AsyncSession - -from toolserve.server.crud.base import CRUDBase -from toolserve.server.models.sys_artifact import Artifact -from toolserve.server.schemas.artifact import CreateArtifactParam, UpdateArtifactParam - - -class CRUDArtifact(CRUDBase[Artifact, CreateArtifactParam, UpdateArtifactParam]): - async def get(self, db: AsyncSession, pk: int) -> Artifact | None: - return await self.get_(db, pk=pk) - - async def get_list(self, name: str = None, file_path: str = None) -> Select: - se = select(self.model).order_by(desc(self.model.created_time)) - where_list = [] - if name: - where_list.append(self.model.name.like(f'%{name}%')) - if file_path: - where_list.append(self.model.file_path.like(f'%{file_path}%', escape='/')) - if where_list: - se = se.where(and_(*where_list)) - return se - - async def get_all(self, db: AsyncSession) -> Sequence[Artifact]: - artifacts = await db.execute(select(self.model)) - return artifacts.scalars().all() - - async def get_by_name(self, db: AsyncSession, name: str) -> Artifact | None: - artifact = await db.execute(select(self.model).where(self.model.name == name)) - return artifact.scalars().first() - - async def create(self, db: AsyncSession, obj_in: CreateArtifactParam) -> None: - await self.create_(db, obj_in) - - async def update(self, db: AsyncSession, pk: int, obj_in: UpdateArtifactParam) -> int: - return await self.update_(db, pk, obj_in) - - async def delete(self, db: AsyncSession, pk: list[int]) -> int: - artifacts = await db.execute(delete(self.model).where(self.model.id.in_(pk))) - return artifacts.rowcount - - -artifact_dao: CRUDArtifact = CRUDArtifact(Artifact) diff --git a/toolserve/toolserve/server/crud/crud_data.py b/toolserve/toolserve/server/crud/crud_data.py deleted file mode 100644 index bf0ecf32..00000000 --- a/toolserve/toolserve/server/crud/crud_data.py +++ /dev/null @@ -1,51 +0,0 @@ -import datetime -from typing import Sequence -from sqlalchemy import Select, and_, delete, desc, select -from sqlalchemy.ext.asyncio import AsyncSession - -from toolserve.server.crud.base import CRUDBase -from toolserve.server.models.sys_data import Data -from toolserve.server.schemas.data import CreateDataParam, DataSchemaBase - -class CRUDData(CRUDBase[Data, CreateDataParam, DataSchemaBase]): - async def get(self, db: AsyncSession, pk: int) -> Data | None: - return await self.get_(db, pk=pk) - - async def get_list(self, file_name: str = None, file_path: str = None) -> Select: - query = select(self.model).order_by(desc(self.model.created_time)) - conditions = [] - if file_name: - conditions.append(self.model.file_name.like(f'%{file_name}%')) - if file_path: - conditions.append(self.model.file_path.like(f'%{file_path}%')) - if conditions: - query = query.where(and_(*conditions)) - return query - - async def get_all(self, db: AsyncSession) -> Sequence[Data]: - result = await db.execute(select(self.model)) - return result.scalars().all() - - async def get_by_file_name(self, db: AsyncSession, file_name: str) -> Data | None: - result = await db.execute(select(self.model).where(self.model.file_name == file_name)) - return result.scalars().first() - - async def create(self, db: AsyncSession, obj_in: CreateDataParam) -> Data: - existing_data = await self.get_by_file_name(db, obj_in.file_name) - if existing_data: - existing_data.updated_time = datetime.datetime.now() - db.add(existing_data) - return existing_data - else: - obj = self.model(**obj_in.dict()) - db.add(obj) - return obj - - async def update(self, db: AsyncSession, pk: int, obj_in: DataSchemaBase) -> int: - return await self.update_(db, pk, obj_in) - - async def delete(self, db: AsyncSession, pk: list[int]) -> int: - result = await db.execute(delete(self.model).where(self.model.id.in_(pk))) - return result.rowcount - -data_dao: CRUDData = CRUDData(Data) diff --git a/toolserve/toolserve/server/crud/crud_log.py b/toolserve/toolserve/server/crud/crud_log.py deleted file mode 100644 index 3167ddd8..00000000 --- a/toolserve/toolserve/server/crud/crud_log.py +++ /dev/null @@ -1,45 +0,0 @@ -from typing import Sequence - -from sqlalchemy import Select, and_, delete, desc, select -from sqlalchemy.ext.asyncio import AsyncSession - -from toolserve.server.crud.base import CRUDBase -from toolserve.server.models.sys_log import Log -from toolserve.server.schemas.log import CreateLog, LogSchemaBase - - -class CRUDLog(CRUDBase[Log, CreateLog, LogSchemaBase]): - async def get(self, db: AsyncSession, pk: int) -> Log | None: - return await self.get_(db, pk=pk) - - async def get_list(self, level: str = None, msg: str = None) -> Select: - query = select(self.model).order_by(desc(self.model.created_time)) - conditions = [] - if level: - conditions.append(self.model.level == level) - if msg: - conditions.append(self.model.msg.like(f'%{msg}%')) - if conditions: - query = query.where(and_(*conditions)) - return query - - async def get_all(self, db: AsyncSession) -> Sequence[Log]: - result = await db.execute(select(self.model)) - return result.scalars().all() - - async def get_by_level(self, db: AsyncSession, level: str) -> Log | None: - result = await db.execute(select(self.model).where(self.model.level == level)) - return result.scalars().first() - - async def create(self, db: AsyncSession, obj_in: CreateLog) -> None: - await self.create_(db, obj_in) - - async def update(self, db: AsyncSession, pk: int, obj_in: LogSchemaBase) -> int: - return await self.update_(db, pk, obj_in) - - async def delete(self, db: AsyncSession, pk: list[int]) -> int: - result = await db.execute(delete(self.model).where(self.model.id.in_(pk))) - return result.rowcount - - -log_dao: CRUDLog = CRUDLog(Log) diff --git a/toolserve/toolserve/server/database/__init__.py b/toolserve/toolserve/server/database/__init__.py deleted file mode 100644 index 56fafa58..00000000 --- a/toolserve/toolserve/server/database/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- diff --git a/toolserve/toolserve/server/database/db_sqlite.py b/toolserve/toolserve/server/database/db_sqlite.py deleted file mode 100644 index 09882786..00000000 --- a/toolserve/toolserve/server/database/db_sqlite.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import sys - -from typing import Annotated -from uuid import uuid4 - -from fastapi import Depends -from sqlalchemy import URL -from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine - -from toolserve.server.common.log import log -from toolserve.server.core.conf import settings -from toolserve.server.models import ( - MappedBase, - Log, - Artifact, - Data, -) - - -def create_engine_and_session(url: str | URL): - try: - engine = create_async_engine(url, echo=settings.DB_ECHO, future=True, pool_pre_ping=True) - except Exception as e: - log.error('❌ Error starting db session {}', e) - sys.exit() - else: - db_session = async_sessionmaker(bind=engine, autoflush=False, expire_on_commit=False) - return engine, db_session - - -SQLALCHEMY_DATABASE_URL = ( - f'sqlite+aiosqlite:///{settings.WORK_DIR}/{settings.DB_DATABASE}.db' -) - -async_engine, async_db_session = create_engine_and_session(SQLALCHEMY_DATABASE_URL) - - -async def get_db() -> AsyncSession: - """Provide a database session for a single request.""" - session = async_db_session() - try: - yield session - except Exception as se: - await session.rollback() - raise se - finally: - await session.close() - - -# Session Annotated -CurrentSession = Annotated[AsyncSession, Depends(get_db)] - - -async def create_table(): - """Create the database tables if they do not exist.""" - async with async_engine.begin() as conn: - try: - await conn.run_sync(MappedBase.metadata.create_all) - except Exception as e: - log.error('❌ Error creating tables {}', e) - raise e - - -def uuid4_str() -> str: - """Generate a UUID4 string.""" - return str(uuid4()) diff --git a/toolserve/toolserve/server/main.py b/toolserve/toolserve/server/main.py deleted file mode 100644 index 3069bb77..00000000 --- a/toolserve/toolserve/server/main.py +++ /dev/null @@ -1,24 +0,0 @@ -import uvicorn - -from pathlib import Path -import asyncio -from toolserve.server.common.log import log -from toolserve.server.core.conf import settings -from toolserve.server.core.registrar import register_app - - -app = register_app() - -if __name__ == '__main__': - try: - log.info( - "Arcade AI Toolserve is starting..." - ) - uvicorn.run( - app=f'{Path(__file__).stem}:app', - host=settings.UVICORN_HOST, - port=settings.UVICORN_PORT, - reload=settings.UVICORN_RELOAD, - ) - except Exception as e: - log.error(f'❌ FastAPI start filed: {e}') diff --git a/toolserve/toolserve/server/models/__init__.py b/toolserve/toolserve/server/models/__init__.py deleted file mode 100644 index 300d0090..00000000 --- a/toolserve/toolserve/server/models/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ - -from toolserve.server.models.base import MappedBase -from toolserve.server.models.sys_data import Data -from toolserve.server.models.sys_log import Log -from toolserve.server.models.sys_artifact import Artifact - -__all__ = ['MappedBase', 'Data', 'Log', 'Artifact'] diff --git a/toolserve/toolserve/server/models/base.py b/toolserve/toolserve/server/models/base.py deleted file mode 100644 index 29177498..00000000 --- a/toolserve/toolserve/server/models/base.py +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from datetime import datetime -from typing import Annotated - -from sqlalchemy.orm import ( - DeclarativeBase, - Mapped, - MappedAsDataclass, - declared_attr, - mapped_column, -) - -from toolserve.server.utils.timezone import timezone - -# Common Mapped type primary key, manual addition required, refer to the following usage -# MappedBase -> id: Mapped[id_key] -# DataClassBase && Base -> id: Mapped[id_key] = mapped_column(init=False) -id_key = Annotated[ - int, - mapped_column( - primary_key=True, - index=True, - autoincrement=True, - sort_order=-999, - comment="Primary key id", - ), -] - - -# Mixin: An object-oriented programming concept, makes the structure clearer, `Wiki `__ -class UserMixin(MappedAsDataclass): - """User Mixin data class""" - - create_user: Mapped[int] = mapped_column(sort_order=998, comment="Creator") - update_user: Mapped[int | None] = mapped_column( - init=False, default=None, sort_order=998, comment="Modifier" - ) - - -class DateTimeMixin(MappedAsDataclass): - """Datetime Mixin data class""" - - created_time: Mapped[datetime] = mapped_column( - init=False, - default_factory=timezone.now, - sort_order=999, - comment="Creation time", - ) - updated_time: Mapped[datetime | None] = mapped_column( - init=False, onupdate=timezone.now, sort_order=999, comment="Update time" - ) - - -class MappedBase(DeclarativeBase): - """ - Declarative base class, the original DeclarativeBase class, serves as the parent class for all base or data model classes - - `DeclarativeBase `__ - `mapped_column() `__ - """ - - @declared_attr.directive - def __tablename__(cls) -> str: - return cls.__name__.lower() - - -class DataClassBase(MappedAsDataclass, MappedBase): - """ - Declarative data class base class, integrates with data classes, allows for advanced configurations, but you must be aware of its characteristics, especially when used with DeclarativeBase - - `MappedAsDataclass `__ - """ # noqa: E501 - - __abstract__ = True - - -class Base(DataClassBase, DateTimeMixin): - """ - Declarative Mixin data class base class, integrates data classes, includes the Mixin data class basic table structure, you can simply understand it as a data class base class with basic table structure - """ # noqa: E501 - - __abstract__ = True diff --git a/toolserve/toolserve/server/models/sys_artifact.py b/toolserve/toolserve/server/models/sys_artifact.py deleted file mode 100644 index 8197b014..00000000 --- a/toolserve/toolserve/server/models/sys_artifact.py +++ /dev/null @@ -1,13 +0,0 @@ -from sqlalchemy import String -from sqlalchemy.orm import Mapped, mapped_column - -from toolserve.server.models.base import Base, id_key - - -class Artifact(Base): - - __tablename__ = 'sys_artifact' - - id: Mapped[id_key] = mapped_column(init=False) - name: Mapped[str] = mapped_column(String(255), comment='Artifact name') - file_path: Mapped[str] = mapped_column(String(255), comment='File path') diff --git a/toolserve/toolserve/server/models/sys_data.py b/toolserve/toolserve/server/models/sys_data.py deleted file mode 100644 index 25acf0fa..00000000 --- a/toolserve/toolserve/server/models/sys_data.py +++ /dev/null @@ -1,14 +0,0 @@ - -from sqlalchemy import String -from sqlalchemy.orm import Mapped, mapped_column - -from toolserve.server.models.base import Base, id_key - - -class Data(Base): - - __tablename__ = 'sys_data' - - id: Mapped[id_key] = mapped_column(init=False) - file_name: Mapped[str] = mapped_column(String(255), comment='File name') - file_path: Mapped[str] = mapped_column(String(255), comment='File path') diff --git a/toolserve/toolserve/server/models/sys_log.py b/toolserve/toolserve/server/models/sys_log.py deleted file mode 100644 index 665f7e6c..00000000 --- a/toolserve/toolserve/server/models/sys_log.py +++ /dev/null @@ -1,15 +0,0 @@ - - -from sqlalchemy import String -from sqlalchemy.orm import Mapped, mapped_column - -from toolserve.server.models.base import Base, id_key - - -class Log(Base): - - __tablename__ = 'sys_log' - - id: Mapped[id_key] = mapped_column(init=False) - level: Mapped[str] = mapped_column(String(50), comment='Log level') - msg: Mapped[str] = mapped_column(String(500), comment='Log message') diff --git a/toolserve/toolserve/server/routes/__init__.py b/toolserve/toolserve/server/routes/__init__.py deleted file mode 100644 index 33b36110..00000000 --- a/toolserve/toolserve/server/routes/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from fastapi import APIRouter -from toolserve.server.core.conf import settings -from toolserve.server.routes.tool import router as tool_router -from toolserve.server.routes.data import router as data_router -from toolserve.server.routes.artifact import router as artifact_router -from toolserve.server.routes.log import router as log_router -from toolserve.server.routes.slack import router as slack_router -from toolserve.server.routes.chat import router as chat_router - -v1 = APIRouter(prefix=settings.API_V1_STR) -v1.include_router(tool_router, prefix="/tools", tags=["Tool Catalog"]) -v1.include_router(data_router, prefix="/data", tags=["Data Management"]) -v1.include_router(artifact_router, prefix="/artifact", tags=["Artifact Management"]) -v1.include_router(log_router, prefix="/log", tags=["Tool Logging API"]) -v1.include_router(slack_router, prefix="/slack", tags=["Slack"]) -v1.include_router(chat_router, prefix="/chat", tags=["Chat"]) diff --git a/toolserve/toolserve/server/routes/artifact.py b/toolserve/toolserve/server/routes/artifact.py deleted file mode 100644 index 1bdaffb2..00000000 --- a/toolserve/toolserve/server/routes/artifact.py +++ /dev/null @@ -1,45 +0,0 @@ - -from typing import Annotated -from fastapi import APIRouter, Path, Query -from toolserve.server.schemas.artifact import ( - ArtifactSchemaBase, - CreateArtifactParam, - DeleteArtifactParam, - GetArtifactDetails -) -from toolserve.server.services.artifact_service import artifact_service -from toolserve.server.common.response import ResponseModel, response_base -from toolserve.server.common.serializers import select_as_dict - -router = APIRouter() - -# Get artifact details by artifact id -@router.get('/{pk}', summary='Get artifact details') -async def get_artifact( - pk: Annotated[int, Path(...)], -) -> ResponseModel: - artifact = await artifact_service.get(pk=pk) - data = GetArtifactDetails(**await select_as_dict(artifact)) - return await response_base.success(data=data) - -# Create a new artifact -@router.post('', summary='Create artifact') -async def create_artifact( - obj: CreateArtifactParam -) -> ResponseModel: - await artifact_service.create(obj=obj) - return await response_base.success() - -# Delete artifact -@router.delete('', summary='Delete artifact') -async def delete_artifact(pk: Annotated[list[int], Query(...)]) -> ResponseModel: - count = await artifact_service.delete(pk=pk) - if count > 0: - return await response_base.success() - return await response_base.fail() - -# Get all artifacts -@router.get('', summary='Get all artifacts') -async def get_all_artifacts() -> ResponseModel: - data = await artifact_service.get_all_artifacts() - return await response_base.success(data=data) diff --git a/toolserve/toolserve/server/routes/chat.py b/toolserve/toolserve/server/routes/chat.py deleted file mode 100644 index d4b2827d..00000000 --- a/toolserve/toolserve/server/routes/chat.py +++ /dev/null @@ -1,124 +0,0 @@ - -from typing import Annotated -from fastapi import APIRouter, Path, Query -from fastapi.responses import StreamingResponse -from toolserve.server.common.response import ResponseModel, response_base -from toolserve.server.common.serializers import select_as_dict - -# to take out later -import openai -import json - -from pydantic import BaseModel, Field -from typing import List, Optional, Union, Literal, Iterable - -from openai.types.chat.chat_completion_tool_param import ChatCompletionToolParam -from openai.types.chat.chat_completion_message_param import ChatCompletionMessageParam -from openai.types.chat.chat_completion_stream_options_param import ChatCompletionStreamOptionsParam -from openai.types.chat.chat_completion_tool_choice_option_param import ChatCompletionToolChoiceOptionParam -from openai.types.chat.chat_completion_function_call_option_param import ChatCompletionFunctionCallOptionParam -from openai.types import shared_params -from openai.types.chat_model import ChatModel -from fastapi import Request, HTTPException, status, Depends - -from toolserve.server.core.depends import get_catalog -from toolserve.utils.openai_tool import schema_to_openai_tool - -router = APIRouter() - - -class FunctionCall(BaseModel): - type: Literal["none", "auto", "function"] - function: Optional[ChatCompletionFunctionCallOptionParam] - -class Function(BaseModel): - name: str - description: Optional[str] - parameters: Optional[shared_params.FunctionParameters] - -class ResponseFormat(BaseModel): - type: Literal["text", "json_object"] - -class CompletionCreateParamsBase(BaseModel): - messages: List[ChatCompletionMessageParam] - model: Union[str, ChatModel] - frequency_penalty: Optional[float] = None - #function_call: Optional[FunctionCall] = None - #functions: Optional[List[Function]] = None - logit_bias: Optional[dict[str, int]] = None - logprobs: Optional[bool] = None - max_tokens: Optional[int] = None - n: Optional[int] = None - presence_penalty: Optional[float] = None - response_format: Optional[ResponseFormat] = None - seed: Optional[int] = None - stop: Optional[Union[str, List[str]]] = None - stream_options: Optional[ChatCompletionStreamOptionsParam] = None - temperature: Optional[float] = None - tool_choice: Optional[ChatCompletionToolChoiceOptionParam] = None - tools: Optional[Union[List[ChatCompletionToolParam], List[str]]] = None - top_logprobs: Optional[int] = None - top_p: Optional[float] = None - user: Optional[str] = None - -class CompletionCreateParamsNonStreaming(CompletionCreateParamsBase): - stream: Literal[False] - -class CompletionCreateParamsStreaming(CompletionCreateParamsBase): - stream: Literal[True] - -CompletionCreateParams = Union[CompletionCreateParamsNonStreaming, CompletionCreateParamsStreaming] - - - -def get_openai_key(request: Request) -> str: - """ - Extracts the API key from the Authorization header as a Bearer token. - - Args: - request (Request): The request object from which the API key is extracted. - - Returns: - str: The API key extracted from the Authorization header. - - Raises: - HTTPException: If the Authorization header is missing or improperly formatted. - """ - auth_header = request.headers.get('Authorization') - if auth_header is None or not auth_header.startswith('Bearer '): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail='Authorization token is missing or improperly formatted' - ) - api_key = auth_header.split(' ')[1] - return api_key - - -@router.post( - '/completions', - summary='Chat Completions Endpoints mimicking OpenAI' -) -async def create_chat_completion( - completion: CompletionCreateParams, - api_key: str = Depends(get_openai_key), - catalog=Depends(get_catalog) -): - """ - Create a chat completion - """ - try: - oai_client = openai.AsyncOpenAI(api_key=api_key) - - if completion.tools: - if isinstance(completion.tools[0], str): - specs = [] - for tool in completion.tools: - specs.append(json.loads(schema_to_openai_tool(catalog[tool]))) - completion.tool_choice = "required" - completion.tools = specs - - result = await oai_client.chat.completions.create(**completion.dict()) - return result - - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/toolserve/toolserve/server/routes/data.py b/toolserve/toolserve/server/routes/data.py deleted file mode 100644 index 36ab2484..00000000 --- a/toolserve/toolserve/server/routes/data.py +++ /dev/null @@ -1,79 +0,0 @@ - -from typing import Annotated -from fastapi import APIRouter, Path, Query -from toolserve.server.schemas.data import ( - CreateDataParam, - DataSchemaBase, - GetDataDetails, - GetDataObject -) -from toolserve.server.services.data_service import data_service -from toolserve.server.common.response import ResponseModel, response_base -from toolserve.server.common.serializers import select_as_dict - -router = APIRouter() - -# Get data details by data id -@router.get('/{pk}', summary='Get data details') -async def get_data( - pk: Annotated[int, Path(...)], -) -> ResponseModel: - data_entry = await data_service.get(pk=pk) - data = GetDataDetails(**await select_as_dict(data_entry)) - return await response_base.success(data=data) - -@router.get('', summary='Get all data files') -async def get_all_data() -> ResponseModel: - data = await data_service.get_all_data() - all_data = [GetDataDetails(**await select_as_dict(data_entry)) for data_entry in data] - return await response_base.success(data=all_data) - -# Create a new data entry -@router.post('', summary='Create data') -async def create_data( - obj: CreateDataParam -) -> ResponseModel: - data_obj = await data_service.create(obj=obj) - data = { - "id": data_obj.id, - "file_path": data_obj.file_path - } - return await response_base.success(data=data) - -# Delete data -@router.delete('', summary='Delete data') -async def delete_data(pk: Annotated[list[int], Query(...)]) -> ResponseModel: - count = await data_service.delete(pk=pk) - if count > 0: - return await response_base.success() - return await response_base.fail() - -@router.get('/object/{pk}', summary='Get data object') -async def get_data_object( - pk: Annotated[int, Path(...)], -) -> ResponseModel: - """ - Retrieve a data object by its primary key using the GetDataObject schema. - - Args: - pk (int): The primary key of the data object to retrieve. - - Returns: - ResponseModel: The response model containing the data object or an error message. - """ - data_entry = await data_service.get(pk=pk) - obj = await select_as_dict(data_entry) - try: - with open(obj["file_path"], 'r', encoding='utf-8') as file: - json_data = file.read() - except FileNotFoundError: - raise HTTPException(status_code=404, detail="File not found") - except IOError: - raise HTTPException(status_code=500, detail="File read error") - - data_object = GetDataObject( - file_name=obj["file_name"], - file_path=obj["file_path"], - json_blob=json_data - ) - return await response_base.success(data=data_object) diff --git a/toolserve/toolserve/server/routes/log.py b/toolserve/toolserve/server/routes/log.py deleted file mode 100644 index 863b51a7..00000000 --- a/toolserve/toolserve/server/routes/log.py +++ /dev/null @@ -1,40 +0,0 @@ - -from typing import Annotated -from fastapi import APIRouter, Path, Query -from toolserve.server.schemas.log import CreateLog, LogSchemaBase, GetLogDetails -from toolserve.server.services.log_service import log_service -from toolserve.server.common.response import ResponseModel, response_base -from toolserve.server.common.serializers import select_as_dict - -router = APIRouter() - -# Get log details by log id -@router.get('/{pk}', summary='Get log details') -async def get_log( - pk: Annotated[int, Path(...)], -) -> ResponseModel: - log = await log_service.get(pk=pk) - data = GetLogDetails(**await select_as_dict(log)) - return await response_base.success(data=data) - -# Create a new log -@router.post('', summary='Create log') -async def create_log( - obj: CreateLog -) -> ResponseModel: - await log_service.create(obj=obj) - return await response_base.success() - -# Delete log -@router.delete('', summary='Delete log') -async def delete_log(pk: Annotated[list[int], Query(...)]) -> ResponseModel: - count = await log_service.delete(pk=pk) - if count > 0: - return await response_base.success() - return await response_base.fail() - -# Get all logs -@router.get('', summary='Get all logs') -async def get_all_logs() -> ResponseModel: - data = await log_service.get_log_list() - return await response_base.success(data=data) diff --git a/toolserve/toolserve/server/routes/tool.py b/toolserve/toolserve/server/routes/tool.py deleted file mode 100644 index ab8ed190..00000000 --- a/toolserve/toolserve/server/routes/tool.py +++ /dev/null @@ -1,44 +0,0 @@ -import os -from typing import Annotated - -from fastapi import APIRouter, Body, Depends, Path, Query -from pydantic import ValidationError - -from toolserve.server.core.conf import settings -from toolserve.server.core.depends import get_catalog -from toolserve.server.common.response_code import CustomResponseCode -from toolserve.server.common.response import ResponseModel, response_base -from toolserve.utils.openai_tool import schema_to_openai_tool - -router = APIRouter() - -@router.get( - '/list', - summary='List available tools', -) -async def list_tools(catalog=Depends(get_catalog)) -> ResponseModel: - """List all available tools""" - - tools = catalog.list_tools() - return await response_base.success(data=tools) - -@router.get( - '/oai_function', - summary="Get the OpenAI function format of an tool" -) -async def get_oai_function( - tool_name: str = Query(..., title="Tool Name", description="The name of the tool"), - catalog=Depends(get_catalog) -) -> ResponseModel: - """Get the OpenAI function format of an tool""" - - try: - # TODO handle keyerror - tool = catalog[tool_name] - json_data = schema_to_openai_tool(tool) - - return await response_base.success(data=json_data) - except ValidationError as e: - return await response_base.fail(res=CustomResponseCode.HTTP_400, data=str(e)) - except Exception as e: - return await response_base.fail(res=CustomResponseCode.HTTP_500, data=str(e)) diff --git a/toolserve/toolserve/server/schemas/artifact.py b/toolserve/toolserve/server/schemas/artifact.py deleted file mode 100644 index 5c431fca..00000000 --- a/toolserve/toolserve/server/schemas/artifact.py +++ /dev/null @@ -1,29 +0,0 @@ -from datetime import datetime - -from pydantic import ConfigDict, Field - -from toolserve.server.schemas.base import SchemaBase - - -class ArtifactSchemaBase(SchemaBase): - name: str - file_path: str - - -class CreateArtifactParam(ArtifactSchemaBase): - data: bytes - media_type: str - - -class DeleteArtifactParam(ArtifactSchemaBase): - pass - -class UpdateArtifactParam(ArtifactSchemaBase): - pass - -class GetArtifactDetails(ArtifactSchemaBase): - model_config = ConfigDict(from_attributes=True) - - id: int - created_time: datetime - updated_time: datetime | None = None diff --git a/toolserve/toolserve/server/schemas/base.py b/toolserve/toolserve/server/schemas/base.py deleted file mode 100644 index f3a53832..00000000 --- a/toolserve/toolserve/server/schemas/base.py +++ /dev/null @@ -1,148 +0,0 @@ -# -*- coding: utf-8 -*- - -from pydantic import BaseModel, ConfigDict, EmailStr, validate_email - -# Custom validation error messages do not include the expected content of validation (i.e., input content). For supported expected content fields, refer to the following link: -# https://github.com/pydantic/pydantic-core/blob/a5cb7382643415b716b1a7a5392914e50f726528/tests/test_errors.py#L266 -# For replacing expected content fields, refer to the following link: -# https://github.com/pydantic/pydantic/blob/caa78016433ec9b16a973f92f187a7b6bfde6cb5/docs/errors/errors.md?plain=1#L232 -CUSTOM_VALIDATION_ERROR_MESSAGES = { - 'arguments_type': 'Incorrect argument type input', - 'assertion_error': 'Assertion execution error', - 'bool_parsing': 'Boolean value parsing error', - 'bool_type': 'Boolean type input error', - 'bytes_too_long': 'Byte length input too long', - 'bytes_too_short': 'Byte length input too short', - 'bytes_type': 'Byte type input error', - 'callable_type': 'Callable object type input error', - 'dataclass_exact_type': 'Dataclass instance type input error', - 'dataclass_type': 'Dataclass type input error', - 'date_from_datetime_inexact': 'Non-zero date component input', - 'date_from_datetime_parsing': 'Date input parsing error', - 'date_future': 'Date input is not in the future', - 'date_parsing': 'Date input validation error', - 'date_past': 'Date input is not in the past', - 'date_type': 'Date type input error', - 'datetime_future': 'Datetime input is not in the future', - 'datetime_object_invalid': 'Datetime input object invalid', - 'datetime_parsing': 'Datetime input parsing error', - 'datetime_past': 'Datetime input is not in the past', - 'datetime_type': 'Datetime type input error', - 'decimal_max_digits': 'Decimal input has too many digits', - 'decimal_max_places': 'Decimal places input error', - 'decimal_parsing': 'Decimal input parsing error', - 'decimal_type': 'Decimal type input error', - 'decimal_whole_digits': 'Decimal whole digits input error', - 'dict_type': 'Dictionary type input error', - 'enum': 'Enum member input error, allowed {expected}', - 'extra_forbidden': 'Extra fields input forbidden', - 'finite_number': 'Finite value input error', - 'float_parsing': 'Float parsing error', - 'float_type': 'Float type input error', - 'frozen_field': 'Frozen field input error', - 'frozen_instance': 'Modification of frozen instance forbidden', - 'frozen_set_type': 'Frozen set type input forbidden', - 'get_attribute_error': 'Attribute retrieval error', - 'greater_than': 'Input value too large', - 'greater_than_equal': 'Input value too large or equal', - 'int_from_float': 'Integer type input error', - 'int_parsing': 'Integer input parsing error', - 'int_parsing_size': 'Integer input parsing size error', - 'int_type': 'Integer type input error', - 'invalid_key': 'Invalid key input', - 'is_instance_of': 'Instance type input error', - 'is_subclass_of': 'Subclass type input error', - 'iterable_type': 'Iterable type input error', - 'iteration_error': 'Iteration value input error', - 'json_invalid': 'JSON string input error', - 'json_type': 'JSON type input error', - 'less_than': 'Input value too small', - 'less_than_equal': 'Input value too small or equal', - 'list_type': 'List type input error', - 'literal_error': 'Literal input error', - 'mapping_type': 'Mapping type input error', - 'missing': 'Missing required field', - 'missing_argument': 'Missing argument', - 'missing_keyword_only_argument': 'Missing keyword-only argument', - 'missing_positional_only_argument': 'Missing positional-only argument', - 'model_attributes_type': 'Model attributes type input error', - 'model_type': 'Model instance input error', - 'multiple_argument_values': 'Multiple argument values input', - 'multiple_of': 'Input value not a multiple', - 'no_such_attribute': 'Invalid attribute assignment', - 'none_required': 'Input value must be None', - 'recursion_loop': 'Recursion loop in input', - 'set_type': 'Set type input error', - 'string_pattern_mismatch': 'String pattern mismatch input', - 'string_sub_type': 'String subtype (non-strict instance) input error', - 'string_too_long': 'String input too long', - 'string_too_short': 'String input too short', - 'string_type': 'String type input error', - 'string_unicode': 'String input not Unicode', - 'time_delta_parsing': 'Time delta parsing error', - 'time_delta_type': 'Time delta type input error', - 'time_parsing': 'Time input parsing error', - 'time_type': 'Time type input error', - 'timezone_aware': 'Missing timezone input', - 'timezone_naive': 'Timezone input forbidden', - 'too_long': 'Input too long', - 'too_short': 'Input too short', - 'tuple_type': 'Tuple type input error', - 'unexpected_keyword_argument': 'Unexpected keyword argument input', - 'unexpected_positional_argument': 'Unexpected positional argument input', - 'union_tag_invalid': 'Union tag literal input error', - 'union_tag_not_found': 'Union tag argument not found', - 'url_parsing': 'URL input parsing error', - 'url_scheme': 'URL scheme input error', - 'url_syntax_violation': 'URL syntax violation', - 'url_too_long': 'URL input too long', - 'url_type': 'URL type input error', - 'uuid_parsing': 'UUID parsing error', - 'uuid_type': 'UUID type input error', - 'uuid_version': 'UUID version type input error', - 'value_error': 'Value input error', -} - -CUSTOM_USAGE_ERROR_MESSAGES = { - 'class-not-fully-defined': 'Class attributes type not fully defined', - 'custom-json-schema': '__modify_schema__ method deprecated in V2', - 'decorator-missing-field': 'Invalid field validator defined', - 'discriminator-no-field': 'Discriminator field not fully defined', - 'discriminator-alias-type': 'Discriminator field defined using non-string type', - 'discriminator-needs-literal': 'Discriminator field requires literal definition', - 'discriminator-alias': 'Inconsistent discriminator field alias definition', - 'discriminator-validator': 'Field validator forbidden on discriminator field', - 'model-field-overridden': 'Typeless field override forbidden', - 'model-field-missing-annotation': 'Missing field type definition', - 'config-both': 'Duplicate configuration item defined', - 'removed-kwargs': 'Removed keyword configuration parameter called', - 'invalid-for-json-schema': 'Invalid JSON type present', - 'base-model-instantiated': 'Instantiation of base model forbidden', - 'undefined-annotation': 'Missing type definition', - 'schema-for-unknown-type': 'Unknown type definition', - 'create-model-field-definitions': 'Field definition error', - 'create-model-config-base': 'Configuration item definition error', - 'validator-no-fields': 'Field validator without specified fields', - 'validator-invalid-fields': 'Field validator fields definition error', - 'validator-instance-method': 'Field validator must be a class method', - 'model-serializer-instance-method': 'Serializer must be an instance method', - 'validator-v1-signature': 'V1 field validator error deprecated', - 'validator-signature': 'Field validator signature error', - 'field-serializer-signature': 'Field serializer signature unrecognized', - 'model-serializer-signature': 'Model serializer signature unrecognized', - 'multiple-field-serializers': 'Field serializers defined multiple times', - 'invalid_annotated_type': 'Invalid type definition', - 'type-adapter-config-unused': 'Type adapter configuration item definition error', - 'root-model-extra': 'Extra fields on root model forbidden', -} - - - -class CustomEmailStr(EmailStr): - @classmethod - def _validate(cls, __input_value: str) -> str: - return None if __input_value == '' else validate_email(__input_value)[1] - - -class SchemaBase(BaseModel): - model_config = ConfigDict(use_enum_values=True) diff --git a/toolserve/toolserve/server/schemas/data.py b/toolserve/toolserve/server/schemas/data.py deleted file mode 100644 index 640dfb73..00000000 --- a/toolserve/toolserve/server/schemas/data.py +++ /dev/null @@ -1,46 +0,0 @@ - -from datetime import datetime - -from pydantic import BaseModel, Field - -from toolserve.server.schemas.base import SchemaBase - - -class DataSchemaBase(SchemaBase): - """ - Base schema for Data model. - """ - file_name: str = Field(..., title="File Name", description="Name of the file") - - -class CreateDataParam(DataSchemaBase): - """ - Parameters required to create a Data entry. - """ - json_blob: str = Field(..., title="JSON Blob", description="JSON blob containing the data") - file_path: str | None = Field(default=None, title="File Path", description="Path of the file") - - -class DeleteDataParam(DataSchemaBase): - """ - Parameters required to delete a Data entry. - """ - pass - - -class GetDataObject(DataSchemaBase): - """ - Schema to retrieve details of a Data entry. - """ - json_blob: str = Field(..., title="JSON Blob", description="JSON blob containing the data") - - -class GetDataDetails(DataSchemaBase): - """ - Schema to retrieve details of a Data entry. - """ - id: int = Field(..., title="ID", description="Unique identifier of the Data entry") - file_path: str = Field(..., title="File Path", description="Path of the file") - - created_time: datetime = Field(..., title="Creation Time", description="Time when the Data entry was created") - updated_time: datetime | None = Field(default=datetime.now(), title="Updated Time", description="Time when the Data entry was last updated") diff --git a/toolserve/toolserve/server/schemas/log.py b/toolserve/toolserve/server/schemas/log.py deleted file mode 100644 index 78bdc71f..00000000 --- a/toolserve/toolserve/server/schemas/log.py +++ /dev/null @@ -1,22 +0,0 @@ -from datetime import datetime - -from pydantic import ConfigDict, Field - -from toolserve.server.schemas.base import SchemaBase - - -class LogSchemaBase(SchemaBase): - level: str = Field(..., title='Log level', description='Log level') - msg: str = Field(..., title='Log message', description='Log message') - - -class CreateLog(LogSchemaBase): - pass - - -class GetLogDetails(LogSchemaBase): - model_config = ConfigDict(from_attributes=True) - - id: int - created_time: datetime - updated_time: datetime | None = None diff --git a/toolserve/toolserve/server/services/artifact_service.py b/toolserve/toolserve/server/services/artifact_service.py deleted file mode 100644 index a21942ac..00000000 --- a/toolserve/toolserve/server/services/artifact_service.py +++ /dev/null @@ -1,47 +0,0 @@ -from typing import Sequence - -from sqlalchemy import select -from toolserve.server.common.exception import errors -from toolserve.server.crud.crud_artifact import artifact_dao -from toolserve.server.database.db_sqlite import async_db_session -from toolserve.server.models.sys_artifact import Artifact -from toolserve.server.schemas.artifact import CreateArtifactParam, ArtifactSchemaBase -from toolserve.server.core.conf import settings -import os - -class ArtifactService: - @staticmethod - async def get(*, pk: int) -> Artifact: - async with async_db_session() as db: - artifact = await artifact_dao.get(db, pk) - if not artifact: - raise errors.NotFoundError(msg='Artifact not found') - return artifact - - @staticmethod - async def get_all_artifacts() -> Sequence[Artifact]: - async with async_db_session() as db: - artifacts = await artifact_dao.get_all(db) - return artifacts - - @staticmethod - async def create(*, obj: CreateArtifactParam) -> None: - async with async_db_session.begin() as db: - artifact = Artifact(name=obj.name, file_path=os.path.join(settings.ARTIFACTS_DIR, obj.name)) - await artifact_dao.create(db, artifact) # This line persists the artifact in the database. - with open(artifact.file_path, 'wb') as file: - file.write(obj.data) - - @staticmethod - async def update(*, pk: int, obj: ArtifactSchemaBase) -> int: - async with async_db_session.begin() as db: - count = await artifact_dao.update(db, pk, obj) - return count - - @staticmethod - async def delete(*, pk: list[int]) -> int: - async with async_db_session.begin() as db: - count = await artifact_dao.delete(db, pk) - return count - -artifact_service: ArtifactService = ArtifactService() diff --git a/toolserve/toolserve/server/services/data_service.py b/toolserve/toolserve/server/services/data_service.py deleted file mode 100644 index 62e09272..00000000 --- a/toolserve/toolserve/server/services/data_service.py +++ /dev/null @@ -1,60 +0,0 @@ -from typing import Sequence - -from sqlalchemy import select -from toolserve.server.common.exception import errors -from toolserve.server.crud.crud_data import data_dao -from toolserve.server.database.db_sqlite import async_db_session -from toolserve.server.models.sys_data import Data -from toolserve.server.schemas.data import CreateDataParam, DataSchemaBase -from toolserve.server.core.conf import settings -import os -import json - -class DataService: - @staticmethod - async def get(*, pk: int) -> Data: - async with async_db_session() as db: - data = await data_dao.get(db, pk) - if not data: - raise errors.NotFoundError(msg='Data not found') - return data - - @staticmethod - async def get_all_data() -> Sequence[Data]: - async with async_db_session() as db: - data_entries = await data_dao.get_all(db) - return data_entries - - @staticmethod - async def create(*, obj: CreateDataParam) -> Data: - async with async_db_session.begin() as db: - file_name = obj.file_name + ".json" if not obj.file_name.endswith(".json") else obj.file_name - obj.file_path = os.path.join(settings.DATA_DIR, file_name) - - data = obj.copy(exclude={'json_blob'}) # Save everything but the data - db_model = await data_dao.create(db, data) # This now returns the ID - - await db.commit() - - # TODO figure out how to save it so it doesnt overwrite the existing file - os.makedirs(os.path.dirname(obj.file_path), exist_ok=True) - with open(obj.file_path, 'w') as file: - file.write(obj.json_blob) - - return db_model - - - @staticmethod - async def update(*, pk: int, obj: DataSchemaBase) -> int: - async with async_db_session.begin() as db: - count = await data_dao.update(db, pk, obj) - return count - - @staticmethod - async def delete(*, pk: list[int]) -> int: - async with async_db_session.begin() as db: - count = await data_dao.delete(db, pk) - return count - - -data_service: DataService = DataService() diff --git a/toolserve/toolserve/server/services/log_service.py b/toolserve/toolserve/server/services/log_service.py deleted file mode 100644 index 72ec8321..00000000 --- a/toolserve/toolserve/server/services/log_service.py +++ /dev/null @@ -1,47 +0,0 @@ - -from typing import Sequence - -from sqlalchemy import Select - -from toolserve.server.common.exception import errors -from toolserve.server.crud.crud_log import log_dao -from toolserve.server.database.db_sqlite import async_db_session -from toolserve.server.models.sys_log import Log -from toolserve.server.schemas.log import CreateLog, LogSchemaBase - - -class LogService: - @staticmethod - async def get(*, pk: int) -> Log: - async with async_db_session() as db: - log = await log_dao.get(db, pk) - if not log: - raise errors.NotFoundError(msg='Log entry not found') - return log - - - @staticmethod - async def get_log_list() -> Sequence[Log]: - async with async_db_session() as db: - logs = await log_dao.get_all(db) - return logs - - @staticmethod - async def create(*, obj: CreateLog) -> None: - async with async_db_session.begin() as db: - await log_dao.create(db, obj) - - @staticmethod - async def update(*, pk: int, obj: LogSchemaBase) -> int: - async with async_db_session.begin() as db: - count = await log_dao.update(db, pk, obj) - return count - - @staticmethod - async def delete(*, pk: list[int]) -> int: - async with async_db_session.begin() as db: - count = await log_dao.delete(db, pk) - return count - - -log_service: LogService = LogService() diff --git a/toolserve/toolserve/server/test.py b/toolserve/toolserve/server/test.py deleted file mode 100644 index d6145adf..00000000 --- a/toolserve/toolserve/server/test.py +++ /dev/null @@ -1,27 +0,0 @@ - -from openai import AsyncOpenAI - -api_key = "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" - -client = AsyncOpenAI(api_key=api_key, base_url="http://localhost:8000/v1") - -# Using 'async' with 'await' for proper asynchronous call -async def get_chat_response(): - response = await client.chat.completions.create( - messages=[ - {"role": "system", "content": "You are a friendly assistant named Jarvis. Help with whatever you can."}, - {"role": "user", "content": "Hey there! What's your name?"}, - ], - model="gpt-4-turbo", - tools=["ReadEmail"], - stream=False - ) - return response - -async def print_chat_responses(): - response = await get_chat_response() - print(response.choices[0].message) - - -import asyncio -asyncio.run(print_chat_responses()) \ No newline at end of file diff --git a/toolserve/toolserve/utils/__init__.py b/toolserve/toolserve/utils/__init__.py deleted file mode 100644 index 4fbf0713..00000000 --- a/toolserve/toolserve/utils/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Utility function to convert CamelCase to snake_case -def camel_to_snake(name): - name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) - return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower() - - -def snake_to_camel(name): - return ''.join(x.capitalize() or '_' for x in name.split('_'))