├── .github └── workflows │ ├── pytest.yml │ └── python-publish.yml ├── .gitignore ├── .pre-commit-config.yaml ├── Dockerfile ├── LICENSE ├── README.md ├── assets └── airflow_mcp_server_demo.mp4 ├── pyproject.toml ├── src └── airflow_mcp_server │ ├── __init__.py │ ├── __main__.py │ ├── config.py │ ├── hierarchical_manager.py │ ├── prompts.py │ ├── resources.py │ ├── server_safe.py │ ├── server_unsafe.py │ └── utils │ ├── __init__.py │ └── category_mapper.py ├── tests ├── __init__.py ├── conftest.py ├── parser │ └── openapi.json ├── test_category_mapper.py ├── test_config.py ├── test_hierarchical_manager.py ├── test_main.py ├── test_server.py └── tools │ └── __init__.py └── uv.lock /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: ["3.11", "3.12"] 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install .[dev] 26 | 27 | - name: Run pytest 28 | run: | 29 | pytest tests/ -v 30 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package to PyPI when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | release-build: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - uses: actions/setup-python@v5 26 | with: 27 | python-version: "3.x" 28 | 29 | - name: Install uv 30 | run: | 31 | pip install uv 32 | 33 | - name: Build release distributions 34 | run: | 35 | uv pip install --system build 36 | python -m build 37 | 38 | - name: Upload distributions 39 | uses: actions/upload-artifact@v4 40 | with: 41 | name: release-dists 42 | path: dist/ 43 | 44 | pypi-publish: 45 | runs-on: ubuntu-latest 46 | needs: 47 | - release-build 48 | permissions: 49 | # IMPORTANT: this permission is mandatory for trusted publishing 50 | id-token: write 51 | 52 | environment: 53 | name: pypi 54 | url: https://pypi.org/project/airflow-mcp-server/ 55 | 56 | steps: 57 | - name: Retrieve release distributions 58 | uses: actions/download-artifact@v4 59 | with: 60 | name: release-dists 61 | path: dist/ 62 | 63 | - name: Publish release distributions to PyPI 64 | uses: pypa/gh-action-pypi-publish@release/v1 65 | with: 66 | packages-dir: dist/ 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | 173 | 174 | # Local Resources 175 | project_resources/ 176 | ._*.py 177 | ._.repo_ignore 178 | .repo_ignore 179 | 180 | # Ruff 181 | .ruff_cache/ 182 | 183 | # Airflow 184 | AIRFLOW_HOME/ 185 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.11.8 4 | hooks: 5 | - id: ruff 6 | args: [--fix] 7 | - id: ruff-format 8 | 9 | - repo: https://github.com/pre-commit/pre-commit-hooks 10 | rev: v5.0.0 11 | hooks: 12 | - id: trailing-whitespace 13 | - id: end-of-file-fixer 14 | - id: check-yaml 15 | - id: check-added-large-files 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use a Python image with uv pre-installed 2 | FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS uv 3 | 4 | WORKDIR /app 5 | 6 | ENV UV_COMPILE_BYTECODE=1 7 | ENV UV_LINK_MODE=copy 8 | 9 | RUN --mount=type=cache,target=/root/.cache/uv \ 10 | --mount=type=bind,source=uv.lock,target=uv.lock \ 11 | --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ 12 | uv sync --frozen --no-install-project --no-dev --no-editable 13 | 14 | ADD . /app 15 | RUN --mount=type=cache,target=/root/.cache/uv \ 16 | uv sync --frozen --no-dev --no-editable 17 | 18 | FROM python:3.12-slim-bookworm 19 | 20 | WORKDIR /app 21 | 22 | COPY --from=uv /root/.local /root/.local 23 | COPY --from=uv --chown=app:app /app/.venv /app/.venv 24 | 25 | ENV PATH="/app/.venv/bin:$PATH" 26 | 27 | ENTRYPOINT ["airflow-mcp-server"] 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Abhishek Bhakat 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 | # airflow-mcp-server: An MCP Server for controlling Airflow 2 | 3 | ### Find on Glama 4 | 5 | 6 | 7 | 8 | 9 | ## Overview 10 | A [Model Context Protocol](https://modelcontextprotocol.io/) server for controlling Airflow via Airflow APIs. 11 | 12 | ## Demo Video 13 | 14 | https://github.com/user-attachments/assets/f3e60fff-8680-4dd9-b08e-fa7db655a705 15 | 16 | ## Setup 17 | 18 | ### Usage with Claude Desktop 19 | 20 | ```json 21 | { 22 | "mcpServers": { 23 | "airflow-mcp-server": { 24 | "command": "uvx", 25 | "args": [ 26 | "airflow-mcp-server", 27 | "--base-url", 28 | "http://localhost:8080", 29 | "--auth-token", 30 | "" 31 | ] 32 | } 33 | } 34 | } 35 | ``` 36 | 37 | > **Note:** 38 | > - Set `base_url` to the root Airflow URL (e.g., `http://localhost:8080`). 39 | > - Do **not** include `/api/v2` in the base URL. The server will automatically fetch the OpenAPI spec from `${base_url}/openapi.json`. 40 | > - Only JWT token is required for authentication. Cookie and basic auth are no longer supported in Airflow 3.0. 41 | 42 | ### Operation Modes 43 | 44 | The server supports two operation modes: 45 | 46 | - **Safe Mode** (`--safe`): Only allows read-only operations (GET requests). This is useful when you want to prevent any modifications to your Airflow instance. 47 | - **Unsafe Mode** (`--unsafe`): Allows all operations including modifications. This is the default mode. 48 | 49 | To start in safe mode: 50 | ```bash 51 | airflow-mcp-server --safe 52 | ``` 53 | 54 | To explicitly start in unsafe mode (though this is default): 55 | ```bash 56 | airflow-mcp-server --unsafe 57 | ``` 58 | 59 | ### Tool Discovery Modes 60 | 61 | The server supports two tool discovery approaches: 62 | 63 | - **Hierarchical Discovery** (default): Tools are organized by categories (DAGs, Tasks, Connections, etc.). Browse categories first, then select specific tools. More manageable for large APIs. 64 | - **Static Tools** (`--static-tools`): All tools available immediately. Better for programmatic access but can be overwhelming. 65 | 66 | To use static tools: 67 | ```bash 68 | airflow-mcp-server --static-tools 69 | ``` 70 | 71 | ### Considerations 72 | 73 | **Authentication** 74 | 75 | - Only JWT authentication is supported in Airflow 3.0. You must provide a valid `AUTH_TOKEN`. 76 | 77 | **Page Limit** 78 | 79 | The default is 100 items, but you can change it using `maximum_page_limit` option in [api] section in the `airflow.cfg` file. 80 | 81 | ## Tasks 82 | 83 | - [x] Airflow 3 readiness 84 | - [x] Parse OpenAPI Spec 85 | - [x] Safe/Unsafe mode implementation 86 | - [x] Parse proper description with list_tools. 87 | - [x] Airflow config fetch (_specifically for page limit_) 88 | - [ ] Env variables optional (_env variables might not be ideal for airflow plugins_) 89 | -------------------------------------------------------------------------------- /assets/airflow_mcp_server_demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhishekbhakat/airflow-mcp-server/91a053dc79a50f625e47b5b8b3e01dbed83e0168/assets/airflow_mcp_server_demo.mp4 -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "airflow-mcp-server" 3 | version = "0.7.0" 4 | description = "MCP Server for Airflow" 5 | readme = "README.md" 6 | requires-python = ">=3.11" 7 | authors = [ 8 | {name = "Abhishek Bhakat", email = "abhishek.bhakat@hotmail.com"} 9 | ] 10 | dependencies = [ 11 | "fastmcp>=2.5.2", 12 | "httpx>=0.28.1", 13 | "click>=8.2.1", 14 | ] 15 | classifiers = [ 16 | "Development Status :: 3 - Alpha", 17 | "Operating System :: OS Independent", 18 | "Programming Language :: Python :: 3.12", 19 | "Programming Language :: Python :: 3.11", 20 | "Programming Language :: Python :: 3.10", 21 | ] 22 | license = "MIT" 23 | license-files = ["LICEN[CS]E*"] 24 | 25 | [project.urls] 26 | GitHub = "https://github.com/abhishekbhakat/airflow-mcp-server" 27 | Issues = "https://github.com/abhishekbhakat/airflow-mcp-server/issues" 28 | 29 | [project.scripts] 30 | airflow-mcp-server = "airflow_mcp_server.__main__:main" 31 | 32 | [project.optional-dependencies] 33 | dev = [ 34 | "build>=1.2.2.post1", 35 | "pre-commit>=4.2.0", 36 | "pytest>=8.3.5", 37 | "pytest-asyncio>=1.0.0", 38 | "pytest-mock>=3.14.1", 39 | "ruff>=0.11.12" 40 | ] 41 | 42 | [build-system] 43 | requires = ["hatchling"] 44 | build-backend = "hatchling.build" 45 | 46 | [tool.hatch.build.targets.sdist] 47 | exclude = [ 48 | "*", 49 | "!src/**", 50 | "!pyproject.toml", 51 | "!assets/**" 52 | ] 53 | 54 | [tool.hatch.build.targets.wheel] 55 | packages = ["src/airflow_mcp_server"] 56 | 57 | [tool.hatch.build.targets.wheel.sources] 58 | "src/airflow_mcp_server" = "airflow_mcp_server" 59 | 60 | [tool.pytest.ini_options] 61 | pythonpath = ["src"] 62 | asyncio_mode = "strict" 63 | testpaths = ["tests"] 64 | python_classes = "!TestRequestModel,!TestResponseModel" 65 | asyncio_default_fixture_loop_scope = "function" 66 | 67 | [tool.ruff] 68 | line-length = 200 69 | indent-width = 4 70 | fix = true 71 | preview = true 72 | 73 | lint.select = [ 74 | "E", # pycodestyle errors 75 | "F", # pyflakes 76 | "I", # isort 77 | "W", # pycodestyle warnings 78 | "C90", # Complexity 79 | "C", # flake8-comprehensions 80 | "ISC", # flake8-implicit-str-concat 81 | "T10", # flake8-debugger 82 | "A", # flake8-builtins 83 | "UP", # pyupgrade 84 | ] 85 | 86 | lint.ignore = [ 87 | "C416", # Unnecessary list comprehension - rewrite as a generator expression 88 | "C408", # Unnecessary `dict` call - rewrite as a literal 89 | "ISC001" # Single line implicit string concatenation 90 | ] 91 | 92 | lint.fixable = ["ALL"] 93 | lint.unfixable = [] 94 | 95 | [tool.ruff.format] 96 | quote-style = "double" 97 | indent-style = "space" 98 | skip-magic-trailing-comma = false 99 | 100 | [tool.ruff.lint.isort] 101 | combine-as-imports = true 102 | 103 | [tool.ruff.lint.mccabe] 104 | max-complexity = 12 105 | -------------------------------------------------------------------------------- /src/airflow_mcp_server/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import os 4 | import sys 5 | 6 | import click 7 | 8 | from airflow_mcp_server.config import AirflowConfig 9 | from airflow_mcp_server.server_safe import serve as serve_safe 10 | from airflow_mcp_server.server_unsafe import serve as serve_unsafe 11 | 12 | 13 | @click.command() 14 | @click.option("-v", "--verbose", count=True, help="Increase verbosity") 15 | @click.option("--safe", "-s", is_flag=True, help="Use only read-only tools") 16 | @click.option("--unsafe", "-u", is_flag=True, help="Use all tools (default)") 17 | @click.option("--static-tools", is_flag=True, help="Use static tools instead of hierarchical discovery") 18 | @click.option("--base-url", help="Airflow API base URL") 19 | @click.option("--auth-token", help="Authentication token (JWT)") 20 | def main(verbose: int, safe: bool, unsafe: bool, static_tools: bool, base_url: str = None, auth_token: str = None) -> None: 21 | """MCP server for Airflow""" 22 | logging_level = logging.WARN 23 | if verbose == 1: 24 | logging_level = logging.INFO 25 | elif verbose >= 2: 26 | logging_level = logging.DEBUG 27 | 28 | logging.basicConfig(level=logging_level, stream=sys.stderr) 29 | 30 | config_base_url = os.environ.get("AIRFLOW_BASE_URL") or base_url 31 | config_auth_token = os.environ.get("AUTH_TOKEN") or auth_token 32 | 33 | try: 34 | config = AirflowConfig(base_url=config_base_url, auth_token=config_auth_token) 35 | except ValueError as e: 36 | click.echo(f"Configuration error: {e}", err=True) 37 | sys.exit(1) 38 | 39 | if safe and unsafe: 40 | raise click.UsageError("Options --safe and --unsafe are mutually exclusive") 41 | elif safe: 42 | asyncio.run(serve_safe(config, static_tools=static_tools)) 43 | elif unsafe: 44 | asyncio.run(serve_unsafe(config, static_tools=static_tools)) 45 | else: 46 | asyncio.run(serve_unsafe(config, static_tools=static_tools)) 47 | 48 | 49 | if __name__ == "__main__": 50 | main() 51 | -------------------------------------------------------------------------------- /src/airflow_mcp_server/__main__.py: -------------------------------------------------------------------------------- 1 | from airflow_mcp_server import main 2 | 3 | main() 4 | -------------------------------------------------------------------------------- /src/airflow_mcp_server/config.py: -------------------------------------------------------------------------------- 1 | class AirflowConfig: 2 | """Centralized configuration for Airflow MCP server.""" 3 | 4 | def __init__(self, base_url: str | None = None, auth_token: str | None = None) -> None: 5 | """Initialize configuration with provided values. 6 | 7 | Args: 8 | base_url: Airflow API base URL 9 | auth_token: Authentication token (JWT) 10 | 11 | Raises: 12 | ValueError: If required configuration is missing 13 | """ 14 | self.base_url = base_url 15 | if not self.base_url: 16 | raise ValueError("Missing required configuration: base_url") 17 | 18 | self.auth_token = auth_token 19 | if not self.auth_token: 20 | raise ValueError("Missing required configuration: auth_token (JWT)") 21 | -------------------------------------------------------------------------------- /src/airflow_mcp_server/hierarchical_manager.py: -------------------------------------------------------------------------------- 1 | """Hierarchical tool manager for dynamic tool discovery in Airflow MCP server.""" 2 | 3 | import logging 4 | 5 | import httpx 6 | from fastmcp import Context, FastMCP 7 | from fastmcp.server.openapi import MCPType, RouteMap 8 | 9 | from .utils.category_mapper import extract_categories_from_openapi, filter_routes_by_methods, get_category_info, get_category_tools_info 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class HierarchicalToolManager: 15 | """Manages dynamic tool state transitions for hierarchical discovery.""" 16 | 17 | PERSISTENT_TOOLS = {"browse_categories", "select_category", "get_current_category"} 18 | 19 | def __init__(self, mcp: FastMCP, openapi_spec: dict, client: httpx.AsyncClient, allowed_methods: set[str] | None = None): 20 | """Initialize hierarchical tool manager. 21 | 22 | Args: 23 | mcp: FastMCP server instance 24 | openapi_spec: OpenAPI specification dictionary 25 | client: HTTP client for API calls 26 | allowed_methods: Set of allowed HTTP methods (e.g., {"GET"} for safe mode) 27 | """ 28 | self.mcp = mcp 29 | self.openapi_spec = openapi_spec 30 | self.client = client 31 | self.allowed_methods = allowed_methods or {"GET", "POST", "PUT", "DELETE", "PATCH"} 32 | self.current_mode = "categories" 33 | self.current_tools = set() 34 | self.category_tool_instances = {} 35 | 36 | all_categories = extract_categories_from_openapi(openapi_spec) 37 | 38 | self.categories = {} 39 | for category, routes in all_categories.items(): 40 | filtered_routes = filter_routes_by_methods(routes, self.allowed_methods) 41 | if filtered_routes: 42 | self.categories[category] = filtered_routes 43 | 44 | logger.info(f"Discovered {len(self.categories)} categories with {sum(len(routes) for routes in self.categories.values())} total tools") 45 | 46 | self._add_persistent_tools() 47 | 48 | def get_categories_info(self) -> str: 49 | """Get formatted information about all available categories.""" 50 | return get_category_info(self.categories) 51 | 52 | def switch_to_category(self, category: str) -> str: 53 | """Switch to tools for specific category. 54 | 55 | Args: 56 | category: Category name to switch to 57 | 58 | Returns: 59 | Status message 60 | """ 61 | if category not in self.categories: 62 | available = ", ".join(self.categories.keys()) 63 | return f"Category '{category}' not found. Available: {available}" 64 | 65 | self._remove_current_tools() 66 | 67 | self._add_category_tools(category) 68 | self.current_mode = category 69 | 70 | routes_count = len(self.categories[category]) 71 | return f"Switched to {category} tools ({routes_count} available). Navigation tools always available." 72 | 73 | def get_current_category(self) -> str: 74 | """Get currently selected category. 75 | 76 | Returns: 77 | Current category status 78 | """ 79 | if self.current_mode == "categories": 80 | return "No category selected. Currently browsing all categories." 81 | else: 82 | routes_count = len(self.categories[self.current_mode]) 83 | return f"Currently selected category: {self.current_mode} ({routes_count} tools available)" 84 | 85 | def _remove_current_tools(self): 86 | """Remove category-specific tools but keep persistent navigation tools.""" 87 | tools_to_remove = self.current_tools - self.PERSISTENT_TOOLS 88 | for tool_name in tools_to_remove: 89 | try: 90 | self.mcp.remove_tool(tool_name) 91 | logger.debug(f"Removed tool: {tool_name}") 92 | except Exception as e: 93 | logger.warning(f"Failed to remove tool {tool_name}: {e}") 94 | 95 | for category, mount_prefix in self.category_tool_instances.items(): 96 | try: 97 | logger.debug(f"Category {category} tools remain mounted under '{mount_prefix}'") 98 | except Exception as e: 99 | logger.warning(f"Failed to unmount {category} tools: {e}") 100 | 101 | self.current_tools = self.current_tools & self.PERSISTENT_TOOLS 102 | 103 | def _add_persistent_tools(self): 104 | """Add persistent navigation tools that are always available.""" 105 | 106 | @self.mcp.tool() 107 | def browse_categories() -> str: 108 | """Show all available Airflow categories with tool counts.""" 109 | return self.get_categories_info() 110 | 111 | @self.mcp.tool() 112 | async def select_category(category: str, ctx: Context) -> str: 113 | """Switch to tools for specific category. 114 | 115 | Args: 116 | category: Name of the category to explore 117 | """ 118 | result = self.switch_to_category(category) 119 | 120 | try: 121 | await ctx.session.send_tool_list_changed() 122 | logger.info(f"Sent tools/list_changed notification after selecting category: {category}") 123 | except Exception as e: 124 | logger.warning(f"Failed to send tool list notification: {e}") 125 | 126 | return result 127 | 128 | @self.mcp.tool() 129 | def get_current_category() -> str: 130 | """Get currently selected category.""" 131 | return self.get_current_category() 132 | 133 | self.current_tools.update(self.PERSISTENT_TOOLS) 134 | 135 | logger.info(f"Added persistent navigation tools: {self.PERSISTENT_TOOLS}") 136 | 137 | def _add_category_tools(self, category: str): 138 | """Add tools for specific category using FastMCP's OpenAPI tool generation. 139 | 140 | Args: 141 | category: Category name 142 | """ 143 | routes = self.categories[category] 144 | 145 | @self.mcp.tool() 146 | def category_info() -> str: 147 | """Show information about current category tools.""" 148 | return get_category_tools_info(category, routes) 149 | 150 | self.current_tools.add("category_info") 151 | 152 | category_tools = self._create_category_api_tools(category, routes) 153 | 154 | logger.info(f"Added {category} tools: category_info + {len(category_tools)} API tools (persistent navigation always available)") 155 | 156 | def _create_category_api_tools(self, category: str, routes: list[dict]) -> list[str]: 157 | """Create actual API tools for a category using FastMCP's composition features. 158 | 159 | Args: 160 | category: Category name 161 | routes: List of route information for the category 162 | 163 | Returns: 164 | List of created tool names 165 | """ 166 | filtered_spec = self._create_filtered_openapi_spec(routes) 167 | 168 | route_maps = [RouteMap(methods=list(self.allowed_methods), mcp_type=MCPType.TOOL)] 169 | 170 | category_mcp = FastMCP.from_openapi(openapi_spec=filtered_spec, client=self.client, route_maps=route_maps) 171 | 172 | category_prefix = category 173 | self.mcp.mount(category_prefix, category_mcp) 174 | 175 | self.category_tool_instances[category] = category_prefix 176 | 177 | created_tools = [] 178 | 179 | logger.info(f"Mounted {category} API tools under prefix '{category_prefix}'") 180 | return created_tools 181 | 182 | def _create_filtered_openapi_spec(self, routes: list[dict]) -> dict: 183 | """Create a filtered OpenAPI spec containing only the specified routes. 184 | 185 | Args: 186 | routes: List of route information to include 187 | 188 | Returns: 189 | Filtered OpenAPI specification 190 | """ 191 | filtered_spec = { 192 | "openapi": self.openapi_spec.get("openapi", "3.0.0"), 193 | "info": self.openapi_spec.get("info", {"title": "Filtered API", "version": "1.0.0"}), 194 | "servers": self.openapi_spec.get("servers", []), 195 | "components": self.openapi_spec.get("components", {}), 196 | "paths": {}, 197 | } 198 | 199 | for route in routes: 200 | path = route["path"] 201 | method = route["method"].lower() 202 | 203 | if path not in filtered_spec["paths"]: 204 | filtered_spec["paths"][path] = {} 205 | 206 | if path in self.openapi_spec.get("paths", {}): 207 | original_path = self.openapi_spec["paths"][path] 208 | if method in original_path: 209 | filtered_spec["paths"][path][method] = original_path[method] 210 | 211 | return filtered_spec 212 | 213 | def get_current_state(self) -> dict: 214 | """Get current state information for debugging. 215 | 216 | Returns: 217 | Dictionary with current state info 218 | """ 219 | return {"mode": self.current_mode, "current_tools": list(self.current_tools), "total_categories": len(self.categories), "allowed_methods": list(self.allowed_methods)} 220 | -------------------------------------------------------------------------------- /src/airflow_mcp_server/prompts.py: -------------------------------------------------------------------------------- 1 | """Airflow-specific prompts for MCP server.""" 2 | 3 | from fastmcp import FastMCP 4 | 5 | 6 | def add_airflow_prompts(mcp: FastMCP, mode: str = "safe") -> None: 7 | """Add Airflow-specific prompts to the MCP server. 8 | 9 | Args: 10 | mcp: FastMCP server instance 11 | mode: Server mode ("safe" or "unsafe") 12 | """ 13 | pass 14 | -------------------------------------------------------------------------------- /src/airflow_mcp_server/resources.py: -------------------------------------------------------------------------------- 1 | """Airflow-specific resources for MCP server.""" 2 | 3 | import logging 4 | 5 | from fastmcp import FastMCP 6 | 7 | from airflow_mcp_server.config import AirflowConfig 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | def add_airflow_resources(mcp: FastMCP, config: AirflowConfig, mode: str = "safe") -> None: 13 | """Add Airflow-specific resources to the MCP server. 14 | 15 | Args: 16 | mcp: FastMCP server instance 17 | config: Airflow configuration 18 | mode: Server mode ("safe" or "unsafe") 19 | """ 20 | -------------------------------------------------------------------------------- /src/airflow_mcp_server/server_safe.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import httpx 4 | from fastmcp import FastMCP 5 | from fastmcp.server.openapi import MCPType, RouteMap 6 | 7 | from airflow_mcp_server.config import AirflowConfig 8 | from airflow_mcp_server.hierarchical_manager import HierarchicalToolManager 9 | from airflow_mcp_server.prompts import add_airflow_prompts 10 | from airflow_mcp_server.resources import add_airflow_resources 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | async def serve(config: AirflowConfig, static_tools: bool = False) -> None: 16 | """Start MCP server in safe mode (read-only operations). 17 | 18 | Args: 19 | config: Configuration object with auth and URL settings 20 | static_tools: If True, use static tools instead of hierarchical discovery 21 | """ 22 | client = httpx.AsyncClient(base_url=config.base_url, headers={"Authorization": f"Bearer {config.auth_token}"}, timeout=30.0) 23 | 24 | try: 25 | response = await client.get("/openapi.json") 26 | response.raise_for_status() 27 | openapi_spec = response.json() 28 | except Exception as e: 29 | logger.error("Failed to fetch OpenAPI spec: %s", e) 30 | await client.aclose() 31 | raise 32 | 33 | if static_tools: 34 | route_maps = [RouteMap(methods=["GET"], mcp_type=MCPType.TOOL)] 35 | mcp = FastMCP.from_openapi(openapi_spec=openapi_spec, client=client, name="Airflow MCP Server (Safe Mode - Static Tools)", route_maps=route_maps) 36 | else: 37 | mcp = FastMCP("Airflow MCP Server (Safe Mode)") 38 | 39 | _ = HierarchicalToolManager( 40 | mcp=mcp, 41 | openapi_spec=openapi_spec, 42 | client=client, 43 | allowed_methods={"GET"}, 44 | ) 45 | 46 | add_airflow_resources(mcp, config, mode="safe") 47 | add_airflow_prompts(mcp, mode="safe") 48 | 49 | try: 50 | await mcp.run_async() 51 | except Exception as e: 52 | logger.error("Server error: %s", e) 53 | await client.aclose() 54 | raise 55 | -------------------------------------------------------------------------------- /src/airflow_mcp_server/server_unsafe.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import httpx 4 | from fastmcp import FastMCP 5 | from fastmcp.server.openapi import MCPType, RouteMap 6 | 7 | from airflow_mcp_server.config import AirflowConfig 8 | from airflow_mcp_server.hierarchical_manager import HierarchicalToolManager 9 | from airflow_mcp_server.prompts import add_airflow_prompts 10 | from airflow_mcp_server.resources import add_airflow_resources 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | async def serve(config: AirflowConfig, static_tools: bool = False) -> None: 16 | """Start MCP server in unsafe mode (all operations). 17 | 18 | Args: 19 | config: Configuration object with auth and URL settings 20 | static_tools: If True, use static tools instead of hierarchical discovery 21 | """ 22 | client = httpx.AsyncClient(base_url=config.base_url, headers={"Authorization": f"Bearer {config.auth_token}"}, timeout=30.0) 23 | 24 | try: 25 | response = await client.get("/openapi.json") 26 | response.raise_for_status() 27 | openapi_spec = response.json() 28 | except Exception as e: 29 | logger.error("Failed to fetch OpenAPI spec: %s", e) 30 | await client.aclose() 31 | raise 32 | 33 | if static_tools: 34 | route_maps = [RouteMap(methods=["GET", "POST", "PUT", "DELETE", "PATCH"], mcp_type=MCPType.TOOL)] 35 | mcp = FastMCP.from_openapi(openapi_spec=openapi_spec, client=client, name="Airflow MCP Server (Unsafe Mode - Static Tools)", route_maps=route_maps) 36 | else: 37 | mcp = FastMCP("Airflow MCP Server (Unsafe Mode)") 38 | 39 | _ = HierarchicalToolManager( 40 | mcp=mcp, 41 | openapi_spec=openapi_spec, 42 | client=client, 43 | allowed_methods={"GET", "POST", "PUT", "DELETE", "PATCH"}, 44 | ) 45 | 46 | add_airflow_resources(mcp, config, mode="unsafe") 47 | add_airflow_prompts(mcp, mode="unsafe") 48 | 49 | try: 50 | await mcp.run_async() 51 | except Exception as e: 52 | logger.error("Server error: %s", e) 53 | await client.aclose() 54 | raise 55 | -------------------------------------------------------------------------------- /src/airflow_mcp_server/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """Utilities for Airflow MCP server.""" 2 | -------------------------------------------------------------------------------- /src/airflow_mcp_server/utils/category_mapper.py: -------------------------------------------------------------------------------- 1 | """Category mapping utilities for Airflow OpenAPI endpoints.""" 2 | 3 | 4 | def extract_categories_from_openapi(openapi_spec: dict) -> dict[str, list[dict]]: 5 | """Extract categories and their routes from OpenAPI spec. 6 | 7 | Args: 8 | openapi_spec: OpenAPI specification dictionary 9 | 10 | Returns: 11 | Dictionary mapping category names to lists of route info 12 | """ 13 | categories = {} 14 | 15 | if "paths" not in openapi_spec: 16 | return categories 17 | 18 | for path, methods in openapi_spec["paths"].items(): 19 | for method, operation in methods.items(): 20 | if method.upper() in ["GET", "POST", "PUT", "DELETE", "PATCH"]: 21 | tags = operation.get("tags", ["Uncategorized"]) 22 | 23 | route_info = { 24 | "path": path, 25 | "method": method.upper(), 26 | "operation_id": operation.get("operationId", ""), 27 | "summary": operation.get("summary", ""), 28 | "description": operation.get("description", ""), 29 | "tags": tags, 30 | } 31 | 32 | for tag in tags: 33 | if tag not in categories: 34 | categories[tag] = [] 35 | categories[tag].append(route_info) 36 | 37 | return categories 38 | 39 | 40 | def get_category_info(categories: dict[str, list[dict]]) -> str: 41 | """Get formatted category information with counts. 42 | 43 | Args: 44 | categories: Dictionary of categories and their routes 45 | 46 | Returns: 47 | Formatted string with category information 48 | """ 49 | if not categories: 50 | return "No categories found." 51 | 52 | lines = ["Available Airflow Categories:\n"] 53 | 54 | sorted_categories = sorted(categories.items(), key=lambda x: len(x[1]), reverse=True) 55 | 56 | for category, routes in sorted_categories: 57 | count = len(routes) 58 | lines.append(f"- {category}: {count} tools") 59 | 60 | lines.append(f"\nTotal: {len(categories)} categories, {sum(len(routes) for routes in categories.values())} tools") 61 | lines.append('\nUse select_category("Category Name") to explore specific tools.') 62 | 63 | return "\n".join(lines) 64 | 65 | 66 | def get_category_tools_info(category: str, routes: list[dict]) -> str: 67 | """Get formatted information about tools in a specific category. 68 | 69 | Args: 70 | category: Category name 71 | routes: List of route information for the category 72 | 73 | Returns: 74 | Formatted string with category tools information 75 | """ 76 | lines = [f"{category} Tools ({len(routes)} available):\n"] 77 | 78 | methods_groups = {} 79 | for route in routes: 80 | method = route["method"] 81 | if method not in methods_groups: 82 | methods_groups[method] = [] 83 | methods_groups[method].append(route) 84 | 85 | for method in ["GET", "POST", "PUT", "DELETE", "PATCH"]: 86 | if method in methods_groups: 87 | lines.append(f"\n{method} Operations:") 88 | 89 | for route in methods_groups[method]: 90 | operation_id = route["operation_id"] 91 | summary = route["summary"] or route["description"] or "No description" 92 | if len(summary) > 80: 93 | summary = summary[:77] + "..." 94 | lines.append(f" - {operation_id}: {summary}") 95 | 96 | lines.append("\nUse back_to_categories() to return to category browser.") 97 | 98 | return "\n".join(lines) 99 | 100 | 101 | def filter_routes_by_methods(routes: list[dict], allowed_methods: set[str]) -> list[dict]: 102 | """Filter routes by allowed HTTP methods. 103 | 104 | Args: 105 | routes: List of route information 106 | allowed_methods: Set of allowed HTTP methods (e.g., {"GET"}) 107 | 108 | Returns: 109 | Filtered list of routes 110 | """ 111 | return [route for route in routes if route["method"] in allowed_methods] 112 | 113 | 114 | def get_tool_name_from_route(route: dict) -> str: 115 | """Generate a tool name from route information. 116 | 117 | Args: 118 | route: Route information dictionary 119 | 120 | Returns: 121 | Generated tool name 122 | """ 123 | operation_id = route.get("operation_id", "") 124 | if operation_id: 125 | return operation_id 126 | 127 | path = route["path"].replace("/", "_").replace("{", "").replace("}", "") 128 | method = route["method"].lower() 129 | return f"{method}{path}" 130 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhishekbhakat/airflow-mcp-server/91a053dc79a50f625e47b5b8b3e01dbed83e0168/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Test configuration and shared fixtures.""" 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture 7 | def sample_openapi_spec(): 8 | """Sample OpenAPI spec for testing across multiple test files.""" 9 | return { 10 | "openapi": "3.0.0", 11 | "info": {"title": "Airflow API", "version": "1.0.0"}, 12 | "paths": { 13 | "/api/v1/dags": { 14 | "get": { 15 | "operationId": "get_dags", 16 | "summary": "Get all DAGs", 17 | "tags": ["DAGs"], 18 | "responses": { 19 | "200": { 20 | "description": "List of DAGs", 21 | "content": { 22 | "application/json": {"schema": {"type": "object", "properties": {"dags": {"type": "array", "items": {"type": "object", "properties": {"dag_id": {"type": "string"}}}}}}} 23 | }, 24 | } 25 | }, 26 | }, 27 | "post": {"operationId": "create_dag", "summary": "Create a DAG", "tags": ["DAGs"], "responses": {"201": {"description": "Created"}}}, 28 | }, 29 | "/api/v1/connections": {"get": {"operationId": "get_connections", "summary": "Get connections", "tags": ["Connections"], "responses": {"200": {"description": "Success"}}}}, 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /tests/test_category_mapper.py: -------------------------------------------------------------------------------- 1 | """Tests for category mapper utilities.""" 2 | 3 | import pytest 4 | 5 | from airflow_mcp_server.utils.category_mapper import extract_categories_from_openapi, filter_routes_by_methods, get_category_info, get_category_tools_info, get_tool_name_from_route 6 | 7 | 8 | @pytest.fixture 9 | def sample_openapi_spec(): 10 | """Sample OpenAPI spec for testing.""" 11 | return { 12 | "openapi": "3.0.0", 13 | "info": {"title": "Test API", "version": "1.0.0"}, 14 | "paths": { 15 | "/api/v1/dags": {"get": {"operationId": "get_dags", "summary": "Get all DAGs", "tags": ["DAGs"]}, "post": {"operationId": "create_dag", "summary": "Create a DAG", "tags": ["DAGs"]}}, 16 | "/api/v1/connections": {"get": {"operationId": "get_connections", "summary": "Get connections", "tags": ["Connections"]}}, 17 | "/api/v1/health": { 18 | "get": { 19 | "operationId": "health_check", 20 | "summary": "Health check", 21 | # No tags - should go to "Uncategorized" 22 | } 23 | }, 24 | }, 25 | } 26 | 27 | 28 | def test_extract_categories_from_openapi(sample_openapi_spec): 29 | """Test extracting categories from OpenAPI spec.""" 30 | categories = extract_categories_from_openapi(sample_openapi_spec) 31 | 32 | assert "DAGs" in categories 33 | assert "Connections" in categories 34 | assert "Uncategorized" in categories 35 | 36 | # Check DAGs category has 2 operations 37 | assert len(categories["DAGs"]) == 2 38 | assert len(categories["Connections"]) == 1 39 | assert len(categories["Uncategorized"]) == 1 40 | 41 | 42 | def test_extract_categories_empty_spec(): 43 | """Test extracting categories from empty spec.""" 44 | categories = extract_categories_from_openapi({}) 45 | assert categories == {} 46 | 47 | 48 | def test_extract_categories_no_paths(): 49 | """Test extracting categories from spec with no paths.""" 50 | spec = {"openapi": "3.0.0", "info": {"title": "Test"}} 51 | categories = extract_categories_from_openapi(spec) 52 | assert categories == {} 53 | 54 | 55 | def test_filter_routes_by_methods(): 56 | """Test filtering routes by allowed methods.""" 57 | routes = [{"method": "GET", "operation_id": "get_dags"}, {"method": "POST", "operation_id": "create_dag"}, {"method": "DELETE", "operation_id": "delete_dag"}] 58 | 59 | # Filter to only GET methods 60 | filtered = filter_routes_by_methods(routes, {"GET"}) 61 | assert len(filtered) == 1 62 | assert filtered[0]["operation_id"] == "get_dags" 63 | 64 | # Filter to GET and POST 65 | filtered = filter_routes_by_methods(routes, {"GET", "POST"}) 66 | assert len(filtered) == 2 67 | 68 | 69 | def test_get_category_info(): 70 | """Test getting formatted category information.""" 71 | categories = {"DAGs": [{"method": "GET"}, {"method": "POST"}], "Connections": [{"method": "GET"}]} 72 | 73 | info = get_category_info(categories) 74 | 75 | assert "Available Airflow Categories:" in info 76 | assert "DAGs: 2 tools" in info 77 | assert "Connections: 1 tools" in info 78 | assert "Total: 2 categories, 3 tools" in info 79 | assert 'select_category("Category Name")' in info 80 | 81 | 82 | def test_get_category_info_empty(): 83 | """Test getting category info for empty categories.""" 84 | info = get_category_info({}) 85 | assert info == "No categories found." 86 | 87 | 88 | def test_get_category_tools_info(): 89 | """Test getting formatted tools information for a category.""" 90 | routes = [ 91 | {"method": "GET", "operation_id": "get_dags", "summary": "Get all DAGs", "description": "Retrieve all DAGs"}, 92 | {"method": "POST", "operation_id": "create_dag", "summary": "Create a new DAG", "description": ""}, 93 | ] 94 | 95 | info = get_category_tools_info("DAGs", routes) 96 | 97 | assert "DAGs Tools (2 available):" in info 98 | assert "GET Operations:" in info 99 | assert "POST Operations:" in info 100 | assert "get_dags: Get all DAGs" in info 101 | assert "create_dag: Create a new DAG" in info 102 | assert "back_to_categories()" in info 103 | 104 | 105 | def test_get_tool_name_from_route(): 106 | """Test generating tool names from route information.""" 107 | # Test with operation_id 108 | route = {"operation_id": "get_dags", "path": "/api/v1/dags", "method": "GET"} 109 | assert get_tool_name_from_route(route) == "get_dags" 110 | 111 | # Test without operation_id (fallback) 112 | route = {"path": "/api/v1/dags/{dag_id}", "method": "POST"} 113 | expected = "post_api_v1_dags_dag_id" 114 | assert get_tool_name_from_route(route) == expected 115 | 116 | 117 | def test_get_tool_name_from_route_empty(): 118 | """Test generating tool name with minimal route info.""" 119 | route = {"path": "/", "method": "GET"} 120 | assert get_tool_name_from_route(route) == "get_" 121 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | """Tests for AirflowConfig.""" 2 | 3 | import pytest 4 | 5 | from airflow_mcp_server.config import AirflowConfig 6 | 7 | 8 | def test_config_valid(): 9 | """Test valid configuration.""" 10 | config = AirflowConfig(base_url="http://localhost:8080", auth_token="test-token") 11 | 12 | assert config.base_url == "http://localhost:8080" 13 | assert config.auth_token == "test-token" 14 | 15 | 16 | def test_config_missing_base_url(): 17 | """Test configuration with missing base_url.""" 18 | with pytest.raises(ValueError, match="Missing required configuration: base_url"): 19 | AirflowConfig(base_url=None, auth_token="test-token") 20 | 21 | 22 | def test_config_empty_base_url(): 23 | """Test configuration with empty base_url.""" 24 | with pytest.raises(ValueError, match="Missing required configuration: base_url"): 25 | AirflowConfig(base_url="", auth_token="test-token") 26 | 27 | 28 | def test_config_missing_auth_token(): 29 | """Test configuration with missing auth_token.""" 30 | with pytest.raises(ValueError, match="Missing required configuration: auth_token"): 31 | AirflowConfig(base_url="http://localhost:8080", auth_token=None) 32 | 33 | 34 | def test_config_empty_auth_token(): 35 | """Test configuration with empty auth_token.""" 36 | with pytest.raises(ValueError, match="Missing required configuration: auth_token"): 37 | AirflowConfig(base_url="http://localhost:8080", auth_token="") 38 | 39 | 40 | def test_config_both_missing(): 41 | """Test configuration with both values missing.""" 42 | with pytest.raises(ValueError, match="Missing required configuration: base_url"): 43 | AirflowConfig(base_url=None, auth_token=None) 44 | -------------------------------------------------------------------------------- /tests/test_hierarchical_manager.py: -------------------------------------------------------------------------------- 1 | """Tests for HierarchicalToolManager.""" 2 | 3 | from unittest.mock import Mock 4 | 5 | import httpx 6 | import pytest 7 | from fastmcp import FastMCP 8 | 9 | from airflow_mcp_server.hierarchical_manager import HierarchicalToolManager 10 | 11 | 12 | @pytest.fixture 13 | def mock_client(): 14 | """Create mock HTTP client.""" 15 | client = Mock(spec=httpx.AsyncClient) 16 | return client 17 | 18 | 19 | @pytest.fixture 20 | def mock_mcp(): 21 | """Create mock FastMCP instance.""" 22 | mcp = Mock(spec=FastMCP) 23 | mcp.tool = Mock() 24 | return mcp 25 | 26 | 27 | @pytest.fixture 28 | def sample_openapi_spec(): 29 | """Sample OpenAPI spec for testing.""" 30 | return { 31 | "openapi": "3.0.0", 32 | "info": {"title": "Airflow API", "version": "1.0.0"}, 33 | "paths": { 34 | "/api/v1/dags": { 35 | "get": {"operationId": "get_dags", "summary": "Get all DAGs", "tags": ["DAGs"], "responses": {"200": {"description": "Success"}}}, 36 | "post": {"operationId": "create_dag", "summary": "Create a DAG", "tags": ["DAGs"], "responses": {"201": {"description": "Created"}}}, 37 | }, 38 | "/api/v1/connections": {"get": {"operationId": "get_connections", "summary": "Get connections", "tags": ["Connections"], "responses": {"200": {"description": "Success"}}}}, 39 | }, 40 | } 41 | 42 | 43 | def test_hierarchical_manager_init(mock_mcp, sample_openapi_spec, mock_client): 44 | """Test HierarchicalToolManager initialization.""" 45 | manager = HierarchicalToolManager(mcp=mock_mcp, openapi_spec=sample_openapi_spec, client=mock_client, allowed_methods={"GET", "POST"}) 46 | 47 | assert manager.mcp == mock_mcp 48 | assert manager.openapi_spec == sample_openapi_spec 49 | assert manager.client == mock_client 50 | assert manager.allowed_methods == {"GET", "POST"} 51 | assert manager.current_mode == "categories" 52 | assert isinstance(manager.current_tools, set) 53 | 54 | 55 | def test_hierarchical_manager_default_methods(mock_mcp, sample_openapi_spec, mock_client): 56 | """Test HierarchicalToolManager with default allowed methods.""" 57 | manager = HierarchicalToolManager(mcp=mock_mcp, openapi_spec=sample_openapi_spec, client=mock_client) 58 | 59 | # Should default to all methods 60 | assert manager.allowed_methods == {"GET", "POST", "PUT", "DELETE", "PATCH"} 61 | 62 | 63 | def test_hierarchical_manager_safe_mode(mock_mcp, sample_openapi_spec, mock_client): 64 | """Test HierarchicalToolManager in safe mode (GET only).""" 65 | manager = HierarchicalToolManager(mcp=mock_mcp, openapi_spec=sample_openapi_spec, client=mock_client, allowed_methods={"GET"}) 66 | 67 | assert manager.allowed_methods == {"GET"} 68 | 69 | 70 | def test_persistent_tools_constant(): 71 | """Test that persistent tools are defined correctly.""" 72 | expected_tools = {"browse_categories", "select_category", "get_current_category"} 73 | assert HierarchicalToolManager.PERSISTENT_TOOLS == expected_tools 74 | 75 | 76 | def test_hierarchical_manager_attributes(mock_mcp, sample_openapi_spec, mock_client): 77 | """Test that all required attributes are set.""" 78 | manager = HierarchicalToolManager(mcp=mock_mcp, openapi_spec=sample_openapi_spec, client=mock_client) 79 | 80 | # Check all required attributes exist 81 | assert hasattr(manager, "mcp") 82 | assert hasattr(manager, "openapi_spec") 83 | assert hasattr(manager, "client") 84 | assert hasattr(manager, "allowed_methods") 85 | assert hasattr(manager, "current_mode") 86 | assert hasattr(manager, "current_tools") 87 | assert hasattr(manager, "category_tool_instances") 88 | 89 | # Check initial values 90 | assert manager.current_mode == "categories" 91 | assert isinstance(manager.current_tools, set) 92 | assert isinstance(manager.category_tool_instances, dict) 93 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | """Tests for main CLI functionality.""" 2 | 3 | import os 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | from click.testing import CliRunner 8 | 9 | from airflow_mcp_server import main 10 | 11 | 12 | @pytest.fixture 13 | def runner(): 14 | """Create CLI test runner.""" 15 | return CliRunner() 16 | 17 | 18 | def test_main_help(runner): 19 | """Test main command help.""" 20 | result = runner.invoke(main, ["--help"]) 21 | assert result.exit_code == 0 22 | assert "MCP server for Airflow" in result.output 23 | assert "--safe" in result.output 24 | assert "--unsafe" in result.output 25 | assert "--base-url" in result.output 26 | assert "--auth-token" in result.output 27 | 28 | 29 | def test_main_missing_config(runner): 30 | """Test main with missing configuration.""" 31 | result = runner.invoke(main, []) 32 | assert result.exit_code == 1 33 | assert "Configuration error" in result.output 34 | 35 | 36 | def test_main_with_cli_args(runner): 37 | """Test main with CLI arguments.""" 38 | with patch("airflow_mcp_server.serve_unsafe") as mock_serve: 39 | mock_serve.return_value = None 40 | with patch("asyncio.run") as mock_asyncio: 41 | result = runner.invoke(main, ["--base-url", "http://localhost:8080", "--auth-token", "test-token"]) 42 | 43 | # Should not exit with error 44 | assert result.exit_code == 0 45 | mock_asyncio.assert_called_once() 46 | 47 | 48 | def test_main_safe_mode(runner): 49 | """Test main in safe mode.""" 50 | with patch("airflow_mcp_server.serve_safe") as mock_serve_safe: 51 | mock_serve_safe.return_value = None 52 | with patch("asyncio.run") as mock_asyncio: 53 | result = runner.invoke(main, ["--safe", "--base-url", "http://localhost:8080", "--auth-token", "test-token"]) 54 | 55 | assert result.exit_code == 0 56 | mock_asyncio.assert_called_once() 57 | 58 | 59 | def test_main_unsafe_mode(runner): 60 | """Test main in unsafe mode.""" 61 | with patch("airflow_mcp_server.serve_unsafe") as mock_serve_unsafe: 62 | mock_serve_unsafe.return_value = None 63 | with patch("asyncio.run") as mock_asyncio: 64 | result = runner.invoke(main, ["--unsafe", "--base-url", "http://localhost:8080", "--auth-token", "test-token"]) 65 | 66 | assert result.exit_code == 0 67 | mock_asyncio.assert_called_once() 68 | 69 | 70 | def test_main_conflicting_modes(runner): 71 | """Test main with conflicting safe/unsafe flags.""" 72 | result = runner.invoke(main, ["--safe", "--unsafe", "--base-url", "http://localhost:8080", "--auth-token", "test-token"]) 73 | 74 | assert result.exit_code == 2 # Click usage error 75 | assert "mutually exclusive" in result.output 76 | 77 | 78 | def test_main_env_variables(runner): 79 | """Test main with environment variables.""" 80 | env_vars = {"AIRFLOW_BASE_URL": "http://localhost:8080", "AUTH_TOKEN": "env-token"} 81 | 82 | with patch.dict(os.environ, env_vars): 83 | with patch("airflow_mcp_server.serve_unsafe") as mock_serve: 84 | mock_serve.return_value = None 85 | with patch("asyncio.run") as mock_asyncio: 86 | result = runner.invoke(main, []) 87 | 88 | assert result.exit_code == 0 89 | mock_asyncio.assert_called_once() 90 | 91 | 92 | def test_main_env_overrides_cli(runner): 93 | """Test that environment variables override CLI args (current behavior).""" 94 | env_vars = {"AIRFLOW_BASE_URL": "http://env:8080", "AUTH_TOKEN": "env-token"} 95 | 96 | with patch.dict(os.environ, env_vars): 97 | with patch("airflow_mcp_server.AirflowConfig") as mock_config: 98 | with patch("airflow_mcp_server.serve_unsafe"): 99 | with patch("asyncio.run"): 100 | result = runner.invoke(main, ["--base-url", "http://cli:8080", "--auth-token", "cli-token"]) 101 | 102 | assert result.exit_code == 0 103 | # Environment variables take precedence in current implementation 104 | mock_config.assert_called_once_with(base_url="http://env:8080", auth_token="env-token") 105 | 106 | 107 | def test_main_verbose_logging(runner): 108 | """Test verbose logging options.""" 109 | with patch("airflow_mcp_server.serve_unsafe"): 110 | with patch("asyncio.run"): 111 | with patch("logging.basicConfig") as mock_logging: 112 | result = runner.invoke( 113 | main, 114 | [ 115 | "-vv", # Very verbose 116 | "--base-url", 117 | "http://localhost:8080", 118 | "--auth-token", 119 | "test-token", 120 | ], 121 | ) 122 | 123 | assert result.exit_code == 0 124 | # Check that logging was configured 125 | mock_logging.assert_called_once() 126 | call_args = mock_logging.call_args 127 | # Just verify that stream parameter was passed (Click uses different stderr) 128 | assert "stream" in call_args[1] 129 | -------------------------------------------------------------------------------- /tests/test_server.py: -------------------------------------------------------------------------------- 1 | """Tests for server modules.""" 2 | 3 | from unittest.mock import AsyncMock, Mock, patch 4 | 5 | import httpx 6 | import pytest 7 | 8 | from airflow_mcp_server import server_safe, server_unsafe 9 | from airflow_mcp_server.config import AirflowConfig 10 | 11 | 12 | @pytest.fixture 13 | def mock_config(): 14 | """Create mock configuration.""" 15 | return AirflowConfig(base_url="http://localhost:8080", auth_token="test-token") 16 | 17 | 18 | @pytest.fixture 19 | def mock_openapi_response(): 20 | """Mock OpenAPI response.""" 21 | return {"openapi": "3.0.0", "info": {"title": "Airflow API", "version": "1.0.0"}, "paths": {"/api/v1/dags": {"get": {"operationId": "get_dags", "summary": "Get all DAGs", "tags": ["DAGs"]}}}} 22 | 23 | 24 | @pytest.mark.asyncio 25 | async def test_safe_server_initialization(mock_config, mock_openapi_response): 26 | """Test safe server initialization.""" 27 | with patch("httpx.AsyncClient") as mock_client_class: 28 | mock_client = AsyncMock() 29 | mock_client_class.return_value = mock_client 30 | 31 | # Mock the HTTP response 32 | mock_response = Mock() 33 | mock_response.raise_for_status = Mock() 34 | mock_response.json.return_value = mock_openapi_response 35 | mock_client.get.return_value = mock_response 36 | 37 | with patch("airflow_mcp_server.server_safe.FastMCP") as mock_fastmcp: 38 | with patch("airflow_mcp_server.server_safe.HierarchicalToolManager") as mock_manager: 39 | with patch("airflow_mcp_server.server_safe.add_airflow_resources") as mock_resources: 40 | with patch("airflow_mcp_server.server_safe.add_airflow_prompts") as mock_prompts: 41 | mock_mcp_instance = Mock() 42 | mock_fastmcp.return_value = mock_mcp_instance 43 | mock_mcp_instance.run_async = AsyncMock() 44 | 45 | # This should not raise an exception 46 | await server_safe.serve(mock_config) 47 | 48 | # Verify client was created with correct parameters 49 | mock_client_class.assert_called_once_with(base_url="http://localhost:8080", headers={"Authorization": "Bearer test-token"}, timeout=30.0) 50 | 51 | # Verify OpenAPI spec was fetched 52 | mock_client.get.assert_called_once_with("/openapi.json") 53 | 54 | # Verify FastMCP was created 55 | mock_fastmcp.assert_called_once_with("Airflow MCP Server (Safe Mode)") 56 | 57 | # Verify HierarchicalToolManager was created with safe mode 58 | mock_manager.assert_called_once() 59 | call_args = mock_manager.call_args 60 | assert call_args[1]["allowed_methods"] == {"GET"} 61 | 62 | # Verify resources and prompts were added 63 | mock_resources.assert_called_once_with(mock_mcp_instance, mock_config, mode="safe") 64 | mock_prompts.assert_called_once_with(mock_mcp_instance, mode="safe") 65 | 66 | 67 | @pytest.mark.asyncio 68 | async def test_unsafe_server_initialization(mock_config, mock_openapi_response): 69 | """Test unsafe server initialization.""" 70 | with patch("httpx.AsyncClient") as mock_client_class: 71 | mock_client = AsyncMock() 72 | mock_client_class.return_value = mock_client 73 | 74 | # Mock the HTTP response 75 | mock_response = Mock() 76 | mock_response.raise_for_status = Mock() 77 | mock_response.json.return_value = mock_openapi_response 78 | mock_client.get.return_value = mock_response 79 | 80 | with patch("airflow_mcp_server.server_unsafe.FastMCP") as mock_fastmcp: 81 | with patch("airflow_mcp_server.server_unsafe.HierarchicalToolManager") as mock_manager: 82 | with patch("airflow_mcp_server.server_unsafe.add_airflow_resources") as mock_resources: 83 | with patch("airflow_mcp_server.server_unsafe.add_airflow_prompts") as mock_prompts: 84 | mock_mcp_instance = Mock() 85 | mock_fastmcp.return_value = mock_mcp_instance 86 | mock_mcp_instance.run_async = AsyncMock() 87 | 88 | # This should not raise an exception 89 | await server_unsafe.serve(mock_config) 90 | 91 | # Verify FastMCP was created 92 | mock_fastmcp.assert_called_once_with("Airflow MCP Server (Unsafe Mode)") 93 | 94 | # Verify HierarchicalToolManager was created with all methods 95 | mock_manager.assert_called_once() 96 | call_args = mock_manager.call_args 97 | assert call_args[1]["allowed_methods"] == {"GET", "POST", "PUT", "DELETE", "PATCH"} 98 | 99 | # Verify resources and prompts were added 100 | mock_resources.assert_called_once_with(mock_mcp_instance, mock_config, mode="unsafe") 101 | mock_prompts.assert_called_once_with(mock_mcp_instance, mode="unsafe") 102 | 103 | 104 | @pytest.mark.asyncio 105 | async def test_server_openapi_fetch_error(mock_config): 106 | """Test server handling of OpenAPI fetch error.""" 107 | with patch("httpx.AsyncClient") as mock_client_class: 108 | mock_client = AsyncMock() 109 | mock_client_class.return_value = mock_client 110 | 111 | # Mock HTTP error 112 | mock_client.get.side_effect = httpx.HTTPError("Connection failed") 113 | 114 | with pytest.raises(httpx.HTTPError): 115 | await server_safe.serve(mock_config) 116 | 117 | # Verify client was closed on error 118 | mock_client.aclose.assert_called_once() 119 | 120 | 121 | @pytest.mark.asyncio 122 | async def test_server_run_error(mock_config, mock_openapi_response): 123 | """Test server handling of run error.""" 124 | with patch("httpx.AsyncClient") as mock_client_class: 125 | mock_client = AsyncMock() 126 | mock_client_class.return_value = mock_client 127 | 128 | # Mock successful OpenAPI fetch 129 | mock_response = Mock() 130 | mock_response.raise_for_status = Mock() 131 | mock_response.json.return_value = mock_openapi_response 132 | mock_client.get.return_value = mock_response 133 | 134 | with patch("airflow_mcp_server.server_safe.FastMCP") as mock_fastmcp: 135 | with patch("airflow_mcp_server.server_safe.HierarchicalToolManager"): 136 | with patch("airflow_mcp_server.server_safe.add_airflow_resources"): 137 | with patch("airflow_mcp_server.server_safe.add_airflow_prompts"): 138 | mock_mcp_instance = Mock() 139 | mock_fastmcp.return_value = mock_mcp_instance 140 | mock_mcp_instance.run_async = AsyncMock(side_effect=RuntimeError("Server error")) 141 | 142 | with pytest.raises(RuntimeError): 143 | await server_safe.serve(mock_config) 144 | 145 | # Verify client was closed on error 146 | mock_client.aclose.assert_called_once() 147 | -------------------------------------------------------------------------------- /tests/tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhishekbhakat/airflow-mcp-server/91a053dc79a50f625e47b5b8b3e01dbed83e0168/tests/tools/__init__.py -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 2 3 | requires-python = ">=3.11" 4 | 5 | [[package]] 6 | name = "airflow-mcp-server" 7 | version = "0.7.0" 8 | source = { editable = "." } 9 | dependencies = [ 10 | { name = "click" }, 11 | { name = "fastmcp" }, 12 | { name = "httpx" }, 13 | ] 14 | 15 | [package.optional-dependencies] 16 | dev = [ 17 | { name = "build" }, 18 | { name = "pre-commit" }, 19 | { name = "pytest" }, 20 | { name = "pytest-asyncio" }, 21 | { name = "pytest-mock" }, 22 | { name = "ruff" }, 23 | ] 24 | 25 | [package.metadata] 26 | requires-dist = [ 27 | { name = "build", marker = "extra == 'dev'", specifier = ">=1.2.2.post1" }, 28 | { name = "click", specifier = ">=8.2.1" }, 29 | { name = "fastmcp", specifier = ">=2.5.2" }, 30 | { name = "httpx", specifier = ">=0.28.1" }, 31 | { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.2.0" }, 32 | { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.5" }, 33 | { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.0.0" }, 34 | { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.14.1" }, 35 | { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.11.12" }, 36 | ] 37 | provides-extras = ["dev"] 38 | 39 | [[package]] 40 | name = "annotated-types" 41 | version = "0.7.0" 42 | source = { registry = "https://pypi.org/simple" } 43 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } 44 | wheels = [ 45 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, 46 | ] 47 | 48 | [[package]] 49 | name = "anyio" 50 | version = "4.9.0" 51 | source = { registry = "https://pypi.org/simple" } 52 | dependencies = [ 53 | { name = "idna" }, 54 | { name = "sniffio" }, 55 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 56 | ] 57 | sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } 58 | wheels = [ 59 | { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, 60 | ] 61 | 62 | [[package]] 63 | name = "build" 64 | version = "1.2.2.post1" 65 | source = { registry = "https://pypi.org/simple" } 66 | dependencies = [ 67 | { name = "colorama", marker = "os_name == 'nt'" }, 68 | { name = "packaging" }, 69 | { name = "pyproject-hooks" }, 70 | ] 71 | sdist = { url = "https://files.pythonhosted.org/packages/7d/46/aeab111f8e06793e4f0e421fcad593d547fb8313b50990f31681ee2fb1ad/build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7", size = 46701, upload-time = "2024-10-06T17:22:25.251Z" } 72 | wheels = [ 73 | { url = "https://files.pythonhosted.org/packages/84/c2/80633736cd183ee4a62107413def345f7e6e3c01563dbca1417363cf957e/build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5", size = 22950, upload-time = "2024-10-06T17:22:23.299Z" }, 74 | ] 75 | 76 | [[package]] 77 | name = "certifi" 78 | version = "2025.4.26" 79 | source = { registry = "https://pypi.org/simple" } 80 | sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } 81 | wheels = [ 82 | { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, 83 | ] 84 | 85 | [[package]] 86 | name = "cfgv" 87 | version = "3.4.0" 88 | source = { registry = "https://pypi.org/simple" } 89 | sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } 90 | wheels = [ 91 | { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, 92 | ] 93 | 94 | [[package]] 95 | name = "click" 96 | version = "8.2.1" 97 | source = { registry = "https://pypi.org/simple" } 98 | dependencies = [ 99 | { name = "colorama", marker = "sys_platform == 'win32'" }, 100 | ] 101 | sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } 102 | wheels = [ 103 | { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, 104 | ] 105 | 106 | [[package]] 107 | name = "colorama" 108 | version = "0.4.6" 109 | source = { registry = "https://pypi.org/simple" } 110 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } 111 | wheels = [ 112 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, 113 | ] 114 | 115 | [[package]] 116 | name = "distlib" 117 | version = "0.3.9" 118 | source = { registry = "https://pypi.org/simple" } 119 | sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" } 120 | wheels = [ 121 | { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" }, 122 | ] 123 | 124 | [[package]] 125 | name = "exceptiongroup" 126 | version = "1.3.0" 127 | source = { registry = "https://pypi.org/simple" } 128 | dependencies = [ 129 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 130 | ] 131 | sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } 132 | wheels = [ 133 | { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, 134 | ] 135 | 136 | [[package]] 137 | name = "fastmcp" 138 | version = "2.5.2" 139 | source = { registry = "https://pypi.org/simple" } 140 | dependencies = [ 141 | { name = "exceptiongroup" }, 142 | { name = "httpx" }, 143 | { name = "mcp" }, 144 | { name = "openapi-pydantic" }, 145 | { name = "python-dotenv" }, 146 | { name = "rich" }, 147 | { name = "typer" }, 148 | { name = "websockets" }, 149 | ] 150 | sdist = { url = "https://files.pythonhosted.org/packages/20/cc/d2c0e63d2b34681bef4e077611dae662ea722add13a83dc4ae08b6e0fd23/fastmcp-2.5.2.tar.gz", hash = "sha256:761c92fb54f561136f631d7d98b4920152978f6f0a66a4cef689a7983fd05c8b", size = 1039189, upload-time = "2025-05-29T18:11:33.088Z" } 151 | wheels = [ 152 | { url = "https://files.pythonhosted.org/packages/3e/ac/caa94ff747e2136829ac2fea33b9583e086ca5431451751bcb2f773e087f/fastmcp-2.5.2-py3-none-any.whl", hash = "sha256:4ea46ef35c1308b369eff7c8a10e4c9639bed046fd646449c1227ac7c3856d83", size = 107502, upload-time = "2025-05-29T18:11:31.577Z" }, 153 | ] 154 | 155 | [[package]] 156 | name = "filelock" 157 | version = "3.18.0" 158 | source = { registry = "https://pypi.org/simple" } 159 | sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } 160 | wheels = [ 161 | { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, 162 | ] 163 | 164 | [[package]] 165 | name = "h11" 166 | version = "0.16.0" 167 | source = { registry = "https://pypi.org/simple" } 168 | sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } 169 | wheels = [ 170 | { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, 171 | ] 172 | 173 | [[package]] 174 | name = "httpcore" 175 | version = "1.0.9" 176 | source = { registry = "https://pypi.org/simple" } 177 | dependencies = [ 178 | { name = "certifi" }, 179 | { name = "h11" }, 180 | ] 181 | sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } 182 | wheels = [ 183 | { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, 184 | ] 185 | 186 | [[package]] 187 | name = "httpx" 188 | version = "0.28.1" 189 | source = { registry = "https://pypi.org/simple" } 190 | dependencies = [ 191 | { name = "anyio" }, 192 | { name = "certifi" }, 193 | { name = "httpcore" }, 194 | { name = "idna" }, 195 | ] 196 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } 197 | wheels = [ 198 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, 199 | ] 200 | 201 | [[package]] 202 | name = "httpx-sse" 203 | version = "0.4.0" 204 | source = { registry = "https://pypi.org/simple" } 205 | sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624, upload-time = "2023-12-22T08:01:21.083Z" } 206 | wheels = [ 207 | { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload-time = "2023-12-22T08:01:19.89Z" }, 208 | ] 209 | 210 | [[package]] 211 | name = "identify" 212 | version = "2.6.12" 213 | source = { registry = "https://pypi.org/simple" } 214 | sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254, upload-time = "2025-05-23T20:37:53.3Z" } 215 | wheels = [ 216 | { url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145, upload-time = "2025-05-23T20:37:51.495Z" }, 217 | ] 218 | 219 | [[package]] 220 | name = "idna" 221 | version = "3.10" 222 | source = { registry = "https://pypi.org/simple" } 223 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } 224 | wheels = [ 225 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, 226 | ] 227 | 228 | [[package]] 229 | name = "iniconfig" 230 | version = "2.1.0" 231 | source = { registry = "https://pypi.org/simple" } 232 | sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } 233 | wheels = [ 234 | { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, 235 | ] 236 | 237 | [[package]] 238 | name = "markdown-it-py" 239 | version = "3.0.0" 240 | source = { registry = "https://pypi.org/simple" } 241 | dependencies = [ 242 | { name = "mdurl" }, 243 | ] 244 | sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } 245 | wheels = [ 246 | { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, 247 | ] 248 | 249 | [[package]] 250 | name = "mcp" 251 | version = "1.9.2" 252 | source = { registry = "https://pypi.org/simple" } 253 | dependencies = [ 254 | { name = "anyio" }, 255 | { name = "httpx" }, 256 | { name = "httpx-sse" }, 257 | { name = "pydantic" }, 258 | { name = "pydantic-settings" }, 259 | { name = "python-multipart" }, 260 | { name = "sse-starlette" }, 261 | { name = "starlette" }, 262 | { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, 263 | ] 264 | sdist = { url = "https://files.pythonhosted.org/packages/ea/03/77c49cce3ace96e6787af624611b627b2828f0dca0f8df6f330a10eea51e/mcp-1.9.2.tar.gz", hash = "sha256:3c7651c053d635fd235990a12e84509fe32780cd359a5bbef352e20d4d963c05", size = 333066, upload-time = "2025-05-29T14:42:17.76Z" } 265 | wheels = [ 266 | { url = "https://files.pythonhosted.org/packages/5d/a6/8f5ee9da9f67c0fd8933f63d6105f02eabdac8a8c0926728368ffbb6744d/mcp-1.9.2-py3-none-any.whl", hash = "sha256:bc29f7fd67d157fef378f89a4210384f5fecf1168d0feb12d22929818723f978", size = 131083, upload-time = "2025-05-29T14:42:16.211Z" }, 267 | ] 268 | 269 | [[package]] 270 | name = "mdurl" 271 | version = "0.1.2" 272 | source = { registry = "https://pypi.org/simple" } 273 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } 274 | wheels = [ 275 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, 276 | ] 277 | 278 | [[package]] 279 | name = "nodeenv" 280 | version = "1.9.1" 281 | source = { registry = "https://pypi.org/simple" } 282 | sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } 283 | wheels = [ 284 | { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, 285 | ] 286 | 287 | [[package]] 288 | name = "openapi-pydantic" 289 | version = "0.5.1" 290 | source = { registry = "https://pypi.org/simple" } 291 | dependencies = [ 292 | { name = "pydantic" }, 293 | ] 294 | sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" } 295 | wheels = [ 296 | { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, 297 | ] 298 | 299 | [[package]] 300 | name = "packaging" 301 | version = "25.0" 302 | source = { registry = "https://pypi.org/simple" } 303 | sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } 304 | wheels = [ 305 | { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, 306 | ] 307 | 308 | [[package]] 309 | name = "platformdirs" 310 | version = "4.3.8" 311 | source = { registry = "https://pypi.org/simple" } 312 | sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } 313 | wheels = [ 314 | { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, 315 | ] 316 | 317 | [[package]] 318 | name = "pluggy" 319 | version = "1.6.0" 320 | source = { registry = "https://pypi.org/simple" } 321 | sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } 322 | wheels = [ 323 | { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, 324 | ] 325 | 326 | [[package]] 327 | name = "pre-commit" 328 | version = "4.2.0" 329 | source = { registry = "https://pypi.org/simple" } 330 | dependencies = [ 331 | { name = "cfgv" }, 332 | { name = "identify" }, 333 | { name = "nodeenv" }, 334 | { name = "pyyaml" }, 335 | { name = "virtualenv" }, 336 | ] 337 | sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" } 338 | wheels = [ 339 | { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, 340 | ] 341 | 342 | [[package]] 343 | name = "pydantic" 344 | version = "2.11.5" 345 | source = { registry = "https://pypi.org/simple" } 346 | dependencies = [ 347 | { name = "annotated-types" }, 348 | { name = "pydantic-core" }, 349 | { name = "typing-extensions" }, 350 | { name = "typing-inspection" }, 351 | ] 352 | sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102, upload-time = "2025-05-22T21:18:08.761Z" } 353 | wheels = [ 354 | { url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229, upload-time = "2025-05-22T21:18:06.329Z" }, 355 | ] 356 | 357 | [[package]] 358 | name = "pydantic-core" 359 | version = "2.33.2" 360 | source = { registry = "https://pypi.org/simple" } 361 | dependencies = [ 362 | { name = "typing-extensions" }, 363 | ] 364 | sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } 365 | wheels = [ 366 | { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, 367 | { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, 368 | { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, 369 | { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, 370 | { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, 371 | { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, 372 | { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, 373 | { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, 374 | { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, 375 | { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, 376 | { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, 377 | { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, 378 | { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, 379 | { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, 380 | { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, 381 | { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, 382 | { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, 383 | { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, 384 | { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, 385 | { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, 386 | { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, 387 | { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, 388 | { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, 389 | { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, 390 | { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, 391 | { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, 392 | { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, 393 | { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, 394 | { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, 395 | { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, 396 | { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, 397 | { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, 398 | { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, 399 | { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, 400 | { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, 401 | { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, 402 | { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, 403 | { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, 404 | { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, 405 | { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, 406 | { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, 407 | { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, 408 | { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, 409 | { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, 410 | { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, 411 | { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, 412 | { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, 413 | { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, 414 | { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, 415 | { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, 416 | { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, 417 | { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, 418 | { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, 419 | { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, 420 | ] 421 | 422 | [[package]] 423 | name = "pydantic-settings" 424 | version = "2.9.1" 425 | source = { registry = "https://pypi.org/simple" } 426 | dependencies = [ 427 | { name = "pydantic" }, 428 | { name = "python-dotenv" }, 429 | { name = "typing-inspection" }, 430 | ] 431 | sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload-time = "2025-04-18T16:44:48.265Z" } 432 | wheels = [ 433 | { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" }, 434 | ] 435 | 436 | [[package]] 437 | name = "pygments" 438 | version = "2.19.1" 439 | source = { registry = "https://pypi.org/simple" } 440 | sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } 441 | wheels = [ 442 | { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, 443 | ] 444 | 445 | [[package]] 446 | name = "pyproject-hooks" 447 | version = "1.2.0" 448 | source = { registry = "https://pypi.org/simple" } 449 | sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } 450 | wheels = [ 451 | { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, 452 | ] 453 | 454 | [[package]] 455 | name = "pytest" 456 | version = "8.3.5" 457 | source = { registry = "https://pypi.org/simple" } 458 | dependencies = [ 459 | { name = "colorama", marker = "sys_platform == 'win32'" }, 460 | { name = "iniconfig" }, 461 | { name = "packaging" }, 462 | { name = "pluggy" }, 463 | ] 464 | sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } 465 | wheels = [ 466 | { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, 467 | ] 468 | 469 | [[package]] 470 | name = "pytest-asyncio" 471 | version = "1.0.0" 472 | source = { registry = "https://pypi.org/simple" } 473 | dependencies = [ 474 | { name = "pytest" }, 475 | ] 476 | sdist = { url = "https://files.pythonhosted.org/packages/d0/d4/14f53324cb1a6381bef29d698987625d80052bb33932d8e7cbf9b337b17c/pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f", size = 46960, upload-time = "2025-05-26T04:54:40.484Z" } 477 | wheels = [ 478 | { url = "https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3", size = 15976, upload-time = "2025-05-26T04:54:39.035Z" }, 479 | ] 480 | 481 | [[package]] 482 | name = "pytest-mock" 483 | version = "3.14.1" 484 | source = { registry = "https://pypi.org/simple" } 485 | dependencies = [ 486 | { name = "pytest" }, 487 | ] 488 | sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" } 489 | wheels = [ 490 | { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" }, 491 | ] 492 | 493 | [[package]] 494 | name = "python-dotenv" 495 | version = "1.1.0" 496 | source = { registry = "https://pypi.org/simple" } 497 | sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" } 498 | wheels = [ 499 | { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" }, 500 | ] 501 | 502 | [[package]] 503 | name = "python-multipart" 504 | version = "0.0.20" 505 | source = { registry = "https://pypi.org/simple" } 506 | sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } 507 | wheels = [ 508 | { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, 509 | ] 510 | 511 | [[package]] 512 | name = "pyyaml" 513 | version = "6.0.2" 514 | source = { registry = "https://pypi.org/simple" } 515 | sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } 516 | wheels = [ 517 | { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, 518 | { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, 519 | { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, 520 | { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, 521 | { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, 522 | { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, 523 | { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, 524 | { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, 525 | { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, 526 | { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, 527 | { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, 528 | { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, 529 | { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, 530 | { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, 531 | { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, 532 | { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, 533 | { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, 534 | { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, 535 | { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, 536 | { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, 537 | { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, 538 | { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, 539 | { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, 540 | { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, 541 | { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, 542 | { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, 543 | { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, 544 | ] 545 | 546 | [[package]] 547 | name = "rich" 548 | version = "14.0.0" 549 | source = { registry = "https://pypi.org/simple" } 550 | dependencies = [ 551 | { name = "markdown-it-py" }, 552 | { name = "pygments" }, 553 | ] 554 | sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } 555 | wheels = [ 556 | { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, 557 | ] 558 | 559 | [[package]] 560 | name = "ruff" 561 | version = "0.11.12" 562 | source = { registry = "https://pypi.org/simple" } 563 | sdist = { url = "https://files.pythonhosted.org/packages/15/0a/92416b159ec00cdf11e5882a9d80d29bf84bba3dbebc51c4898bfbca1da6/ruff-0.11.12.tar.gz", hash = "sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603", size = 4202289, upload-time = "2025-05-29T13:31:40.037Z" } 564 | wheels = [ 565 | { url = "https://files.pythonhosted.org/packages/60/cc/53eb79f012d15e136d40a8e8fc519ba8f55a057f60b29c2df34efd47c6e3/ruff-0.11.12-py3-none-linux_armv6l.whl", hash = "sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc", size = 10285597, upload-time = "2025-05-29T13:30:57.539Z" }, 566 | { url = "https://files.pythonhosted.org/packages/e7/d7/73386e9fb0232b015a23f62fea7503f96e29c29e6c45461d4a73bac74df9/ruff-0.11.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cad64843da9f134565c20bcc430642de897b8ea02e2e79e6e02a76b8dcad7c3", size = 11053154, upload-time = "2025-05-29T13:31:00.865Z" }, 567 | { url = "https://files.pythonhosted.org/packages/4e/eb/3eae144c5114e92deb65a0cb2c72326c8469e14991e9bc3ec0349da1331c/ruff-0.11.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9b6886b524a1c659cee1758140138455d3c029783d1b9e643f3624a5ee0cb0aa", size = 10403048, upload-time = "2025-05-29T13:31:03.413Z" }, 568 | { url = "https://files.pythonhosted.org/packages/29/64/20c54b20e58b1058db6689e94731f2a22e9f7abab74e1a758dfba058b6ca/ruff-0.11.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc3a3690aad6e86c1958d3ec3c38c4594b6ecec75c1f531e84160bd827b2012", size = 10597062, upload-time = "2025-05-29T13:31:05.539Z" }, 569 | { url = "https://files.pythonhosted.org/packages/29/3a/79fa6a9a39422a400564ca7233a689a151f1039110f0bbbabcb38106883a/ruff-0.11.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97fdbc2549f456c65b3b0048560d44ddd540db1f27c778a938371424b49fe4a", size = 10155152, upload-time = "2025-05-29T13:31:07.986Z" }, 570 | { url = "https://files.pythonhosted.org/packages/e5/a4/22c2c97b2340aa968af3a39bc38045e78d36abd4ed3fa2bde91c31e712e3/ruff-0.11.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74adf84960236961090e2d1348c1a67d940fd12e811a33fb3d107df61eef8fc7", size = 11723067, upload-time = "2025-05-29T13:31:10.57Z" }, 571 | { url = "https://files.pythonhosted.org/packages/bc/cf/3e452fbd9597bcd8058856ecd42b22751749d07935793a1856d988154151/ruff-0.11.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b56697e5b8bcf1d61293ccfe63873aba08fdbcbbba839fc046ec5926bdb25a3a", size = 12460807, upload-time = "2025-05-29T13:31:12.88Z" }, 572 | { url = "https://files.pythonhosted.org/packages/2f/ec/8f170381a15e1eb7d93cb4feef8d17334d5a1eb33fee273aee5d1f8241a3/ruff-0.11.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d47afa45e7b0eaf5e5969c6b39cbd108be83910b5c74626247e366fd7a36a13", size = 12063261, upload-time = "2025-05-29T13:31:15.236Z" }, 573 | { url = "https://files.pythonhosted.org/packages/0d/bf/57208f8c0a8153a14652a85f4116c0002148e83770d7a41f2e90b52d2b4e/ruff-0.11.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bf9603fe1bf949de8b09a2da896f05c01ed7a187f4a386cdba6760e7f61be", size = 11329601, upload-time = "2025-05-29T13:31:18.68Z" }, 574 | { url = "https://files.pythonhosted.org/packages/c3/56/edf942f7fdac5888094d9ffa303f12096f1a93eb46570bcf5f14c0c70880/ruff-0.11.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08033320e979df3b20dba567c62f69c45e01df708b0f9c83912d7abd3e0801cd", size = 11522186, upload-time = "2025-05-29T13:31:21.216Z" }, 575 | { url = "https://files.pythonhosted.org/packages/ed/63/79ffef65246911ed7e2290aeece48739d9603b3a35f9529fec0fc6c26400/ruff-0.11.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:929b7706584f5bfd61d67d5070f399057d07c70585fa8c4491d78ada452d3bef", size = 10449032, upload-time = "2025-05-29T13:31:23.417Z" }, 576 | { url = "https://files.pythonhosted.org/packages/88/19/8c9d4d8a1c2a3f5a1ea45a64b42593d50e28b8e038f1aafd65d6b43647f3/ruff-0.11.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7de4a73205dc5756b8e09ee3ed67c38312dce1aa28972b93150f5751199981b5", size = 10129370, upload-time = "2025-05-29T13:31:25.777Z" }, 577 | { url = "https://files.pythonhosted.org/packages/bc/0f/2d15533eaa18f460530a857e1778900cd867ded67f16c85723569d54e410/ruff-0.11.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2635c2a90ac1b8ca9e93b70af59dfd1dd2026a40e2d6eebaa3efb0465dd9cf02", size = 11123529, upload-time = "2025-05-29T13:31:28.396Z" }, 578 | { url = "https://files.pythonhosted.org/packages/4f/e2/4c2ac669534bdded835356813f48ea33cfb3a947dc47f270038364587088/ruff-0.11.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d05d6a78a89166f03f03a198ecc9d18779076ad0eec476819467acb401028c0c", size = 11577642, upload-time = "2025-05-29T13:31:30.647Z" }, 579 | { url = "https://files.pythonhosted.org/packages/a7/9b/c9ddf7f924d5617a1c94a93ba595f4b24cb5bc50e98b94433ab3f7ad27e5/ruff-0.11.12-py3-none-win32.whl", hash = "sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6", size = 10475511, upload-time = "2025-05-29T13:31:32.917Z" }, 580 | { url = "https://files.pythonhosted.org/packages/fd/d6/74fb6d3470c1aada019ffff33c0f9210af746cca0a4de19a1f10ce54968a/ruff-0.11.12-py3-none-win_amd64.whl", hash = "sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832", size = 11523573, upload-time = "2025-05-29T13:31:35.782Z" }, 581 | { url = "https://files.pythonhosted.org/packages/44/42/d58086ec20f52d2b0140752ae54b355ea2be2ed46f914231136dd1effcc7/ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5", size = 10697770, upload-time = "2025-05-29T13:31:38.009Z" }, 582 | ] 583 | 584 | [[package]] 585 | name = "shellingham" 586 | version = "1.5.4" 587 | source = { registry = "https://pypi.org/simple" } 588 | sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } 589 | wheels = [ 590 | { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, 591 | ] 592 | 593 | [[package]] 594 | name = "sniffio" 595 | version = "1.3.1" 596 | source = { registry = "https://pypi.org/simple" } 597 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } 598 | wheels = [ 599 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, 600 | ] 601 | 602 | [[package]] 603 | name = "sse-starlette" 604 | version = "2.3.5" 605 | source = { registry = "https://pypi.org/simple" } 606 | dependencies = [ 607 | { name = "anyio" }, 608 | { name = "starlette" }, 609 | ] 610 | sdist = { url = "https://files.pythonhosted.org/packages/10/5f/28f45b1ff14bee871bacafd0a97213f7ec70e389939a80c60c0fb72a9fc9/sse_starlette-2.3.5.tar.gz", hash = "sha256:228357b6e42dcc73a427990e2b4a03c023e2495ecee82e14f07ba15077e334b2", size = 17511, upload-time = "2025-05-12T18:23:52.601Z" } 611 | wheels = [ 612 | { url = "https://files.pythonhosted.org/packages/c8/48/3e49cf0f64961656402c0023edbc51844fe17afe53ab50e958a6dbbbd499/sse_starlette-2.3.5-py3-none-any.whl", hash = "sha256:251708539a335570f10eaaa21d1848a10c42ee6dc3a9cf37ef42266cdb1c52a8", size = 10233, upload-time = "2025-05-12T18:23:50.722Z" }, 613 | ] 614 | 615 | [[package]] 616 | name = "starlette" 617 | version = "0.47.0" 618 | source = { registry = "https://pypi.org/simple" } 619 | dependencies = [ 620 | { name = "anyio" }, 621 | ] 622 | sdist = { url = "https://files.pythonhosted.org/packages/8b/d0/0332bd8a25779a0e2082b0e179805ad39afad642938b371ae0882e7f880d/starlette-0.47.0.tar.gz", hash = "sha256:1f64887e94a447fed5f23309fb6890ef23349b7e478faa7b24a851cd4eb844af", size = 2582856, upload-time = "2025-05-29T15:45:27.628Z" } 623 | wheels = [ 624 | { url = "https://files.pythonhosted.org/packages/e3/81/c60b35fe9674f63b38a8feafc414fca0da378a9dbd5fa1e0b8d23fcc7a9b/starlette-0.47.0-py3-none-any.whl", hash = "sha256:9d052d4933683af40ffd47c7465433570b4949dc937e20ad1d73b34e72f10c37", size = 72796, upload-time = "2025-05-29T15:45:26.305Z" }, 625 | ] 626 | 627 | [[package]] 628 | name = "typer" 629 | version = "0.16.0" 630 | source = { registry = "https://pypi.org/simple" } 631 | dependencies = [ 632 | { name = "click" }, 633 | { name = "rich" }, 634 | { name = "shellingham" }, 635 | { name = "typing-extensions" }, 636 | ] 637 | sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625, upload-time = "2025-05-26T14:30:31.824Z" } 638 | wheels = [ 639 | { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317, upload-time = "2025-05-26T14:30:30.523Z" }, 640 | ] 641 | 642 | [[package]] 643 | name = "typing-extensions" 644 | version = "4.13.2" 645 | source = { registry = "https://pypi.org/simple" } 646 | sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } 647 | wheels = [ 648 | { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, 649 | ] 650 | 651 | [[package]] 652 | name = "typing-inspection" 653 | version = "0.4.1" 654 | source = { registry = "https://pypi.org/simple" } 655 | dependencies = [ 656 | { name = "typing-extensions" }, 657 | ] 658 | sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } 659 | wheels = [ 660 | { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, 661 | ] 662 | 663 | [[package]] 664 | name = "uvicorn" 665 | version = "0.34.2" 666 | source = { registry = "https://pypi.org/simple" } 667 | dependencies = [ 668 | { name = "click" }, 669 | { name = "h11" }, 670 | ] 671 | sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815, upload-time = "2025-04-19T06:02:50.101Z" } 672 | wheels = [ 673 | { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483, upload-time = "2025-04-19T06:02:48.42Z" }, 674 | ] 675 | 676 | [[package]] 677 | name = "virtualenv" 678 | version = "20.31.2" 679 | source = { registry = "https://pypi.org/simple" } 680 | dependencies = [ 681 | { name = "distlib" }, 682 | { name = "filelock" }, 683 | { name = "platformdirs" }, 684 | ] 685 | sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316, upload-time = "2025-05-08T17:58:23.811Z" } 686 | wheels = [ 687 | { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" }, 688 | ] 689 | 690 | [[package]] 691 | name = "websockets" 692 | version = "15.0.1" 693 | source = { registry = "https://pypi.org/simple" } 694 | sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } 695 | wheels = [ 696 | { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, 697 | { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, 698 | { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, 699 | { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, 700 | { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, 701 | { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, 702 | { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, 703 | { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, 704 | { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, 705 | { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, 706 | { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, 707 | { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, 708 | { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, 709 | { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, 710 | { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, 711 | { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, 712 | { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, 713 | { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, 714 | { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, 715 | { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, 716 | { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, 717 | { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, 718 | { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, 719 | { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, 720 | { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, 721 | { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, 722 | { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, 723 | { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, 724 | { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, 725 | { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, 726 | { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, 727 | { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, 728 | { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, 729 | { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, 730 | ] 731 | --------------------------------------------------------------------------------