├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── pyproject.toml ├── requirements-dev.txt ├── requirements.txt ├── src └── mcp_server_tavily │ ├── __init__.py │ ├── __main__.py │ └── server.py ├── tests ├── .env.sample ├── README.md ├── __init__.py ├── compatibility_test.py ├── conftest.py ├── helpers.py ├── run_tests.sh ├── server_test.py ├── test_docker.py ├── test_integration.py ├── test_models.py ├── test_server_api.py └── test_utils.py └── uv.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # SonarQube 7 | .scannerwork/ 8 | 9 | # Tree-sitter 10 | vendor/ 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | pip-wheel-metadata/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | db.sqlite3-journal 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | 137 | # Secrets 138 | creds.py 139 | 140 | # Test environment 141 | #*.ipynb 142 | 143 | # Generated data files 144 | *.gzip 145 | 146 | # DSPy 147 | local_cache/ 148 | 149 | # Cookies 150 | cookies.txt 151 | 152 | # Misc 153 | *~ 154 | 155 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 156 | 157 | # dependencies 158 | /node_modules 159 | /.pnp 160 | .pnp.js 161 | 162 | # testing 163 | /coverage 164 | 165 | # production 166 | /build 167 | 168 | # misc 169 | .DS_Store 170 | .env.local 171 | .env.development.local 172 | .env.test.local 173 | .env.production.local 174 | 175 | npm-debug.log* 176 | yarn-debug.log* 177 | yarn-error.log* 178 | 179 | # VScode 180 | .vscode/ 181 | 182 | # Other 183 | local_cache/ 184 | 185 | # Ruff 186 | .ruff_cache 187 | 188 | # Dapr 189 | .dapr 190 | 191 | # aider 192 | .aider* 193 | 194 | Makefile -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to MCP Tavily 2 | 3 | Thank you for your interest in contributing to MCP Tavily! This document provides guidelines and instructions for contributing to the project. 4 | 5 | ## Project Structure 6 | 7 | ``` 8 | mcp-tavily/ 9 | ├── dist/ # Distribution files 10 | ├── src/ 11 | │ └── mcp_server_tavily/ # Source code 12 | │ ├── __init__.py # Package initialization and CLI 13 | │ ├── __main__.py # Entry point 14 | │ └── server.py # Server implementation 15 | ├── tests/ 16 | │ ├── conftest.py # Test fixtures 17 | │ ├── helpers.py # Test helpers 18 | │ ├── test_models.py # Tests for data models 19 | │ ├── test_utils.py # Tests for utility functions 20 | │ ├── test_server_api.py # Tests for server API handlers 21 | │ └── test_integration.py # Integration tests 22 | ├── .env.sample # Sample environment variables 23 | ├── LICENSE # MIT License 24 | ├── pyproject.toml # Project configuration 25 | ├── README.md # Project documentation 26 | └── uv.lock # Dependency lock file 27 | ``` 28 | 29 | ## Development Setup 30 | 31 | 1. **Clone the repository**: 32 | ```bash 33 | git clone https://github.com/RamXX/mcp-tavily.git 34 | cd mcp-tavily 35 | ``` 36 | 37 | 2. **Create a virtual environment**: 38 | ```bash 39 | python -m venv .venv 40 | source .venv/bin/activate # On Windows: .venv\Scripts\activate 41 | ``` 42 | 43 | 3. **Install dependencies**: 44 | ```bash 45 | uv sync 46 | ``` 47 | 48 | 4. **Install development dependencies**: 49 | ```bash 50 | uv add --dev pytest pytest-asyncio pytest-mock pytest-cov 51 | ``` 52 | 53 | 5. **Install the package in development mode**: 54 | ```bash 55 | uv pip install -e . 56 | ``` 57 | 58 | 6. **Set up your Tavily API key**: 59 | Create a `.env` file in the project root with your Tavily API key: 60 | ``` 61 | TAVILY_API_KEY=your_api_key_here 62 | ``` 63 | 64 | ## Testing 65 | 66 | ### Running Tests 67 | 68 | The project includes a test suite that verifies the functionality of various components: 69 | 70 | ```bash 71 | # Run all tests 72 | ./tests/run_tests.sh 73 | 74 | # Run specific test files 75 | python -m pytest tests/test_models.py 76 | python -m pytest tests/test_utils.py 77 | 78 | # Run with increased verbosity 79 | python -m pytest -v tests/test_models.py 80 | ``` 81 | 82 | ### Test Structure 83 | 84 | - **Model Tests**: Verify the validation and behavior of data models 85 | - **Utility Tests**: Test formatting and parsing functions 86 | - **API Tests**: Test server API handlers and error handling 87 | - **Integration Tests**: Test the complete server behavior 88 | 89 | ### Adding New Tests 90 | 91 | When adding new features, please include appropriate tests: 92 | 93 | 1. For new data models, add tests in `tests/test_models.py` 94 | 2. For utility functions, add tests in `tests/test_utils.py` 95 | 3. For API handlers, add tests in `tests/test_server_api.py` 96 | 4. For integration tests, add tests in `tests/test_integration.py` 97 | 98 | ## Code Style 99 | 100 | Please follow these guidelines when contributing: 101 | 102 | - Use [Black](https://black.readthedocs.io/en/stable/) for code formatting 103 | - Follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guidelines 104 | - Include docstrings for new functions and classes 105 | - Write clear commit messages that explain the "why" behind changes 106 | 107 | ## Pull Request Process 108 | 109 | 1. Fork the repository 110 | 2. Create a feature branch (`git checkout -b feature/amazing-feature`) 111 | 3. Make your changes 112 | 4. Run tests to ensure they pass 113 | 5. Commit your changes with descriptive commit messages 114 | 6. Push to your feature branch 115 | 7. Open a Pull Request against the main repository 116 | 117 | ## License 118 | 119 | By contributing to MCP Tavily, you agree that your contributions will be licensed under the project's MIT License. -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM python:3.13-slim AS builder 3 | 4 | WORKDIR /app 5 | 6 | # Prevent Python from writing .pyc files and buffering stdout/stderr 7 | ENV PYTHONDONTWRITEBYTECODE=1 8 | ENV PYTHONUNBUFFERED=1 9 | 10 | # Install build tools 11 | RUN pip install --upgrade pip wheel build 12 | 13 | # Copy project files 14 | COPY pyproject.toml . 15 | COPY src src 16 | 17 | # Build the project wheel without dependencies 18 | RUN pip wheel . --no-deps --wheel-dir /app/dist 19 | 20 | FROM python:3.13-slim AS runtime 21 | 22 | WORKDIR /app 23 | 24 | # Install the wheel from the builder stage 25 | COPY --from=builder /app/dist/*.whl /app/ 26 | RUN pip install --no-cache-dir /app/*.whl 27 | 28 | # Default entrypoint for the MCP Tavily server 29 | ENTRYPOINT ["python", "-m", "mcp_server_tavily"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024, Ramiro Salas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tavily MCP Server 2 | 3 | A Model Context Protocol server that provides AI-powered web search capabilities using Tavily's search API. This server enables LLMs to perform sophisticated web searches, get direct answers to questions, and search recent news articles with AI-extracted relevant content. 4 | 5 | ## Features 6 | 7 | ### Available Tools 8 | 9 | - `tavily_web_search` - Performs comprehensive web searches with AI-powered content extraction. 10 | 11 | - `query` (string, required): Search query 12 | - `max_results` (integer, optional): Maximum number of results to return (default: 5, max: 20) 13 | - `search_depth` (string, optional): Either "basic" or "advanced" search depth (default: "basic") 14 | - `include_domains` (list or string, optional): List of domains to specifically include in results 15 | - `exclude_domains` (list or string, optional): List of domains to exclude from results 16 | 17 | - `tavily_answer_search` - Performs web searches and generates direct answers with supporting evidence. 18 | 19 | - `query` (string, required): Search query 20 | - `max_results` (integer, optional): Maximum number of results to return (default: 5, max: 20) 21 | - `search_depth` (string, optional): Either "basic" or "advanced" search depth (default: "advanced") 22 | - `include_domains` (list or string, optional): List of domains to specifically include in results 23 | - `exclude_domains` (list or string, optional): List of domains to exclude from results 24 | 25 | - `tavily_news_search` - Searches recent news articles with publication dates. 26 | - `query` (string, required): Search query 27 | - `max_results` (integer, optional): Maximum number of results to return (default: 5, max: 20) 28 | - `days` (integer, optional): Number of days back to search (default: 3) 29 | - `include_domains` (list or string, optional): List of domains to specifically include in results 30 | - `exclude_domains` (list or string, optional): List of domains to exclude from results 31 | 32 | ### Prompts 33 | 34 | The server also provides prompt templates for each search type: 35 | 36 | - **tavily_web_search** - Search the web using Tavily's AI-powered search engine 37 | - **tavily_answer_search** - Search the web and get an AI-generated answer with supporting evidence 38 | - **tavily_news_search** - Search recent news articles with Tavily's news search 39 | 40 | ## Prerequisites 41 | 42 | - Python 3.11 or later 43 | - A Tavily API key (obtain from [Tavily's website](https://tavily.com)) 44 | - `uv` Python package manager (recommended) 45 | 46 | ## Installation 47 | 48 | ### Option 1: Using pip or uv 49 | 50 | ```bash 51 | # With pip 52 | pip install mcp-tavily 53 | 54 | # Or with uv (recommended) 55 | uv add mcp-tavily 56 | ``` 57 | 58 | You should see output similar to: 59 | 60 | ``` 61 | Resolved packages: mcp-tavily, mcp, pydantic, python-dotenv, tavily-python [...] 62 | Successfully installed mcp-tavily-0.1.4 mcp-1.0.0 [...] 63 | ``` 64 | 65 | ### Option 2: From source 66 | 67 | ```bash 68 | # Clone the repository 69 | git clone https://github.com/RamXX/mcp-tavily.git 70 | cd mcp-tavily 71 | 72 | # Create a virtual environment (optional but recommended) 73 | python -m venv .venv 74 | source .venv/bin/activate # On Windows: .venv\Scripts\activate 75 | 76 | # Install dependencies and build 77 | uv sync # Or: pip install -r requirements.txt 78 | uv build # Or: pip install -e . 79 | 80 | # To install with test dependencies: 81 | uv sync --dev # Or: pip install -r requirements-dev.txt 82 | ``` 83 | 84 | During installation, you should see the package being built and installed with its dependencies. 85 | 86 | ### Usage with VS Code 87 | 88 | For quick installation, use one of the one-click install buttons below: 89 | 90 | [![Install with UV in VS Code](https://img.shields.io/badge/VS_Code-UV-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=tavily&inputs=%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22apiKey%22%2C%22description%22%3A%22Tavily%20API%20Key%22%2C%22password%22%3Atrue%7D%5D&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22mcp-tavily%22%5D%2C%22env%22%3A%7B%22TAVILY_API_KEY%22%3A%22%24%7Binput%3AapiKey%7D%22%7D%7D) [![Install with UV in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-UV-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=tavily&inputs=%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22apiKey%22%2C%22description%22%3A%22Tavily%20API%20Key%22%2C%22password%22%3Atrue%7D%5D&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22mcp-tavily%22%5D%2C%22env%22%3A%7B%22TAVILY_API_KEY%22%3A%22%24%7Binput%3AapiKey%7D%22%7D%7D&quality=insiders) 91 | 92 | For manual installation, add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`. 93 | 94 | Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others. 95 | 96 | > Note that the `mcp` key is not needed in the `.vscode/mcp.json` file. 97 | 98 | ```json 99 | { 100 | "mcp": { 101 | "inputs": [ 102 | { 103 | "type": "promptString", 104 | "id": "apiKey", 105 | "description": "Tavily API Key", 106 | "password": true 107 | } 108 | ], 109 | "servers": { 110 | "tavily": { 111 | "command": "uvx", 112 | "args": ["mcp-tavily"], 113 | "env": { 114 | "TAVILY_API_KEY": "${input:apiKey}" 115 | } 116 | } 117 | } 118 | } 119 | } 120 | ``` 121 | 122 | ## Configuration 123 | 124 | ### API Key Setup 125 | 126 | The server requires a Tavily API key, which can be provided in three ways: 127 | 128 | 1. Through a `.env` file in your project directory: 129 | 130 | ``` 131 | TAVILY_API_KEY=your_api_key_here 132 | ``` 133 | 134 | 2. As an environment variable: 135 | 136 | ```bash 137 | export TAVILY_API_KEY=your_api_key_here 138 | ``` 139 | 140 | 3. As a command-line argument: 141 | ```bash 142 | python -m mcp_server_tavily --api-key=your_api_key_here 143 | ``` 144 | 145 | ### Configure for Claude.app 146 | 147 | Add to your Claude settings: 148 | 149 | ```json 150 | "mcpServers": { 151 | "tavily": { 152 | "command": "python", 153 | "args": ["-m", "mcp_server_tavily"] 154 | }, 155 | "env": { 156 | "TAVILY_API_KEY": "your_api_key_here" 157 | } 158 | } 159 | ``` 160 | 161 | If you encounter issues, you may need to specify the full path to your Python interpreter. Run `which python` to find the exact path. 162 | 163 | ## Usage Examples 164 | 165 | For a regular web search: 166 | 167 | ``` 168 | Tell me about Anthropic's newly released MCP protocol 169 | ``` 170 | 171 | To generate a report with domain filtering: 172 | 173 | ``` 174 | Tell me about redwood trees. Please use MLA format in markdown syntax and include the URLs in the citations. Exclude Wikipedia sources. 175 | ``` 176 | 177 | To use answer search mode for direct answers: 178 | 179 | ``` 180 | I want a concrete answer backed by current web sources: What is the average lifespan of redwood trees? 181 | ``` 182 | 183 | For news search: 184 | 185 | ``` 186 | Give me the top 10 AI-related news in the last 5 days 187 | ``` 188 | 189 | ## Testing 190 | 191 | The project includes a comprehensive test suite. To run the tests: 192 | 193 | 1. Install test dependencies: 194 | 195 | ```bash 196 | source .venv/bin/activate # If using a virtual environment 197 | uv sync --dev # Or: pip install -r requirements-dev.txt 198 | ``` 199 | 200 | 2. Run the tests: 201 | ```bash 202 | ./tests/run_tests.sh 203 | ``` 204 | 205 | You should see output similar to: 206 | 207 | ``` 208 | ======================================================= test session starts ======================================================== 209 | platform darwin -- Python 3.13.3, pytest-8.3.5, pluggy-1.5.0 210 | rootdir: /Users/ramirosalas/workspace/mcp-tavily 211 | configfile: pyproject.toml 212 | plugins: cov-6.0.0, asyncio-0.25.3, anyio-4.8.0, mock-3.14.0 213 | asyncio: mode=Mode.STRICT, asyncio_default_fixture_loop_scope=function 214 | collected 50 items 215 | 216 | tests/test_docker.py .. [ 4%] 217 | tests/test_integration.py ..... [ 14%] 218 | tests/test_models.py ................. [ 48%] 219 | tests/test_server_api.py ..................... [ 90%] 220 | tests/test_utils.py ..... [100%] 221 | 222 | ---------- coverage: platform darwin, python 3.13.3-final-0 ---------- 223 | Name Stmts Miss Cover 224 | ------------------------------------------------------- 225 | src/mcp_server_tavily/__init__.py 16 2 88% 226 | src/mcp_server_tavily/__main__.py 2 2 0% 227 | src/mcp_server_tavily/server.py 149 16 89% 228 | ------------------------------------------------------- 229 | TOTAL 167 20 88% 230 | ``` 231 | 232 | The test suite includes tests for data models, utility functions, integration testing, error handling, and parameter validation. It focuses on verifying that all API capabilities work correctly, including handling of domain filters and various input formats. 233 | 234 | ## Docker 235 | 236 | Build the Docker image: 237 | 238 | ```bash 239 | make docker-build 240 | ``` 241 | 242 | Alternatively, build directly with Docker: 243 | 244 | ```bash 245 | docker build -t mcp_tavily . 246 | ``` 247 | 248 | Run a detached Docker container (default name `mcp_tavily_container`, port 8000 → 8000): 249 | 250 | ```bash 251 | make docker-run 252 | ``` 253 | 254 | Or manually: 255 | 256 | ```bash 257 | docker run -d --name mcp_tavily_container \ 258 | -e TAVILY_API_KEY=your_api_key_here \ 259 | -p 8000:8000 mcp_tavily 260 | ``` 261 | 262 | Stop and remove the container: 263 | 264 | ```bash 265 | make docker-stop 266 | ``` 267 | 268 | Follow container logs: 269 | 270 | ```bash 271 | make docker-logs 272 | ``` 273 | 274 | You can override defaults by setting environment variables: 275 | - DOCKER_IMAGE: image name (default `mcp_tavily`) 276 | - DOCKER_CONTAINER: container name (default `mcp_tavily_container`) 277 | - HOST_PORT: host port to bind (default `8000`) 278 | - CONTAINER_PORT: container port (default `8000`) 279 | 280 | ## Debugging 281 | 282 | You can use the MCP inspector to debug the server: 283 | 284 | ```bash 285 | # Using npx 286 | npx @modelcontextprotocol/inspector python -m mcp_server_tavily 287 | 288 | # For development 289 | cd path/to/mcp-tavily 290 | npx @modelcontextprotocol/inspector python -m mcp_server_tavily 291 | ``` 292 | 293 | ## Contributing 294 | 295 | We welcome contributions to improve mcp-tavily! Here's how you can help: 296 | 297 | 1. Fork the repository 298 | 2. Create a feature branch (`git checkout -b feature/amazing-feature`) 299 | 3. Make your changes 300 | 4. Run tests to ensure they pass 301 | 5. Commit your changes (`git commit -m 'Add amazing feature'`) 302 | 6. Push to the branch (`git push origin feature/amazing-feature`) 303 | 7. Open a Pull Request 304 | 305 | For examples of other MCP servers and implementation patterns, see: 306 | https://github.com/modelcontextprotocol/servers 307 | 308 | ## License 309 | 310 | mcp-tavily is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 311 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mcp_tavily" 3 | version = "0.1.8" 4 | description = "A Model Context Protocol server that provides AI-powered web search capabilities using Tavily's search API" 5 | readme = "README.md" 6 | license = {text = "MIT"} 7 | requires-python = ">=3.11" 8 | dependencies = [ 9 | "mcp>=1.0.0", 10 | "pydantic>=2.10.2", 11 | "python-dotenv>=1.0.1", 12 | "tavily-python>=0.5.0", 13 | ] 14 | 15 | [project.optional-dependencies] 16 | test = [ 17 | "pytest>=7.0.0", 18 | "pytest-asyncio>=0.23.0", 19 | "pytest-mock>=3.10.0", 20 | "pytest-cov>=4.1.0", 21 | ] 22 | 23 | [tool.pytest.ini_options] 24 | testpaths = ["tests"] 25 | python_files = ["test_*.py"] 26 | pythonpath = ["src"] 27 | addopts = ["--asyncio-mode=strict", "-W", "ignore::RuntimeWarning"] 28 | asyncio_default_fixture_loop_scope = "function" 29 | 30 | [dependency-groups] 31 | dev = [ 32 | "pytest>=8.3.5", 33 | "pytest-asyncio>=0.25.3", 34 | "pytest-cov>=6.0.0", 35 | "pytest-mock>=3.14.0", 36 | ] 37 | 38 | [build-system] 39 | requires = ["setuptools==67.8.0"] 40 | build-backend = "setuptools.build_meta" 41 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # Include main dependencies 2 | -r requirements.txt 3 | 4 | # build 5 | build 6 | # Test dependencies 7 | pytest>=8.3.5 8 | pytest-asyncio>=0.25.3 9 | pytest-cov>=6.0.0 10 | pytest-mock>=3.14.0 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Main dependencies 2 | mcp>=1.0.0 3 | pydantic>=2.10.2 4 | python-dotenv>=1.0.1 5 | tavily-python>=0.5.0 6 | 7 | # Test dependencies (optional) 8 | # To install test dependencies: pip install -r requirements-dev.txt -------------------------------------------------------------------------------- /src/mcp_server_tavily/__init__.py: -------------------------------------------------------------------------------- 1 | from .server import serve 2 | from dotenv import load_dotenv 3 | 4 | 5 | def main(): 6 | """MCP Tavily Server - AI-powered web search functionality for MCP""" 7 | import argparse 8 | import asyncio 9 | import os 10 | 11 | # Load environment variables from .env file 12 | load_dotenv() 13 | 14 | parser = argparse.ArgumentParser( 15 | description="give a model the ability to perform AI-powered web searches using Tavily" 16 | ) 17 | parser.add_argument( 18 | "--api-key", 19 | type=str, 20 | help="Tavily API key (can also be set via TAVILY_API_KEY environment variable)", 21 | ) 22 | 23 | args = parser.parse_args() 24 | 25 | # Check for API key in args first, then environment 26 | api_key = args.api_key or os.getenv("TAVILY_API_KEY") 27 | if not api_key: 28 | parser.error("Tavily API key must be provided either via --api-key or TAVILY_API_KEY environment variable") 29 | 30 | asyncio.run(serve(api_key)) 31 | 32 | 33 | if __name__ == "__main__": 34 | main() -------------------------------------------------------------------------------- /src/mcp_server_tavily/__main__.py: -------------------------------------------------------------------------------- 1 | # __main__.py 2 | 3 | from mcp_server_tavily import main 4 | 5 | main() 6 | -------------------------------------------------------------------------------- /src/mcp_server_tavily/server.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | from mcp.server import Server 3 | from mcp.shared.exceptions import McpError 4 | from mcp.types import ErrorData 5 | from mcp.server.stdio import stdio_server 6 | from mcp.types import ( 7 | GetPromptResult, 8 | Prompt, 9 | PromptArgument, 10 | PromptMessage, 11 | TextContent, 12 | Tool, 13 | INVALID_PARAMS, 14 | INTERNAL_ERROR, 15 | ) 16 | from pydantic import BaseModel, Field 17 | from tavily import TavilyClient, InvalidAPIKeyError, UsageLimitExceededError 18 | 19 | from typing import Literal 20 | 21 | import json 22 | import asyncio 23 | from pydantic import field_validator 24 | 25 | class SearchBase(BaseModel): 26 | """Base parameters for Tavily search.""" 27 | query: Annotated[str, Field(description="Search query")] 28 | max_results: Annotated[ 29 | int, 30 | Field( 31 | default=5, 32 | description="Maximum number of results to return", 33 | gt=0, 34 | lt=20, 35 | ), 36 | ] 37 | include_domains: Annotated[ 38 | list[str] | None, 39 | Field( 40 | default=None, 41 | description="List of domains to specifically include in the search results (e.g. ['example.com', 'test.org'] or 'example.com')", 42 | ), 43 | ] 44 | exclude_domains: Annotated[ 45 | list[str] | None, 46 | Field( 47 | default=None, 48 | description="List of domains to specifically exclude from the search results (e.g. ['example.com', 'test.org'] or 'example.com')", 49 | ), 50 | ] 51 | 52 | @field_validator('include_domains', 'exclude_domains', mode='before') 53 | @classmethod 54 | def parse_domains_list(cls, v): 55 | """Parse domain lists from various input formats. 56 | 57 | Handles: 58 | - None -> [] 59 | - String JSON arrays -> list 60 | - Single domain string -> [string] 61 | - Comma-separated string -> list of domains 62 | - List of domains -> unchanged 63 | """ 64 | if v is None: 65 | return [] 66 | if isinstance(v, list): 67 | return [domain.strip() for domain in v if domain.strip()] 68 | if isinstance(v, str): 69 | v = v.strip() 70 | if not v: 71 | return [] 72 | try: 73 | # Try to parse as JSON string 74 | parsed = json.loads(v) 75 | if isinstance(parsed, list): 76 | return [domain.strip() for domain in parsed if domain.strip()] 77 | return [parsed.strip()] # Single value from JSON 78 | except json.JSONDecodeError: 79 | # Not JSON, check if comma-separated 80 | if ',' in v: 81 | return [domain.strip() for domain in v.split(',') if domain.strip()] 82 | return [v] # Single domain 83 | return [] 84 | 85 | class GeneralSearch(SearchBase): 86 | """Parameters for general web search.""" 87 | search_depth: Annotated[ 88 | Literal["basic", "advanced"], 89 | Field( 90 | default="basic", 91 | description="Depth of search - 'basic' or 'advanced'", 92 | ), 93 | ] 94 | 95 | class AnswerSearch(SearchBase): 96 | """Parameters for search with answer.""" 97 | search_depth: Annotated[ 98 | Literal["basic", "advanced"], 99 | Field( 100 | default="advanced", 101 | description="Depth of search - 'basic' or 'advanced'", 102 | ), 103 | ] 104 | 105 | class NewsSearch(SearchBase): 106 | """Parameters for news search.""" 107 | days: Annotated[ 108 | int | None, 109 | Field( 110 | default=None, 111 | description="Number of days back to search (default is 3)", 112 | gt=0, 113 | le=365, 114 | ), 115 | ] 116 | 117 | async def serve(api_key: str) -> None: 118 | """Run the Tavily MCP server. 119 | 120 | Args: 121 | api_key: Tavily API key 122 | """ 123 | # Ensure we don't have any lingering tasks 124 | for task in asyncio.all_tasks(): 125 | if task is not asyncio.current_task() and task.get_name().startswith('tavily_'): 126 | task.cancel() 127 | 128 | server = Server("mcp-tavily") 129 | client = TavilyClient(api_key=api_key) 130 | 131 | @server.list_tools() 132 | async def list_tools() -> list[Tool]: 133 | return [ 134 | Tool( 135 | name="tavily_web_search", 136 | description="""Performs a comprehensive web search using Tavily's AI-powered search engine. 137 | Excels at extracting and summarizing relevant content from web pages, making it ideal for research, 138 | fact-finding, and gathering detailed information. Can run in either 'basic' mode for faster, simpler searches 139 | or 'advanced' mode for more thorough analysis. Basic is cheaper and good for most use cases. 140 | Supports filtering results by including or excluding specific domains. 141 | Use include_domains/exclude_domains parameters to filter by specific websites. 142 | Returns multiple search results with AI-extracted relevant content.""", 143 | inputSchema=GeneralSearch.model_json_schema(), 144 | ), 145 | Tool( 146 | name="tavily_answer_search", 147 | description="""Performs a web search using Tavily's AI search engine and generates a direct answer to the query, 148 | along with supporting search results. Best used for questions that need concrete answers backed by current web sources. 149 | Uses advanced search depth by default for comprehensive analysis. 150 | 151 | Features powerful source control through domain filtering: 152 | - For academic research: exclude_domains=["wikipedia.org"] for more scholarly sources 153 | - For financial analysis: include_domains=["wsj.com", "bloomberg.com", "ft.com"] 154 | - For technical documentation: include_domains=["docs.python.org", "developer.mozilla.org"] 155 | - For scientific papers: include_domains=["nature.com", "sciencedirect.com"] 156 | - Can combine includes and excludes to fine-tune your sources 157 | 158 | Particularly effective for factual queries, technical questions, and queries requiring synthesis of multiple sources.""", 159 | inputSchema=AnswerSearch.model_json_schema(), 160 | ), 161 | Tool( 162 | name="tavily_news_search", 163 | description="""Searches recent news articles using Tavily's specialized news search functionality. 164 | Ideal for current events, recent developments, and trending topics. Can filter results by recency 165 | (number of days back to search) and by including or excluding specific news domains. 166 | 167 | Powerful domain filtering for news sources: 168 | - For mainstream news: include_domains=["reuters.com", "apnews.com", "bbc.com"] 169 | - For financial news: include_domains=["bloomberg.com", "wsj.com", "ft.com"] 170 | - For tech news: include_domains=["techcrunch.com", "theverge.com"] 171 | - To exclude paywalled content: exclude_domains=["wsj.com", "ft.com"] 172 | - To focus on specific regions: include_domains=["bbc.co.uk"] for UK news 173 | 174 | Returns news articles with publication dates and relevant excerpts.""", 175 | inputSchema=NewsSearch.model_json_schema(), 176 | ), 177 | ] 178 | 179 | @server.list_prompts() 180 | async def list_prompts() -> list[Prompt]: 181 | return [ 182 | Prompt( 183 | name="tavily_web_search", 184 | description="Search the web using Tavily's AI-powered search engine", 185 | arguments=[ 186 | PromptArgument( 187 | name="query", 188 | description="Search query", 189 | required=True, 190 | ), 191 | PromptArgument( 192 | name="include_domains", 193 | description="Optional list of domains to specifically include (e.g., 'wsj.com,bloomberg.com' for financial sources, 'nature.com,sciencedirect.com' for scientific sources)", 194 | required=False, 195 | ), 196 | PromptArgument( 197 | name="exclude_domains", 198 | description="Optional list of domains to exclude from results (e.g., 'wikipedia.org' to exclude Wikipedia, or 'wsj.com,ft.com' to exclude paywalled sources)", 199 | required=False, 200 | ), 201 | ], 202 | ), 203 | Prompt( 204 | name="tavily_answer_search", 205 | description="Search the web and get an AI-generated answer with supporting evidence", 206 | arguments=[ 207 | PromptArgument( 208 | name="query", 209 | description="Search query", 210 | required=True, 211 | ), 212 | PromptArgument( 213 | name="include_domains", 214 | description="Optional comma-separated list of domains to include", 215 | required=False, 216 | ), 217 | PromptArgument( 218 | name="exclude_domains", 219 | description="Optional comma-separated list of domains to exclude", 220 | required=False, 221 | ), 222 | ], 223 | ), 224 | Prompt( 225 | name="tavily_news_search", 226 | description="Search recent news articles with Tavily's news search", 227 | arguments=[ 228 | PromptArgument( 229 | name="query", 230 | description="Search query", 231 | required=True, 232 | ), 233 | PromptArgument( 234 | name="days", 235 | description="Number of days back to search", 236 | required=False, 237 | ), 238 | PromptArgument( 239 | name="include_domains", 240 | description="Optional comma-separated list of domains to include", 241 | required=False, 242 | ), 243 | PromptArgument( 244 | name="exclude_domains", 245 | description="Optional comma-separated list of domains to exclude", 246 | required=False, 247 | ), 248 | ], 249 | ), 250 | ] 251 | 252 | def format_results(response: dict) -> str: 253 | """Format Tavily search results into a readable string.""" 254 | output = [] 255 | 256 | # Add domain filter information if present 257 | if response.get("included_domains") or response.get("excluded_domains"): 258 | filters = [] 259 | if response.get("included_domains"): 260 | filters.append(f"Including domains: {', '.join(response['included_domains'])}") 261 | if response.get("excluded_domains"): 262 | filters.append(f"Excluding domains: {', '.join(response['excluded_domains'])}") 263 | output.append("Search Filters:") 264 | output.extend(filters) 265 | output.append("") # Empty line for separation 266 | 267 | if response.get("answer"): 268 | output.append(f"Answer: {response['answer']}") 269 | output.append("\nSources:") 270 | # Add immediate source references for the answer 271 | for result in response["results"]: 272 | output.append(f"- {result['title']}: {result['url']}") 273 | output.append("") # Empty line for separation 274 | 275 | output.append("Detailed Results:") 276 | for result in response["results"]: 277 | output.append(f"\nTitle: {result['title']}") 278 | output.append(f"URL: {result['url']}") 279 | output.append(f"Content: {result['content']}") 280 | if result.get("published_date"): 281 | output.append(f"Published: {result['published_date']}") 282 | 283 | return "\n".join(output) 284 | 285 | @server.call_tool() 286 | async def call_tool(name: str, arguments: dict) -> list[TextContent]: 287 | try: 288 | if name == "tavily_web_search": 289 | args = GeneralSearch(**arguments) 290 | response = client.search( 291 | query=args.query, 292 | max_results=args.max_results, 293 | search_depth=args.search_depth, 294 | include_domains=args.include_domains or [], # Convert None to empty list 295 | exclude_domains=args.exclude_domains or [], # Convert None to empty list 296 | ) 297 | elif name == "tavily_answer_search": 298 | args = AnswerSearch(**arguments) 299 | response = client.search( 300 | query=args.query, 301 | max_results=args.max_results, 302 | search_depth=args.search_depth, 303 | include_answer=True, 304 | include_domains=args.include_domains or [], # Convert None to empty list 305 | exclude_domains=args.exclude_domains or [], # Convert None to empty list 306 | ) 307 | elif name == "tavily_news_search": 308 | args = NewsSearch(**arguments) 309 | response = client.search( 310 | query=args.query, 311 | max_results=args.max_results, 312 | topic="news", 313 | days=args.days if args.days is not None else 3, 314 | include_domains=args.include_domains or [], 315 | exclude_domains=args.exclude_domains or [], 316 | ) 317 | else: 318 | raise ValueError(f"Unknown tool: {name}") 319 | 320 | # Add domain filter information to response for formatting 321 | if args.include_domains: 322 | response["included_domains"] = args.include_domains 323 | if args.exclude_domains: 324 | response["excluded_domains"] = args.exclude_domains 325 | 326 | except (InvalidAPIKeyError, UsageLimitExceededError) as e: 327 | raise McpError(ErrorData(code=INTERNAL_ERROR, message=str(e))) 328 | except ValueError as e: 329 | raise McpError(ErrorData(code=INVALID_PARAMS, message=str(e))) 330 | 331 | return [TextContent( 332 | type="text", 333 | text=format_results(response), 334 | )] 335 | 336 | @server.get_prompt() 337 | async def get_prompt(name: str, arguments: dict | None) -> GetPromptResult: 338 | if not arguments or "query" not in arguments: 339 | raise McpError(ErrorData(code=INVALID_PARAMS, message="Query is required")) 340 | 341 | try: 342 | # Parse domain filters if provided 343 | include_domains = None 344 | exclude_domains = None 345 | if "include_domains" in arguments: 346 | include_domains = SearchBase.parse_domains_list(arguments["include_domains"]) 347 | if "exclude_domains" in arguments: 348 | exclude_domains = SearchBase.parse_domains_list(arguments["exclude_domains"]) 349 | 350 | if name == "tavily_web_search": 351 | response = client.search( 352 | query=arguments["query"], 353 | include_domains=include_domains or [], # Convert None to empty list 354 | exclude_domains=exclude_domains or [], # Convert None to empty list 355 | ) 356 | elif name == "tavily_answer_search": 357 | response = client.search( 358 | query=arguments["query"], 359 | include_answer=True, 360 | search_depth="advanced", 361 | include_domains=include_domains or [], 362 | exclude_domains=exclude_domains or [], 363 | ) 364 | elif name == "tavily_news_search": 365 | days = arguments.get("days") 366 | response = client.search( 367 | query=arguments["query"], 368 | topic="news", 369 | days=int(days) if days else 3, 370 | include_domains=include_domains or [], 371 | exclude_domains=exclude_domains or [], 372 | ) 373 | else: 374 | raise McpError(ErrorData(code=INVALID_PARAMS, message=f"Unknown prompt: {name}")) 375 | 376 | # Add domain filter information to response for formatting 377 | if include_domains: 378 | response["included_domains"] = include_domains 379 | if exclude_domains: 380 | response["excluded_domains"] = exclude_domains 381 | 382 | except (InvalidAPIKeyError, UsageLimitExceededError) as e: 383 | return GetPromptResult( 384 | description=f"Failed to search: {str(e)}", 385 | messages=[ 386 | PromptMessage( 387 | role="user", 388 | content=TextContent(type="text", text=str(e)), 389 | ) 390 | ], 391 | ) 392 | 393 | return GetPromptResult( 394 | description=f"Search results for: {arguments['query']}", 395 | messages=[ 396 | PromptMessage( 397 | role="user", 398 | content=TextContent(type="text", text=format_results(response)), 399 | ) 400 | ], 401 | ) 402 | 403 | options = server.create_initialization_options() 404 | async with stdio_server() as (read_stream, write_stream): 405 | try: 406 | await server.run(read_stream, write_stream, options, raise_exceptions=True) 407 | finally: 408 | # Clean up any lingering tasks 409 | for task in asyncio.all_tasks(): 410 | if task is not asyncio.current_task() and task.get_name().startswith('tavily_'): 411 | task.cancel() 412 | try: 413 | await asyncio.wait_for(task, timeout=0.1) 414 | except (asyncio.CancelledError, asyncio.TimeoutError): 415 | pass 416 | 417 | if __name__ == "__main__": 418 | import asyncio 419 | import os 420 | from dotenv import load_dotenv 421 | 422 | # Load environment variables from .env file 423 | load_dotenv() 424 | 425 | api_key = os.getenv("TAVILY_API_KEY") 426 | if not api_key: 427 | raise ValueError("TAVILY_API_KEY environment variable is required") 428 | 429 | asyncio.run(serve(api_key)) 430 | -------------------------------------------------------------------------------- /tests/.env.sample: -------------------------------------------------------------------------------- 1 | # Sample .env file for testing 2 | # Copy this file to .env and replace the value with your actual Tavily API key 3 | TAVILY_API_KEY=sample_api_key_for_testing -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Tests for MCP Tavily 2 | 3 | This directory contains tests for the MCP Tavily server, which provides web search functionality via the Tavily API. 4 | 5 | ## Test Structure 6 | 7 | - `test_models.py` - Tests for the data models and validation 8 | - `test_utils.py` - Tests for utility functions like `format_results` 9 | - `test_server_api.py` - Tests for server API handlers (list_tools, call_tool, etc.) 10 | - `test_integration.py` - Integration tests for the server as a whole 11 | 12 | ## Running Tests 13 | 14 | Make sure your virtual environment is activated, then run: 15 | 16 | ```bash 17 | ./tests/run_tests.sh 18 | ``` 19 | 20 | This will: 21 | 1. Install test dependencies using `uv` 22 | 2. Run tests with coverage reporting 23 | 3. Generate a coverage report in the `htmlcov` directory 24 | 25 | ## Test Coverage 26 | 27 | The tests cover: 28 | - Data model validation and parameter validation 29 | - Parameter parsing (especially domain lists) 30 | - Utility functions for formatting results 31 | - Command-line interface 32 | - Error handling (API errors, validation errors) 33 | - Domain filtering functionality 34 | - String-to-int conversion of numeric parameters 35 | - JSON input formats 36 | 37 | Current test coverage is around 46%, focusing on the most critical parts of the API functionality. The remaining uncovered areas mostly involve the stdio server interaction and parts of the main server loop, which depend on the MCP framework's components. -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Test package initialization -------------------------------------------------------------------------------- /tests/compatibility_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Basic compatibility test for MCP Tavily. 4 | This script verifies that the package can be imported and basic classes can be instantiated. 5 | """ 6 | 7 | import sys 8 | import importlib 9 | import asyncio 10 | 11 | 12 | def run_compatibility_test(): 13 | """Run basic compatibility tests to verify the package works.""" 14 | print(f"Testing with Python {sys.version}") 15 | 16 | # Test importing the package 17 | print("Testing imports...") 18 | import mcp_server_tavily 19 | from mcp_server_tavily.server import SearchBase, GeneralSearch, AnswerSearch, NewsSearch 20 | print("✓ Imports successful") 21 | 22 | # Test basic class instantiation 23 | print("Testing model instantiation...") 24 | test_model = SearchBase(query="test query") 25 | assert test_model.query == "test query" 26 | assert test_model.max_results == 5 27 | 28 | # Test domain parsing 29 | domains = SearchBase.parse_domains_list("example.com,test.org") 30 | assert domains == ["example.com", "test.org"] 31 | print("✓ Models working correctly") 32 | 33 | # Print successful result 34 | print("\n✅ Compatibility test passed!") 35 | 36 | 37 | if __name__ == "__main__": 38 | run_compatibility_test() -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | import asyncio 4 | from unittest.mock import MagicMock, patch 5 | from typing import Dict, List, Any, Optional 6 | from dotenv import load_dotenv 7 | 8 | # Create a custom AsyncMock that's safer for our tests 9 | class SafeAsyncMock: 10 | def __init__(self, return_value=None): 11 | self._return_value = return_value if return_value is not None else None 12 | self.call_args = None 13 | self.call_count = 0 14 | self.call_args_list = [] 15 | 16 | async def __call__(self, *args, **kwargs): 17 | self.call_args = (args, kwargs) 18 | self.call_args_list.append(self.call_args) 19 | self.call_count += 1 20 | if isinstance(self._return_value, asyncio.Future): 21 | return await self._return_value 22 | elif asyncio.iscoroutine(self._return_value): 23 | return await self._return_value 24 | else: 25 | return self._return_value 26 | 27 | # Load environment variables from .env file or .env.sample if .env doesn't exist 28 | if os.path.exists(os.path.join(os.path.dirname(__file__), '.env')): 29 | load_dotenv(os.path.join(os.path.dirname(__file__), '.env')) 30 | else: 31 | load_dotenv(os.path.join(os.path.dirname(__file__), '.env.sample')) 32 | 33 | 34 | @pytest.fixture 35 | def mock_tavily_client(): 36 | """Mock the TavilyClient to avoid real API calls during tests.""" 37 | with patch('mcp_server_tavily.server.TavilyClient') as mock_client_class: 38 | client_instance = MagicMock() 39 | # Use regular MagicMock for synchronous methods 40 | client_instance.search = MagicMock() 41 | mock_client_class.return_value = client_instance 42 | yield client_instance 43 | 44 | 45 | @pytest.fixture 46 | def mock_server(): 47 | """Mock the MCP Server to test server functions.""" 48 | with patch('mcp_server_tavily.server.Server') as mock_server_class: 49 | server_instance = MagicMock() 50 | 51 | # Set up mocks for decorator methods 52 | handler_registry = {} 53 | 54 | def mock_decorator(name): 55 | def decorator(func): 56 | handler_registry[name] = func 57 | return func 58 | return decorator 59 | 60 | # Create decorator functions that return decorator functions 61 | server_instance.list_tools = MagicMock(return_value=mock_decorator('list_tools')) 62 | server_instance.list_prompts = MagicMock(return_value=mock_decorator('list_prompts')) 63 | server_instance.call_tool = MagicMock(return_value=mock_decorator('call_tool')) 64 | server_instance.get_prompt = MagicMock(return_value=mock_decorator('get_prompt')) 65 | 66 | # For accessing the registered handlers 67 | server_instance.handler_registry = handler_registry 68 | 69 | # Ensure these methods are called during serve() 70 | server_instance.create_initialization_options = MagicMock(return_value={}) 71 | 72 | # Use our SafeAsyncMock for the server.run method 73 | server_instance.run = SafeAsyncMock(return_value=None) 74 | 75 | mock_server_class.return_value = server_instance 76 | yield server_instance 77 | 78 | 79 | @pytest.fixture 80 | def mock_stdio_server(): 81 | """Mock the stdio_server to test server run function.""" 82 | with patch('mcp_server_tavily.server.stdio_server') as mock_stdio: 83 | # Create a context manager that can be entered and exited without error 84 | mock_context = MagicMock() 85 | 86 | # Create proper SafeAsyncMock that returns a future 87 | enter_future = asyncio.Future() 88 | enter_future.set_result((MagicMock(), MagicMock())) 89 | 90 | exit_future = asyncio.Future() 91 | exit_future.set_result(None) 92 | 93 | mock_context.__aenter__ = SafeAsyncMock(return_value=enter_future) 94 | mock_context.__aexit__ = SafeAsyncMock(return_value=exit_future) 95 | mock_stdio.return_value = mock_context 96 | yield mock_stdio 97 | 98 | 99 | @pytest.fixture 100 | def server_handlers(mock_server): 101 | """Return the registered handlers after calling serve.""" 102 | import asyncio 103 | from mcp_server_tavily.server import serve 104 | # Run serve to register all handlers 105 | asyncio.run(serve("fake_api_key")) 106 | return mock_server.handler_registry 107 | 108 | 109 | @pytest.fixture 110 | def web_search_response(): 111 | """Sample response for web search.""" 112 | return { 113 | "results": [ 114 | { 115 | "title": "Sample Result 1", 116 | "url": "https://example.com/1", 117 | "content": "This is sample content from the first result." 118 | }, 119 | { 120 | "title": "Sample Result 2", 121 | "url": "https://example.com/2", 122 | "content": "This is sample content from the second result." 123 | } 124 | ] 125 | } 126 | 127 | 128 | @pytest.fixture 129 | def answer_search_response(): 130 | """Sample response for answer search.""" 131 | return { 132 | "answer": "This is a sample answer.", 133 | "results": [ 134 | { 135 | "title": "Sample Result 1", 136 | "url": "https://example.com/1", 137 | "content": "This is sample content from the first result." 138 | }, 139 | { 140 | "title": "Sample Result 2", 141 | "url": "https://example.com/2", 142 | "content": "This is sample content from the second result." 143 | } 144 | ] 145 | } 146 | 147 | 148 | @pytest.fixture 149 | def news_search_response(): 150 | """Sample response for news search.""" 151 | return { 152 | "results": [ 153 | { 154 | "title": "Sample News 1", 155 | "url": "https://example.com/news/1", 156 | "content": "This is sample content from the first news result.", 157 | "published_date": "2023-09-01" 158 | }, 159 | { 160 | "title": "Sample News 2", 161 | "url": "https://example.com/news/2", 162 | "content": "This is sample content from the second news result.", 163 | "published_date": "2023-09-02" 164 | } 165 | ] 166 | } -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | """Test helpers for extracting handlers from the server module.""" 2 | import asyncio 3 | import inspect 4 | from unittest.mock import patch, MagicMock, AsyncMock 5 | import mcp_server_tavily.server as server_module 6 | 7 | 8 | class ServerHandlerExtractor: 9 | """Extract handler functions from the server module.""" 10 | 11 | def __init__(self): 12 | self.handlers = {} 13 | 14 | async def extract_handlers(self): 15 | """Extract all handler functions from the server module.""" 16 | # Create mocks for all decorators 17 | with patch('mcp_server_tavily.server.Server') as mock_server_class: 18 | mock_server = MagicMock() 19 | mock_server_class.return_value = mock_server 20 | 21 | # Mock the decorator methods to capture handlers 22 | def capture_handler(decorator_name): 23 | def decorator(func): 24 | self.handlers[decorator_name] = func 25 | return func 26 | return decorator 27 | 28 | mock_server.list_tools.side_effect = capture_handler('list_tools') 29 | mock_server.list_prompts.side_effect = capture_handler('list_prompts') 30 | mock_server.call_tool.side_effect = capture_handler('call_tool') 31 | mock_server.get_prompt.side_effect = capture_handler('get_prompt') 32 | 33 | # Mock stdio_server to prevent actual I/O 34 | with patch('mcp_server_tavily.server.stdio_server') as mock_stdio: 35 | mock_stdio.return_value.__aenter__.return_value = (AsyncMock(), AsyncMock()) 36 | 37 | # Mock TavilyClient to prevent actual API calls 38 | with patch('mcp_server_tavily.server.TavilyClient') as mock_client_class: 39 | # Call serve to register all handlers 40 | await server_module.serve("fake_api_key") 41 | 42 | return self.handlers 43 | 44 | 45 | async def get_server_handlers(): 46 | """Get all handler functions from the server module.""" 47 | extractor = ServerHandlerExtractor() 48 | return await extractor.extract_handlers() -------------------------------------------------------------------------------- /tests/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Ensure we're in the virtual environment 5 | if [ -z "$VIRTUAL_ENV" ]; then 6 | echo "Activating virtual environment..." 7 | if [ -f ".venv/bin/activate" ]; then 8 | source .venv/bin/activate 9 | else 10 | echo "Error: Virtual environment not found. Please create it first." 11 | exit 1 12 | fi 13 | fi 14 | 15 | # Install test dependencies 16 | echo "Installing test dependencies..." 17 | if command -v uv &> /dev/null; then 18 | # Use uv if available 19 | uv sync --dev 20 | uv pip install -e . 21 | else 22 | # Fall back to pip 23 | pip install -r requirements-dev.txt 24 | pip install -e . 25 | fi 26 | 27 | # Run all tests with coverage and suppress warnings 28 | echo "Running all tests with coverage..." 29 | python -W ignore -m pytest tests --cov=src/mcp_server_tavily --cov-report=term 30 | 31 | echo "Tests complete!" -------------------------------------------------------------------------------- /tests/server_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Test script to check if the server module can be instantiated. 4 | This is a minimal test that doesn't start an actual server. 5 | """ 6 | 7 | import sys 8 | import asyncio 9 | from mcp_server_tavily.server import Server 10 | from unittest.mock import MagicMock 11 | 12 | 13 | async def test_server(): 14 | """Test if the server can be instantiated.""" 15 | print(f"Testing with Python {sys.version}") 16 | 17 | print("Creating server instance...") 18 | server = Server("mcp-tavily-test") 19 | 20 | # Set up mock handler for list_tools 21 | tools_called = False 22 | 23 | @server.list_tools() 24 | async def list_tools(): 25 | nonlocal tools_called 26 | tools_called = True 27 | return [] 28 | 29 | print("✓ Server created and decorator registered") 30 | print("\n✅ Server initialization test passed!") 31 | 32 | 33 | if __name__ == "__main__": 34 | asyncio.run(test_server()) -------------------------------------------------------------------------------- /tests/test_docker.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import subprocess 4 | import pytest 5 | 6 | # Determine the project root directory (one level up from tests) 7 | PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) 8 | 9 | @pytest.fixture(scope="session") 10 | def docker_cli(): 11 | """Ensure Docker CLI is available, otherwise skip tests.""" 12 | docker = shutil.which("docker") 13 | if docker is None: 14 | pytest.skip("Docker CLI is not available") 15 | return docker 16 | 17 | def test_dockerfile_exists(): 18 | """Check that the Dockerfile exists in the project root.""" 19 | dockerfile = os.path.join(PROJECT_ROOT, "Dockerfile") 20 | assert os.path.isfile(dockerfile), "Dockerfile is missing" 21 | 22 | def test_docker_build_and_help(docker_cli): 23 | """Build the Docker image and verify the help message.""" 24 | image = "mcp_tavily_test_image" 25 | # Build the Docker image 26 | build_cmd = [docker_cli, "build", "-t", image, "."] 27 | result = subprocess.run( 28 | build_cmd, 29 | cwd=PROJECT_ROOT, 30 | stdout=subprocess.PIPE, 31 | stderr=subprocess.STDOUT, 32 | text=True, 33 | ) 34 | assert result.returncode == 0, f"Docker build failed: {result.stdout}" 35 | 36 | # Run the container with the help flag 37 | run_cmd = [docker_cli, "run", "--rm", image, "-h"] 38 | result = subprocess.run( 39 | run_cmd, 40 | stdout=subprocess.PIPE, 41 | stderr=subprocess.STDOUT, 42 | text=True, 43 | ) 44 | assert result.returncode == 0, f"Docker run -h failed: {result.stdout}" 45 | # Verify expected help text from the module's argparse description 46 | assert ( 47 | "give a model the ability to perform AI-powered web searches using Tavily" 48 | in result.stdout 49 | ), "Help message content is incorrect" 50 | 51 | # Clean up the Docker image 52 | subprocess.run([docker_cli, "rmi", image], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) -------------------------------------------------------------------------------- /tests/test_integration.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import asyncio 3 | import os 4 | import sys 5 | import argparse 6 | from unittest.mock import MagicMock, patch 7 | from mcp_server_tavily.server import serve 8 | from mcp_server_tavily import main 9 | 10 | # Create a custom AsyncMock that's safer for our tests 11 | class SafeAsyncMock: 12 | def __init__(self, return_value=None): 13 | self._return_value = return_value if return_value is not None else None 14 | self.call_args = None 15 | self.call_count = 0 16 | self.call_args_list = [] 17 | 18 | async def __call__(self, *args, **kwargs): 19 | self.call_args = (args, kwargs) 20 | self.call_args_list.append(self.call_args) 21 | self.call_count += 1 22 | if isinstance(self._return_value, asyncio.Future): 23 | return self._return_value.result() 24 | elif asyncio.iscoroutine(self._return_value): 25 | return await self._return_value 26 | else: 27 | return self._return_value 28 | 29 | 30 | @pytest.mark.asyncio 31 | class TestServerIntegration: 32 | @patch('mcp_server_tavily.server.Server') 33 | @patch('mcp_server_tavily.server.TavilyClient') 34 | @patch('mcp_server_tavily.server.stdio_server') 35 | async def test_serve_function(self, mock_stdio, mock_client, mock_server): 36 | """Test that the serve function initializes and runs the server correctly.""" 37 | # Create mock with asyncio.Future as return value (can be awaited) 38 | future = asyncio.Future() 39 | future.set_result(None) 40 | 41 | # Setup mocks properly for serve function 42 | mock_server_instance = MagicMock() 43 | mock_server_instance.create_initialization_options.return_value = {} 44 | mock_server_instance.run.return_value = future 45 | mock_server.return_value = mock_server_instance 46 | 47 | # Mock stdio_server context manager 48 | mock_stdio.return_value.__aenter__.return_value = (MagicMock(), MagicMock()) 49 | mock_stdio.return_value.__aexit__.return_value = None 50 | 51 | # Call serve with a fake API key 52 | await serve("fake_api_key") 53 | 54 | # Verify server was instantiated and methods were called 55 | mock_server.assert_called_once_with("mcp-tavily") 56 | assert mock_server_instance.create_initialization_options.called 57 | assert mock_server_instance.run.called 58 | 59 | # Ensure there are no pending tasks 60 | for task in asyncio.all_tasks(): 61 | if task is not asyncio.current_task(): 62 | task.cancel() 63 | try: 64 | await task 65 | except asyncio.CancelledError: 66 | pass 67 | 68 | @patch('mcp_server_tavily.server.Server') 69 | @patch('mcp_server_tavily.server.TavilyClient') 70 | @patch('mcp_server_tavily.server.stdio_server') 71 | async def test_stdio_server_context(self, mock_stdio, mock_client, mock_server): 72 | """Test that the stdio_server context manager is used correctly.""" 73 | # Create mock with asyncio.Future as return value (can be awaited) 74 | future = asyncio.Future() 75 | future.set_result(None) 76 | 77 | # Setup mocks properly for serve function 78 | mock_server_instance = MagicMock() 79 | mock_server_instance.create_initialization_options.return_value = {"options": "test"} 80 | mock_server_instance.run.return_value = future 81 | mock_server.return_value = mock_server_instance 82 | 83 | # Mock stdin and stdout streams 84 | mock_stdin = MagicMock() 85 | mock_stdout = MagicMock() 86 | mock_stdio.return_value.__aenter__.return_value = (mock_stdin, mock_stdout) 87 | mock_stdio.return_value.__aexit__.return_value = None 88 | 89 | # Call serve with a fake API key 90 | await serve("fake_api_key") 91 | 92 | # Verify that the stdio_server context manager was used 93 | assert mock_stdio.called 94 | assert mock_stdio.return_value.__aenter__.called 95 | 96 | # Verify that the server.run was called with the streams from stdio_server 97 | mock_server_instance.run.assert_called_once_with( 98 | mock_stdin, mock_stdout, 99 | mock_server_instance.create_initialization_options.return_value, 100 | raise_exceptions=True 101 | ) 102 | 103 | # Ensure there are no pending tasks 104 | for task in asyncio.all_tasks(): 105 | if task is not asyncio.current_task(): 106 | task.cancel() 107 | try: 108 | await task 109 | except asyncio.CancelledError: 110 | pass 111 | 112 | 113 | class TestMainFunction: 114 | @patch('mcp_server_tavily.serve') # Patch at the level it's imported in __init__.py 115 | @patch('asyncio.run') 116 | @patch('argparse.ArgumentParser') 117 | def test_main_with_api_key_arg(self, mock_parser_class, mock_run, mock_serve): 118 | """Test that main correctly handles API key from arguments.""" 119 | # Setup mock parser 120 | mock_parser = MagicMock() 121 | mock_parser.parse_args.return_value = MagicMock(api_key="test_key_from_arg") 122 | mock_parser_class.return_value = mock_parser 123 | 124 | # Setup mock serve function - use a plain function to avoid coroutine issues 125 | async def mock_serve_func(api_key): 126 | return f"Mock serve called with {api_key}" 127 | mock_serve.side_effect = mock_serve_func 128 | 129 | # Call main 130 | main() 131 | 132 | # Verify that serve was called with the API key from args 133 | mock_serve.assert_called_once_with("test_key_from_arg") 134 | mock_run.assert_called_once() 135 | 136 | @patch('mcp_server_tavily.serve') # Patch at the level it's imported in __init__.py 137 | @patch('asyncio.run') 138 | @patch('argparse.ArgumentParser') 139 | def test_main_with_env_var(self, mock_parser_class, mock_run, mock_serve, monkeypatch): 140 | """Test that main correctly handles API key from environment variable.""" 141 | # Set up environment variable 142 | monkeypatch.setenv("TAVILY_API_KEY", "test_key_from_env") 143 | 144 | # Setup mock parser 145 | mock_parser = MagicMock() 146 | mock_parser.parse_args.return_value = MagicMock(api_key=None) # No arg provided 147 | mock_parser_class.return_value = mock_parser 148 | 149 | # Setup mock serve function with an async function for proper typing 150 | async def mock_serve_func(api_key): 151 | return f"Mock serve called with {api_key}" 152 | mock_serve.side_effect = mock_serve_func 153 | 154 | # Call main 155 | main() 156 | 157 | # Verify that serve was called with the API key from env 158 | mock_serve.assert_called_once_with("test_key_from_env") 159 | mock_run.assert_called_once() 160 | 161 | def test_main_missing_api_key(self): 162 | """Test API key validation in the script's main function""" 163 | # This test will simply verify that the main function validates an API key 164 | # is present. We can't easily verify the exact flow with mocks, so we'll 165 | # just assert the existence of key validation code in the __init__.py file 166 | 167 | # Read the source code of the main function 168 | import inspect 169 | from mcp_server_tavily import main 170 | source = inspect.getsource(main) 171 | 172 | # Check that the main function validates the API key presence 173 | assert "API key" in source 174 | assert "os.getenv" in source 175 | assert "TAVILY_API_KEY" in source -------------------------------------------------------------------------------- /tests/test_models.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pydantic import ValidationError 3 | import json 4 | from mcp_server_tavily.server import SearchBase, GeneralSearch, AnswerSearch, NewsSearch 5 | 6 | 7 | class TestSearchBase: 8 | def test_base_model_required_fields(self): 9 | """Test that query is required for SearchBase.""" 10 | # Should raise error when query is missing 11 | with pytest.raises(ValidationError): 12 | SearchBase() 13 | 14 | # Should work with just query provided 15 | model = SearchBase(query="test query") 16 | assert model.query == "test query" 17 | assert model.max_results == 5 # default value 18 | # include_domains and exclude_domains are None by default in the model 19 | # but get converted to [] when used 20 | assert model.include_domains is None 21 | assert model.exclude_domains is None 22 | 23 | def test_max_results_validation(self): 24 | """Test max_results validation rules.""" 25 | # Valid values 26 | model = SearchBase(query="test", max_results=1) 27 | assert model.max_results == 1 28 | 29 | model = SearchBase(query="test", max_results=19) 30 | assert model.max_results == 19 31 | 32 | # Too small 33 | with pytest.raises(ValidationError): 34 | SearchBase(query="test", max_results=0) 35 | 36 | # Too large 37 | with pytest.raises(ValidationError): 38 | SearchBase(query="test", max_results=20) 39 | 40 | @pytest.mark.parametrize( 41 | "input_value,expected_output", 42 | [ 43 | (None, []), # None -> empty list 44 | ([], []), # Empty list -> empty list 45 | (["example.com"], ["example.com"]), # List with single item 46 | (["example.com", "test.org"], ["example.com", "test.org"]), # List with multiple items 47 | ("example.com", ["example.com"]), # Single string -> list with single item 48 | ("example.com,test.org", ["example.com", "test.org"]), # Comma-separated string 49 | (" example.com , test.org ", ["example.com", "test.org"]), # Whitespace in comma-separated string 50 | ('["example.com", "test.org"]', ["example.com", "test.org"]), # JSON string array 51 | ("", []), # Empty string -> empty list 52 | (" ", []), # Whitespace string -> empty list 53 | ], 54 | ) 55 | def test_parse_domains_list(self, input_value, expected_output): 56 | """Test that domain list parsing works correctly for various input formats.""" 57 | # Test include_domains 58 | model = SearchBase(query="test", include_domains=input_value) 59 | assert model.include_domains == expected_output 60 | 61 | # Test exclude_domains 62 | model = SearchBase(query="test", exclude_domains=input_value) 63 | assert model.exclude_domains == expected_output 64 | 65 | 66 | class TestGeneralSearch: 67 | def test_general_search_defaults(self): 68 | """Test GeneralSearch default values.""" 69 | model = GeneralSearch(query="test query") 70 | assert model.query == "test query" 71 | assert model.search_depth == "basic" # default for GeneralSearch 72 | assert model.max_results == 5 73 | assert model.include_domains is None 74 | assert model.exclude_domains is None 75 | 76 | def test_search_depth_validation(self): 77 | """Test search_depth validation.""" 78 | # Valid values 79 | model = GeneralSearch(query="test", search_depth="basic") 80 | assert model.search_depth == "basic" 81 | 82 | model = GeneralSearch(query="test", search_depth="advanced") 83 | assert model.search_depth == "advanced" 84 | 85 | # Invalid value 86 | with pytest.raises(ValidationError): 87 | GeneralSearch(query="test", search_depth="super_advanced") 88 | 89 | 90 | class TestAnswerSearch: 91 | def test_answer_search_defaults(self): 92 | """Test AnswerSearch default values.""" 93 | model = AnswerSearch(query="test query") 94 | assert model.query == "test query" 95 | assert model.search_depth == "advanced" # default for AnswerSearch 96 | assert model.max_results == 5 97 | assert model.include_domains is None 98 | assert model.exclude_domains is None 99 | 100 | 101 | class TestNewsSearch: 102 | def test_news_search_defaults(self): 103 | """Test NewsSearch default values.""" 104 | model = NewsSearch(query="test query") 105 | assert model.query == "test query" 106 | assert model.days is None 107 | assert model.max_results == 5 108 | assert model.include_domains is None 109 | assert model.exclude_domains is None 110 | 111 | def test_days_validation(self): 112 | """Test days validation.""" 113 | # Valid values 114 | model = NewsSearch(query="test", days=1) 115 | assert model.days == 1 116 | 117 | model = NewsSearch(query="test", days=365) 118 | assert model.days == 365 119 | 120 | # Too small 121 | with pytest.raises(ValidationError): 122 | NewsSearch(query="test", days=0) 123 | 124 | # Too large 125 | with pytest.raises(ValidationError): 126 | NewsSearch(query="test", days=366) -------------------------------------------------------------------------------- /tests/test_server_api.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from unittest.mock import MagicMock, patch, call 3 | import asyncio 4 | import inspect 5 | from mcp.types import Tool, TextContent, GetPromptResult, PromptMessage 6 | from mcp.shared.exceptions import McpError 7 | from mcp.types import INVALID_PARAMS, INTERNAL_ERROR 8 | from tavily import InvalidAPIKeyError, UsageLimitExceededError 9 | from mcp.server import Server 10 | 11 | # Create a custom AsyncMock that's safer for our tests 12 | class SafeAsyncMock: 13 | def __init__(self, return_value=None): 14 | self._return_value = return_value if return_value is not None else None 15 | self.call_args = None 16 | self.call_count = 0 17 | self.call_args_list = [] 18 | 19 | async def __call__(self, *args, **kwargs): 20 | self.call_args = call(*args, **kwargs) 21 | self.call_args_list.append(self.call_args) 22 | self.call_count += 1 23 | if isinstance(self._return_value, asyncio.Future): 24 | return await self._return_value 25 | elif asyncio.iscoroutine(self._return_value): 26 | return await self._return_value 27 | else: 28 | return self._return_value 29 | 30 | # Import the server module directly 31 | import mcp_server_tavily.server as server_module 32 | 33 | # Patch the stdio_server to avoid actual I/O operations 34 | stdio_mock = patch('mcp_server_tavily.server.stdio_server', autospec=True).start() 35 | 36 | # Create proper SafeAsyncMock for aenter 37 | enter_future = asyncio.Future() 38 | enter_future.set_result((MagicMock(), MagicMock())) 39 | enter_mock = SafeAsyncMock(return_value=enter_future) 40 | 41 | # Create proper SafeAsyncMock for aexit 42 | exit_future = asyncio.Future() 43 | exit_future.set_result(None) 44 | exit_mock = SafeAsyncMock(return_value=exit_future) 45 | 46 | # Apply the mocks 47 | stdio_context = MagicMock() 48 | stdio_context.__aenter__ = enter_mock 49 | stdio_context.__aexit__ = exit_mock 50 | stdio_mock.return_value = stdio_context 51 | 52 | 53 | @pytest.mark.asyncio 54 | class TestServerListTools: 55 | async def test_list_tools(self, server_handlers): 56 | """Test that the list_tools handler returns the expected tools.""" 57 | # Get the registered list_tools handler 58 | list_tools_handler = server_handlers['list_tools'] 59 | 60 | # Call the function 61 | tools = await list_tools_handler() 62 | 63 | # Verify that we get 3 tools as expected 64 | assert len(tools) == 3 65 | 66 | # Check that the tool names are correct 67 | tool_names = [tool.name for tool in tools] 68 | assert "tavily_web_search" in tool_names 69 | assert "tavily_answer_search" in tool_names 70 | assert "tavily_news_search" in tool_names 71 | 72 | # Check that each tool has a description and schema 73 | for tool in tools: 74 | assert isinstance(tool, Tool) 75 | assert tool.description 76 | assert tool.inputSchema 77 | 78 | 79 | @pytest.mark.asyncio 80 | class TestServerListPrompts: 81 | async def test_list_prompts(self, mock_server): 82 | """Test that the list_prompts handler returns the expected prompts.""" 83 | # Create a server instance to get the decorated function 84 | await server_module.serve("fake_api_key") 85 | 86 | # Get the function that was registered with @server.list_prompts() 87 | list_prompts_handler = mock_server.handler_registry['list_prompts'] 88 | 89 | # Call the function 90 | prompts = await list_prompts_handler() 91 | 92 | # Verify that we get 3 prompts as expected 93 | assert len(prompts) == 3 94 | 95 | # Check that the prompt names are correct 96 | prompt_names = [prompt.name for prompt in prompts] 97 | assert "tavily_web_search" in prompt_names 98 | assert "tavily_answer_search" in prompt_names 99 | assert "tavily_news_search" in prompt_names 100 | 101 | # Check that each prompt has a description and required arguments 102 | for prompt in prompts: 103 | assert prompt.description 104 | assert any(arg.name == "query" and arg.required for arg in prompt.arguments) 105 | 106 | 107 | @pytest.mark.asyncio 108 | class TestServerCallTool: 109 | async def test_call_tool_web_search(self, mock_tavily_client, mock_server, web_search_response): 110 | """Test that the call_tool handler correctly calls the Tavily client for web search.""" 111 | # Set up the mock client to return our test response 112 | mock_tavily_client.search.return_value = web_search_response 113 | 114 | # Create a server instance to get the decorated function 115 | await server_module.serve("fake_api_key") 116 | 117 | # Get the function that was registered with @server.call_tool() 118 | call_tool_handler = mock_server.handler_registry['call_tool'] 119 | 120 | # Call the function with web search parameters 121 | result = await call_tool_handler( 122 | name="tavily_web_search", 123 | arguments={ 124 | "query": "test query", 125 | "max_results": 5, 126 | "search_depth": "basic", 127 | "include_domains": ["example.com"], 128 | "exclude_domains": ["spam.com"] 129 | } 130 | ) 131 | 132 | # Verify the client was called with correct parameters 133 | mock_tavily_client.search.assert_called_once_with( 134 | query="test query", 135 | max_results=5, 136 | search_depth="basic", 137 | include_domains=["example.com"], 138 | exclude_domains=["spam.com"] 139 | ) 140 | 141 | # Verify the result is a list of TextContent 142 | assert isinstance(result, list) 143 | assert len(result) == 1 144 | assert isinstance(result[0], TextContent) 145 | assert result[0].type == "text" 146 | assert "Detailed Results:" in result[0].text 147 | 148 | async def test_call_tool_answer_search(self, mock_tavily_client, mock_server, answer_search_response): 149 | """Test that the call_tool handler correctly calls the Tavily client for answer search.""" 150 | # Set up the mock client to return our test response 151 | mock_tavily_client.search.return_value = answer_search_response 152 | 153 | # Create a server instance to get the decorated function 154 | await server_module.serve("fake_api_key") 155 | 156 | # Get the function that was registered with @server.call_tool() 157 | call_tool_handler = mock_server.handler_registry['call_tool'] 158 | 159 | # Call the function with answer search parameters 160 | result = await call_tool_handler( 161 | name="tavily_answer_search", 162 | arguments={ 163 | "query": "test query", 164 | "max_results": 5, 165 | "search_depth": "advanced" 166 | } 167 | ) 168 | 169 | # Verify the client was called with correct parameters 170 | mock_tavily_client.search.assert_called_once_with( 171 | query="test query", 172 | max_results=5, 173 | search_depth="advanced", 174 | include_answer=True, 175 | include_domains=[], 176 | exclude_domains=[] 177 | ) 178 | 179 | # Verify the result includes the answer 180 | assert isinstance(result, list) 181 | assert "Answer:" in result[0].text 182 | 183 | async def test_call_tool_news_search(self, mock_tavily_client, mock_server, news_search_response): 184 | """Test that the call_tool handler correctly calls the Tavily client for news search.""" 185 | # Set up the mock client to return our test response 186 | mock_tavily_client.search.return_value = news_search_response 187 | 188 | # Create a server instance to get the decorated function 189 | await server_module.serve("fake_api_key") 190 | 191 | # Get the function that was registered with @server.call_tool() 192 | call_tool_handler = mock_server.handler_registry['call_tool'] 193 | 194 | # Call the function with news search parameters 195 | result = await call_tool_handler( 196 | name="tavily_news_search", 197 | arguments={ 198 | "query": "test query", 199 | "max_results": 5, 200 | "days": 7 201 | } 202 | ) 203 | 204 | # Verify the client was called with correct parameters 205 | mock_tavily_client.search.assert_called_once_with( 206 | query="test query", 207 | max_results=5, 208 | topic="news", 209 | days=7, 210 | include_domains=[], 211 | exclude_domains=[] 212 | ) 213 | 214 | # Verify the result includes published dates 215 | assert isinstance(result, list) 216 | assert "Published:" in result[0].text 217 | 218 | async def test_call_tool_news_search_default_days(self, mock_tavily_client, mock_server, news_search_response): 219 | """Test that the news search uses default days value when not specified.""" 220 | # Set up the mock client to return our test response 221 | mock_tavily_client.search.return_value = news_search_response 222 | 223 | # Create a server instance to get the decorated function 224 | await server_module.serve("fake_api_key") 225 | 226 | # Get the function that was registered with @server.call_tool() 227 | call_tool_handler = mock_server.handler_registry['call_tool'] 228 | 229 | # Call the function with news search parameters, without days 230 | result = await call_tool_handler( 231 | name="tavily_news_search", 232 | arguments={ 233 | "query": "test query" 234 | } 235 | ) 236 | 237 | # Verify days defaults to 3 238 | mock_tavily_client.search.assert_called_once_with( 239 | query="test query", 240 | max_results=5, 241 | topic="news", 242 | days=3, 243 | include_domains=[], 244 | exclude_domains=[] 245 | ) 246 | 247 | async def test_call_tool_invalid_tool(self, mock_server): 248 | """Test that call_tool raises an error for an invalid tool name.""" 249 | # Create a server instance to get the decorated function 250 | await server_module.serve("fake_api_key") 251 | 252 | # Get the function that was registered with @server.call_tool() 253 | call_tool_handler = mock_server.handler_registry['call_tool'] 254 | 255 | # Call with an invalid tool name should raise McpError with INVALID_PARAMS 256 | with pytest.raises(McpError) as exc_info: 257 | await call_tool_handler(name="invalid_tool", arguments={"query": "test"}) 258 | assert exc_info.value.error.code == INVALID_PARAMS 259 | assert "Unknown tool" in str(exc_info.value) 260 | 261 | async def test_call_tool_api_key_error(self, mock_tavily_client, mock_server): 262 | """Test that call_tool handles API key errors correctly.""" 263 | # Set up the mock client to raise an API key error 264 | # Mock API key error (raised by client) 265 | mock_tavily_client.search.side_effect = InvalidAPIKeyError 266 | 267 | # Create a server instance to get the decorated function 268 | await server_module.serve("fake_api_key") 269 | 270 | # Get the function that was registered with @server.call_tool() 271 | call_tool_handler = mock_server.handler_registry['call_tool'] 272 | 273 | # Call the function and expect an McpError 274 | with pytest.raises(McpError) as exc_info: 275 | await call_tool_handler(name="tavily_web_search", arguments={"query": "test"}) 276 | # API key errors map to INTERNAL_ERROR 277 | assert exc_info.value.error.code == INTERNAL_ERROR 278 | 279 | async def test_call_tool_usage_limit_error(self, mock_tavily_client, mock_server): 280 | """Test that call_tool handles usage limit errors correctly.""" 281 | # Set up the mock client to raise a usage limit error 282 | # Mock usage limit exceeded error 283 | mock_tavily_client.search.side_effect = UsageLimitExceededError("Usage limit exceeded") 284 | 285 | # Create a server instance to get the decorated function 286 | await server_module.serve("fake_api_key") 287 | 288 | # Get the function that was registered with @server.call_tool() 289 | call_tool_handler = mock_server.handler_registry['call_tool'] 290 | 291 | # Call the function and expect an McpError 292 | with pytest.raises(McpError) as exc_info: 293 | await call_tool_handler(name="tavily_web_search", arguments={"query": "test"}) 294 | # Usage limit errors map to INTERNAL_ERROR 295 | assert exc_info.value.error.code == INTERNAL_ERROR 296 | 297 | async def test_call_tool_validation_error(self, mock_server): 298 | """Test that call_tool properly validates input parameters.""" 299 | # Create a server instance to get the decorated function 300 | await server_module.serve("fake_api_key") 301 | 302 | # Get the function that was registered with @server.call_tool() 303 | call_tool_handler = mock_server.handler_registry['call_tool'] 304 | 305 | # Test with invalid max_results 306 | with pytest.raises(McpError) as exc_info: 307 | await call_tool_handler( 308 | name="tavily_web_search", 309 | arguments={"query": "test", "max_results": 25} # Too large 310 | ) 311 | assert "max_results" in str(exc_info.value).lower() 312 | 313 | # Test with invalid search_depth 314 | with pytest.raises(McpError) as exc_info: 315 | await call_tool_handler( 316 | name="tavily_web_search", 317 | arguments={"query": "test", "search_depth": "ultra"} # Invalid option 318 | ) 319 | assert "search_depth" in str(exc_info.value).lower() 320 | 321 | # Test with invalid days for news search 322 | with pytest.raises(McpError) as exc_info: 323 | await call_tool_handler( 324 | name="tavily_news_search", 325 | arguments={"query": "test", "days": 400} # Too large 326 | ) 327 | assert "days" in str(exc_info.value).lower() 328 | 329 | async def test_call_tool_json_domain_input(self, mock_tavily_client, mock_server, web_search_response): 330 | """Test that call_tool properly handles JSON format for domain lists.""" 331 | # Set up the mock client to return our test response 332 | mock_tavily_client.search.return_value = web_search_response 333 | 334 | # Create a server instance to get the decorated function 335 | await server_module.serve("fake_api_key") 336 | 337 | # Get the function that was registered with @server.call_tool() 338 | call_tool_handler = mock_server.handler_registry['call_tool'] 339 | 340 | # Call the function with JSON formatted domain lists 341 | await call_tool_handler( 342 | name="tavily_web_search", 343 | arguments={ 344 | "query": "test query", 345 | "include_domains": '["example.com", "test.org"]', 346 | "exclude_domains": '["spam.com"]' 347 | } 348 | ) 349 | 350 | # Verify the client was called with correct parsed parameters 351 | mock_tavily_client.search.assert_called_once_with( 352 | query="test query", 353 | max_results=5, 354 | search_depth="basic", 355 | include_domains=["example.com", "test.org"], 356 | exclude_domains=["spam.com"] 357 | ) 358 | 359 | 360 | @pytest.mark.asyncio 361 | class TestServerGetPrompt: 362 | async def test_get_prompt_web_search(self, mock_tavily_client, mock_server, web_search_response): 363 | """Test that the get_prompt handler correctly calls the Tavily client for web search.""" 364 | # Set up the mock client to return our test response 365 | mock_tavily_client.search.return_value = web_search_response 366 | 367 | # Create a server instance to get the decorated function 368 | await server_module.serve("fake_api_key") 369 | 370 | # Get the function that was registered with @server.get_prompt() 371 | get_prompt_handler = mock_server.handler_registry['get_prompt'] 372 | 373 | # Call the function with web search parameters 374 | result = await get_prompt_handler( 375 | name="tavily_web_search", 376 | arguments={ 377 | "query": "test query", 378 | "include_domains": "example.com", 379 | "exclude_domains": "spam.com" 380 | } 381 | ) 382 | 383 | # Verify the client was called with correct parameters 384 | mock_tavily_client.search.assert_called_once_with( 385 | query="test query", 386 | include_domains=["example.com"], 387 | exclude_domains=["spam.com"] 388 | ) 389 | 390 | # Verify the result is a GetPromptResult 391 | assert isinstance(result, GetPromptResult) 392 | assert "test query" in result.description 393 | assert len(result.messages) == 1 394 | assert result.messages[0].role == "user" 395 | assert isinstance(result.messages[0].content, TextContent) 396 | assert result.messages[0].content.type == "text" 397 | 398 | async def test_get_prompt_answer_search(self, mock_tavily_client, mock_server, answer_search_response): 399 | """Test that the get_prompt handler correctly calls the Tavily client for answer search.""" 400 | # Set up the mock client to return our test response 401 | mock_tavily_client.search.return_value = answer_search_response 402 | 403 | # Create a server instance to get the decorated function 404 | await server_module.serve("fake_api_key") 405 | 406 | # Get the function that was registered with @server.get_prompt() 407 | get_prompt_handler = mock_server.handler_registry['get_prompt'] 408 | 409 | # Call the function with answer search parameters 410 | result = await get_prompt_handler( 411 | name="tavily_answer_search", 412 | arguments={ 413 | "query": "test question", 414 | "include_domains": "example.com,test.org", 415 | "exclude_domains": "spam.com" 416 | } 417 | ) 418 | 419 | # Verify the client was called with correct parameters 420 | mock_tavily_client.search.assert_called_once_with( 421 | query="test question", 422 | include_answer=True, 423 | search_depth="advanced", 424 | include_domains=["example.com", "test.org"], 425 | exclude_domains=["spam.com"] 426 | ) 427 | 428 | # Verify the result is a GetPromptResult with answer content 429 | assert isinstance(result, GetPromptResult) 430 | assert "test question" in result.description 431 | assert "This is a sample answer" in result.messages[0].content.text 432 | 433 | async def test_get_prompt_news_search(self, mock_tavily_client, mock_server, news_search_response): 434 | """Test that the get_prompt handler correctly calls the Tavily client for news search.""" 435 | # Set up the mock client to return our test response 436 | mock_tavily_client.search.return_value = news_search_response 437 | 438 | # Create a server instance to get the decorated function 439 | await server_module.serve("fake_api_key") 440 | 441 | # Get the function that was registered with @server.get_prompt() 442 | get_prompt_handler = mock_server.handler_registry['get_prompt'] 443 | 444 | # Call the function with news search parameters including days 445 | result = await get_prompt_handler( 446 | name="tavily_news_search", 447 | arguments={ 448 | "query": "breaking news", 449 | "days": "5", 450 | "include_domains": "reuters.com,bbc.com" 451 | } 452 | ) 453 | 454 | # Verify the client was called with correct parameters 455 | mock_tavily_client.search.assert_called_once_with( 456 | query="breaking news", 457 | topic="news", 458 | days=5, 459 | include_domains=["reuters.com", "bbc.com"], 460 | exclude_domains=[] 461 | ) 462 | 463 | # Verify the result contains news-specific elements 464 | assert isinstance(result, GetPromptResult) 465 | assert "breaking news" in result.description 466 | assert "Published:" in result.messages[0].content.text 467 | 468 | async def test_get_prompt_news_search_default_days(self, mock_tavily_client, mock_server, news_search_response): 469 | """Test that the news search uses default days value when not specified in get_prompt.""" 470 | # Set up the mock client to return our test response 471 | mock_tavily_client.search.return_value = news_search_response 472 | 473 | # Create a server instance to get the decorated function 474 | await server_module.serve("fake_api_key") 475 | 476 | # Get the function that was registered with @server.get_prompt() 477 | get_prompt_handler = mock_server.handler_registry['get_prompt'] 478 | 479 | # Call the function without days parameter 480 | result = await get_prompt_handler( 481 | name="tavily_news_search", 482 | arguments={ 483 | "query": "breaking news" 484 | } 485 | ) 486 | 487 | # Verify days defaults to 3 488 | mock_tavily_client.search.assert_called_once_with( 489 | query="breaking news", 490 | topic="news", 491 | days=3, 492 | include_domains=[], 493 | exclude_domains=[] 494 | ) 495 | 496 | async def test_get_prompt_missing_query(self, mock_server): 497 | """Test that get_prompt raises an error when query is missing.""" 498 | # Create a server instance to get the decorated function 499 | await server_module.serve("fake_api_key") 500 | 501 | # Get the function that was registered with @server.get_prompt() 502 | get_prompt_handler = mock_server.handler_registry['get_prompt'] 503 | 504 | # Call with missing query 505 | with pytest.raises(McpError, match="Query is required"): 506 | await get_prompt_handler(name="tavily_web_search", arguments={}) 507 | 508 | # Call with None arguments 509 | with pytest.raises(McpError, match="Query is required"): 510 | await get_prompt_handler(name="tavily_web_search", arguments=None) 511 | 512 | async def test_get_prompt_invalid_prompt(self, mock_server): 513 | """Test that get_prompt raises an error for an invalid prompt name.""" 514 | # Create a server instance to get the decorated function 515 | await server_module.serve("fake_api_key") 516 | 517 | # Get the function that was registered with @server.get_prompt() 518 | get_prompt_handler = mock_server.handler_registry['get_prompt'] 519 | 520 | # Call with an invalid prompt name should raise McpError INVALID_PARAMS 521 | with pytest.raises(McpError) as exc_info: 522 | await get_prompt_handler(name="invalid_prompt", arguments={"query": "test"}) 523 | assert exc_info.value.error.code == INVALID_PARAMS 524 | assert "Unknown prompt" in str(exc_info.value) 525 | 526 | async def test_get_prompt_api_error(self, mock_tavily_client, mock_server): 527 | """Test that get_prompt handles API errors gracefully.""" 528 | # Set up the mock client to raise an API key error 529 | mock_tavily_client.search.side_effect = InvalidAPIKeyError 530 | 531 | # Create a server instance to get the decorated function 532 | await server_module.serve("fake_api_key") 533 | 534 | # Get the function that was registered with @server.get_prompt() 535 | get_prompt_handler = mock_server.handler_registry['get_prompt'] 536 | 537 | # Call the function - should return an error message instead of raising 538 | result = await get_prompt_handler( 539 | name="tavily_web_search", 540 | arguments={"query": "test query"} 541 | ) 542 | # Verify the result contains the error message 543 | assert "failed to search" in result.description.lower() 544 | assert len(result.messages) == 1 545 | # Default Tavily error message contains 'api key is invalid' 546 | assert "api key is invalid" in result.messages[0].content.text.lower() 547 | 548 | async def test_get_prompt_usage_limit_error(self, mock_tavily_client, mock_server): 549 | """Test that get_prompt handles usage limit errors gracefully.""" 550 | # Set up the mock client to raise a usage limit error 551 | mock_tavily_client.search.side_effect = UsageLimitExceededError("Usage limit exceeded") 552 | 553 | # Create a server instance to get the decorated function 554 | await server_module.serve("fake_api_key") 555 | 556 | # Get the function that was registered with @server.get_prompt() 557 | get_prompt_handler = mock_server.handler_registry['get_prompt'] 558 | 559 | # Call the function - should return an error message instead of raising 560 | result = await get_prompt_handler( 561 | name="tavily_answer_search", 562 | arguments={"query": "test query"} 563 | ) 564 | # Verify the result contains the error message 565 | assert "failed to search" in result.description.lower() 566 | assert "usage limit exceeded" in result.messages[0].content.text.lower() 567 | 568 | async def test_get_prompt_json_domain_input(self, mock_tavily_client, mock_server, web_search_response): 569 | """Test that get_prompt correctly handles JSON domain input.""" 570 | # Set up the mock client to return our test response 571 | mock_tavily_client.search.return_value = web_search_response 572 | 573 | # Create a server instance to get the decorated function 574 | await server_module.serve("fake_api_key") 575 | 576 | # Get the function that was registered with @server.get_prompt() 577 | get_prompt_handler = mock_server.handler_registry['get_prompt'] 578 | 579 | # Call the function with JSON formatted domain lists 580 | result = await get_prompt_handler( 581 | name="tavily_web_search", 582 | arguments={ 583 | "query": "test query", 584 | "include_domains": '["example.com", "test.org"]', 585 | "exclude_domains": '["spam.com"]' 586 | } 587 | ) 588 | 589 | # Verify the client was called with correct parsed parameters 590 | mock_tavily_client.search.assert_called_once_with( 591 | query="test query", 592 | include_domains=["example.com", "test.org"], 593 | exclude_domains=["spam.com"] 594 | ) 595 | 596 | async def test_get_prompt_string_to_int_conversion(self, mock_tavily_client, mock_server, news_search_response): 597 | """Test that get_prompt correctly converts string days parameter to int.""" 598 | # Set up the mock client to return our test response 599 | mock_tavily_client.search.return_value = news_search_response 600 | 601 | # Create a server instance to get the decorated function 602 | await server_module.serve("fake_api_key") 603 | 604 | # Get the function that was registered with @server.get_prompt() 605 | get_prompt_handler = mock_server.handler_registry['get_prompt'] 606 | 607 | # Call the function with days as string 608 | await get_prompt_handler( 609 | name="tavily_news_search", 610 | arguments={ 611 | "query": "news", 612 | "days": "7" # String instead of int 613 | } 614 | ) 615 | 616 | # Verify the client was called with days converted to int 617 | mock_tavily_client.search.assert_called_once_with( 618 | query="news", 619 | topic="news", 620 | days=7, # Should be converted to int 621 | include_domains=[], 622 | exclude_domains=[] 623 | ) -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import re 3 | import inspect 4 | from mcp_server_tavily.server import serve 5 | 6 | # Extract the format_results function from inside the serve function 7 | serve_source = inspect.getsource(serve) 8 | format_results_code = re.search(r'def format_results\(response: dict\) -> str:(.*?)(?=\n @|\n\n)', serve_source, re.DOTALL).group(0) 9 | 10 | # Define format_results in this module's scope 11 | exec(format_results_code, globals()) 12 | 13 | 14 | class TestFormatResults: 15 | def test_format_basic_results(self, web_search_response): 16 | """Test formatting of basic search results without filters or answer.""" 17 | formatted = format_results(web_search_response) 18 | 19 | # Check expected sections in the output 20 | assert "Detailed Results:" in formatted 21 | 22 | # Check that each result is included 23 | for result in web_search_response["results"]: 24 | assert result["title"] in formatted 25 | assert result["url"] in formatted 26 | assert result["content"] in formatted 27 | 28 | # Check that filter sections are not included 29 | assert "Search Filters:" not in formatted 30 | assert "Including domains:" not in formatted 31 | assert "Excluding domains:" not in formatted 32 | 33 | # Check that answer section is not included 34 | assert "Answer:" not in formatted 35 | 36 | def test_format_results_with_answer(self, answer_search_response): 37 | """Test formatting of search results with an answer.""" 38 | formatted = format_results(answer_search_response) 39 | 40 | # Check that answer is included 41 | assert "Answer:" in formatted 42 | assert answer_search_response["answer"] in formatted 43 | assert "Sources:" in formatted 44 | 45 | # Check that each result is included 46 | for result in answer_search_response["results"]: 47 | assert result["title"] in formatted 48 | assert result["url"] in formatted 49 | assert result["content"] in formatted 50 | 51 | def test_format_results_with_news(self, news_search_response): 52 | """Test formatting of news search results with published dates.""" 53 | formatted = format_results(news_search_response) 54 | 55 | # Check expected sections in the output 56 | assert "Detailed Results:" in formatted 57 | 58 | # Check that each result is included with published date 59 | for result in news_search_response["results"]: 60 | assert result["title"] in formatted 61 | assert result["url"] in formatted 62 | assert result["content"] in formatted 63 | assert "Published:" in formatted 64 | assert result["published_date"] in formatted 65 | 66 | def test_format_results_with_filters(self, web_search_response): 67 | """Test formatting of search results with domain filters.""" 68 | # Add domain filters to the response 69 | response = web_search_response.copy() 70 | response["included_domains"] = ["example.com"] 71 | response["excluded_domains"] = ["spam.com"] 72 | 73 | formatted = format_results(response) 74 | 75 | # Check that filter sections are included 76 | assert "Search Filters:" in formatted 77 | assert "Including domains: example.com" in formatted 78 | assert "Excluding domains: spam.com" in formatted 79 | 80 | # Check that each result is included 81 | for result in response["results"]: 82 | assert result["title"] in formatted 83 | assert result["url"] in formatted 84 | assert result["content"] in formatted 85 | 86 | def test_format_results_complete(self, answer_search_response): 87 | """Test formatting of complete search results with all elements.""" 88 | # Add domain filters and published dates to the response 89 | response = answer_search_response.copy() 90 | response["included_domains"] = ["example.com"] 91 | response["excluded_domains"] = ["spam.com"] 92 | 93 | # Add published_date to results 94 | for result in response["results"]: 95 | result["published_date"] = "2023-09-01" 96 | 97 | formatted = format_results(response) 98 | 99 | # Check that all sections are included 100 | assert "Search Filters:" in formatted 101 | assert "Including domains: example.com" in formatted 102 | assert "Excluding domains: spam.com" in formatted 103 | assert "Answer:" in formatted 104 | assert response["answer"] in formatted 105 | assert "Sources:" in formatted 106 | assert "Detailed Results:" in formatted 107 | 108 | # Check that each result is included with published date 109 | for result in response["results"]: 110 | assert result["title"] in formatted 111 | assert result["url"] in formatted 112 | assert result["content"] in formatted 113 | assert "Published:" in formatted 114 | assert result["published_date"] in formatted -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.11" 3 | 4 | [[package]] 5 | name = "annotated-types" 6 | version = "0.7.0" 7 | source = { registry = "https://pypi.org/simple" } 8 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } 9 | wheels = [ 10 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, 11 | ] 12 | 13 | [[package]] 14 | name = "anyio" 15 | version = "4.8.0" 16 | source = { registry = "https://pypi.org/simple" } 17 | dependencies = [ 18 | { name = "idna" }, 19 | { name = "sniffio" }, 20 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 21 | ] 22 | sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } 23 | wheels = [ 24 | { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, 25 | ] 26 | 27 | [[package]] 28 | name = "certifi" 29 | version = "2025.1.31" 30 | source = { registry = "https://pypi.org/simple" } 31 | sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } 32 | wheels = [ 33 | { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, 34 | ] 35 | 36 | [[package]] 37 | name = "charset-normalizer" 38 | version = "3.4.1" 39 | source = { registry = "https://pypi.org/simple" } 40 | sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } 41 | wheels = [ 42 | { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, 43 | { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, 44 | { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, 45 | { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, 46 | { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, 47 | { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, 48 | { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, 49 | { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, 50 | { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, 51 | { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, 52 | { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, 53 | { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, 54 | { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, 55 | { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, 56 | { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, 57 | { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, 58 | { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, 59 | { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, 60 | { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, 61 | { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, 62 | { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, 63 | { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, 64 | { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, 65 | { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, 66 | { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, 67 | { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, 68 | { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, 69 | { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, 70 | { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, 71 | { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, 72 | { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, 73 | { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, 74 | { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, 75 | { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, 76 | { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, 77 | { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, 78 | { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, 79 | { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, 80 | { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, 81 | { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, 82 | ] 83 | 84 | [[package]] 85 | name = "click" 86 | version = "8.1.8" 87 | source = { registry = "https://pypi.org/simple" } 88 | dependencies = [ 89 | { name = "colorama", marker = "sys_platform == 'win32'" }, 90 | ] 91 | sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } 92 | wheels = [ 93 | { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, 94 | ] 95 | 96 | [[package]] 97 | name = "colorama" 98 | version = "0.4.6" 99 | source = { registry = "https://pypi.org/simple" } 100 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 101 | wheels = [ 102 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 103 | ] 104 | 105 | [[package]] 106 | name = "coverage" 107 | version = "7.6.12" 108 | source = { registry = "https://pypi.org/simple" } 109 | sdist = { url = "https://files.pythonhosted.org/packages/0c/d6/2b53ab3ee99f2262e6f0b8369a43f6d66658eab45510331c0b3d5c8c4272/coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2", size = 805941 } 110 | wheels = [ 111 | { url = "https://files.pythonhosted.org/packages/64/2d/da78abbfff98468c91fd63a73cccdfa0e99051676ded8dd36123e3a2d4d5/coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015", size = 208464 }, 112 | { url = "https://files.pythonhosted.org/packages/31/f2/c269f46c470bdabe83a69e860c80a82e5e76840e9f4bbd7f38f8cebbee2f/coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45", size = 208893 }, 113 | { url = "https://files.pythonhosted.org/packages/47/63/5682bf14d2ce20819998a49c0deadb81e608a59eed64d6bc2191bc8046b9/coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702", size = 241545 }, 114 | { url = "https://files.pythonhosted.org/packages/6a/b6/6b6631f1172d437e11067e1c2edfdb7238b65dff965a12bce3b6d1bf2be2/coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0", size = 239230 }, 115 | { url = "https://files.pythonhosted.org/packages/c7/01/9cd06cbb1be53e837e16f1b4309f6357e2dfcbdab0dd7cd3b1a50589e4e1/coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f", size = 241013 }, 116 | { url = "https://files.pythonhosted.org/packages/4b/26/56afefc03c30871326e3d99709a70d327ac1f33da383cba108c79bd71563/coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f", size = 239750 }, 117 | { url = "https://files.pythonhosted.org/packages/dd/ea/88a1ff951ed288f56aa561558ebe380107cf9132facd0b50bced63ba7238/coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d", size = 238462 }, 118 | { url = "https://files.pythonhosted.org/packages/6e/d4/1d9404566f553728889409eff82151d515fbb46dc92cbd13b5337fa0de8c/coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba", size = 239307 }, 119 | { url = "https://files.pythonhosted.org/packages/12/c1/e453d3b794cde1e232ee8ac1d194fde8e2ba329c18bbf1b93f6f5eef606b/coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f", size = 211117 }, 120 | { url = "https://files.pythonhosted.org/packages/d5/db/829185120c1686fa297294f8fcd23e0422f71070bf85ef1cc1a72ecb2930/coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558", size = 212019 }, 121 | { url = "https://files.pythonhosted.org/packages/e2/7f/4af2ed1d06ce6bee7eafc03b2ef748b14132b0bdae04388e451e4b2c529b/coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad", size = 208645 }, 122 | { url = "https://files.pythonhosted.org/packages/dc/60/d19df912989117caa95123524d26fc973f56dc14aecdec5ccd7d0084e131/coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3", size = 208898 }, 123 | { url = "https://files.pythonhosted.org/packages/bd/10/fecabcf438ba676f706bf90186ccf6ff9f6158cc494286965c76e58742fa/coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574", size = 242987 }, 124 | { url = "https://files.pythonhosted.org/packages/4c/53/4e208440389e8ea936f5f2b0762dcd4cb03281a7722def8e2bf9dc9c3d68/coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985", size = 239881 }, 125 | { url = "https://files.pythonhosted.org/packages/c4/47/2ba744af8d2f0caa1f17e7746147e34dfc5f811fb65fc153153722d58835/coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750", size = 242142 }, 126 | { url = "https://files.pythonhosted.org/packages/e9/90/df726af8ee74d92ee7e3bf113bf101ea4315d71508952bd21abc3fae471e/coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea", size = 241437 }, 127 | { url = "https://files.pythonhosted.org/packages/f6/af/995263fd04ae5f9cf12521150295bf03b6ba940d0aea97953bb4a6db3e2b/coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3", size = 239724 }, 128 | { url = "https://files.pythonhosted.org/packages/1c/8e/5bb04f0318805e190984c6ce106b4c3968a9562a400180e549855d8211bd/coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a", size = 241329 }, 129 | { url = "https://files.pythonhosted.org/packages/9e/9d/fa04d9e6c3f6459f4e0b231925277cfc33d72dfab7fa19c312c03e59da99/coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95", size = 211289 }, 130 | { url = "https://files.pythonhosted.org/packages/53/40/53c7ffe3c0c3fff4d708bc99e65f3d78c129110d6629736faf2dbd60ad57/coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288", size = 212079 }, 131 | { url = "https://files.pythonhosted.org/packages/76/89/1adf3e634753c0de3dad2f02aac1e73dba58bc5a3a914ac94a25b2ef418f/coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1", size = 208673 }, 132 | { url = "https://files.pythonhosted.org/packages/ce/64/92a4e239d64d798535c5b45baac6b891c205a8a2e7c9cc8590ad386693dc/coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd", size = 208945 }, 133 | { url = "https://files.pythonhosted.org/packages/b4/d0/4596a3ef3bca20a94539c9b1e10fd250225d1dec57ea78b0867a1cf9742e/coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9", size = 242484 }, 134 | { url = "https://files.pythonhosted.org/packages/1c/ef/6fd0d344695af6718a38d0861408af48a709327335486a7ad7e85936dc6e/coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e", size = 239525 }, 135 | { url = "https://files.pythonhosted.org/packages/0c/4b/373be2be7dd42f2bcd6964059fd8fa307d265a29d2b9bcf1d044bcc156ed/coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4", size = 241545 }, 136 | { url = "https://files.pythonhosted.org/packages/a6/7d/0e83cc2673a7790650851ee92f72a343827ecaaea07960587c8f442b5cd3/coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6", size = 241179 }, 137 | { url = "https://files.pythonhosted.org/packages/ff/8c/566ea92ce2bb7627b0900124e24a99f9244b6c8c92d09ff9f7633eb7c3c8/coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3", size = 239288 }, 138 | { url = "https://files.pythonhosted.org/packages/7d/e4/869a138e50b622f796782d642c15fb5f25a5870c6d0059a663667a201638/coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc", size = 241032 }, 139 | { url = "https://files.pythonhosted.org/packages/ae/28/a52ff5d62a9f9e9fe9c4f17759b98632edd3a3489fce70154c7d66054dd3/coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3", size = 211315 }, 140 | { url = "https://files.pythonhosted.org/packages/bc/17/ab849b7429a639f9722fa5628364c28d675c7ff37ebc3268fe9840dda13c/coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef", size = 212099 }, 141 | { url = "https://files.pythonhosted.org/packages/d2/1c/b9965bf23e171d98505eb5eb4fb4d05c44efd256f2e0f19ad1ba8c3f54b0/coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e", size = 209511 }, 142 | { url = "https://files.pythonhosted.org/packages/57/b3/119c201d3b692d5e17784fee876a9a78e1b3051327de2709392962877ca8/coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703", size = 209729 }, 143 | { url = "https://files.pythonhosted.org/packages/52/4e/a7feb5a56b266304bc59f872ea07b728e14d5a64f1ad3a2cc01a3259c965/coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0", size = 253988 }, 144 | { url = "https://files.pythonhosted.org/packages/65/19/069fec4d6908d0dae98126aa7ad08ce5130a6decc8509da7740d36e8e8d2/coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924", size = 249697 }, 145 | { url = "https://files.pythonhosted.org/packages/1c/da/5b19f09ba39df7c55f77820736bf17bbe2416bbf5216a3100ac019e15839/coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b", size = 252033 }, 146 | { url = "https://files.pythonhosted.org/packages/1e/89/4c2750df7f80a7872267f7c5fe497c69d45f688f7b3afe1297e52e33f791/coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d", size = 251535 }, 147 | { url = "https://files.pythonhosted.org/packages/78/3b/6d3ae3c1cc05f1b0460c51e6f6dcf567598cbd7c6121e5ad06643974703c/coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827", size = 249192 }, 148 | { url = "https://files.pythonhosted.org/packages/6e/8e/c14a79f535ce41af7d436bbad0d3d90c43d9e38ec409b4770c894031422e/coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9", size = 250627 }, 149 | { url = "https://files.pythonhosted.org/packages/cb/79/b7cee656cfb17a7f2c1b9c3cee03dd5d8000ca299ad4038ba64b61a9b044/coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3", size = 212033 }, 150 | { url = "https://files.pythonhosted.org/packages/b6/c3/f7aaa3813f1fa9a4228175a7bd368199659d392897e184435a3b66408dd3/coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f", size = 213240 }, 151 | { url = "https://files.pythonhosted.org/packages/fb/b2/f655700e1024dec98b10ebaafd0cedbc25e40e4abe62a3c8e2ceef4f8f0a/coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953", size = 200552 }, 152 | ] 153 | 154 | [package.optional-dependencies] 155 | toml = [ 156 | { name = "tomli", marker = "python_full_version <= '3.11'" }, 157 | ] 158 | 159 | [[package]] 160 | name = "h11" 161 | version = "0.14.0" 162 | source = { registry = "https://pypi.org/simple" } 163 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } 164 | wheels = [ 165 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, 166 | ] 167 | 168 | [[package]] 169 | name = "httpcore" 170 | version = "1.0.7" 171 | source = { registry = "https://pypi.org/simple" } 172 | dependencies = [ 173 | { name = "certifi" }, 174 | { name = "h11" }, 175 | ] 176 | sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } 177 | wheels = [ 178 | { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, 179 | ] 180 | 181 | [[package]] 182 | name = "httpx" 183 | version = "0.28.1" 184 | source = { registry = "https://pypi.org/simple" } 185 | dependencies = [ 186 | { name = "anyio" }, 187 | { name = "certifi" }, 188 | { name = "httpcore" }, 189 | { name = "idna" }, 190 | ] 191 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } 192 | wheels = [ 193 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, 194 | ] 195 | 196 | [[package]] 197 | name = "httpx-sse" 198 | version = "0.4.0" 199 | source = { registry = "https://pypi.org/simple" } 200 | sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } 201 | wheels = [ 202 | { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, 203 | ] 204 | 205 | [[package]] 206 | name = "idna" 207 | version = "3.10" 208 | source = { registry = "https://pypi.org/simple" } 209 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 210 | wheels = [ 211 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 212 | ] 213 | 214 | [[package]] 215 | name = "iniconfig" 216 | version = "2.0.0" 217 | source = { registry = "https://pypi.org/simple" } 218 | sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } 219 | wheels = [ 220 | { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, 221 | ] 222 | 223 | [[package]] 224 | name = "mcp" 225 | version = "1.4.1" 226 | source = { registry = "https://pypi.org/simple" } 227 | dependencies = [ 228 | { name = "anyio" }, 229 | { name = "httpx" }, 230 | { name = "httpx-sse" }, 231 | { name = "pydantic" }, 232 | { name = "pydantic-settings" }, 233 | { name = "sse-starlette" }, 234 | { name = "starlette" }, 235 | { name = "uvicorn" }, 236 | ] 237 | sdist = { url = "https://files.pythonhosted.org/packages/50/cc/5c5bb19f1a0f8f89a95e25cb608b0b07009e81fd4b031e519335404e1422/mcp-1.4.1.tar.gz", hash = "sha256:b9655d2de6313f9d55a7d1df62b3c3fe27a530100cc85bf23729145b0dba4c7a", size = 154942 } 238 | wheels = [ 239 | { url = "https://files.pythonhosted.org/packages/e8/0e/885f156ade60108e67bf044fada5269da68e29d758a10b0c513f4d85dd76/mcp-1.4.1-py3-none-any.whl", hash = "sha256:a7716b1ec1c054e76f49806f7d96113b99fc1166fc9244c2c6f19867cb75b593", size = 72448 }, 240 | ] 241 | 242 | [[package]] 243 | name = "mcp-tavily" 244 | version = "0.1.8" 245 | source = { editable = "." } 246 | dependencies = [ 247 | { name = "mcp" }, 248 | { name = "pydantic" }, 249 | { name = "python-dotenv" }, 250 | { name = "tavily-python" }, 251 | ] 252 | 253 | [package.optional-dependencies] 254 | test = [ 255 | { name = "pytest" }, 256 | { name = "pytest-asyncio" }, 257 | { name = "pytest-cov" }, 258 | { name = "pytest-mock" }, 259 | ] 260 | 261 | [package.dev-dependencies] 262 | dev = [ 263 | { name = "pytest" }, 264 | { name = "pytest-asyncio" }, 265 | { name = "pytest-cov" }, 266 | { name = "pytest-mock" }, 267 | ] 268 | 269 | [package.metadata] 270 | requires-dist = [ 271 | { name = "mcp", specifier = ">=1.0.0" }, 272 | { name = "pydantic", specifier = ">=2.10.2" }, 273 | { name = "pytest", marker = "extra == 'test'", specifier = ">=7.0.0" }, 274 | { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.23.0" }, 275 | { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=4.1.0" }, 276 | { name = "pytest-mock", marker = "extra == 'test'", specifier = ">=3.10.0" }, 277 | { name = "python-dotenv", specifier = ">=1.0.1" }, 278 | { name = "tavily-python", specifier = ">=0.5.0" }, 279 | ] 280 | 281 | [package.metadata.requires-dev] 282 | dev = [ 283 | { name = "pytest", specifier = ">=8.3.5" }, 284 | { name = "pytest-asyncio", specifier = ">=0.25.3" }, 285 | { name = "pytest-cov", specifier = ">=6.0.0" }, 286 | { name = "pytest-mock", specifier = ">=3.14.0" }, 287 | ] 288 | 289 | [[package]] 290 | name = "packaging" 291 | version = "24.2" 292 | source = { registry = "https://pypi.org/simple" } 293 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } 294 | wheels = [ 295 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, 296 | ] 297 | 298 | [[package]] 299 | name = "pluggy" 300 | version = "1.5.0" 301 | source = { registry = "https://pypi.org/simple" } 302 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } 303 | wheels = [ 304 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, 305 | ] 306 | 307 | [[package]] 308 | name = "pydantic" 309 | version = "2.10.6" 310 | source = { registry = "https://pypi.org/simple" } 311 | dependencies = [ 312 | { name = "annotated-types" }, 313 | { name = "pydantic-core" }, 314 | { name = "typing-extensions" }, 315 | ] 316 | sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } 317 | wheels = [ 318 | { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, 319 | ] 320 | 321 | [[package]] 322 | name = "pydantic-core" 323 | version = "2.27.2" 324 | source = { registry = "https://pypi.org/simple" } 325 | dependencies = [ 326 | { name = "typing-extensions" }, 327 | ] 328 | sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } 329 | wheels = [ 330 | { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, 331 | { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, 332 | { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, 333 | { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, 334 | { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, 335 | { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, 336 | { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, 337 | { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, 338 | { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, 339 | { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, 340 | { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, 341 | { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, 342 | { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, 343 | { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, 344 | { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, 345 | { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, 346 | { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, 347 | { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, 348 | { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, 349 | { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, 350 | { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, 351 | { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, 352 | { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, 353 | { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, 354 | { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, 355 | { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, 356 | { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, 357 | { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, 358 | { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, 359 | { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, 360 | { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, 361 | { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, 362 | { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, 363 | { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, 364 | { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, 365 | { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, 366 | { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, 367 | { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, 368 | { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, 369 | { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, 370 | { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, 371 | { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, 372 | ] 373 | 374 | [[package]] 375 | name = "pydantic-settings" 376 | version = "2.8.1" 377 | source = { registry = "https://pypi.org/simple" } 378 | dependencies = [ 379 | { name = "pydantic" }, 380 | { name = "python-dotenv" }, 381 | ] 382 | sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } 383 | wheels = [ 384 | { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, 385 | ] 386 | 387 | [[package]] 388 | name = "pytest" 389 | version = "8.3.5" 390 | source = { registry = "https://pypi.org/simple" } 391 | dependencies = [ 392 | { name = "colorama", marker = "sys_platform == 'win32'" }, 393 | { name = "iniconfig" }, 394 | { name = "packaging" }, 395 | { name = "pluggy" }, 396 | ] 397 | sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } 398 | wheels = [ 399 | { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, 400 | ] 401 | 402 | [[package]] 403 | name = "pytest-asyncio" 404 | version = "0.25.3" 405 | source = { registry = "https://pypi.org/simple" } 406 | dependencies = [ 407 | { name = "pytest" }, 408 | ] 409 | sdist = { url = "https://files.pythonhosted.org/packages/f2/a8/ecbc8ede70921dd2f544ab1cadd3ff3bf842af27f87bbdea774c7baa1d38/pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a", size = 54239 } 410 | wheels = [ 411 | { url = "https://files.pythonhosted.org/packages/67/17/3493c5624e48fd97156ebaec380dcaafee9506d7e2c46218ceebbb57d7de/pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3", size = 19467 }, 412 | ] 413 | 414 | [[package]] 415 | name = "pytest-cov" 416 | version = "6.0.0" 417 | source = { registry = "https://pypi.org/simple" } 418 | dependencies = [ 419 | { name = "coverage", extra = ["toml"] }, 420 | { name = "pytest" }, 421 | ] 422 | sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } 423 | wheels = [ 424 | { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, 425 | ] 426 | 427 | [[package]] 428 | name = "pytest-mock" 429 | version = "3.14.0" 430 | source = { registry = "https://pypi.org/simple" } 431 | dependencies = [ 432 | { name = "pytest" }, 433 | ] 434 | sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 } 435 | wheels = [ 436 | { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, 437 | ] 438 | 439 | [[package]] 440 | name = "python-dotenv" 441 | version = "1.0.1" 442 | source = { registry = "https://pypi.org/simple" } 443 | sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } 444 | wheels = [ 445 | { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, 446 | ] 447 | 448 | [[package]] 449 | name = "regex" 450 | version = "2024.11.6" 451 | source = { registry = "https://pypi.org/simple" } 452 | sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } 453 | wheels = [ 454 | { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, 455 | { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, 456 | { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, 457 | { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, 458 | { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, 459 | { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, 460 | { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, 461 | { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, 462 | { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, 463 | { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, 464 | { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, 465 | { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, 466 | { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, 467 | { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, 468 | { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, 469 | { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, 470 | { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, 471 | { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, 472 | { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, 473 | { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, 474 | { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, 475 | { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, 476 | { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, 477 | { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, 478 | { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, 479 | { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, 480 | { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, 481 | { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, 482 | { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, 483 | { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, 484 | { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, 485 | { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, 486 | { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, 487 | { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, 488 | { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, 489 | { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, 490 | { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, 491 | { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, 492 | { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, 493 | { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, 494 | { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, 495 | { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, 496 | { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, 497 | { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, 498 | { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, 499 | ] 500 | 501 | [[package]] 502 | name = "requests" 503 | version = "2.32.3" 504 | source = { registry = "https://pypi.org/simple" } 505 | dependencies = [ 506 | { name = "certifi" }, 507 | { name = "charset-normalizer" }, 508 | { name = "idna" }, 509 | { name = "urllib3" }, 510 | ] 511 | sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } 512 | wheels = [ 513 | { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, 514 | ] 515 | 516 | [[package]] 517 | name = "sniffio" 518 | version = "1.3.1" 519 | source = { registry = "https://pypi.org/simple" } 520 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 521 | wheels = [ 522 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 523 | ] 524 | 525 | [[package]] 526 | name = "sse-starlette" 527 | version = "2.2.1" 528 | source = { registry = "https://pypi.org/simple" } 529 | dependencies = [ 530 | { name = "anyio" }, 531 | { name = "starlette" }, 532 | ] 533 | sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } 534 | wheels = [ 535 | { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, 536 | ] 537 | 538 | [[package]] 539 | name = "starlette" 540 | version = "0.46.1" 541 | source = { registry = "https://pypi.org/simple" } 542 | dependencies = [ 543 | { name = "anyio" }, 544 | ] 545 | sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 } 546 | wheels = [ 547 | { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, 548 | ] 549 | 550 | [[package]] 551 | name = "tavily-python" 552 | version = "0.5.1" 553 | source = { registry = "https://pypi.org/simple" } 554 | dependencies = [ 555 | { name = "httpx" }, 556 | { name = "requests" }, 557 | { name = "tiktoken" }, 558 | ] 559 | sdist = { url = "https://files.pythonhosted.org/packages/db/ff/ba1a3769c34d022aeba544ff7b18cbcd0d23a6358fc3566b2101c6bf2817/tavily_python-0.5.1.tar.gz", hash = "sha256:44b0eefe79a057cd11d3cd03780b63b4913400122350e38285acfb502c2fffc1", size = 107503 } 560 | wheels = [ 561 | { url = "https://files.pythonhosted.org/packages/a5/cd/71088461d7720128c78802289b3b36298f42745e5f8c334b0ffc157b881e/tavily_python-0.5.1-py3-none-any.whl", hash = "sha256:169601f703c55cf338758dcacfa7102473b479a9271d65a3af6fc3668990f757", size = 43767 }, 562 | ] 563 | 564 | [[package]] 565 | name = "tiktoken" 566 | version = "0.9.0" 567 | source = { registry = "https://pypi.org/simple" } 568 | dependencies = [ 569 | { name = "regex" }, 570 | { name = "requests" }, 571 | ] 572 | sdist = { url = "https://files.pythonhosted.org/packages/ea/cf/756fedf6981e82897f2d570dd25fa597eb3f4459068ae0572d7e888cfd6f/tiktoken-0.9.0.tar.gz", hash = "sha256:d02a5ca6a938e0490e1ff957bc48c8b078c88cb83977be1625b1fd8aac792c5d", size = 35991 } 573 | wheels = [ 574 | { url = "https://files.pythonhosted.org/packages/4d/ae/4613a59a2a48e761c5161237fc850eb470b4bb93696db89da51b79a871f1/tiktoken-0.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f32cc56168eac4851109e9b5d327637f15fd662aa30dd79f964b7c39fbadd26e", size = 1065987 }, 575 | { url = "https://files.pythonhosted.org/packages/3f/86/55d9d1f5b5a7e1164d0f1538a85529b5fcba2b105f92db3622e5d7de6522/tiktoken-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:45556bc41241e5294063508caf901bf92ba52d8ef9222023f83d2483a3055348", size = 1009155 }, 576 | { url = "https://files.pythonhosted.org/packages/03/58/01fb6240df083b7c1916d1dcb024e2b761213c95d576e9f780dfb5625a76/tiktoken-0.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03935988a91d6d3216e2ec7c645afbb3d870b37bcb67ada1943ec48678e7ee33", size = 1142898 }, 577 | { url = "https://files.pythonhosted.org/packages/b1/73/41591c525680cd460a6becf56c9b17468d3711b1df242c53d2c7b2183d16/tiktoken-0.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b3d80aad8d2c6b9238fc1a5524542087c52b860b10cbf952429ffb714bc1136", size = 1197535 }, 578 | { url = "https://files.pythonhosted.org/packages/7d/7c/1069f25521c8f01a1a182f362e5c8e0337907fae91b368b7da9c3e39b810/tiktoken-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b2a21133be05dc116b1d0372af051cd2c6aa1d2188250c9b553f9fa49301b336", size = 1259548 }, 579 | { url = "https://files.pythonhosted.org/packages/6f/07/c67ad1724b8e14e2b4c8cca04b15da158733ac60136879131db05dda7c30/tiktoken-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:11a20e67fdf58b0e2dea7b8654a288e481bb4fc0289d3ad21291f8d0849915fb", size = 893895 }, 580 | { url = "https://files.pythonhosted.org/packages/cf/e5/21ff33ecfa2101c1bb0f9b6df750553bd873b7fb532ce2cb276ff40b197f/tiktoken-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e88f121c1c22b726649ce67c089b90ddda8b9662545a8aeb03cfef15967ddd03", size = 1065073 }, 581 | { url = "https://files.pythonhosted.org/packages/8e/03/a95e7b4863ee9ceec1c55983e4cc9558bcfd8f4f80e19c4f8a99642f697d/tiktoken-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a6600660f2f72369acb13a57fb3e212434ed38b045fd8cc6cdd74947b4b5d210", size = 1008075 }, 582 | { url = "https://files.pythonhosted.org/packages/40/10/1305bb02a561595088235a513ec73e50b32e74364fef4de519da69bc8010/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95e811743b5dfa74f4b227927ed86cbc57cad4df859cb3b643be797914e41794", size = 1140754 }, 583 | { url = "https://files.pythonhosted.org/packages/1b/40/da42522018ca496432ffd02793c3a72a739ac04c3794a4914570c9bb2925/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99376e1370d59bcf6935c933cb9ba64adc29033b7e73f5f7569f3aad86552b22", size = 1196678 }, 584 | { url = "https://files.pythonhosted.org/packages/5c/41/1e59dddaae270ba20187ceb8aa52c75b24ffc09f547233991d5fd822838b/tiktoken-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:badb947c32739fb6ddde173e14885fb3de4d32ab9d8c591cbd013c22b4c31dd2", size = 1259283 }, 585 | { url = "https://files.pythonhosted.org/packages/5b/64/b16003419a1d7728d0d8c0d56a4c24325e7b10a21a9dd1fc0f7115c02f0a/tiktoken-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:5a62d7a25225bafed786a524c1b9f0910a1128f4232615bf3f8257a73aaa3b16", size = 894897 }, 586 | { url = "https://files.pythonhosted.org/packages/7a/11/09d936d37f49f4f494ffe660af44acd2d99eb2429d60a57c71318af214e0/tiktoken-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b0e8e05a26eda1249e824156d537015480af7ae222ccb798e5234ae0285dbdb", size = 1064919 }, 587 | { url = "https://files.pythonhosted.org/packages/80/0e/f38ba35713edb8d4197ae602e80837d574244ced7fb1b6070b31c29816e0/tiktoken-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:27d457f096f87685195eea0165a1807fae87b97b2161fe8c9b1df5bd74ca6f63", size = 1007877 }, 588 | { url = "https://files.pythonhosted.org/packages/fe/82/9197f77421e2a01373e27a79dd36efdd99e6b4115746ecc553318ecafbf0/tiktoken-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cf8ded49cddf825390e36dd1ad35cd49589e8161fdcb52aa25f0583e90a3e01", size = 1140095 }, 589 | { url = "https://files.pythonhosted.org/packages/f2/bb/4513da71cac187383541facd0291c4572b03ec23c561de5811781bbd988f/tiktoken-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc156cb314119a8bb9748257a2eaebd5cc0753b6cb491d26694ed42fc7cb3139", size = 1195649 }, 590 | { url = "https://files.pythonhosted.org/packages/fa/5c/74e4c137530dd8504e97e3a41729b1103a4ac29036cbfd3250b11fd29451/tiktoken-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cd69372e8c9dd761f0ab873112aba55a0e3e506332dd9f7522ca466e817b1b7a", size = 1258465 }, 591 | { url = "https://files.pythonhosted.org/packages/de/a8/8f499c179ec900783ffe133e9aab10044481679bb9aad78436d239eee716/tiktoken-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5ea0edb6f83dc56d794723286215918c1cde03712cbbafa0348b33448faf5b95", size = 894669 }, 592 | ] 593 | 594 | [[package]] 595 | name = "tomli" 596 | version = "2.2.1" 597 | source = { registry = "https://pypi.org/simple" } 598 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } 599 | wheels = [ 600 | { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, 601 | { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, 602 | { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, 603 | { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, 604 | { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, 605 | { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, 606 | { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, 607 | { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, 608 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, 609 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, 610 | { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, 611 | { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, 612 | { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, 613 | { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, 614 | { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, 615 | { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, 616 | { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, 617 | { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, 618 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, 619 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, 620 | { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, 621 | { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, 622 | { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, 623 | { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, 624 | { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, 625 | { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, 626 | { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, 627 | { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, 628 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, 629 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, 630 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, 631 | ] 632 | 633 | [[package]] 634 | name = "typing-extensions" 635 | version = "4.12.2" 636 | source = { registry = "https://pypi.org/simple" } 637 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 638 | wheels = [ 639 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 640 | ] 641 | 642 | [[package]] 643 | name = "urllib3" 644 | version = "2.3.0" 645 | source = { registry = "https://pypi.org/simple" } 646 | sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } 647 | wheels = [ 648 | { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, 649 | ] 650 | 651 | [[package]] 652 | name = "uvicorn" 653 | version = "0.34.0" 654 | source = { registry = "https://pypi.org/simple" } 655 | dependencies = [ 656 | { name = "click" }, 657 | { name = "h11" }, 658 | ] 659 | sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } 660 | wheels = [ 661 | { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, 662 | ] 663 | --------------------------------------------------------------------------------