diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..222bb09
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,35 @@
+---
+name: Bug report
+about: Help us fix it by providing detailed information about the issue
+title: "[BUG]"
+labels: bug
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Log file / Error Message:**
+Paste any relevant logs or error messages here
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..3ab81cf
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: "[FEAT]"
+labels: feature
+assignees: ''
+
+---
+
+**Summary**
+What change you think needs making.
+
+**Motivation**
+Please give examples of your use case, e.g. when would you use this.
+
+**Proposal**
+How do you think this should be implemented?
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..e7c4acd
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,14 @@
+## Overview
+
+
+## Key Changes
+
+-
+-
+
+## Related Issues
+
+-
+
+## Additional context
+
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..a90744c
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,56 @@
+name: CI
+
+on:
+ pull_request:
+ branches: [ develop ]
+ push:
+ branches: [ develop ]
+
+jobs:
+ lint-and-test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v3
+
+ - name: Set up Python
+ run: uv python install
+
+ - name: Install dependencies
+ run: uv sync --all-groups
+
+ - name: Run ruff check
+ run: uv run ruff check
+
+ - name: Run ruff format check
+ run: uv run ruff format --check .
+
+ - name: Run tests with pytest
+ run: uv run pytest
+
+
+ test-matrix:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v3
+
+ - name: Set up Python ${{ matrix.python-version }}
+ run: uv python install ${{ matrix.python-version }}
+
+ - name: Install dependencies
+ run: uv sync --all-groups
+
+ - name: Run tests
+ run: uv run pytest
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..5789fe1
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,39 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+permissions:
+ contents: write
+ id-token: write
+
+jobs:
+ build-and-publish:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v3
+
+ - name: Set up Python
+ run: uv python install
+
+ - name: Install dependencies
+ run: uv sync --group dev
+
+ - name: Build package
+ run: uv build
+
+ - name: Publish to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ with:
+ generate_release_notes: true
diff --git a/.gitignore b/.gitignore
index 7b004e5..da3d1cc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ __pycache__/
# C extensions
*.so
+.idea
# Distribution / packaging
.Python
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..07de2db
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,10 @@
+repos:
+- repo: https://github.com/astral-sh/ruff-pre-commit
+ # Ruff version.
+ rev: v0.12.7
+ hooks:
+ # Run the linter.
+ - id: ruff-check
+ args: [ --fix ]
+ # Run the formatter.
+ - id: ruff-format
\ No newline at end of file
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..c8cfe39
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.10
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
new file mode 100644
index 0000000..3859ae7
--- /dev/null
+++ b/CONTRIBUTING.rst
@@ -0,0 +1,91 @@
+==============================
+Contributing to OpenStack MCP Server
+==============================
+
+About the Project
+==================
+
+OpenStack MCP Server is a project that integrates various OpenStack functionalities with the Model Context Protocol (MCP), enabling LLM-powered management of OpenStack resources.
+
+How to Contribute
+=================
+
+First, thank you for reading this document to contribute to our OpenStack MCP Server project. The following content explains the guidelines for contributing to our project, how to set up the development environment, and coding style guidelines.
+
+PR Guidelines
+=============
+
+Issue Report
+------------
+
+Before submitting code for new features (and this also applies to some complex bug fixes), please first raise a **Feature request** or **Bug report**.
+
+Review Process
+--------------
+
+- This project currently uses main and develop branches as the base. PRs to the main branch are restricted to the develop branch.
+- All patches must be first merged into the develop branch and require approval from at least 2 code reviewers to be merged into develop.
+
+Commit Message
+--------------
+
+We use the `Conventional Commits `_ convention for writing commit messages.
+
+Format::
+
+ [optional scope]:
+ [optional body]
+ [optional footer(s)]
+
+**Example**::
+
+ feat(compute): implement server management tools
+
+ Add Compute server listing and detail retrieval functionality
+ for MCP clients with proper error handling and OpenStack SDK integration.
+
+ - Add get_compute_servers tool
+ - Add get_server_details tool
+ - Implement server status filtering
+ - Add comprehensive error handling
+
+ Closes #123
+
+Development Environment
+=======================
+
+The following content is a guide for contributors to set up a development environment in their local environment.
+
+Prerequisites
+=============
+
+- This project uses uv to manage Python packages. Please set up an environment where you can use uv in your local environment for development environment setup. `Reference `_
+- We use ``python3.10`` as the default version. This is to ensure compatibility with other OpenStack projects.
+
+UV Package Build
+----------------
+
+.. code-block:: bash
+
+ uv sync --all-groups
+
+Pre-commit
+----------
+
+Code style is managed uniformly through ruff. We recommend setting up pre-commit hooks so that auto-formatting is applied at the commit stage.
+
+.. code-block:: bash
+
+ pre-commit install
+
+Testing
+=======
+
+Unit Tests
+----------
+
+All patches related to feature additions must implement unit test code. This project uses Pytest as the testing library, and if the project has been built successfully, you can run tests with the following command:
+
+.. code-block:: bash
+
+ uv run pytest
\ No newline at end of file
diff --git a/README.md b/README.md
index 83e760c..e7151f1 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,102 @@
# python-openstackmcp-server
-openstack mcp server
+
+Openstack mcp server is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/docs/getting-started/intro) server that provides an interface for AI assistants to interact with OpenStack services.
+
+```mermaid
+flowchart LR
+ AI[AI Assistant] <-->|MCP Protocol| Server[OpenStack MCP Server]
+ Server <-->|OpenStack SDK| SDK[OpenStack SDK]
+ SDK <-->|REST API| Cloud[OpenStack Cloud]
+```
+
+# Table of Contents
+- [Table of Contents](#table-of-contents)
+- [Features](#features)
+- [Quick Start with Claude Desktop](#quick-start-with-claude-desktop)
+ - [Requirements](#requirements)
+ - [Using python](#using-python)
+ - [Using uvx](#using-uvx)
+ - [Contributing](#contributing)
+ - [License](#license)
+
+# Features
+- **MCP Protocol Support**: Implements the Model Context Protocol for AI assistants.
+- **Compute Tools**: Manage OpenStack compute resources (servers, flavors).
+- **Image Tools**: Manage OpenStack images.
+- **Identity Tools**: Handle OpenStack identity and authentication.
+- **Network Tools**: Manage OpenStack networking resources.
+- **Block Storage Tools**: Manage OpenStack block storage resources.
+
+# Quick Start with Claude Desktop
+
+Get started quickly with the OpenStack MCP server using Claude Desktop
+
+## Requirements
+- Python 3.10 or higher
+- OpenStack credentials configured in `clouds.yaml` file
+- Claude Desktop installed
+
+1. **Create or update your `clouds.yaml` file with your OpenStack credentials.**
+
+ Example `clouds.yaml`:
+ ```yaml
+ clouds:
+ openstack:
+ auth:
+ auth_url: https://your-openstack-auth-url.com
+ username: your-username
+ password: your-password
+ project_name: your-project-name
+ user_domain_name: Default
+ project_domain_name: Default
+ region_name: your-region
+ interface: public
+ identity_api_version: 3
+ ```
+
+2. **Create or update your Claude Desktop configuration file**:
+ - **macOS**: Edit `$HOME/Library/Application Support/Claude/claude_desktop_config.json`
+ - **Windows**: Edit `%APPDATA%\Claude\claude_desktop_config.json`
+ - **Linux**: Edit `$HOME/.config/Claude/claude_desktop_config.json`
+
+### Using python
+
+ ```json
+ {
+ "mcpServers": {
+ "openstack-mcp-server": {
+ "command": "/path/to/your/python",
+ "args": [
+ "python-openstackmcp-server"
+ ],
+ "env" : {
+ "OS_CLIENT_CONFIG_FILE": "/path/to/your/clouds.yaml"
+ }
+ }
+ }
+ }
+ ```
+
+### Using uvx
+
+ ```json
+ {
+ "mcpServers": {
+ "openstack-mcp-server": {
+ "command": "uvx",
+ "args": [
+ "python-openstackmcp-server"
+ ],
+ "env" : {
+ "OS_CLIENT_CONFIG_FILE": "/path/to/your/clouds.yaml"
+ }
+ }
+ }
+ }
+ ```
+
+# Contributing
+Contributions are welcome! Please see the [CONTRIBUTING](CONTRIBUTING.rst) file for details on how to contribute to this project.
+
+# License
+This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details.
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..ebd9042
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,95 @@
+[project]
+name = "python-openstackmcp-server"
+dynamic = ["version"]
+description = "A MCP server providing OpenStack services for MCP clients"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+ "fastmcp>=2.11.3",
+ "openstacksdk>=4.6.0",
+ "pydantic>=2.11.7",
+]
+
+[project.scripts]
+python-openstackmcp-server = "openstack_mcp_server:main"
+
+[dependency-groups]
+dev = [
+ "ruff>=0.12.5",
+ "pre-commit>=4.2.0",
+ "setuptools-scm>=9.2.0",
+]
+test = [
+ "pytest>=8.4.1",
+]
+
+
+[build-system]
+requires = ["setuptools>=61.0", "setuptools-scm"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools_scm]
+write_to = "src/openstack_mcp_server/_version.py"
+
+[tool.setuptools.packages.find]
+where = ["src"]
+
+
+[tool.pytest.ini_options]
+testpaths = [
+ "tests",
+]
+pythonpath = [
+ "src",
+]
+
+python_files = [
+ "test_*.py",
+ "*_test.py",
+]
+python_classes = [
+ "Test*",
+]
+python_functions = [
+ "test_*",
+]
+
+
+[tool.ruff]
+line-length = 79
+exclude = [
+ "docs",
+ "_version.py",
+]
+
+[tool.ruff.format]
+quote-style = "double"
+indent-style = "space"
+line-ending = "auto"
+docstring-code-format = true
+
+[tool.ruff.lint]
+select = [
+ "C",
+ "E",
+ "F",
+ "S",
+ "U",
+ "I",
+]
+ignore = [
+ "C901",
+ "E501",
+]
+
+[tool.ruff.lint.isort]
+lines-after-imports = 2
+lines-between-types = 1
+
+[tool.ruff.lint.per-file-ignores]
+"tests/*" = [
+ "S101",
+]
+
+[tool.ruff.lint.pydocstyle]
+convention = "google"
diff --git a/src/openstack_mcp_server/__init__.py b/src/openstack_mcp_server/__init__.py
new file mode 100644
index 0000000..b143047
--- /dev/null
+++ b/src/openstack_mcp_server/__init__.py
@@ -0,0 +1,61 @@
+import argparse
+import logging
+import signal
+import sys
+
+
+# Configure root logger
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+ handlers=[logging.StreamHandler(sys.stderr)],
+)
+logger = logging.getLogger("openstack-mcp-server")
+
+
+def handle_interrupt(signum, frame):
+ """Handle keyboard interrupt (Ctrl+C) gracefully."""
+ logger.info(f"Received signal {signum}, shutting down gracefully...")
+ sys.exit(0)
+
+
+def main():
+ """Openstack MCP Server main entry point."""
+ try:
+ # Import here to avoid circular imports
+ from openstack_mcp_server.config import MCP_TRANSPORT
+ from openstack_mcp_server.server import serve
+
+ parser = argparse.ArgumentParser(
+ description="Openstack MCP Server",
+ )
+
+ # Set up signal handler for graceful shutdown
+ signal.signal(signal.SIGINT, handle_interrupt)
+ signal.signal(signal.SIGTERM, handle_interrupt)
+
+ # Validate transport protocol
+ if MCP_TRANSPORT not in ["stdio", "sse", "streamable-http"]:
+ logger.error(
+ f"Invalid transport protocol: {MCP_TRANSPORT}. Using stdio instead.",
+ )
+ transport = "stdio"
+ else:
+ transport = MCP_TRANSPORT
+
+ # Start the server
+ logger.info(
+ f"Starting Openstack MCP Server with {transport} transport",
+ )
+
+ args = parser.parse_args()
+
+ serve(transport=transport, **vars(args))
+
+ except KeyboardInterrupt:
+ logger.info("Keyboard interrupt received. Shutting down gracefully...")
+ sys.exit(0)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/openstack_mcp_server/__main__.py b/src/openstack_mcp_server/__main__.py
new file mode 100644
index 0000000..0d7f812
--- /dev/null
+++ b/src/openstack_mcp_server/__main__.py
@@ -0,0 +1,4 @@
+from openstack_mcp_server import main
+
+
+main()
diff --git a/src/openstack_mcp_server/config.py b/src/openstack_mcp_server/config.py
new file mode 100644
index 0000000..9160f10
--- /dev/null
+++ b/src/openstack_mcp_server/config.py
@@ -0,0 +1,14 @@
+import os
+
+from pathlib import Path
+
+
+# Transport protocol
+MCP_TRANSPORT: str = os.environ.get("TRANSPORT", "stdio")
+
+# Openstack client settings
+MCP_CLOUD_NAME: str = os.environ.get("CLOUD_NAME", "openstack")
+MCP_DEBUG_MODE: bool = os.environ.get("DEBUG_MODE", "true").lower() == "true"
+
+# Application paths
+BASE_DIR = Path(__file__).parent.parent.parent
diff --git a/src/openstack_mcp_server/prompts/__init__.py b/src/openstack_mcp_server/prompts/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/openstack_mcp_server/resources/__init__.py b/src/openstack_mcp_server/resources/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/openstack_mcp_server/server.py b/src/openstack_mcp_server/server.py
new file mode 100644
index 0000000..0b28827
--- /dev/null
+++ b/src/openstack_mcp_server/server.py
@@ -0,0 +1,29 @@
+from fastmcp.server import FastMCP
+from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware
+from fastmcp.server.middleware.logging import LoggingMiddleware
+
+from openstack_mcp_server.tools import register_tool
+
+
+def serve(transport: str, **kwargs):
+ """Serve the MCP server with the specified transport."""
+ mcp = FastMCP(
+ "openstack_mcp_server",
+ )
+
+ register_tool(mcp)
+ # resister_resources(mcp)
+ # register_prompt(mcp)
+
+ # Add middlewares
+ mcp.add_middleware(ErrorHandlingMiddleware())
+ mcp.add_middleware(LoggingMiddleware())
+
+ if transport == "stdio":
+ mcp.run(transport="stdio", **kwargs)
+ elif transport == "streamable-http":
+ mcp.run(transport="streamable-http", **kwargs)
+ elif transport == "sse":
+ mcp.run(transport="sse", **kwargs)
+ else:
+ raise ValueError(f"Unsupported transport: {transport}")
diff --git a/src/openstack_mcp_server/tools/__init__.py b/src/openstack_mcp_server/tools/__init__.py
new file mode 100644
index 0000000..e42a705
--- /dev/null
+++ b/src/openstack_mcp_server/tools/__init__.py
@@ -0,0 +1,19 @@
+from fastmcp import FastMCP
+
+
+def register_tool(mcp: FastMCP):
+ """
+ Register Openstack MCP tools.
+ """
+
+ from .block_storage_tools import BlockStorageTools
+ from .compute_tools import ComputeTools
+ from .identity_tools import IdentityTools
+ from .image_tools import ImageTools
+ from .network_tools import NetworkTools
+
+ ComputeTools().register_tools(mcp)
+ ImageTools().register_tools(mcp)
+ IdentityTools().register_tools(mcp)
+ NetworkTools().register_tools(mcp)
+ BlockStorageTools().register_tools(mcp)
diff --git a/src/openstack_mcp_server/tools/base.py b/src/openstack_mcp_server/tools/base.py
new file mode 100644
index 0000000..03333ad
--- /dev/null
+++ b/src/openstack_mcp_server/tools/base.py
@@ -0,0 +1,27 @@
+import openstack
+
+from openstack import connection
+
+from openstack_mcp_server import config
+
+
+class OpenStackConnectionManager:
+ """OpenStack Connection Manager"""
+
+ _connection: connection.Connection | None = None
+
+ @classmethod
+ def get_connection(cls) -> connection.Connection:
+ """OpenStack Connection"""
+ if cls._connection is None:
+ openstack.enable_logging(debug=config.MCP_DEBUG_MODE)
+ cls._connection = openstack.connect(cloud=config.MCP_CLOUD_NAME)
+ return cls._connection
+
+
+_openstack_connection_manager = OpenStackConnectionManager()
+
+
+def get_openstack_conn():
+ """Get OpenStack Connection"""
+ return _openstack_connection_manager.get_connection()
diff --git a/src/openstack_mcp_server/tools/block_storage_tools.py b/src/openstack_mcp_server/tools/block_storage_tools.py
new file mode 100644
index 0000000..d682ebd
--- /dev/null
+++ b/src/openstack_mcp_server/tools/block_storage_tools.py
@@ -0,0 +1,185 @@
+from fastmcp import FastMCP
+
+from .base import get_openstack_conn
+from .response.block_storage import (
+ Volume,
+ VolumeAttachment,
+)
+
+
+class BlockStorageTools:
+ """
+ A class to encapsulate Block Storage-related tools and utilities.
+ """
+
+ def register_tools(self, mcp: FastMCP):
+ """
+ Register Block Storage-related tools with the FastMCP instance.
+ """
+ mcp.tool()(self.get_volumes)
+ mcp.tool()(self.get_volume_details)
+ mcp.tool()(self.create_volume)
+ mcp.tool()(self.delete_volume)
+ mcp.tool()(self.extend_volume)
+
+ def get_volumes(self) -> list[Volume]:
+ """
+ Get the list of Block Storage volumes.
+
+ :return: A list of Volume objects representing the volumes.
+ """
+ conn = get_openstack_conn()
+
+ # List the volumes
+ volume_list = []
+ for volume in conn.block_storage.volumes():
+ attachments = []
+ for attachment in volume.attachments or []:
+ attachments.append(
+ VolumeAttachment(
+ server_id=attachment.get("server_id"),
+ device=attachment.get("device"),
+ attachment_id=attachment.get("id"),
+ ),
+ )
+
+ volume_list.append(
+ Volume(
+ id=volume.id,
+ name=volume.name,
+ status=volume.status,
+ size=volume.size,
+ volume_type=volume.volume_type,
+ availability_zone=volume.availability_zone,
+ created_at=str(volume.created_at)
+ if volume.created_at
+ else None,
+ is_bootable=volume.is_bootable,
+ is_encrypted=volume.is_encrypted,
+ description=volume.description,
+ attachments=attachments,
+ ),
+ )
+
+ return volume_list
+
+ def get_volume_details(self, volume_id: str) -> Volume:
+ """
+ Get detailed information about a specific volume.
+
+ :param volume_id: The ID of the volume to get details for
+ :return: A Volume object with detailed information
+ """
+ conn = get_openstack_conn()
+
+ volume = conn.block_storage.get_volume(volume_id)
+
+ attachments = []
+ for attachment in volume.attachments or []:
+ attachments.append(
+ VolumeAttachment(
+ server_id=attachment.get("server_id"),
+ device=attachment.get("device"),
+ attachment_id=attachment.get("id"),
+ ),
+ )
+
+ return Volume(
+ id=volume.id,
+ name=volume.name,
+ status=volume.status,
+ size=volume.size,
+ volume_type=volume.volume_type,
+ availability_zone=volume.availability_zone,
+ created_at=str(volume.created_at),
+ is_bootable=volume.is_bootable,
+ is_encrypted=volume.is_encrypted,
+ description=volume.description,
+ attachments=attachments,
+ )
+
+ def create_volume(
+ self,
+ name: str,
+ size: int,
+ description: str | None = None,
+ volume_type: str | None = None,
+ availability_zone: str | None = None,
+ bootable: bool | None = None,
+ image: str | None = None,
+ ) -> Volume:
+ """
+ Create a new volume.
+
+ :param name: Name for the new volume
+ :param size: Size of the volume in GB
+ :param description: Optional description for the volume
+ :param volume_type: Optional volume type
+ :param availability_zone: Optional availability zone
+ :param bootable: Optional flag to make the volume bootable
+ :param image: Optional Image name, ID or object from which to create
+ :return: The created Volume object
+ """
+ conn = get_openstack_conn()
+
+ volume_kwargs = {
+ "name": name,
+ }
+
+ if description is not None:
+ volume_kwargs["description"] = description
+ if volume_type is not None:
+ volume_kwargs["volume_type"] = volume_type
+ if availability_zone is not None:
+ volume_kwargs["availability_zone"] = availability_zone
+
+ volume = conn.block_storage.create_volume(
+ size=size,
+ image=image,
+ bootable=bootable,
+ **volume_kwargs,
+ )
+
+ volume_obj = Volume(
+ id=volume.id,
+ name=volume.name,
+ status=volume.status,
+ size=volume.size,
+ volume_type=volume.volume_type,
+ availability_zone=volume.availability_zone,
+ created_at=str(volume.created_at),
+ is_bootable=volume.is_bootable,
+ is_encrypted=volume.is_encrypted,
+ description=volume.description,
+ attachments=[],
+ )
+
+ return volume_obj
+
+ def delete_volume(self, volume_id: str, force: bool = False) -> None:
+ """
+ Delete a volume.
+
+ :param volume_id: The ID of the volume to delete
+ :param force: Whether to force delete the volume
+ :return: None
+ """
+ conn = get_openstack_conn()
+
+ conn.block_storage.delete_volume(
+ volume_id,
+ force=force,
+ ignore_missing=False,
+ )
+
+ def extend_volume(self, volume_id: str, new_size: int) -> None:
+ """
+ Extend a volume to a new size.
+
+ :param volume_id: The ID of the volume to extend
+ :param new_size: The new size in GB (must be larger than current size)
+ :return: None
+ """
+ conn = get_openstack_conn()
+
+ conn.block_storage.extend_volume(volume_id, new_size)
diff --git a/src/openstack_mcp_server/tools/compute_tools.py b/src/openstack_mcp_server/tools/compute_tools.py
new file mode 100644
index 0000000..342c4cb
--- /dev/null
+++ b/src/openstack_mcp_server/tools/compute_tools.py
@@ -0,0 +1,216 @@
+from enum import Enum
+from typing import Any
+
+from fastmcp import FastMCP
+
+from openstack_mcp_server.tools.response.compute import (
+ Flavor,
+ Server,
+)
+
+from .base import get_openstack_conn
+
+
+class ServerActionEnum(str, Enum):
+ """available actions without parameter for compute tools"""
+
+ PAUSE = "pause"
+ UNPAUSE = "unpause"
+ SUSPEND = "suspend"
+ RESUME = "resume"
+ LOCK = "lock"
+ UNLOCK = "unlock"
+ RESCUE = "rescue"
+ UNRESCUE = "unrescue"
+ START = "start"
+ STOP = "stop"
+ SHELVE = "shelve"
+ SHELVE_OFFLOAD = "shelve_offload"
+ UNSHELVE = "unshelve"
+
+
+class ComputeTools:
+ """
+ A class to encapsulate Compute-related tools and utilities.
+ """
+
+ def register_tools(self, mcp: FastMCP):
+ """
+ Register Compute-related tools with the FastMCP instance.
+ """
+ mcp.tool()(self.get_servers)
+ mcp.tool()(self.get_server)
+ mcp.tool()(self.create_server)
+ mcp.tool()(self.get_flavors)
+ mcp.tool()(self.action_server)
+ mcp.tool()(self.update_server)
+ mcp.tool()(self.delete_server)
+
+ def get_servers(self) -> list[Server]:
+ """
+ Get the list of Compute servers.
+
+ :return: A list of Server objects.
+ """
+ conn = get_openstack_conn()
+ server_list = []
+ for server in conn.compute.servers():
+ server_list.append(Server(**server))
+
+ return server_list
+
+ def get_server(self, id: str) -> Server:
+ """
+ Get a specific Compute server.
+
+ :param id: The ID of the server to retrieve.
+ :return: A Server object.
+ """
+ conn = get_openstack_conn()
+ server = conn.compute.get_server(id)
+ return Server(**server)
+
+ def create_server(
+ self,
+ name: str,
+ image: str,
+ flavor: int,
+ network: str,
+ key_name: str | None = None,
+ security_groups: list[str] | None = None,
+ user_data: str | None = None,
+ ) -> Server:
+ """
+ Create a new Compute server.
+
+ :param name: The name of the server.
+ :param image: The ID of the image to use.
+ :param flavor: The ID of the flavor to use.
+ :param network: The ID of the network to attach.
+ :param key_name: The name of the key pair to use.
+ :param security_groups: A list of security group names to attach.
+ :param user_data: User data to pass to the server.
+ :return: A Server object
+ """
+ conn = get_openstack_conn()
+ server_params: dict[str, Any] = {
+ "name": name,
+ "flavorRef": flavor,
+ "imageRef": image,
+ "networks": [{"uuid": network}],
+ "key_name": key_name,
+ "security_groups": security_groups,
+ "user_data": user_data,
+ }
+ server_params = {
+ k: v for k, v in server_params.items() if v is not None
+ }
+
+ resp = conn.compute.create_server(**server_params)
+ # NOTE: The create_server method returns a server object with minimal information.
+ # To get the full server details, we need to fetch it again.
+ server = conn.compute.get_server(resp.id)
+
+ return Server(**server)
+
+ def get_flavors(self) -> list[Flavor]:
+ """
+ Get flavors (server hardware configurations).
+
+ :return: A list of Flavor objects.
+ """
+ conn = get_openstack_conn()
+ flavor_list = []
+ for flavor in conn.compute.flavors():
+ flavor_list.append(Flavor(**flavor))
+ return flavor_list
+
+ def action_server(self, id: str, action: ServerActionEnum) -> None:
+ """
+ Perform an action on a Compute server.
+
+ :param id: The ID of the server.
+ :param action: The action to perform.
+ Available actions:
+ - pause: Pauses a server. Changes its status to PAUSED
+ - unpause: Unpauses a paused server and changes its status to ACTIVE
+ - suspend: Suspends a server and changes its status to SUSPENDED
+ - resume: Resumes a suspended server and changes its status to ACTIVE
+ - lock: Locks a server
+ - unlock: Unlocks a locked server
+ - rescue: Puts a server in rescue mode and changes its status to RESCUE
+ - unrescue: Unrescues a server. Changes status to ACTIVE
+ - start: Starts a stopped server and changes its status to ACTIVE
+ - stop: Stops a running server and changes its status to SHUTOFF
+ - shelve: Shelves a server
+ - shelve_offload: Shelf-offloads, or removes, a shelved server
+ - unshelve: Unshelves, or restores, a shelved server
+ Only above actions are currently supported
+ :raises ValueError: If the action is not supported or invalid(ConflictException).
+ """
+ conn = get_openstack_conn()
+
+ action_methods = {
+ ServerActionEnum.PAUSE: conn.compute.pause_server,
+ ServerActionEnum.UNPAUSE: conn.compute.unpause_server,
+ ServerActionEnum.SUSPEND: conn.compute.suspend_server,
+ ServerActionEnum.RESUME: conn.compute.resume_server,
+ ServerActionEnum.LOCK: conn.compute.lock_server,
+ ServerActionEnum.UNLOCK: conn.compute.unlock_server,
+ ServerActionEnum.RESCUE: conn.compute.rescue_server,
+ ServerActionEnum.UNRESCUE: conn.compute.unrescue_server,
+ ServerActionEnum.START: conn.compute.start_server,
+ ServerActionEnum.STOP: conn.compute.stop_server,
+ ServerActionEnum.SHELVE: conn.compute.shelve_server,
+ ServerActionEnum.SHELVE_OFFLOAD: conn.compute.shelve_offload_server,
+ ServerActionEnum.UNSHELVE: conn.compute.unshelve_server,
+ }
+
+ if action not in action_methods:
+ raise ValueError(f"Unsupported action: {action}")
+
+ action_methods[action](id)
+ return None
+
+ def update_server(
+ self,
+ id: str,
+ accessIPv4: str | None = None,
+ accessIPv6: str | None = None,
+ name: str | None = None,
+ hostname: str | None = None,
+ description: str | None = None,
+ ) -> Server:
+ """
+ Update a Compute server's name, hostname, or description.
+
+ :param id: The UUID of the server.
+ :param accessIPv4: IPv4 address that should be used to access this server.
+ :param accessIPv6: IPv6 address that should be used to access this server.
+ :param name: The server name.
+ :param hostname: The hostname to configure for the instance in the metadata service.
+ :param description: A free form description of the server.
+ :return: The updated Server object.
+ """
+ conn = get_openstack_conn()
+ server_params = {
+ "accessIPv4": accessIPv4,
+ "accessIPv6": accessIPv6,
+ "name": name,
+ "hostname": hostname,
+ "description": description,
+ }
+ server_params = {
+ k: v for k, v in server_params.items() if v is not None
+ }
+ server = conn.compute.update_server(id, **server_params)
+ return Server(**server)
+
+ def delete_server(self, id: str) -> None:
+ """
+ Delete a Compute server.
+
+ :param id: The UUID of the server.
+ """
+ conn = get_openstack_conn()
+ conn.compute.delete_server(id)
diff --git a/src/openstack_mcp_server/tools/identity_tools.py b/src/openstack_mcp_server/tools/identity_tools.py
new file mode 100644
index 0000000..f3fe85d
--- /dev/null
+++ b/src/openstack_mcp_server/tools/identity_tools.py
@@ -0,0 +1,222 @@
+from fastmcp import FastMCP
+
+from .base import get_openstack_conn
+from .response.identity import Domain, Region
+
+
+class IdentityTools:
+ """
+ A class to encapsulate Identity-related tools and utilities.
+ """
+
+ def register_tools(self, mcp: FastMCP):
+ """
+ Register Identity-related tools with the FastMCP instance.
+ """
+
+ mcp.tool()(self.get_regions)
+ mcp.tool()(self.get_region)
+ mcp.tool()(self.create_region)
+ mcp.tool()(self.delete_region)
+ mcp.tool()(self.update_region)
+
+ mcp.tool()(self.get_domains)
+ mcp.tool()(self.get_domain)
+ mcp.tool()(self.create_domain)
+ mcp.tool()(self.delete_domain)
+ mcp.tool()(self.update_domain)
+
+ def get_regions(self) -> list[Region]:
+ """
+ Get the list of Identity regions.
+
+ :return: A list of Region objects representing the regions.
+ """
+ conn = get_openstack_conn()
+
+ region_list = []
+ for region in conn.identity.regions():
+ region_list.append(
+ Region(id=region.id, description=region.description),
+ )
+
+ return region_list
+
+ def get_region(self, id: str) -> Region:
+ """
+ Get a region.
+
+ :param id: The ID of the region.
+
+ :return: The Region object.
+ """
+ conn = get_openstack_conn()
+
+ region = conn.identity.get_region(region=id)
+
+ return Region(id=region.id, description=region.description)
+
+ def create_region(self, id: str, description: str | None = None) -> Region:
+ """
+ Create a new region.
+
+ :param id: The ID of the region.
+ :param description: The description of the region.
+
+ :return: The created Region object.
+ """
+ conn = get_openstack_conn()
+
+ region = conn.identity.create_region(id=id, description=description)
+
+ return Region(id=region.id, description=region.description)
+
+ def delete_region(self, id: str) -> None:
+ """
+ Delete a region.
+
+ :param id: The ID of the region.
+
+ :return: None
+ """
+ conn = get_openstack_conn()
+
+ # ignore_missing is set to False to raise an exception if the region does not exist.
+ conn.identity.delete_region(region=id, ignore_missing=False)
+
+ return None
+
+ def update_region(self, id: str, description: str | None = None) -> Region:
+ """
+ Update a region.
+
+ :param id: The ID of the region.
+ :param description: The string description of the region.
+
+ :return: The updated Region object.
+ """
+ conn = get_openstack_conn()
+
+ updated_region = conn.identity.update_region(
+ region=id,
+ description=description,
+ )
+
+ return Region(
+ id=updated_region.id,
+ description=updated_region.description,
+ )
+
+ def get_domains(self) -> list[Domain]:
+ """
+ Get the list of Identity domains.
+
+ :return: A list of Domain objects representing the domains.
+ """
+ conn = get_openstack_conn()
+
+ domain_list = []
+ for domain in conn.identity.domains():
+ domain_list.append(
+ Domain(
+ id=domain.id,
+ name=domain.name,
+ description=domain.description,
+ is_enabled=domain.is_enabled,
+ ),
+ )
+ return domain_list
+
+ def get_domain(self, name: str) -> Domain:
+ """
+ Get a domain.
+
+ :param name: The name of the domain.
+
+ :return: The Domain object.
+ """
+ conn = get_openstack_conn()
+
+ domain = conn.identity.find_domain(name_or_id=name)
+
+ return Domain(
+ id=domain.id,
+ name=domain.name,
+ description=domain.description,
+ is_enabled=domain.is_enabled,
+ )
+
+ def create_domain(
+ self,
+ name: str,
+ description: str | None = None,
+ is_enabled: bool | None = False,
+ ) -> Domain:
+ """
+ Create a new domain.
+
+ :param name: The name of the domain.
+ :param description: The description of the domain.
+ :param is_enabled: Whether the domain is enabled.
+ """
+ conn = get_openstack_conn()
+
+ domain = conn.identity.create_domain(
+ name=name,
+ description=description,
+ enabled=is_enabled,
+ )
+
+ return Domain(
+ id=domain.id,
+ name=domain.name,
+ description=domain.description,
+ is_enabled=domain.is_enabled,
+ )
+
+ def delete_domain(self, name: str) -> None:
+ """
+ Delete a domain.
+
+ :param name: The name of the domain.
+ """
+ conn = get_openstack_conn()
+
+ domain = conn.identity.find_domain(name_or_id=name)
+ conn.identity.delete_domain(domain=domain, ignore_missing=False)
+
+ return None
+
+ def update_domain(
+ self,
+ id: str,
+ name: str | None = None,
+ description: str | None = None,
+ is_enabled: bool | None = None,
+ ) -> Domain:
+ """
+ Update a domain.
+
+ :param id: The ID of the domain.
+ :param name: The name of the domain.
+ :param description: The description of the domain.
+ :param is_enabled: Whether the domain is enabled.
+ """
+ conn = get_openstack_conn()
+
+ args = {}
+ if name is not None:
+ args["name"] = name
+ if description is not None:
+ args["description"] = description
+ if is_enabled is not None:
+ args["is_enabled"] = is_enabled
+
+ updated_domain = conn.identity.update_domain(domain=id, **args)
+
+ return Domain(
+ id=updated_domain.id,
+ name=updated_domain.name,
+ description=updated_domain.description,
+ is_enabled=updated_domain.is_enabled,
+ )
diff --git a/src/openstack_mcp_server/tools/image_tools.py b/src/openstack_mcp_server/tools/image_tools.py
new file mode 100644
index 0000000..953f603
--- /dev/null
+++ b/src/openstack_mcp_server/tools/image_tools.py
@@ -0,0 +1,97 @@
+from fastmcp import FastMCP
+
+from openstack_mcp_server.tools.request.image import CreateImage
+from openstack_mcp_server.tools.response.image import Image
+
+from .base import get_openstack_conn
+
+
+class ImageTools:
+ """
+ A class to encapsulate Image-related tools and utilities.
+ """
+
+ def register_tools(self, mcp: FastMCP):
+ """
+ Register Image-related tools with the FastMCP instance.
+ """
+
+ mcp.tool()(self.get_image_images)
+ mcp.tool()(self.create_image)
+
+ def get_image_images(self) -> str:
+ """
+ Get the list of Image images by invoking the registered tool.
+
+ :return: A string containing the names, IDs, and statuses of the images.
+ """
+ # Initialize connection
+ conn = get_openstack_conn()
+
+ # List the servers
+ image_list = []
+ for image in conn.image.images():
+ image_list.append(
+ f"{image.name} ({image.id}) - Status: {image.status}",
+ )
+
+ return "\n".join(image_list)
+
+ def create_image(self, image_data: CreateImage) -> Image:
+ """Create a new Openstack image.
+ This method handles both cases of image creation:
+ 1. If a volume is provided, it creates an image from the volume.
+ 2. If no volume is provided, it creates an image using the Image imports method
+ import_options field is required for this method.
+ Following import methods are supported:
+ - glance-direct: The image data is made available to the Image service via the Stage binary
+ - web-download: The image data is made available to the Image service by being posted to an accessible location with a URL that you know.
+ - must provide a URI to the image data.
+ - copy-image: The image data is made available to the Image service by copying existing image
+ - glance-download: The image data is made available to the Image service by fetching an image accessible from another glance service specified by a region name and an image id that you know.
+ - must provide a glance_region and glance_image_id.
+
+ :param image_data: An instance of CreateImage containing the image details.
+ :return: An Image object representing the created image.
+ """
+ conn = get_openstack_conn()
+
+ if image_data.volume:
+ created_image = conn.block_storage.create_image(
+ name=image_data.name,
+ volume=image_data.volume,
+ allow_duplicates=image_data.allow_duplicates,
+ container_format=image_data.container_format,
+ disk_format=image_data.disk_format,
+ wait=False,
+ timeout=3600,
+ )
+ else:
+ # Create an image with Image imports
+ # First, Creates a catalog record for an operating system disk image.
+ created_image = conn.image.create_image(
+ name=image_data.name,
+ container=image_data.container,
+ container_format=image_data.container_format,
+ disk_format=image_data.disk_format,
+ min_disk=image_data.min_disk,
+ min_ram=image_data.min_ram,
+ tags=image_data.tags,
+ protected=image_data.protected,
+ visibility=image_data.visibility,
+ allow_duplicates=image_data.allow_duplicates,
+ )
+
+ # Then, import the image data
+ conn.image.import_image(
+ image=created_image,
+ method=image_data.import_options.import_method,
+ uri=image_data.import_options.uri,
+ stores=image_data.import_options.stores,
+ remote_region=image_data.import_options.glance_region,
+ remote_image_id=image_data.import_options.glance_image_id,
+ remote_service_interface=image_data.import_options.glance_service_interface,
+ )
+
+ image = conn.get_image(created_image.id)
+ return Image(**image)
diff --git a/src/openstack_mcp_server/tools/network_tools.py b/src/openstack_mcp_server/tools/network_tools.py
new file mode 100644
index 0000000..8e5c337
--- /dev/null
+++ b/src/openstack_mcp_server/tools/network_tools.py
@@ -0,0 +1,862 @@
+from fastmcp import FastMCP
+
+from .base import get_openstack_conn
+from .response.network import (
+ FloatingIP,
+ Network,
+ Port,
+ Subnet,
+)
+
+
+class NetworkTools:
+ """
+ A class to encapsulate Network-related tools and utilities.
+ """
+
+ def register_tools(self, mcp: FastMCP):
+ """
+ Register Network-related tools with the FastMCP instance.
+ """
+
+ mcp.tool()(self.get_networks)
+ mcp.tool()(self.create_network)
+ mcp.tool()(self.get_network_detail)
+ mcp.tool()(self.update_network)
+ mcp.tool()(self.delete_network)
+ mcp.tool()(self.get_subnets)
+ mcp.tool()(self.create_subnet)
+ mcp.tool()(self.get_subnet_detail)
+ mcp.tool()(self.update_subnet)
+ mcp.tool()(self.delete_subnet)
+ mcp.tool()(self.get_ports)
+ mcp.tool()(self.create_port)
+ mcp.tool()(self.get_port_detail)
+ mcp.tool()(self.update_port)
+ mcp.tool()(self.delete_port)
+ mcp.tool()(self.get_port_allowed_address_pairs)
+ mcp.tool()(self.set_port_binding)
+ mcp.tool()(self.get_floating_ips)
+ mcp.tool()(self.create_floating_ip)
+ mcp.tool()(self.delete_floating_ip)
+ mcp.tool()(self.update_floating_ip)
+ mcp.tool()(self.create_floating_ips_bulk)
+ mcp.tool()(self.assign_first_available_floating_ip)
+
+ def get_networks(
+ self,
+ status_filter: str | None = None,
+ shared_only: bool = False,
+ ) -> list[Network]:
+ """
+ Get the list of Networks with optional filtering.
+
+ :param status_filter: Filter networks by status (e.g., `ACTIVE`, `DOWN`)
+ :param shared_only: If True, only show shared networks
+ :return: List of Network objects
+ """
+ conn = get_openstack_conn()
+
+ filters = {}
+
+ if status_filter:
+ filters["status"] = status_filter.upper()
+
+ if shared_only:
+ filters["shared"] = True
+
+ networks = conn.list_networks(filters=filters)
+
+ return [
+ self._convert_to_network_model(network) for network in networks
+ ]
+
+ def create_network(
+ self,
+ name: str,
+ description: str | None = None,
+ is_admin_state_up: bool = True,
+ is_shared: bool = False,
+ provider_network_type: str | None = None,
+ provider_physical_network: str | None = None,
+ provider_segmentation_id: int | None = None,
+ ) -> Network:
+ """
+ Create a new Network.
+
+ :param name: Network name
+ :param description: Network description
+ :param is_admin_state_up: Administrative state
+ :param is_shared: Whether the network is shared
+ :param provider_network_type: Provider network type (e.g., 'vlan', 'flat', 'vxlan')
+ :param provider_physical_network: Physical network name
+ :param provider_segmentation_id: Segmentation ID for VLAN/VXLAN
+ :return: Created Network object
+ """
+ conn = get_openstack_conn()
+
+ network_args = {
+ "name": name,
+ "admin_state_up": is_admin_state_up,
+ "shared": is_shared,
+ }
+
+ if description:
+ network_args["description"] = description
+
+ if provider_network_type:
+ network_args["provider_network_type"] = provider_network_type
+
+ if provider_physical_network:
+ network_args["provider_physical_network"] = (
+ provider_physical_network
+ )
+
+ if provider_segmentation_id is not None:
+ network_args["provider_segmentation_id"] = provider_segmentation_id
+
+ network = conn.network.create_network(**network_args)
+
+ return self._convert_to_network_model(network)
+
+ def get_network_detail(self, network_id: str) -> Network:
+ """
+ Get detailed information about a specific Network.
+
+ :param network_id: ID of the network to retrieve
+ :return: Network details
+ """
+ conn = get_openstack_conn()
+
+ network = conn.network.get_network(network_id)
+ return self._convert_to_network_model(network)
+
+ def update_network(
+ self,
+ network_id: str,
+ name: str | None = None,
+ description: str | None = None,
+ is_admin_state_up: bool | None = None,
+ is_shared: bool | None = None,
+ ) -> Network:
+ """
+ Update an existing Network.
+
+ :param network_id: ID of the network to update
+ :param name: New network name
+ :param description: New network description
+ :param is_admin_state_up: New administrative state
+ :param is_shared: New shared state
+ :return: Updated Network object
+ """
+ conn = get_openstack_conn()
+
+ update_args = {}
+
+ if name is not None:
+ update_args["name"] = name
+ if description is not None:
+ update_args["description"] = description
+ if is_admin_state_up is not None:
+ update_args["admin_state_up"] = is_admin_state_up
+ if is_shared is not None:
+ update_args["shared"] = is_shared
+
+ if not update_args:
+ current = conn.network.get_network(network_id)
+ return self._convert_to_network_model(current)
+ network = conn.network.update_network(network_id, **update_args)
+ return self._convert_to_network_model(network)
+
+ def delete_network(self, network_id: str) -> None:
+ """
+ Delete a Network.
+
+ :param network_id: ID of the network to delete
+ :return: None
+ """
+ conn = get_openstack_conn()
+ conn.network.delete_network(network_id, ignore_missing=False)
+
+ return None
+
+ def _convert_to_network_model(self, openstack_network) -> Network:
+ """
+ Convert an OpenStack network object to a Network pydantic model.
+
+ :param openstack_network: OpenStack network object
+ :return: Pydantic Network model
+ """
+ return Network(
+ id=openstack_network.id,
+ name=openstack_network.name or "",
+ status=openstack_network.status or "",
+ description=openstack_network.description or None,
+ is_admin_state_up=openstack_network.is_admin_state_up or False,
+ is_shared=openstack_network.is_shared or False,
+ mtu=openstack_network.mtu or None,
+ provider_network_type=openstack_network.provider_network_type
+ or None,
+ provider_physical_network=openstack_network.provider_physical_network
+ or None,
+ provider_segmentation_id=openstack_network.provider_segmentation_id
+ or None,
+ project_id=openstack_network.project_id or None,
+ )
+
+ def get_subnets(
+ self,
+ network_id: str | None = None,
+ ip_version: int | None = None,
+ project_id: str | None = None,
+ has_gateway: bool | None = None,
+ is_dhcp_enabled: bool | None = None,
+ ) -> list[Subnet]:
+ """
+ Get the list of Subnets with optional filtering.
+
+ Use this to narrow results by network, project, IP version, gateway presence, and
+ DHCP-enabled state.
+
+ Notes:
+ - has_gateway is applied client-side after retrieval and checks whether `gateway_ip` is set.
+ - `is_dhcp_enabled` maps to Neutron's `enable_dhcp` filter.
+ - Combining filters further restricts the result (logical AND).
+
+ Examples:
+ - All IPv4 subnets in a network: `network_id="net-1"`, `ip_version=4`
+ - Only subnets with a gateway: `has_gateway=True`
+ - DHCP-enabled subnets for a project: `project_id="proj-1"`, `is_dhcp_enabled=True`
+
+ :param network_id: Filter by network ID
+ :param ip_version: Filter by IP version (e.g., 4, 6)
+ :param project_id: Filter by project ID
+ :param has_gateway: True for subnets with a gateway, False for no gateway
+ :param is_dhcp_enabled: True for DHCP-enabled subnets, False for disabled
+ :return: List of Subnet objects
+ """
+ conn = get_openstack_conn()
+ filters: dict = {}
+ if network_id:
+ filters["network_id"] = network_id
+ if ip_version is not None:
+ filters["ip_version"] = ip_version
+ if project_id:
+ filters["project_id"] = project_id
+ if is_dhcp_enabled is not None:
+ filters["enable_dhcp"] = is_dhcp_enabled
+ subnets = conn.list_subnets(filters=filters)
+ if has_gateway is not None:
+ subnets = [
+ s for s in subnets if (s.gateway_ip is not None) == has_gateway
+ ]
+ return [self._convert_to_subnet_model(subnet) for subnet in subnets]
+
+ def create_subnet(
+ self,
+ network_id: str,
+ cidr: str,
+ name: str | None = None,
+ ip_version: int = 4,
+ gateway_ip: str | None = None,
+ is_dhcp_enabled: bool = True,
+ description: str | None = None,
+ dns_nameservers: list[str] | None = None,
+ allocation_pools: list[dict] | None = None,
+ host_routes: list[dict] | None = None,
+ ) -> Subnet:
+ """
+ Create a new Subnet.
+
+ :param network_id: ID of the parent network
+ :param cidr: Subnet CIDR
+ :param name: Subnet name
+ :param ip_version: IP version
+ :param gateway_ip: Gateway IP address
+ :param is_dhcp_enabled: Whether DHCP is enabled
+ :param description: Subnet description
+ :param dns_nameservers: DNS nameserver list
+ :param allocation_pools: Allocation pool list
+ :param host_routes: Static host routes
+ :return: Created Subnet object
+ """
+ conn = get_openstack_conn()
+ subnet_args: dict = {
+ "network_id": network_id,
+ "cidr": cidr,
+ "ip_version": ip_version,
+ "enable_dhcp": is_dhcp_enabled,
+ }
+ if name is not None:
+ subnet_args["name"] = name
+ if description is not None:
+ subnet_args["description"] = description
+ if gateway_ip is not None:
+ subnet_args["gateway_ip"] = gateway_ip
+ if dns_nameservers is not None:
+ subnet_args["dns_nameservers"] = dns_nameservers
+ if allocation_pools is not None:
+ subnet_args["allocation_pools"] = allocation_pools
+ if host_routes is not None:
+ subnet_args["host_routes"] = host_routes
+ subnet = conn.network.create_subnet(**subnet_args)
+ return self._convert_to_subnet_model(subnet)
+
+ def get_subnet_detail(self, subnet_id: str) -> Subnet:
+ """
+ Get detailed information about a specific Subnet.
+
+ :param subnet_id: ID of the subnet to retrieve
+ :return: Subnet details
+ """
+ conn = get_openstack_conn()
+ subnet = conn.network.get_subnet(subnet_id)
+ return self._convert_to_subnet_model(subnet)
+
+ def update_subnet(
+ self,
+ subnet_id: str,
+ name: str | None = None,
+ description: str | None = None,
+ gateway_ip: str | None = None,
+ clear_gateway: bool = False,
+ is_dhcp_enabled: bool | None = None,
+ dns_nameservers: list[str] | None = None,
+ allocation_pools: list[dict] | None = None,
+ host_routes: list[dict] | None = None,
+ ) -> Subnet:
+ """
+ Update subnet attributes atomically. Only provided parameters are changed; omitted
+ parameters remain untouched.
+
+ Typical use-cases:
+ - Set gateway: `gateway_ip="10.0.0.1"`.
+ - Clear gateway: `clear_gateway=True`.
+ - Enable/disable DHCP: `is_dhcp_enabled=True or False`.
+ - Batch updates: update name/description and DNS nameservers together.
+
+ Notes:
+ - `clear_gateway=True` explicitly clears `gateway_ip` (sets to None). If both `gateway_ip`
+ and `clear_gateway=True` are provided, `clear_gateway` takes precedence.
+ - For list-typed fields (`dns_nameservers`, `allocation_pools`, `host_routes`), the provided
+ list replaces the entire list on the server. Pass `[]` to remove all entries.
+ - For a DHCP toggle, read the current value via `get_subnet_detail()` and pass the inverted
+ boolean to `is_dhcp_enabled`.
+
+ Examples:
+ - Clear the gateway and disable DHCP: `clear_gateway=True`, `is_dhcp_enabled=False`
+ - Replace DNS servers: `dns_nameservers=["8.8.8.8", "1.1.1.1"]`
+
+ :param subnet_id: ID of the subnet to update
+ :param name: New subnet name
+ :param description: New subnet description
+ :param gateway_ip: New gateway IP
+ :param clear_gateway: If True, clear the gateway IP (sets to None)
+ :param is_dhcp_enabled: DHCP enabled state
+ :param dns_nameservers: DNS nameserver list (replaces entire list)
+ :param allocation_pools: Allocation pool list (replaces entire list)
+ :param host_routes: Static host routes (replaces entire list)
+ :return: Updated Subnet object
+ """
+ conn = get_openstack_conn()
+ update_args: dict = {}
+ if name is not None:
+ update_args["name"] = name
+ if description is not None:
+ update_args["description"] = description
+ if clear_gateway:
+ update_args["gateway_ip"] = None
+ elif gateway_ip is not None:
+ update_args["gateway_ip"] = gateway_ip
+ if is_dhcp_enabled is not None:
+ update_args["enable_dhcp"] = is_dhcp_enabled
+ if dns_nameservers is not None:
+ update_args["dns_nameservers"] = dns_nameservers
+ if allocation_pools is not None:
+ update_args["allocation_pools"] = allocation_pools
+ if host_routes is not None:
+ update_args["host_routes"] = host_routes
+ if not update_args:
+ current = conn.network.get_subnet(subnet_id)
+ return self._convert_to_subnet_model(current)
+ subnet = conn.network.update_subnet(subnet_id, **update_args)
+ return self._convert_to_subnet_model(subnet)
+
+ def delete_subnet(self, subnet_id: str) -> None:
+ """
+ Delete a Subnet.
+
+ :param subnet_id: ID of the subnet to delete
+ :return: None
+ """
+ conn = get_openstack_conn()
+ conn.network.delete_subnet(subnet_id, ignore_missing=False)
+ return None
+
+ def _convert_to_subnet_model(self, openstack_subnet) -> Subnet:
+ """
+ Convert an OpenStack subnet object to a Subnet pydantic model.
+
+ :param openstack_subnet: OpenStack subnet object
+ :return: Pydantic Subnet model
+ """
+ return Subnet(
+ id=openstack_subnet.id,
+ name=openstack_subnet.name,
+ status=getattr(openstack_subnet, "status", None),
+ description=openstack_subnet.description,
+ project_id=openstack_subnet.project_id,
+ network_id=openstack_subnet.network_id,
+ cidr=openstack_subnet.cidr,
+ ip_version=openstack_subnet.ip_version,
+ gateway_ip=openstack_subnet.gateway_ip,
+ is_dhcp_enabled=openstack_subnet.is_dhcp_enabled,
+ allocation_pools=getattr(
+ openstack_subnet, "allocation_pools", None
+ ),
+ dns_nameservers=getattr(openstack_subnet, "dns_nameservers", None),
+ host_routes=getattr(openstack_subnet, "host_routes", None),
+ )
+
+ def get_ports(
+ self,
+ status_filter: str | None = None,
+ device_id: str | None = None,
+ network_id: str | None = None,
+ ) -> list[Port]:
+ """
+ Get the list of Ports with optional filtering.
+
+ :param status_filter: Filter by port status (e.g., `ACTIVE`, `DOWN`)
+ :param device_id: Filter by device ID
+ :param network_id: Filter by network ID
+ :return: List of Port objects
+ """
+ conn = get_openstack_conn()
+ filters: dict = {}
+ if status_filter:
+ filters["status"] = status_filter.upper()
+ if device_id:
+ filters["device_id"] = device_id
+ if network_id:
+ filters["network_id"] = network_id
+ ports = conn.list_ports(filters=filters)
+ return [self._convert_to_port_model(port) for port in ports]
+
+ def get_port_allowed_address_pairs(self, port_id: str) -> list[dict]:
+ """
+ Get allowed address pairs configured on a port.
+
+ :param port_id: Port ID
+ :return: Allowed address pairs
+ """
+ conn = get_openstack_conn()
+ port = conn.network.get_port(port_id)
+ return list(port.allowed_address_pairs or [])
+
+ def set_port_binding(
+ self,
+ port_id: str,
+ host_id: str | None = None,
+ vnic_type: str | None = None,
+ profile: dict | None = None,
+ ) -> Port:
+ """
+ Set binding attributes for a port.
+
+ :param port_id: Port ID
+ :param host_id: Binding host ID
+ :param vnic_type: VNIC type
+ :param profile: Binding profile
+ :return: Updated Port object
+ """
+ conn = get_openstack_conn()
+ update_args: dict = {}
+ if host_id is not None:
+ update_args["binding_host_id"] = host_id
+ if vnic_type is not None:
+ update_args["binding_vnic_type"] = vnic_type
+ if profile is not None:
+ update_args["binding_profile"] = profile
+ if not update_args:
+ current = conn.network.get_port(port_id)
+ return self._convert_to_port_model(current)
+ updated = conn.network.update_port(port_id, **update_args)
+ return self._convert_to_port_model(updated)
+
+ def create_port(
+ self,
+ network_id: str,
+ name: str | None = None,
+ description: str | None = None,
+ is_admin_state_up: bool = True,
+ device_id: str | None = None,
+ fixed_ips: list[dict] | None = None,
+ security_group_ids: list[str] | None = None,
+ ) -> Port:
+ """
+ Create a new Port.
+
+ :param network_id: ID of the parent network
+ :param name: Port name
+ :param description: Port description
+ :param is_admin_state_up: Administrative state
+ :param device_id: Device ID
+ :param fixed_ips: Fixed IP list
+ :param security_group_ids: Security group ID list
+ :return: Created Port object
+ """
+ conn = get_openstack_conn()
+ port_args: dict = {
+ "network_id": network_id,
+ "admin_state_up": is_admin_state_up,
+ }
+ if name is not None:
+ port_args["name"] = name
+ if description is not None:
+ port_args["description"] = description
+ if device_id is not None:
+ port_args["device_id"] = device_id
+ if fixed_ips is not None:
+ port_args["fixed_ips"] = fixed_ips
+ if security_group_ids is not None:
+ port_args["security_groups"] = security_group_ids
+ port = conn.network.create_port(**port_args)
+ return self._convert_to_port_model(port)
+
+ def get_port_detail(self, port_id: str) -> Port:
+ """
+ Get detailed information about a specific Port.
+
+ :param port_id: ID of the port to retrieve
+ :return: Port details
+ """
+ conn = get_openstack_conn()
+ port = conn.network.get_port(port_id)
+ return self._convert_to_port_model(port)
+
+ def update_port(
+ self,
+ port_id: str,
+ name: str | None = None,
+ description: str | None = None,
+ is_admin_state_up: bool | None = None,
+ device_id: str | None = None,
+ security_group_ids: list[str] | None = None,
+ allowed_address_pairs: list[dict] | None = None,
+ fixed_ips: list[dict] | None = None,
+ ) -> Port:
+ """
+ Update an existing Port. Only provided parameters are changed; omitted parameters remain untouched.
+
+ Typical use-cases:
+ - Set admin state down: is_admin_state_up=False
+ - Toggle admin state: read current via get_port_detail(); pass inverted value
+ - Replace security groups: security_group_ids=["sg-1", "sg-2"]
+ - Replace allowed address pairs:
+ 1) current = get_port_allowed_address_pairs(port_id)
+ 2) edit the list (append/remove dicts)
+ 3) update_port(port_id, allowed_address_pairs=current)
+ - Replace fixed IPs:
+ 1) current = get_port_detail(port_id).fixed_ips
+ 2) edit the list
+ 3) update_port(port_id, fixed_ips=current)
+
+ Notes:
+ - List-typed fields (security groups, allowed address pairs, fixed IPs) replace the entire list
+ with the provided value. Pass [] to remove all entries.
+ - For fixed IPs, each dict typically includes keys like "subnet_id" and/or "ip_address".
+
+ Examples:
+ - Add a fixed IP: read current, append a new {"subnet_id": "subnet-2", "ip_address": "10.0.1.10"},
+ then pass fixed_ips=[...]
+ - Clear all security groups: security_group_ids=[]
+
+ :param port_id: ID of the port to update
+ :param name: New port name
+ :param description: New port description
+ :param is_admin_state_up: Administrative state
+ :param device_id: Device ID
+ :param security_group_ids: Security group ID list (replaces entire list)
+ :param allowed_address_pairs: Allowed address pairs (replaces entire list)
+ :param fixed_ips: Fixed IP assignments (replaces entire list)
+ :return: Updated Port object
+ """
+ conn = get_openstack_conn()
+ update_args: dict = {}
+ if name is not None:
+ update_args["name"] = name
+ if description is not None:
+ update_args["description"] = description
+ if is_admin_state_up is not None:
+ update_args["admin_state_up"] = is_admin_state_up
+ if device_id is not None:
+ update_args["device_id"] = device_id
+ if security_group_ids is not None:
+ update_args["security_groups"] = security_group_ids
+ if allowed_address_pairs is not None:
+ update_args["allowed_address_pairs"] = allowed_address_pairs
+ if fixed_ips is not None:
+ update_args["fixed_ips"] = fixed_ips
+ if not update_args:
+ current = conn.network.get_port(port_id)
+ return self._convert_to_port_model(current)
+ port = conn.network.update_port(port_id, **update_args)
+ return self._convert_to_port_model(port)
+
+ def delete_port(self, port_id: str) -> None:
+ """
+ Delete a Port.
+
+ :param port_id: ID of the port to delete
+ :return: None
+ """
+ conn = get_openstack_conn()
+ conn.network.delete_port(port_id, ignore_missing=False)
+ return None
+
+ def _convert_to_port_model(self, openstack_port) -> Port:
+ """
+ Convert an OpenStack Port object to a Port pydantic model.
+
+ :param openstack_port: OpenStack port object
+ :return: Pydantic Port model
+ """
+ return Port(
+ id=openstack_port.id,
+ name=openstack_port.name,
+ status=openstack_port.status,
+ description=openstack_port.description,
+ project_id=openstack_port.project_id,
+ network_id=openstack_port.network_id,
+ is_admin_state_up=openstack_port.is_admin_state_up,
+ device_id=openstack_port.device_id,
+ device_owner=openstack_port.device_owner,
+ mac_address=openstack_port.mac_address,
+ fixed_ips=openstack_port.fixed_ips,
+ security_group_ids=openstack_port.security_group_ids
+ if hasattr(openstack_port, "security_group_ids")
+ else None,
+ )
+
+ def get_floating_ips(
+ self,
+ status_filter: str | None = None,
+ project_id: str | None = None,
+ port_id: str | None = None,
+ floating_network_id: str | None = None,
+ unassigned_only: bool | None = None,
+ ) -> list[FloatingIP]:
+ """
+ Get the list of Floating IPs with optional filtering.
+
+ :param status_filter: Filter by IP status (e.g., `ACTIVE`)
+ :param project_id: Filter by project ID
+ :param port_id: Filter by attached port ID
+ :param floating_network_id: Filter by external network ID
+ :param unassigned_only: If True, return only unassigned IPs
+ :return: List of FloatingIP objects
+ """
+ conn = get_openstack_conn()
+ filters: dict = {}
+ if status_filter:
+ filters["status"] = status_filter.upper()
+ if project_id:
+ filters["project_id"] = project_id
+ if port_id:
+ filters["port_id"] = port_id
+ if floating_network_id:
+ filters["floating_network_id"] = floating_network_id
+ ips = list(conn.network.ips(**filters))
+ if unassigned_only:
+ ips = [i for i in ips if not i.port_id]
+ return [self._convert_to_floating_ip_model(ip) for ip in ips]
+
+ def create_floating_ip(
+ self,
+ floating_network_id: str,
+ description: str | None = None,
+ fixed_ip_address: str | None = None,
+ port_id: str | None = None,
+ project_id: str | None = None,
+ ) -> FloatingIP:
+ """
+ Create a new Floating IP.
+
+ Typical use-cases:
+ - Allocate in a pool and attach immediately: provide port_id (and optionally fixed_ip_address).
+ - Allocate for later use: omit port_id (unassigned state).
+ - Add metadata: provide description.
+
+ :param floating_network_id: External (floating) network ID
+ :param description: Floating IP description (omit to keep empty)
+ :param fixed_ip_address: Internal fixed IP to map when attaching to a port
+ :param port_id: Port ID to attach (omit for unassigned allocation)
+ :param project_id: Project ID to assign ownership
+ :return: Created FloatingIP object
+ """
+ conn = get_openstack_conn()
+ ip_args: dict = {"floating_network_id": floating_network_id}
+ if description is not None:
+ ip_args["description"] = description
+ if fixed_ip_address is not None:
+ ip_args["fixed_ip_address"] = fixed_ip_address
+ if port_id is not None:
+ ip_args["port_id"] = port_id
+ if project_id is not None:
+ ip_args["project_id"] = project_id
+ ip = conn.network.create_ip(**ip_args)
+ return self._convert_to_floating_ip_model(ip)
+
+ def attach_floating_ip_to_port(
+ self,
+ floating_ip_id: str,
+ port_id: str,
+ fixed_ip_address: str | None = None,
+ ) -> FloatingIP:
+ """
+ Attach a Floating IP to a Port.
+
+ :param floating_ip_id: Floating IP ID
+ :param port_id: Port ID to attach
+ :param fixed_ip_address: Specific fixed IP on the port (optional)
+ :return: Updated Floating IP object
+ """
+ conn = get_openstack_conn()
+ update_args: dict = {"port_id": port_id}
+ if fixed_ip_address is not None:
+ update_args["fixed_ip_address"] = fixed_ip_address
+ ip = conn.network.update_ip(floating_ip_id, **update_args)
+ return self._convert_to_floating_ip_model(ip)
+
+ def update_floating_ip(
+ self,
+ floating_ip_id: str,
+ description: str | None = None,
+ port_id: str | None = None,
+ fixed_ip_address: str | None = None,
+ clear_port: bool = False,
+ ) -> FloatingIP:
+ """
+ Update Floating IP attributes. Only provided parameters are changed; omitted
+ parameters remain untouched.
+
+ Typical use-cases:
+ - Attach to a port: port_id="port-1" (optionally fixed_ip_address="10.0.0.10").
+ - Detach from its port: clear_port=True and omit port_id (sets port_id=None).
+ - Keep current port: clear_port=False and omit port_id.
+ - Update description: description="new desc" or clear with description=None.
+ - Reassign to another port: port_id="new-port" (optionally with fixed_ip_address).
+
+ Notes:
+ - Passing None for description clears it.
+ - clear_port controls whether to detach when no port_id is provided.
+ - fixed_ip_address is optional and can be provided alongside port_id.
+
+ :param floating_ip_id: Floating IP ID to update
+ :param description: New description (omit to keep unchanged, None to clear)
+ :param port_id: Port ID to attach; omit to keep or detach depending on clear_port
+ :param clear_port: If True and port_id is omitted, detach (set port_id=None); if False and
+ port_id is omitted, keep current attachment
+ :param fixed_ip_address: Specific fixed IP to map when attaching
+ :return: Updated FloatingIP object
+ """
+ conn = get_openstack_conn()
+ update_args: dict = {}
+ if description is not None:
+ update_args["description"] = description
+ if port_id is not None:
+ update_args["port_id"] = port_id
+ if fixed_ip_address is not None:
+ update_args["fixed_ip_address"] = fixed_ip_address
+ else:
+ if clear_port:
+ update_args["port_id"] = None
+ if not update_args:
+ current = conn.network.get_ip(floating_ip_id)
+ return self._convert_to_floating_ip_model(current)
+ ip = conn.network.update_ip(floating_ip_id, **update_args)
+ return self._convert_to_floating_ip_model(ip)
+
+ def delete_floating_ip(self, floating_ip_id: str) -> None:
+ """
+ Delete a Floating IP.
+
+ :param floating_ip_id: Floating IP ID to delete
+ :return: None
+ """
+ conn = get_openstack_conn()
+ conn.network.delete_ip(floating_ip_id, ignore_missing=False)
+ return None
+
+ def create_floating_ips_bulk(
+ self,
+ floating_network_id: str,
+ count: int,
+ ) -> list[FloatingIP]:
+ """
+ Create multiple floating IPs on the specified external network.
+
+ :param floating_network_id: External network ID
+ :param count: Number of floating IPs to create (negative treated as 0)
+ :return: List of created FloatingIP objects
+ """
+ conn = get_openstack_conn()
+ created = []
+ for _ in range(max(0, count)):
+ ip = conn.network.create_ip(
+ floating_network_id=floating_network_id,
+ )
+ created.append(self._convert_to_floating_ip_model(ip))
+ return created
+
+ def assign_first_available_floating_ip(
+ self,
+ floating_network_id: str,
+ port_id: str,
+ ) -> FloatingIP:
+ """
+ Assign the first available floating IP from a network to a port.
+ If none are available, create a new one and assign it.
+
+ :param floating_network_id: External network ID
+ :param port_id: Target port ID
+ :return: Updated FloatingIP object
+ """
+ conn = get_openstack_conn()
+ existing = list(
+ conn.network.ips(floating_network_id=floating_network_id),
+ )
+ available = next(
+ (i for i in existing if not i.port_id),
+ None,
+ )
+ if available is None:
+ created = conn.network.create_ip(
+ floating_network_id=floating_network_id,
+ )
+ target_id = created.id
+ else:
+ target_id = available.id
+ ip = conn.network.update_ip(target_id, port_id=port_id)
+ return self._convert_to_floating_ip_model(ip)
+
+ def _convert_to_floating_ip_model(self, openstack_ip) -> FloatingIP:
+ """
+ Convert an OpenStack floating IP object to a FloatingIP pydantic model.
+
+ :param openstack_ip: OpenStack floating IP object
+ :return: Pydantic FloatingIP model
+ """
+ return FloatingIP(
+ id=openstack_ip.id,
+ name=openstack_ip.name,
+ status=openstack_ip.status,
+ description=openstack_ip.description,
+ project_id=openstack_ip.project_id,
+ floating_ip_address=openstack_ip.floating_ip_address,
+ floating_network_id=openstack_ip.floating_network_id,
+ fixed_ip_address=openstack_ip.fixed_ip_address,
+ port_id=openstack_ip.port_id,
+ router_id=openstack_ip.router_id,
+ )
diff --git a/src/openstack_mcp_server/tools/request/__init__.py b/src/openstack_mcp_server/tools/request/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/openstack_mcp_server/tools/request/image.py b/src/openstack_mcp_server/tools/request/image.py
new file mode 100644
index 0000000..4eb7404
--- /dev/null
+++ b/src/openstack_mcp_server/tools/request/image.py
@@ -0,0 +1,31 @@
+from __future__ import annotations
+
+from pydantic import BaseModel, Field
+
+
+class CreateImage(BaseModel):
+ """OpenStack Glance Image Creation Request Pydantic Model"""
+
+ id: str | None = Field(default=None)
+ volume: str | None = Field(default=None)
+ name: str | None = Field(default=None)
+ container: str | None = Field(default=None)
+ container_format: str | None = Field(default=None)
+ allow_duplicates: bool = Field(default=False)
+ disk_format: str | None = Field(default=None)
+ min_disk: int | None = Field(default=None)
+ min_ram: int | None = Field(default=None)
+ tags: list[str] | None = Field(default=[])
+ protected: bool | None = Field(default=False)
+ visibility: str | None = Field(default="public")
+ import_options: ImportOptions | None = Field(default=None)
+
+ class ImportOptions(BaseModel):
+ """Options for image import"""
+
+ import_method: str | None = Field(default=None)
+ stores: list[str] | None = Field(default=None)
+ uri: str | None = Field(default=None)
+ glance_region: str | None = Field(default=None)
+ glance_image_id: str | None = Field(default=None)
+ glance_service_interface: str | None = Field(default=None)
diff --git a/src/openstack_mcp_server/tools/response/__init__.py b/src/openstack_mcp_server/tools/response/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/openstack_mcp_server/tools/response/block_storage.py b/src/openstack_mcp_server/tools/response/block_storage.py
new file mode 100644
index 0000000..d4f8be3
--- /dev/null
+++ b/src/openstack_mcp_server/tools/response/block_storage.py
@@ -0,0 +1,21 @@
+from pydantic import BaseModel
+
+
+class VolumeAttachment(BaseModel):
+ server_id: str | None = None
+ device: str | None = None
+ attachment_id: str | None = None
+
+
+class Volume(BaseModel):
+ id: str
+ name: str | None = None
+ status: str
+ size: int
+ volume_type: str | None = None
+ availability_zone: str | None = None
+ created_at: str
+ is_bootable: bool | None = None
+ is_encrypted: bool | None = None
+ description: str | None = None
+ attachments: list[VolumeAttachment] = []
diff --git a/src/openstack_mcp_server/tools/response/compute.py b/src/openstack_mcp_server/tools/response/compute.py
new file mode 100644
index 0000000..cc13d55
--- /dev/null
+++ b/src/openstack_mcp_server/tools/response/compute.py
@@ -0,0 +1,49 @@
+from pydantic import BaseModel, ConfigDict, Field
+
+
+class Server(BaseModel):
+ class Flavor(BaseModel):
+ id: str | None = Field(default=None, exclude=True)
+ name: str | None = Field(
+ default=None,
+ validation_alias="original_name",
+ )
+ model_config = ConfigDict(validate_by_name=True)
+
+ class Image(BaseModel):
+ id: str | None = Field(default=None)
+
+ class IPAddress(BaseModel):
+ addr: str
+ version: int
+ type: str = Field(validation_alias="OS-EXT-IPS:type")
+
+ model_config = ConfigDict(validate_by_name=True)
+
+ class SecurityGroup(BaseModel):
+ name: str
+
+ id: str
+ name: str
+ hostname: str | None = None
+ description: str | None = None
+ status: str | None = None
+ flavor: Flavor | None = None
+ image: Image | None = None
+ addresses: dict[str, list[IPAddress]] | None = None
+ key_name: str | None = None
+ security_groups: list[SecurityGroup] | None = None
+ accessIPv4: str | None = None
+ accessIPv6: str | None = None
+
+
+class Flavor(BaseModel):
+ id: str
+ name: str
+ vcpus: int
+ ram: int
+ disk: int
+ swap: int | None = None
+ is_public: bool = Field(validation_alias="os-flavor-access:is_public")
+
+ model_config = ConfigDict(validate_by_name=True)
diff --git a/src/openstack_mcp_server/tools/response/identity.py b/src/openstack_mcp_server/tools/response/identity.py
new file mode 100644
index 0000000..527ff4d
--- /dev/null
+++ b/src/openstack_mcp_server/tools/response/identity.py
@@ -0,0 +1,15 @@
+from pydantic import BaseModel
+
+
+# NOTE: In openstacksdk, all of the fields are optional.
+# In this case, we are only using description field as optional.
+class Region(BaseModel):
+ id: str
+ description: str | None = None
+
+
+class Domain(BaseModel):
+ id: str
+ name: str
+ description: str | None = None
+ is_enabled: bool | None = None
diff --git a/src/openstack_mcp_server/tools/response/image.py b/src/openstack_mcp_server/tools/response/image.py
new file mode 100644
index 0000000..1726f25
--- /dev/null
+++ b/src/openstack_mcp_server/tools/response/image.py
@@ -0,0 +1,50 @@
+from pydantic import BaseModel, ConfigDict, Field
+
+
+class OwnerSpecified(BaseModel):
+ """Owner specified metadata for OpenStack images"""
+
+ openstack_object: str | None = Field(
+ default=None,
+ alias="owner_specified.openstack.object",
+ )
+ openstack_sha256: str | None = Field(
+ default=None,
+ alias="'owner_specified.openstack.sha256'",
+ )
+ openstack_md5: str | None = Field(
+ default=None,
+ alias="'owner_specified.openstack.md5'",
+ )
+
+ model_config = ConfigDict(validate_by_name=True)
+
+
+class Image(BaseModel):
+ """OpenStack Glance Image Pydantic Model"""
+
+ id: str
+ name: str | None = Field(default=None)
+ checksum: str | None = Field(default=None)
+ container_format: str | None = Field(default=None)
+ disk_format: str | None = Field(default=None)
+ file: str | None = Field(default=None)
+ min_disk: int | None = Field(default=None)
+ min_ram: int | None = Field(default=None)
+ os_hash_algo: str | None = Field(default=None)
+ os_hash_value: str | None = Field(default=None)
+ size: int | None = Field(default=None)
+ virtual_size: int | None = Field(default=None)
+ owner: str | None = Field(default=None)
+ visibility: str | None = Field(default=None)
+ hw_rng_model: str | None = Field(default=None)
+ status: str | None = Field(default=None)
+ schema_: str | None = Field(default=None, alias="schema")
+ protected: bool | None = Field(default=None)
+ os_hidden: bool | None = Field(default=None)
+ tags: list[str] | None = Field(default=None)
+ properties: OwnerSpecified | None = Field(default=None)
+ model_config = ConfigDict(validate_by_name=True)
+
+ created_at: str | None = Field(default=None)
+ updated_at: str | None = Field(default=None)
diff --git a/src/openstack_mcp_server/tools/response/network.py b/src/openstack_mcp_server/tools/response/network.py
new file mode 100644
index 0000000..6894bd5
--- /dev/null
+++ b/src/openstack_mcp_server/tools/response/network.py
@@ -0,0 +1,97 @@
+from pydantic import BaseModel
+
+
+class Network(BaseModel):
+ id: str
+ name: str
+ status: str
+ description: str | None = None
+ is_admin_state_up: bool = True
+ is_shared: bool = False
+ mtu: int | None = None
+ provider_network_type: str | None = None
+ provider_physical_network: str | None = None
+ provider_segmentation_id: int | None = None
+ project_id: str | None = None
+
+
+class Subnet(BaseModel):
+ id: str
+ name: str | None = None
+ status: str | None = None
+ description: str | None = None
+ project_id: str | None = None
+ network_id: str | None = None
+ cidr: str | None = None
+ ip_version: int | None = None
+ gateway_ip: str | None = None
+ is_dhcp_enabled: bool | None = None
+ allocation_pools: list[dict] | None = None
+ dns_nameservers: list[str] | None = None
+ host_routes: list[dict] | None = None
+
+
+class Port(BaseModel):
+ id: str
+ name: str | None = None
+ status: str | None = None
+ description: str | None = None
+ project_id: str | None = None
+ network_id: str | None = None
+ is_admin_state_up: bool | None = None
+ device_id: str | None = None
+ device_owner: str | None = None
+ mac_address: str | None = None
+ fixed_ips: list[dict] | None = None
+ security_group_ids: list[str] | None = None
+
+
+class Router(BaseModel):
+ id: str
+ name: str | None = None
+ status: str | None = None
+ description: str | None = None
+ project_id: str | None = None
+ is_admin_state_up: bool | None = None
+ external_gateway_info: dict | None = None
+ is_distributed: bool | None = None
+ is_ha: bool | None = None
+ routes: list[dict] | None = None
+
+
+class SecurityGroup(BaseModel):
+ id: str
+ name: str | None = None
+ status: str | None = None
+ description: str | None = None
+ project_id: str | None = None
+ security_group_rule_ids: list[str] | None = None
+
+
+class SecurityGroupRule(BaseModel):
+ id: str
+ name: str | None = None
+ status: str | None = None
+ description: str | None = None
+ project_id: str | None = None
+ direction: str | None = None
+ ethertype: str | None = None
+ protocol: str | None = None
+ port_range_min: int | None = None
+ port_range_max: int | None = None
+ remote_ip_prefix: str | None = None
+ remote_group_id: str | None = None
+ security_group_id: str | None = None
+
+
+class FloatingIP(BaseModel):
+ id: str
+ name: str | None = None
+ status: str | None = None
+ description: str | None = None
+ project_id: str | None = None
+ floating_ip_address: str | None = None
+ floating_network_id: str | None = None
+ fixed_ip_address: str | None = None
+ port_id: str | None = None
+ router_id: str | None = None
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..0979358
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,75 @@
+from unittest.mock import Mock, patch
+
+import pytest
+
+
+@pytest.fixture
+def mock_get_openstack_conn():
+ """Mock get_openstack_conn function for compute_tools."""
+ mock_conn = Mock()
+
+ with patch(
+ "openstack_mcp_server.tools.compute_tools.get_openstack_conn",
+ return_value=mock_conn,
+ ):
+ yield mock_conn
+
+
+@pytest.fixture
+def mock_get_openstack_conn_image():
+ """Mock get_openstack_conn function for image_tools."""
+ mock_conn = Mock()
+
+ with patch(
+ "openstack_mcp_server.tools.image_tools.get_openstack_conn",
+ return_value=mock_conn,
+ ):
+ yield mock_conn
+
+
+@pytest.fixture
+def mock_get_openstack_conn_identity():
+ """Mock get_openstack_conn function for identity_tools."""
+ mock_conn = Mock()
+
+ with patch(
+ "openstack_mcp_server.tools.identity_tools.get_openstack_conn",
+ return_value=mock_conn,
+ ):
+ yield mock_conn
+
+
+@pytest.fixture
+def mock_openstack_base():
+ """Mock base module functions."""
+ mock_conn = Mock()
+
+ with patch(
+ "openstack_mcp_server.tools.base.get_openstack_conn",
+ return_value=mock_conn,
+ ):
+ yield mock_conn
+
+
+@pytest.fixture
+def mock_openstack_connect_network():
+ """Mock get_openstack_conn function for network_tools."""
+ mock_conn = Mock()
+
+ with patch(
+ "openstack_mcp_server.tools.network_tools.get_openstack_conn",
+ return_value=mock_conn,
+ ):
+ yield mock_conn
+
+
+@pytest.fixture
+def mock_get_openstack_conn_block_storage():
+ """Mock get_openstack_conn function for block_storage_tools."""
+ mock_conn = Mock()
+
+ with patch(
+ "openstack_mcp_server.tools.block_storage_tools.get_openstack_conn",
+ return_value=mock_conn,
+ ):
+ yield mock_conn
diff --git a/tests/prompts/__init__.py b/tests/prompts/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/resources/__init__.py b/tests/resources/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/tools/__init__.py b/tests/tools/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/tools/test_block_storage_tools.py b/tests/tools/test_block_storage_tools.py
new file mode 100644
index 0000000..305f66e
--- /dev/null
+++ b/tests/tools/test_block_storage_tools.py
@@ -0,0 +1,685 @@
+from unittest.mock import Mock
+
+import pytest
+
+from openstack_mcp_server.tools.block_storage_tools import BlockStorageTools
+from openstack_mcp_server.tools.response.block_storage import (
+ Volume,
+ VolumeAttachment,
+)
+
+
+class TestBlockStorageTools:
+ """Test cases for BlockStorageTools class."""
+
+ def test_get_volumes_success(self, mock_get_openstack_conn_block_storage):
+ """Test getting volumes successfully."""
+ mock_conn = mock_get_openstack_conn_block_storage
+
+ # Create mock volume objects
+ mock_volume1 = Mock()
+ mock_volume1.name = "web-data-volume"
+ mock_volume1.id = "abc123-def456-ghi789"
+ mock_volume1.status = "available"
+ mock_volume1.size = 10
+ mock_volume1.volume_type = "ssd"
+ mock_volume1.availability_zone = "nova"
+ mock_volume1.created_at = "2024-01-01T12:00:00Z"
+ mock_volume1.is_bootable = False
+ mock_volume1.is_encrypted = False
+ mock_volume1.description = "Web data volume"
+ mock_volume1.attachments = []
+
+ mock_volume2 = Mock()
+ mock_volume2.name = "db-backup-volume"
+ mock_volume2.id = "xyz789-uvw456-rst123"
+ mock_volume2.status = "in-use"
+ mock_volume2.size = 20
+ mock_volume2.volume_type = "hdd"
+ mock_volume2.availability_zone = "nova"
+ mock_volume2.created_at = "2024-01-02T12:00:00Z"
+ mock_volume2.is_bootable = True
+ mock_volume2.is_encrypted = True
+ mock_volume2.description = "DB backup volume"
+ mock_volume2.attachments = []
+
+ # Configure mock block_storage.volumes()
+ mock_conn.block_storage.volumes.return_value = [
+ mock_volume1,
+ mock_volume2,
+ ]
+
+ # Test BlockStorageTools
+ block_storage_tools = BlockStorageTools()
+ result = block_storage_tools.get_volumes()
+
+ # Verify results
+ assert isinstance(result, list)
+ assert len(result) == 2
+ assert all(isinstance(vol, Volume) for vol in result)
+
+ # Check first volume
+ vol1 = result[0]
+ assert vol1.id == "abc123-def456-ghi789"
+ assert vol1.name == "web-data-volume"
+ assert vol1.status == "available"
+ assert vol1.size == 10
+
+ # Check second volume
+ vol2 = result[1]
+ assert vol2.id == "xyz789-uvw456-rst123"
+ assert vol2.name == "db-backup-volume"
+ assert vol2.status == "in-use"
+ assert vol2.size == 20
+
+ # Verify mock calls
+ mock_conn.block_storage.volumes.assert_called_once()
+
+ def test_get_volumes_empty_list(
+ self,
+ mock_get_openstack_conn_block_storage,
+ ):
+ """Test getting volumes when no volumes exist."""
+ mock_conn = mock_get_openstack_conn_block_storage
+
+ # Empty volume list
+ mock_conn.block_storage.volumes.return_value = []
+
+ block_storage_tools = BlockStorageTools()
+ result = block_storage_tools.get_volumes()
+
+ # Verify empty list
+ assert isinstance(result, list)
+ assert len(result) == 0
+
+ mock_conn.block_storage.volumes.assert_called_once()
+
+ def test_get_volumes_single_volume(
+ self,
+ mock_get_openstack_conn_block_storage,
+ ):
+ """Test getting volumes with a single volume."""
+ mock_conn = mock_get_openstack_conn_block_storage
+
+ # Single volume
+ mock_volume = Mock()
+ mock_volume.name = "test-volume"
+ mock_volume.id = "single-123"
+ mock_volume.status = "creating"
+ mock_volume.size = 5
+ mock_volume.volume_type = None
+ mock_volume.availability_zone = "nova"
+ mock_volume.created_at = "2024-01-01T12:00:00Z"
+ mock_volume.is_bootable = False
+ mock_volume.is_encrypted = False
+ mock_volume.description = None
+ mock_volume.attachments = []
+
+ mock_conn.block_storage.volumes.return_value = [mock_volume]
+
+ block_storage_tools = BlockStorageTools()
+ result = block_storage_tools.get_volumes()
+
+ assert isinstance(result, list)
+ assert len(result) == 1
+ assert result[0].name == "test-volume"
+ assert result[0].id == "single-123"
+ assert result[0].status == "creating"
+
+ mock_conn.block_storage.volumes.assert_called_once()
+
+ def test_get_volumes_multiple_statuses(
+ self,
+ mock_get_openstack_conn_block_storage,
+ ):
+ """Test volumes with various statuses."""
+ mock_conn = mock_get_openstack_conn_block_storage
+
+ # Volumes with different statuses
+ volumes_data = [
+ ("volume-available", "id-1", "available"),
+ ("volume-in-use", "id-2", "in-use"),
+ ("volume-error", "id-3", "error"),
+ ("volume-creating", "id-4", "creating"),
+ ("volume-deleting", "id-5", "deleting"),
+ ]
+
+ mock_volumes = []
+ for name, volume_id, status in volumes_data:
+ mock_volume = Mock()
+ mock_volume.name = name
+ mock_volume.id = volume_id
+ mock_volume.status = status
+ mock_volume.size = 10
+ mock_volume.volume_type = "standard"
+ mock_volume.availability_zone = "nova"
+ mock_volume.created_at = "2024-01-01T12:00:00Z"
+ mock_volume.is_bootable = False
+ mock_volume.is_encrypted = False
+ mock_volume.description = f"Description for {name}"
+ mock_volume.attachments = []
+ mock_volumes.append(mock_volume)
+
+ mock_conn.block_storage.volumes.return_value = mock_volumes
+
+ block_storage_tools = BlockStorageTools()
+ result = block_storage_tools.get_volumes()
+
+ # Verify result is a list with correct length
+ assert isinstance(result, list)
+ assert len(result) == 5
+
+ # Verify each volume is included in the result
+ result_by_id = {vol.id: vol for vol in result}
+ for name, volume_id, status in volumes_data:
+ assert volume_id in result_by_id
+ vol = result_by_id[volume_id]
+ assert vol.name == name
+ assert vol.status == status
+
+ mock_conn.block_storage.volumes.assert_called_once()
+
+ def test_get_volumes_with_special_characters(
+ self,
+ mock_get_openstack_conn_block_storage,
+ ):
+ """Test volumes with special characters in names."""
+ mock_conn = mock_get_openstack_conn_block_storage
+
+ # Volume names with special characters
+ mock_volume1 = Mock()
+ mock_volume1.name = "web-volume_test-01"
+ mock_volume1.id = "id-with-dashes"
+ mock_volume1.status = "available"
+ mock_volume1.size = 15
+ mock_volume1.volume_type = "ssd"
+ mock_volume1.availability_zone = "nova"
+ mock_volume1.created_at = "2024-01-01T12:00:00Z"
+ mock_volume1.is_bootable = False
+ mock_volume1.is_encrypted = False
+ mock_volume1.description = None
+ mock_volume1.attachments = []
+
+ mock_volume2 = Mock()
+ mock_volume2.name = "db.volume.prod"
+ mock_volume2.id = "id.with.dots"
+ mock_volume2.status = "in-use"
+ mock_volume2.size = 25
+ mock_volume2.volume_type = "hdd"
+ mock_volume2.availability_zone = "nova"
+ mock_volume2.created_at = "2024-01-02T12:00:00Z"
+ mock_volume2.is_bootable = True
+ mock_volume2.is_encrypted = True
+ mock_volume2.description = "Production DB volume"
+ mock_volume2.attachments = []
+
+ mock_conn.block_storage.volumes.return_value = [
+ mock_volume1,
+ mock_volume2,
+ ]
+
+ block_storage_tools = BlockStorageTools()
+ result = block_storage_tools.get_volumes()
+
+ assert isinstance(result, list)
+ assert len(result) == 2
+
+ # Find volumes by name
+ vol1 = next(vol for vol in result if vol.name == "web-volume_test-01")
+ vol2 = next(vol for vol in result if vol.name == "db.volume.prod")
+
+ assert vol1.id == "id-with-dashes"
+ assert vol1.status == "available"
+ assert vol2.id == "id.with.dots"
+ assert vol2.status == "in-use"
+
+ mock_conn.block_storage.volumes.assert_called_once()
+
+ def test_get_volume_details_success(
+ self,
+ mock_get_openstack_conn_block_storage,
+ ):
+ """Test getting volume details successfully."""
+ mock_conn = mock_get_openstack_conn_block_storage
+
+ # Create mock volume with detailed info
+ mock_volume = Mock()
+ mock_volume.name = "test-volume"
+ mock_volume.id = "vol-123"
+ mock_volume.status = "available"
+ mock_volume.size = 20
+ mock_volume.volume_type = "ssd"
+ mock_volume.availability_zone = "nova"
+ mock_volume.created_at = "2024-01-01T12:00:00Z"
+ mock_volume.is_bootable = False
+ mock_volume.is_encrypted = True
+ mock_volume.description = "Test volume description"
+ mock_volume.attachments = []
+
+ mock_conn.block_storage.get_volume.return_value = mock_volume
+
+ block_storage_tools = BlockStorageTools()
+ result = block_storage_tools.get_volume_details("vol-123")
+
+ # Verify result is a Volume object
+ assert isinstance(result, Volume)
+ assert result.name == "test-volume"
+ assert result.id == "vol-123"
+ assert result.status == "available"
+ assert result.size == 20
+ assert result.volume_type == "ssd"
+ assert result.availability_zone == "nova"
+ assert not result.is_bootable
+ assert result.is_encrypted
+ assert result.description == "Test volume description"
+ assert len(result.attachments) == 0
+
+ mock_conn.block_storage.get_volume.assert_called_once_with("vol-123")
+
+ def test_get_volume_details_with_attachments(
+ self,
+ mock_get_openstack_conn_block_storage,
+ ):
+ """Test getting volume details with attachments."""
+ mock_conn = mock_get_openstack_conn_block_storage
+
+ # Create mock volume with attachments
+ mock_volume = Mock()
+ mock_volume.name = "attached-volume"
+ mock_volume.id = "vol-attached"
+ mock_volume.status = "in-use"
+ mock_volume.size = 10
+ mock_volume.volume_type = None
+ mock_volume.availability_zone = "nova"
+ mock_volume.created_at = "2024-01-01T12:00:00Z"
+ mock_volume.is_bootable = True
+ mock_volume.is_encrypted = False
+ mock_volume.description = "Attached volume"
+ mock_volume.attachments = [
+ {
+ "server_id": "server-123",
+ "device": "/dev/vdb",
+ "id": "attach-1",
+ },
+ {
+ "server_id": "server-456",
+ "device": "/dev/vdc",
+ "id": "attach-2",
+ },
+ ]
+
+ mock_conn.block_storage.get_volume.return_value = mock_volume
+
+ block_storage_tools = BlockStorageTools()
+ result = block_storage_tools.get_volume_details("vol-attached")
+
+ # Verify result is a Volume object
+ assert isinstance(result, Volume)
+ assert result.name == "attached-volume"
+ assert result.status == "in-use"
+ assert len(result.attachments) == 2
+
+ # Verify attachment details
+ attach1 = result.attachments[0]
+ attach2 = result.attachments[1]
+
+ assert isinstance(attach1, VolumeAttachment)
+ assert attach1.server_id == "server-123"
+ assert attach1.device == "/dev/vdb"
+ assert attach1.attachment_id == "attach-1"
+
+ assert isinstance(attach2, VolumeAttachment)
+ assert attach2.server_id == "server-456"
+ assert attach2.device == "/dev/vdc"
+ assert attach2.attachment_id == "attach-2"
+
+ def test_get_volume_details_error(
+ self,
+ mock_get_openstack_conn_block_storage,
+ ):
+ """Test getting volume details with error."""
+ mock_conn = mock_get_openstack_conn_block_storage
+ mock_conn.block_storage.get_volume.side_effect = Exception(
+ "Volume not found",
+ )
+
+ block_storage_tools = BlockStorageTools()
+
+ # Should raise exception directly
+ with pytest.raises(Exception, match="Volume not found"):
+ block_storage_tools.get_volume_details("nonexistent-vol")
+
+ def test_create_volume_success(
+ self,
+ mock_get_openstack_conn_block_storage,
+ ):
+ """Test creating volume successfully."""
+ mock_conn = mock_get_openstack_conn_block_storage
+
+ # Mock created volume
+ mock_volume = Mock()
+ mock_volume.name = "new-volume"
+ mock_volume.id = "vol-new-123"
+ mock_volume.size = 10
+ mock_volume.status = "creating"
+ mock_volume.volume_type = "ssd"
+ mock_volume.availability_zone = "nova"
+ mock_volume.created_at = "2024-01-01T12:00:00Z"
+ mock_volume.is_bootable = False
+ mock_volume.is_encrypted = False
+ mock_volume.description = "Test volume"
+ mock_volume.attachments = []
+
+ mock_conn.block_storage.create_volume.return_value = mock_volume
+
+ block_storage_tools = BlockStorageTools()
+ result = block_storage_tools.create_volume(
+ "new-volume",
+ 10,
+ "Test volume",
+ "ssd",
+ "nova",
+ )
+
+ # Verify result is a Volume object
+ assert isinstance(result, Volume)
+ assert result.name == "new-volume"
+ assert result.id == "vol-new-123"
+ assert result.size == 10
+ assert result.status == "creating"
+ assert result.volume_type == "ssd"
+ assert result.availability_zone == "nova"
+
+ mock_conn.block_storage.create_volume.assert_called_once_with(
+ size=10,
+ image=None,
+ bootable=None,
+ name="new-volume",
+ description="Test volume",
+ volume_type="ssd",
+ availability_zone="nova",
+ )
+
+ def test_create_volume_minimal_params(
+ self,
+ mock_get_openstack_conn_block_storage,
+ ):
+ """Test creating volume with minimal parameters."""
+ mock_conn = mock_get_openstack_conn_block_storage
+
+ mock_volume = Mock()
+ mock_volume.name = "minimal-volume"
+ mock_volume.id = "vol-minimal"
+ mock_volume.size = 5
+ mock_volume.status = "creating"
+ mock_volume.volume_type = None
+ mock_volume.availability_zone = None
+ mock_volume.created_at = "2024-01-01T12:00:00Z"
+ mock_volume.is_bootable = False
+ mock_volume.is_encrypted = False
+ mock_volume.description = None
+ mock_volume.attachments = []
+
+ mock_conn.block_storage.create_volume.return_value = mock_volume
+
+ block_storage_tools = BlockStorageTools()
+ result = block_storage_tools.create_volume("minimal-volume", 5)
+
+ # Verify result structure
+ assert isinstance(result, Volume)
+ assert result.name == "minimal-volume"
+ assert result.size == 5
+
+ mock_conn.block_storage.create_volume.assert_called_once_with(
+ size=5,
+ image=None,
+ bootable=None,
+ name="minimal-volume",
+ )
+
+ def test_create_volume_with_image_and_bootable(
+ self,
+ mock_get_openstack_conn_block_storage,
+ ):
+ """Test creating volume with image and bootable parameters."""
+ mock_conn = mock_get_openstack_conn_block_storage
+
+ mock_volume = Mock()
+ mock_volume.name = "bootable-volume"
+ mock_volume.id = "vol-bootable"
+ mock_volume.size = 20
+ mock_volume.status = "creating"
+ mock_volume.volume_type = "ssd"
+ mock_volume.availability_zone = "nova"
+ mock_volume.created_at = "2024-01-01T12:00:00Z"
+ mock_volume.is_bootable = True
+ mock_volume.is_encrypted = False
+ mock_volume.description = "Bootable volume from image"
+ mock_volume.attachments = []
+
+ mock_conn.block_storage.create_volume.return_value = mock_volume
+
+ block_storage_tools = BlockStorageTools()
+ result = block_storage_tools.create_volume(
+ "bootable-volume",
+ 20,
+ "Bootable volume from image",
+ "ssd",
+ "nova",
+ True,
+ "ubuntu-20.04",
+ )
+
+ assert isinstance(result, Volume)
+ assert result.name == "bootable-volume"
+ assert result.id == "vol-bootable"
+ assert result.size == 20
+ assert result.is_bootable
+
+ mock_conn.block_storage.create_volume.assert_called_once_with(
+ size=20,
+ image="ubuntu-20.04",
+ bootable=True,
+ name="bootable-volume",
+ description="Bootable volume from image",
+ volume_type="ssd",
+ availability_zone="nova",
+ )
+
+ def test_create_volume_error(self, mock_get_openstack_conn_block_storage):
+ """Test creating volume with error."""
+ mock_conn = mock_get_openstack_conn_block_storage
+ mock_conn.block_storage.create_volume.side_effect = Exception(
+ "Quota exceeded",
+ )
+
+ block_storage_tools = BlockStorageTools()
+
+ with pytest.raises(Exception, match="Quota exceeded"):
+ block_storage_tools.create_volume("fail-volume", 100)
+
+ def test_delete_volume_success(
+ self,
+ mock_get_openstack_conn_block_storage,
+ ):
+ """Test deleting volume successfully."""
+ mock_conn = mock_get_openstack_conn_block_storage
+
+ # Mock volume to be deleted
+ mock_volume = Mock()
+ mock_volume.name = "delete-me"
+ mock_volume.id = "vol-delete"
+
+ mock_conn.block_storage.get_volume.return_value = mock_volume
+
+ block_storage_tools = BlockStorageTools()
+ result = block_storage_tools.delete_volume("vol-delete", False)
+
+ # Verify result is None
+ assert result is None
+ mock_conn.block_storage.delete_volume.assert_called_once_with(
+ "vol-delete",
+ force=False,
+ ignore_missing=False,
+ )
+
+ def test_delete_volume_force(self, mock_get_openstack_conn_block_storage):
+ """Test force deleting volume."""
+ mock_conn = mock_get_openstack_conn_block_storage
+
+ mock_volume = Mock()
+ mock_volume.name = None # Test unnamed volume
+ mock_volume.id = "vol-force-delete"
+
+ mock_conn.block_storage.get_volume.return_value = mock_volume
+
+ block_storage_tools = BlockStorageTools()
+ result = block_storage_tools.delete_volume("vol-force-delete", True)
+
+ # Verify result is None
+ assert result is None
+
+ mock_conn.block_storage.delete_volume.assert_called_once_with(
+ "vol-force-delete",
+ force=True,
+ ignore_missing=False,
+ )
+
+ def test_delete_volume_error(self, mock_get_openstack_conn_block_storage):
+ """Test deleting volume with error."""
+ mock_conn = mock_get_openstack_conn_block_storage
+ mock_conn.block_storage.delete_volume.side_effect = Exception(
+ "Volume not found",
+ )
+
+ block_storage_tools = BlockStorageTools()
+
+ # Should raise exception directly
+ with pytest.raises(Exception, match="Volume not found"):
+ block_storage_tools.delete_volume("nonexistent-vol")
+
+ def test_extend_volume_success(
+ self,
+ mock_get_openstack_conn_block_storage,
+ ):
+ """Test extending volume successfully."""
+ mock_conn = mock_get_openstack_conn_block_storage
+
+ block_storage_tools = BlockStorageTools()
+ result = block_storage_tools.extend_volume("vol-extend", 20)
+
+ # Verify result is None
+ assert result is None
+
+ mock_conn.block_storage.extend_volume.assert_called_once_with(
+ "vol-extend",
+ 20,
+ )
+
+ def test_extend_volume_invalid_size(
+ self,
+ mock_get_openstack_conn_block_storage,
+ ):
+ """Test extending volume with invalid size."""
+ mock_conn = mock_get_openstack_conn_block_storage
+ mock_conn.block_storage.extend_volume.side_effect = Exception(
+ "Invalid size",
+ )
+
+ block_storage_tools = BlockStorageTools()
+
+ with pytest.raises(Exception, match="Invalid size"):
+ block_storage_tools.extend_volume("vol-extend", 15)
+
+ def test_extend_volume_error(self, mock_get_openstack_conn_block_storage):
+ """Test extending volume with error."""
+ mock_conn = mock_get_openstack_conn_block_storage
+ mock_conn.block_storage.extend_volume.side_effect = Exception(
+ "Volume busy",
+ )
+
+ block_storage_tools = BlockStorageTools()
+
+ with pytest.raises(Exception, match="Volume busy"):
+ block_storage_tools.extend_volume("vol-busy", 30)
+
+ def test_register_tools(self):
+ """Test that tools are properly registered with FastMCP."""
+ # Create FastMCP mock
+ mock_mcp = Mock()
+ mock_tool_decorator = Mock()
+ mock_mcp.tool.return_value = mock_tool_decorator
+
+ block_storage_tools = BlockStorageTools()
+ block_storage_tools.register_tools(mock_mcp)
+
+ # Verify mcp.tool() was called for each method
+ assert mock_mcp.tool.call_count == 5
+
+ # Verify all methods were registered
+ registered_methods = [
+ call[0][0] for call in mock_tool_decorator.call_args_list
+ ]
+ expected_methods = [
+ block_storage_tools.get_volumes,
+ block_storage_tools.get_volume_details,
+ block_storage_tools.create_volume,
+ block_storage_tools.delete_volume,
+ block_storage_tools.extend_volume,
+ ]
+
+ for method in expected_methods:
+ assert method in registered_methods
+
+ def test_block_storage_tools_instantiation(self):
+ """Test BlockStorageTools can be instantiated."""
+ block_storage_tools = BlockStorageTools()
+ assert block_storage_tools is not None
+ assert hasattr(block_storage_tools, "register_tools")
+ assert hasattr(block_storage_tools, "get_volumes")
+ assert hasattr(block_storage_tools, "get_volume_details")
+ assert hasattr(block_storage_tools, "create_volume")
+ assert hasattr(block_storage_tools, "delete_volume")
+ assert hasattr(block_storage_tools, "extend_volume")
+ # Verify all methods are callable
+ assert callable(block_storage_tools.register_tools)
+ assert callable(block_storage_tools.get_volumes)
+ assert callable(block_storage_tools.get_volume_details)
+ assert callable(block_storage_tools.create_volume)
+ assert callable(block_storage_tools.delete_volume)
+ assert callable(block_storage_tools.extend_volume)
+
+ def test_get_volumes_docstring(self):
+ """Test that get_volumes has proper docstring."""
+ block_storage_tools = BlockStorageTools()
+ docstring = block_storage_tools.get_volumes.__doc__
+
+ assert docstring is not None
+ assert "Get the list of Block Storage volumes" in docstring
+ assert "return" in docstring.lower() or "Return" in docstring
+ assert (
+ "list[Volume]" in docstring
+ or "A list of Volume objects" in docstring
+ )
+
+ def test_all_block_storage_methods_have_docstrings(self):
+ """Test that all public BlockStorageTools methods have proper docstrings."""
+ block_storage_tools = BlockStorageTools()
+
+ methods_to_check = [
+ "get_volumes",
+ "get_volume_details",
+ "create_volume",
+ "delete_volume",
+ "extend_volume",
+ ]
+
+ for method_name in methods_to_check:
+ method = getattr(block_storage_tools, method_name)
+ docstring = method.__doc__
+ assert docstring is not None, (
+ f"{method_name} should have a docstring"
+ )
+ assert len(docstring.strip()) > 0, (
+ f"{method_name} docstring should not be empty"
+ )
diff --git a/tests/tools/test_compute_tools.py b/tests/tools/test_compute_tools.py
new file mode 100644
index 0000000..5c78435
--- /dev/null
+++ b/tests/tools/test_compute_tools.py
@@ -0,0 +1,557 @@
+from unittest.mock import Mock, call
+
+import pytest
+
+from openstack.exceptions import ConflictException, NotFoundException
+
+from openstack_mcp_server.tools.compute_tools import ComputeTools
+from openstack_mcp_server.tools.response.compute import Flavor, Server
+
+
+class TestComputeTools:
+ """Test cases for ComputeTools class."""
+
+ def test_get_servers_success(self, mock_get_openstack_conn):
+ """Test getting servers successfully."""
+ mock_conn = mock_get_openstack_conn
+
+ # Create mock server objects
+ mock_server1 = {
+ "name": "web-server-01",
+ "id": "434eb822-3fbd-44a1-a000-3b511ac9b516",
+ "status": "ACTIVE",
+ "flavor": {
+ "original_name": "m1.tiny",
+ "vcpus": 1,
+ "ram": 512,
+ "disk": 1,
+ },
+ "image": {"id": "de527f30-d078-41f4-8f18-a23bf2d39366"},
+ "addresses": {
+ "private": [
+ {
+ "addr": "192.168.1.10",
+ "version": 4,
+ "OS-EXT-IPS:type": "fixed",
+ },
+ ],
+ },
+ "key_name": "my-key",
+ "security_groups": [{"name": "default"}],
+ }
+
+ mock_server2 = {
+ "name": "db-server-01",
+ "id": "ffd071fe-1334-45f6-8894-5b0bcac262a6",
+ "status": "SHUTOFF",
+ "flavor": {
+ "original_name": "m1.small",
+ "vcpus": 2,
+ "ram": 2048,
+ "disk": 20,
+ },
+ "image": {"id": "3d897e0e-4117-46bb-ae77-e734bb16a1ca"},
+ "addresses": {
+ "net1": [
+ {
+ "addr": "192.168.1.11",
+ "version": 4,
+ "OS-EXT-IPS:type": "fixed",
+ },
+ ],
+ },
+ "key_name": None,
+ "security_groups": [{"name": "default"}, {"name": "group1"}],
+ }
+
+ # Configure mock compute.servers()
+ mock_conn.compute.servers.return_value = [mock_server1, mock_server2]
+
+ # Test ComputeTools
+ compute_tools = ComputeTools()
+ result = compute_tools.get_servers()
+
+ # Verify results
+ expected_output = [
+ Server(
+ id="434eb822-3fbd-44a1-a000-3b511ac9b516",
+ name="web-server-01",
+ status="ACTIVE",
+ flavor=Server.Flavor(id=None, name="m1.tiny"),
+ image=Server.Image(id="de527f30-d078-41f4-8f18-a23bf2d39366"),
+ addresses={
+ "private": [
+ Server.IPAddress(
+ addr="192.168.1.10",
+ version=4,
+ type="fixed",
+ ),
+ ],
+ },
+ key_name="my-key",
+ security_groups=[Server.SecurityGroup(name="default")],
+ ),
+ Server(
+ id="ffd071fe-1334-45f6-8894-5b0bcac262a6",
+ name="db-server-01",
+ status="SHUTOFF",
+ flavor=Server.Flavor(id=None, name="m1.small"),
+ image=Server.Image(id="3d897e0e-4117-46bb-ae77-e734bb16a1ca"),
+ addresses={
+ "net1": [
+ Server.IPAddress(
+ addr="192.168.1.11",
+ version=4,
+ type="fixed",
+ ),
+ ],
+ },
+ key_name=None,
+ security_groups=[
+ Server.SecurityGroup(name="default"),
+ Server.SecurityGroup(name="group1"),
+ ],
+ ),
+ ]
+ assert result == expected_output
+
+ # Verify mock calls
+ mock_conn.compute.servers.assert_called_once()
+
+ def test_get_servers_empty_list(self, mock_get_openstack_conn):
+ """Test getting servers when no servers exist."""
+ mock_conn = mock_get_openstack_conn
+
+ # Empty server list
+ mock_conn.compute.servers.return_value = []
+
+ compute_tools = ComputeTools()
+ result = compute_tools.get_servers()
+
+ # Verify empty list
+ assert result == []
+
+ mock_conn.compute.servers.assert_called_once()
+
+ def test_get_server_success(self, mock_get_openstack_conn):
+ """Test getting a specific server successfully."""
+ mock_conn = mock_get_openstack_conn
+
+ # Create mock server object
+ mock_server = {
+ "name": "test-server",
+ "id": "fe4b6b9b-090c-4dee-ab27-5155476e8e7d",
+ "status": "ACTIVE",
+ }
+
+ mock_conn.compute.get_server.return_value = mock_server
+
+ compute_tools = ComputeTools()
+ result = compute_tools.get_server(
+ "fe4b6b9b-090c-4dee-ab27-5155476e8e7d",
+ )
+
+ expected_output = Server(
+ name="test-server",
+ id="fe4b6b9b-090c-4dee-ab27-5155476e8e7d",
+ status="ACTIVE",
+ )
+ assert result == expected_output
+ mock_conn.compute.get_server.assert_called_once_with(
+ "fe4b6b9b-090c-4dee-ab27-5155476e8e7d",
+ )
+
+ def test_create_server_success(self, mock_get_openstack_conn):
+ """Test creating a server successfully."""
+ mock_conn = mock_get_openstack_conn
+
+ # Mock the create and get operations
+ mock_create_response = Mock()
+ mock_create_response.id = "5f4ce035-79a3-4feb-a011-9c256789f380"
+
+ mock_server = {
+ "name": "new-server",
+ "id": mock_create_response.id,
+ "status": "BUILDING",
+ }
+
+ mock_conn.compute.create_server.return_value = mock_create_response
+ mock_conn.compute.get_server.return_value = mock_server
+
+ compute_tools = ComputeTools()
+ result = compute_tools.create_server(
+ name="new-server",
+ image="a6c3a174-b3d1-4019-8023-fef9518fbaff",
+ flavor=1,
+ network="49173e57-f96e-474b-b36b-2f3f432ef7aa",
+ )
+
+ expected_output = Server(
+ name="new-server",
+ id="5f4ce035-79a3-4feb-a011-9c256789f380",
+ status="BUILDING",
+ )
+ assert result == expected_output
+
+ expected_params = {
+ "name": "new-server",
+ "flavorRef": 1,
+ "imageRef": "a6c3a174-b3d1-4019-8023-fef9518fbaff",
+ "networks": [{"uuid": "49173e57-f96e-474b-b36b-2f3f432ef7aa"}],
+ }
+ mock_conn.compute.create_server.assert_called_once_with(
+ **expected_params,
+ )
+ mock_conn.compute.get_server.assert_called_once_with(
+ mock_create_response.id,
+ )
+
+ def test_create_server_with_optional_params(self, mock_get_openstack_conn):
+ """Test creating a server with optional parameters."""
+ mock_conn = mock_get_openstack_conn
+
+ mock_create_response = Mock()
+ mock_create_response.id = "b6bcd30f-f150-4751-998e-fd7349f50160"
+
+ mock_server = {
+ "name": "server-with-options",
+ "id": mock_create_response.id,
+ "status": "BUILDING",
+ }
+
+ mock_conn.compute.create_server.return_value = mock_create_response
+ mock_conn.compute.get_server.return_value = mock_server
+
+ compute_tools = ComputeTools()
+ compute_tools.create_server(
+ name="server-with-options",
+ image="a6c3a174-b3d1-4019-8023-fef9518fbaff",
+ flavor=2,
+ network="49173e57-f96e-474b-b36b-2f3f432ef7aa",
+ key_name="my-key",
+ security_groups=["default", "web"],
+ user_data="#!/bin/bash\necho 'Hello World'",
+ )
+
+ expected_params = {
+ "name": "server-with-options",
+ "flavorRef": 2,
+ "imageRef": "a6c3a174-b3d1-4019-8023-fef9518fbaff",
+ "networks": [{"uuid": "49173e57-f96e-474b-b36b-2f3f432ef7aa"}],
+ "key_name": "my-key",
+ "security_groups": ["default", "web"],
+ "user_data": "#!/bin/bash\necho 'Hello World'",
+ }
+ mock_conn.compute.create_server.assert_called_once_with(
+ **expected_params,
+ )
+ mock_conn.compute.get_server.assert_called_once_with(
+ mock_create_response.id,
+ )
+
+ def test_register_tools(self):
+ """Test that tools are properly registered with FastMCP."""
+ # Create FastMCP mock
+ mock_mcp = Mock()
+ mock_tool_decorator = Mock()
+ mock_mcp.tool.return_value = mock_tool_decorator
+
+ compute_tools = ComputeTools()
+ compute_tools.register_tools(mock_mcp)
+
+ mock_tool_decorator.assert_has_calls(
+ [
+ call(compute_tools.get_servers),
+ call(compute_tools.get_server),
+ call(compute_tools.create_server),
+ call(compute_tools.get_flavors),
+ call(compute_tools.action_server),
+ call(compute_tools.update_server),
+ call(compute_tools.delete_server),
+ ],
+ )
+ assert mock_tool_decorator.call_count == 7
+
+ def test_compute_tools_instantiation(self):
+ """Test ComputeTools can be instantiated."""
+ compute_tools = ComputeTools()
+ assert compute_tools is not None
+ assert hasattr(compute_tools, "register_tools")
+ assert hasattr(compute_tools, "get_servers")
+ assert callable(compute_tools.register_tools)
+ assert callable(compute_tools.get_servers)
+
+ def test_get_servers_docstring(self):
+ """Test that get_servers has proper docstring."""
+ compute_tools = ComputeTools()
+ docstring = compute_tools.get_servers.__doc__
+
+ assert docstring is not None
+ assert "Get the list of Compute servers" in docstring
+ assert "return" in docstring.lower() or "Return" in docstring
+
+ def test_get_flavors_success(self, mock_get_openstack_conn):
+ """Test getting flavors successfully."""
+ mock_conn = mock_get_openstack_conn
+
+ # Create mock flavor objects
+ mock_flavor1 = {
+ "id": "1",
+ "name": "m1.tiny",
+ "vcpus": 1,
+ "ram": 512,
+ "disk": 1,
+ "swap": 0,
+ "os-flavor-access:is_public": True,
+ }
+
+ mock_flavor2 = {
+ "id": "2",
+ "name": "m1.small",
+ "vcpus": 2,
+ "ram": 2048,
+ "disk": 20,
+ "swap": 0,
+ "os-flavor-access:is_public": True,
+ }
+
+ mock_conn.compute.flavors.return_value = [mock_flavor1, mock_flavor2]
+
+ compute_tools = ComputeTools()
+ result = compute_tools.get_flavors()
+
+ expected_output = [
+ Flavor(
+ id="1",
+ name="m1.tiny",
+ vcpus=1,
+ ram=512,
+ disk=1,
+ swap=0,
+ is_public=True,
+ ),
+ Flavor(
+ id="2",
+ name="m1.small",
+ vcpus=2,
+ ram=2048,
+ disk=20,
+ swap=0,
+ is_public=True,
+ ),
+ ]
+ assert result == expected_output
+ mock_conn.compute.flavors.assert_called_once()
+
+ def test_get_flavors_empty_list(self, mock_get_openstack_conn):
+ """Test getting flavors when no flavors exist."""
+ mock_conn = mock_get_openstack_conn
+ mock_conn.compute.flavors.return_value = []
+
+ compute_tools = ComputeTools()
+ result = compute_tools.get_flavors()
+
+ assert result == []
+ mock_conn.compute.flavors.assert_called_once()
+
+ @pytest.mark.parametrize(
+ "action",
+ [
+ "pause",
+ "unpause",
+ "suspend",
+ "resume",
+ "lock",
+ "unlock",
+ "rescue",
+ "unrescue",
+ "start",
+ "stop",
+ "shelve",
+ "shelve_offload",
+ "unshelve",
+ ],
+ )
+ def test_action_server_success(self, mock_get_openstack_conn, action):
+ """Test action_server with all supported actions."""
+ mock_conn = mock_get_openstack_conn
+ server_id = "test-server-id"
+
+ # Mock the action method to avoid calling actual methods
+ action_method = getattr(mock_conn.compute, f"{action}_server")
+ action_method.return_value = None
+
+ compute_tools = ComputeTools()
+ result = compute_tools.action_server(server_id, action)
+
+ # Verify the result is None (void function)
+ assert result is None
+
+ # Verify the correct method was called with server ID
+ action_method.assert_called_once_with(server_id)
+
+ def test_action_server_unsupported_action(self, mock_get_openstack_conn):
+ """Test action_server with unsupported action raises ValueError."""
+ server_id = "test-server-id"
+ unsupported_action = "invalid_action"
+
+ compute_tools = ComputeTools()
+
+ with pytest.raises(
+ ValueError,
+ match=f"Unsupported action: {unsupported_action}",
+ ):
+ compute_tools.action_server(server_id, unsupported_action)
+
+ def test_action_server_not_found(self, mock_get_openstack_conn):
+ """Test action_server when server does not exist."""
+ mock_conn = mock_get_openstack_conn
+ server_id = "non-existent-server-id"
+ action = "pause"
+
+ # Mock the action method to raise NotFoundException
+ mock_conn.compute.pause_server.side_effect = NotFoundException()
+
+ compute_tools = ComputeTools()
+
+ with pytest.raises(NotFoundException):
+ compute_tools.action_server(server_id, action)
+
+ mock_conn.compute.pause_server.assert_called_once_with(server_id)
+
+ def test_action_server_conflict_exception(self, mock_get_openstack_conn):
+ """Test action_server when action cannot be performed due to Conflict Exception."""
+ mock_conn = mock_get_openstack_conn
+ server_id = "test-server-id"
+ action = "start"
+
+ # Mock the action method to raise ConflictException
+ mock_conn.compute.start_server.side_effect = ConflictException()
+
+ compute_tools = ComputeTools()
+
+ with pytest.raises(ConflictException):
+ compute_tools.action_server(server_id, action)
+
+ mock_conn.compute.start_server.assert_called_once_with(server_id)
+
+ def test_update_server_success(self, mock_get_openstack_conn):
+ """Test updating a server successfully with all parameters."""
+ mock_conn = mock_get_openstack_conn
+ server_id = "test-server-id"
+
+ mock_server = {
+ "name": "updated-server",
+ "id": server_id,
+ "status": "ACTIVE",
+ "hostname": "updated-hostname",
+ "description": "Updated server description",
+ "accessIPv4": "192.168.1.100",
+ "accessIPv6": "2001:db8::1",
+ }
+
+ mock_conn.compute.update_server.return_value = mock_server
+
+ compute_tools = ComputeTools()
+ server_params = mock_server.copy()
+ server_params.pop("status")
+ result = compute_tools.update_server(**server_params)
+
+ expected_output = Server(**mock_server)
+ assert result == expected_output
+
+ expected_params = {
+ "accessIPv4": "192.168.1.100",
+ "accessIPv6": "2001:db8::1",
+ "name": "updated-server",
+ "hostname": "updated-hostname",
+ "description": "Updated server description",
+ }
+ mock_conn.compute.update_server.assert_called_once_with(
+ server_id, **expected_params
+ )
+
+ @pytest.mark.parametrize(
+ "params",
+ [
+ {"param_key": "name", "value": "new-name"},
+ {"param_key": "hostname", "value": "new-hostname"},
+ {"param_key": "description", "value": "New description"},
+ {"param_key": "accessIPv4", "value": "192.168.1.100"},
+ {"param_key": "accessIPv6", "value": "2001:db8::1"},
+ ],
+ )
+ def test_update_server_optional_params(
+ self, mock_get_openstack_conn, params
+ ):
+ """Test updating a server with optional parameters."""
+ mock_conn = mock_get_openstack_conn
+ server_id = "test-server-id"
+
+ mock_server = {
+ "id": server_id,
+ "name": "original-name",
+ "description": "Original description",
+ "hostname": "original-hostname",
+ "accessIPv4": "1.1.1.1",
+ "accessIPv6": "::",
+ "status": "ACTIVE",
+ **{params["param_key"]: params["value"]},
+ }
+
+ mock_conn.compute.update_server.return_value = mock_server
+
+ compute_tools = ComputeTools()
+ result = compute_tools.update_server(
+ id=server_id,
+ **{params["param_key"]: params["value"]},
+ )
+ assert result == Server(**mock_server)
+
+ expected_params = {params["param_key"]: params["value"]}
+ mock_conn.compute.update_server.assert_called_once_with(
+ server_id, **expected_params
+ )
+
+ def test_update_server_not_found(self, mock_get_openstack_conn):
+ """Test updating a server that does not exist."""
+ mock_conn = mock_get_openstack_conn
+ server_id = "non-existent-server-id"
+
+ # Mock the update_server method to raise NotFoundException
+ mock_conn.compute.update_server.side_effect = NotFoundException()
+
+ compute_tools = ComputeTools()
+
+ with pytest.raises(NotFoundException):
+ compute_tools.update_server(id=server_id)
+
+ mock_conn.compute.update_server.assert_called_once_with(server_id)
+
+ def test_delete_server_success(self, mock_get_openstack_conn):
+ """Test deleting a server successfully."""
+ mock_conn = mock_get_openstack_conn
+ server_id = "test-server-id"
+
+ mock_conn.compute.delete_server.return_value = None
+
+ compute_tools = ComputeTools()
+ result = compute_tools.delete_server(server_id)
+
+ assert result is None
+ mock_conn.compute.delete_server.assert_called_once_with(server_id)
+
+ def test_delete_server_not_found(self, mock_get_openstack_conn):
+ """Test deleting a server that does not exist."""
+ mock_conn = mock_get_openstack_conn
+ server_id = "non-existent-server-id"
+
+ # Mock the delete_server method to raise NotFoundException
+ mock_conn.compute.delete_server.side_effect = NotFoundException()
+
+ compute_tools = ComputeTools()
+
+ with pytest.raises(NotFoundException):
+ compute_tools.delete_server(server_id)
+
+ mock_conn.compute.delete_server.assert_called_once_with(server_id)
diff --git a/tests/tools/test_identity_tools.py b/tests/tools/test_identity_tools.py
new file mode 100644
index 0000000..47965bc
--- /dev/null
+++ b/tests/tools/test_identity_tools.py
@@ -0,0 +1,717 @@
+from unittest.mock import Mock
+
+import pydantic
+import pytest
+
+from openstack import exceptions
+
+from openstack_mcp_server.tools.identity_tools import IdentityTools
+from openstack_mcp_server.tools.response.identity import Domain, Region
+
+
+class TestIdentityTools:
+ """Test cases for IdentityTools class."""
+
+ def get_identity_tools(self) -> IdentityTools:
+ """Get an instance of IdentityTools."""
+ return IdentityTools()
+
+ def test_get_regions_success(self, mock_get_openstack_conn_identity):
+ """Test getting identity regions successfully."""
+ mock_conn = mock_get_openstack_conn_identity
+
+ # Create mock region objects
+ mock_region1 = Mock()
+ mock_region1.id = "RegionOne"
+ mock_region1.description = "Region One description"
+
+ mock_region2 = Mock()
+ mock_region2.id = "RegionTwo"
+ mock_region2.description = "Region Two description"
+
+ # Configure mock region.regions()
+ mock_conn.identity.regions.return_value = [mock_region1, mock_region2]
+
+ # Test get_regions()
+ identity_tools = self.get_identity_tools()
+ result = identity_tools.get_regions()
+
+ # Verify results
+ assert result == [
+ Region(id="RegionOne", description="Region One description"),
+ Region(id="RegionTwo", description="Region Two description"),
+ ]
+
+ # Verify mock calls
+ mock_conn.identity.regions.assert_called_once()
+
+ def test_get_regions_empty_list(self, mock_get_openstack_conn_identity):
+ """Test getting identity regions when there are no regions."""
+ mock_conn = mock_get_openstack_conn_identity
+
+ # Empty region list
+ mock_conn.identity.regions.return_value = []
+
+ # Test get_regions()
+ identity_tools = self.get_identity_tools()
+ result = identity_tools.get_regions()
+
+ # Verify results
+ assert result == []
+
+ # Verify mock calls
+ mock_conn.identity.regions.assert_called_once()
+
+ def test_create_region_success(self, mock_get_openstack_conn_identity):
+ """Test creating a identity region successfully."""
+ mock_conn = mock_get_openstack_conn_identity
+
+ # Create mock region object
+ mock_region = Mock()
+ mock_region.id = "RegionOne"
+ mock_region.description = "Region One description"
+
+ # Configure mock region.create_region()
+ mock_conn.identity.create_region.return_value = mock_region
+
+ # Test create_region()
+ identity_tools = self.get_identity_tools()
+ result = identity_tools.create_region(
+ id="RegionOne",
+ description="Region One description",
+ )
+
+ # Verify results
+ assert result == Region(
+ id="RegionOne",
+ description="Region One description",
+ )
+
+ # Verify mock calls
+ mock_conn.identity.create_region.assert_called_once_with(
+ id="RegionOne",
+ description="Region One description",
+ )
+
+ def test_create_region_without_description(
+ self,
+ mock_get_openstack_conn_identity,
+ ):
+ """Test creating a identity region without a description."""
+ mock_conn = mock_get_openstack_conn_identity
+
+ # Create mock region object
+ mock_region = Mock()
+ mock_region.id = "RegionOne"
+ mock_region.description = None
+
+ # Configure mock region.create_region()
+ mock_conn.identity.create_region.return_value = mock_region
+
+ # Test create_region()
+ identity_tools = self.get_identity_tools()
+ result = identity_tools.create_region(id="RegionOne")
+
+ # Verify results
+ assert result == Region(id="RegionOne")
+
+ def test_create_region_invalid_id_format(
+ self,
+ mock_get_openstack_conn_identity,
+ ):
+ """Test creating a identity region with an invalid ID format."""
+ mock_conn = mock_get_openstack_conn_identity
+
+ # Configure mock region.create_region() to raise an exception
+ mock_conn.identity.create_region.side_effect = (
+ exceptions.BadRequestException(
+ "Invalid input for field 'id': Expected string, got integer",
+ )
+ )
+
+ # Test create_region()
+ identity_tools = self.get_identity_tools()
+
+ # Verify results
+ with pytest.raises(
+ exceptions.BadRequestException,
+ match="Invalid input for field 'id': Expected string, got integer",
+ ):
+ identity_tools.create_region(
+ id=1,
+ description="Region One description",
+ )
+
+ # Verify mock calls
+ mock_conn.identity.create_region.assert_called_once_with(
+ id=1,
+ description="Region One description",
+ )
+
+ def test_delete_region_success(self, mock_get_openstack_conn_identity):
+ """Test deleting a identity region successfully."""
+ mock_conn = mock_get_openstack_conn_identity
+
+ # Test delete_region()
+ identity_tools = self.get_identity_tools()
+ result = identity_tools.delete_region(id="RegionOne")
+
+ # Verify results
+ assert result is None
+
+ # Verify mock calls
+ mock_conn.identity.delete_region.assert_called_once_with(
+ region="RegionOne",
+ ignore_missing=False,
+ )
+
+ def test_delete_region_not_found(self, mock_get_openstack_conn_identity):
+ """Test deleting a identity region that does not exist."""
+ mock_conn = mock_get_openstack_conn_identity
+
+ # Configure mock to raise NotFoundException
+ mock_conn.identity.delete_region.side_effect = (
+ exceptions.NotFoundException(
+ "Region 'RegionOne' not found",
+ )
+ )
+
+ # Test delete_region()
+ identity_tools = self.get_identity_tools()
+
+ # Verify exception is raised
+ with pytest.raises(
+ exceptions.NotFoundException,
+ match="Region 'RegionOne' not found",
+ ):
+ identity_tools.delete_region(id="RegionOne")
+
+ # Verify mock calls
+ mock_conn.identity.delete_region.assert_called_once_with(
+ region="RegionOne",
+ ignore_missing=False,
+ )
+
+ def test_update_region_success(self, mock_get_openstack_conn_identity):
+ """Test updating a identity region successfully."""
+ mock_conn = mock_get_openstack_conn_identity
+
+ # Create mock region object
+ mock_region = Mock()
+ mock_region.id = "RegionOne"
+ mock_region.description = "Region One description"
+
+ # Configure mock region.update_region()
+ mock_conn.identity.update_region.return_value = mock_region
+
+ # Test update_region()
+ identity_tools = self.get_identity_tools()
+ result = identity_tools.update_region(
+ id="RegionOne",
+ description="Region One description",
+ )
+
+ # Verify results
+ assert result == Region(
+ id="RegionOne",
+ description="Region One description",
+ )
+
+ # Verify mock calls
+ mock_conn.identity.update_region.assert_called_once_with(
+ region="RegionOne",
+ description="Region One description",
+ )
+
+ def test_update_region_without_description(
+ self,
+ mock_get_openstack_conn_identity,
+ ):
+ """Test updating a identity region without a description."""
+ mock_conn = mock_get_openstack_conn_identity
+
+ # Create mock region object
+ mock_region = Mock()
+ mock_region.id = "RegionOne"
+ mock_region.description = None
+
+ # Configure mock region.update_region()
+ mock_conn.identity.update_region.return_value = mock_region
+
+ # Test update_region()
+ identity_tools = self.get_identity_tools()
+ result = identity_tools.update_region(id="RegionOne")
+
+ # Verify results
+ assert result == Region(id="RegionOne")
+
+ # Verify mock calls
+ mock_conn.identity.update_region.assert_called_once_with(
+ region="RegionOne",
+ description=None,
+ )
+
+ def test_update_region_invalid_id_format(
+ self,
+ mock_get_openstack_conn_identity,
+ ):
+ """Test updating a identity region with an invalid ID format."""
+ mock_conn = mock_get_openstack_conn_identity
+
+ # Configure mock region.update_region() to raise an exception
+ mock_conn.identity.update_region.side_effect = (
+ exceptions.BadRequestException(
+ "Invalid input for field 'id': Expected string, got integer",
+ )
+ )
+
+ # Test update_region()
+ identity_tools = self.get_identity_tools()
+
+ # Verify exception is raised
+ with pytest.raises(
+ exceptions.BadRequestException,
+ match="Invalid input for field 'id': Expected string, got integer",
+ ):
+ identity_tools.update_region(
+ id=1,
+ description="Region One description",
+ )
+
+ # Verify mock calls
+ mock_conn.identity.update_region.assert_called_once_with(
+ region=1,
+ description="Region One description",
+ )
+
+ def test_get_region_success(self, mock_get_openstack_conn_identity):
+ """Test getting a identity region successfully."""
+ mock_conn = mock_get_openstack_conn_identity
+
+ # Create mock region object
+ mock_region = Mock()
+ mock_region.id = "RegionOne"
+ mock_region.description = "Region One description"
+
+ # Configure mock region.get_region()
+ mock_conn.identity.get_region.return_value = mock_region
+
+ # Test get_region()
+ identity_tools = self.get_identity_tools()
+ result = identity_tools.get_region(id="RegionOne")
+
+ # Verify results
+ assert result == Region(
+ id="RegionOne",
+ description="Region One description",
+ )
+
+ # Verify mock calls
+ mock_conn.identity.get_region.assert_called_once_with(
+ region="RegionOne",
+ )
+
+ def test_get_region_not_found(self, mock_get_openstack_conn_identity):
+ """Test getting a identity region that does not exist."""
+ mock_conn = mock_get_openstack_conn_identity
+
+ # Configure mock to raise NotFoundException
+ mock_conn.identity.get_region.side_effect = (
+ exceptions.NotFoundException(
+ "Region 'RegionOne' not found",
+ )
+ )
+
+ # Test get_region()
+ identity_tools = self.get_identity_tools()
+
+ # Verify exception is raised
+ with pytest.raises(
+ exceptions.NotFoundException,
+ match="Region 'RegionOne' not found",
+ ):
+ identity_tools.get_region(id="RegionOne")
+
+ # Verify mock calls
+ mock_conn.identity.get_region.assert_called_once_with(
+ region="RegionOne",
+ )
+
+ def test_get_domains_success(self, mock_get_openstack_conn_identity):
+ """Test getting identity domains successfully."""
+ mock_conn = mock_get_openstack_conn_identity
+
+ # Create mock domain objects
+ mock_domain1 = Mock()
+ mock_domain1.id = "domainone"
+ mock_domain1.name = "DomainOne"
+ mock_domain1.description = "Domain One description"
+ mock_domain1.is_enabled = True
+
+ mock_domain2 = Mock()
+ mock_domain2.id = "domaintwo"
+ mock_domain2.name = "DomainTwo"
+ mock_domain2.description = "Domain Two description"
+ mock_domain2.is_enabled = False
+
+ # Configure mock domain.domains()
+ mock_conn.identity.domains.return_value = [mock_domain1, mock_domain2]
+
+ # Test get_domains()
+ identity_tools = self.get_identity_tools()
+ result = identity_tools.get_domains()
+
+ # Verify results
+ assert result == [
+ Domain(
+ id="domainone",
+ name="DomainOne",
+ description="Domain One description",
+ is_enabled=True,
+ ),
+ Domain(
+ id="domaintwo",
+ name="DomainTwo",
+ description="Domain Two description",
+ is_enabled=False,
+ ),
+ ]
+
+ # Verify mock calls
+ mock_conn.identity.domains.assert_called_once()
+
+ def test_get_domains_empty_list(self, mock_get_openstack_conn_identity):
+ """Test getting identity domains when there are no domains."""
+ mock_conn = mock_get_openstack_conn_identity
+
+ # Empty domain list
+ mock_conn.identity.domains.return_value = []
+
+ # Test get_domains()
+ identity_tools = self.get_identity_tools()
+ result = identity_tools.get_domains()
+
+ # Verify results
+ assert result == []
+
+ # Verify mock calls
+ mock_conn.identity.domains.assert_called_once()
+
+ def test_get_domain_success(self, mock_get_openstack_conn_identity):
+ """Test getting a identity domain successfully."""
+ mock_conn = mock_get_openstack_conn_identity
+
+ # Create mock domain object
+ mock_domain = Mock()
+ mock_domain.id = "d01a81393377480cbd75c0210442e687"
+ mock_domain.name = "domainone"
+ mock_domain.description = "domainone description"
+ mock_domain.is_enabled = True
+
+ # Configure mock domain.get_domain()
+ mock_conn.identity.find_domain.return_value = mock_domain
+
+ # Test get_domain()
+ identity_tools = self.get_identity_tools()
+ result = identity_tools.get_domain(name="domainone")
+
+ # Verify results
+ assert result == Domain(
+ id="d01a81393377480cbd75c0210442e687",
+ name="domainone",
+ description="domainone description",
+ is_enabled=True,
+ )
+
+ # Verify mock calls
+ mock_conn.identity.find_domain.assert_called_once_with(
+ name_or_id="domainone",
+ )
+
+ def test_get_domain_not_found(self, mock_get_openstack_conn_identity):
+ """Test getting a identity domain that does not exist."""
+ mock_conn = mock_get_openstack_conn_identity
+
+ # Configure mock to raise NotFoundException
+ mock_conn.identity.find_domain.side_effect = (
+ exceptions.NotFoundException(
+ "Domain 'domainone' not found",
+ )
+ )
+
+ # Test get_domain()
+ identity_tools = self.get_identity_tools()
+
+ # Verify exception is raised
+ with pytest.raises(
+ exceptions.NotFoundException,
+ match="Domain 'domainone' not found",
+ ):
+ identity_tools.get_domain(name="domainone")
+
+ # Verify mock calls
+ mock_conn.identity.find_domain.assert_called_once_with(
+ name_or_id="domainone",
+ )
+
+ def test_create_domain_success(self, mock_get_openstack_conn_identity):
+ """Test creating a identity domain successfully."""
+ mock_conn = mock_get_openstack_conn_identity
+
+ # Create mock domain object
+ mock_domain = Mock()
+ mock_domain.id = "d01a81393377480cbd75c0210442e687"
+ mock_domain.name = "domainone"
+ mock_domain.description = "domainone description"
+ mock_domain.is_enabled = True
+
+ # Configure mock domain.create_domain()
+ mock_conn.identity.create_domain.return_value = mock_domain
+
+ # Test create_domain()
+ identity_tools = self.get_identity_tools()
+ result = identity_tools.create_domain(
+ name="domainone",
+ description="domainone description",
+ is_enabled=True,
+ )
+
+ # Verify results
+ assert result == Domain(
+ id="d01a81393377480cbd75c0210442e687",
+ name="domainone",
+ description="domainone description",
+ is_enabled=True,
+ )
+
+ # Verify mock calls
+ mock_conn.identity.create_domain.assert_called_once_with(
+ name="domainone",
+ description="domainone description",
+ enabled=True,
+ )
+
+ def test_create_domain_without_name(
+ self,
+ mock_get_openstack_conn_identity,
+ ):
+ """Test creating a identity domain without a name."""
+
+ # Test create_domain()
+ identity_tools = self.get_identity_tools()
+
+ # Verify pydantic validation exception is raised
+ with pytest.raises(pydantic.ValidationError):
+ identity_tools.create_domain(
+ name="",
+ description="domainone description",
+ is_enabled=False,
+ )
+
+ def test_create_domain_without_description(
+ self,
+ mock_get_openstack_conn_identity,
+ ):
+ """Test creating a identity domain without a description."""
+ mock_conn = mock_get_openstack_conn_identity
+
+ # Create mock domain object
+ mock_domain = Mock()
+ mock_domain.id = "d01a81393377480cbd75c0210442e687"
+ mock_domain.name = "domainone"
+ mock_domain.description = None
+ mock_domain.is_enabled = False
+
+ # Configure mock domain.create_domain()
+ mock_conn.identity.create_domain.return_value = mock_domain
+
+ # Test create_domain()
+ identity_tools = self.get_identity_tools()
+ result = identity_tools.create_domain(name="domainone")
+
+ # Verify results
+ assert result == Domain(
+ id="d01a81393377480cbd75c0210442e687",
+ name="domainone",
+ description=None,
+ is_enabled=False,
+ )
+
+ # Verify mock calls
+ mock_conn.identity.create_domain.assert_called_once_with(
+ name="domainone",
+ description=None,
+ enabled=False,
+ )
+
+ def test_delete_domain_success(self, mock_get_openstack_conn_identity):
+ """Test deleting a identity domain successfully."""
+ mock_conn = mock_get_openstack_conn_identity
+
+ # mock
+ mock_domain = Mock()
+ mock_domain.id = "d01a81393377480cbd75c0210442e687"
+ mock_domain.name = "domainone"
+ mock_domain.description = "domainone description"
+ mock_domain.is_enabled = True
+
+ mock_conn.identity.find_domain.return_value = mock_domain
+
+ # Test delete_domain()
+ identity_tools = self.get_identity_tools()
+ result = identity_tools.delete_domain(name="domainone")
+
+ # Verify results
+ assert result is None
+
+ # Verify mock calls
+ mock_conn.identity.find_domain.assert_called_once_with(
+ name_or_id="domainone",
+ )
+ mock_conn.identity.delete_domain.assert_called_once_with(
+ domain=mock_domain,
+ ignore_missing=False,
+ )
+
+ def test_delete_domain_not_found(self, mock_get_openstack_conn_identity):
+ """Test deleting a identity domain that does not exist."""
+ mock_conn = mock_get_openstack_conn_identity
+
+ # Create mock domain object
+ mock_domain = Mock()
+ mock_domain.id = "d01a81393377480cbd75c0210442e687"
+ mock_domain.name = "domainone"
+ mock_domain.description = "domainone description"
+ mock_domain.is_enabled = True
+
+ mock_conn.identity.find_domain.return_value = mock_domain
+
+ # Configure mock to raise NotFoundException
+ mock_conn.identity.delete_domain.side_effect = (
+ exceptions.NotFoundException(
+ "Domain 'domainone' not found",
+ )
+ )
+
+ # Test delete_domain()
+ identity_tools = self.get_identity_tools()
+
+ # Verify exception is raised
+ with pytest.raises(
+ exceptions.NotFoundException,
+ match="Domain 'domainone' not found",
+ ):
+ identity_tools.delete_domain(name="domainone")
+
+ # Verify mock calls
+ mock_conn.identity.find_domain.assert_called_once_with(
+ name_or_id="domainone",
+ )
+ mock_conn.identity.delete_domain.assert_called_once_with(
+ domain=mock_domain,
+ ignore_missing=False,
+ )
+
+ def test_update_domain_with_all_fields_success(
+ self,
+ mock_get_openstack_conn_identity,
+ ):
+ """Test updating a identity domain successfully."""
+ mock_conn = mock_get_openstack_conn_identity
+
+ # Create mock domain object
+ mock_domain = Mock()
+ mock_domain.id = "d01a81393377480cbd75c0210442e687"
+ mock_domain.name = "domainone"
+ mock_domain.description = "domainone description"
+ mock_domain.is_enabled = True
+
+ # Configure mock domain.update_domain()
+ mock_conn.identity.update_domain.return_value = mock_domain
+
+ # Test update_domain()
+ identity_tools = self.get_identity_tools()
+ result = identity_tools.update_domain(
+ id="d01a81393377480cbd75c0210442e687",
+ name="domainone",
+ description="domainone description",
+ is_enabled=True,
+ )
+
+ # Verify results
+ assert result == Domain(
+ id="d01a81393377480cbd75c0210442e687",
+ name="domainone",
+ description="domainone description",
+ is_enabled=True,
+ )
+
+ # Verify mock calls
+ mock_conn.identity.update_domain.assert_called_once_with(
+ domain="d01a81393377480cbd75c0210442e687",
+ name="domainone",
+ description="domainone description",
+ is_enabled=True,
+ )
+
+ def test_update_domain_with_empty_args(
+ self,
+ mock_get_openstack_conn_identity,
+ ):
+ """Test updating a identity domain with empty arguments."""
+ mock_conn = mock_get_openstack_conn_identity
+
+ # Create mock domain object
+ mock_domain = Mock()
+ mock_domain.id = "d01a81393377480cbd75c0210442e687"
+ mock_domain.name = "domainone"
+ mock_domain.description = "domainone description"
+ mock_domain.is_enabled = True
+
+ # Configure mock domain.update_domain()
+ mock_conn.identity.update_domain.return_value = mock_domain
+
+ # Test update_domain()
+ identity_tools = self.get_identity_tools()
+ result = identity_tools.update_domain(
+ id="d01a81393377480cbd75c0210442e687",
+ )
+
+ # Verify results
+ assert result == Domain(
+ id="d01a81393377480cbd75c0210442e687",
+ name="domainone",
+ description="domainone description",
+ is_enabled=True,
+ )
+
+ # Verify mock calls
+ mock_conn.identity.update_domain.assert_called_once_with(
+ domain="d01a81393377480cbd75c0210442e687",
+ )
+
+ def test_update_domain_with_empty_id(
+ self,
+ mock_get_openstack_conn_identity,
+ ):
+ """Test updating a identity domain with an empty name."""
+ mock_conn = mock_get_openstack_conn_identity
+
+ mock_conn.identity.update_domain.side_effect = (
+ exceptions.BadRequestException(
+ "Field required",
+ )
+ )
+
+ # Test update_domain()
+ identity_tools = self.get_identity_tools()
+
+ # Verify exception is raised
+ with pytest.raises(
+ exceptions.BadRequestException,
+ match="Field required",
+ ):
+ identity_tools.update_domain(id="")
+
+ # Verify mock calls
+ mock_conn.identity.update_domain.assert_called_once_with(domain="")
diff --git a/tests/tools/test_image_tools.py b/tests/tools/test_image_tools.py
new file mode 100644
index 0000000..f29dac8
--- /dev/null
+++ b/tests/tools/test_image_tools.py
@@ -0,0 +1,232 @@
+import uuid
+
+from unittest.mock import Mock
+
+from openstack_mcp_server.tools.image_tools import ImageTools
+from openstack_mcp_server.tools.request.image import CreateImage
+from openstack_mcp_server.tools.response.image import Image
+
+
+class TestImageTools:
+ """Test cases for ImageTools class."""
+
+ @staticmethod
+ def image_factory(**overrides):
+ defaults = {
+ "id": str(uuid.uuid4()),
+ "name": "test-image",
+ "checksum": "abc123",
+ "container_format": "bare",
+ "disk_format": "qcow2",
+ "file": None,
+ "min_disk": 1,
+ "min_ram": 512,
+ "os_hash_algo": "sha512",
+ "os_hash_value": "hash123",
+ "size": 1073741824,
+ "virtual_size": None,
+ "owner": str(uuid.uuid4()),
+ "visibility": "public",
+ "hw_rng_model": None,
+ "status": "active",
+ "schema": "/v2/schemas/image",
+ "protected": False,
+ "os_hidden": False,
+ "tags": [],
+ "properties": None,
+ "created_at": "2025-01-01T00:00:00Z",
+ "updated_at": "2025-01-01T00:00:00Z",
+ "owner_specified.openstack.md5": "a1b2c3d4e5f6",
+ "owner_specified.openstack.sha256": "a1b2c3d",
+ "owner_specified.openstack.object": "image",
+ }
+ for key, value in overrides.items():
+ if value is not None:
+ defaults[key] = value
+
+ return defaults
+
+ def test_get_image_images_success(self, mock_get_openstack_conn_image):
+ """Test getting image images successfully."""
+ mock_conn = mock_get_openstack_conn_image
+
+ # Create mock image objects
+ mock_image1 = Mock()
+ mock_image1.name = "ubuntu-20.04-server"
+ mock_image1.id = "img-123-abc-def"
+ mock_image1.status = "active"
+
+ mock_image2 = Mock()
+ mock_image2.name = "centos-8-stream"
+ mock_image2.id = "img-456-ghi-jkl"
+ mock_image2.status = "active"
+
+ # Configure mock image.images()
+ mock_conn.image.images.return_value = [mock_image1, mock_image2]
+
+ # Test ImageTools
+ image_tools = ImageTools()
+ result = image_tools.get_image_images()
+
+ # Verify results
+ expected_output = (
+ "ubuntu-20.04-server (img-123-abc-def) - Status: active\n"
+ "centos-8-stream (img-456-ghi-jkl) - Status: active"
+ )
+ assert result == expected_output
+
+ # Verify mock calls
+ mock_conn.image.images.assert_called_once()
+
+ def test_get_image_images_empty_list(self, mock_get_openstack_conn_image):
+ """Test getting image images when no images exist."""
+ mock_conn = mock_get_openstack_conn_image
+
+ # Empty image list
+ mock_conn.image.images.return_value = []
+
+ image_tools = ImageTools()
+ result = image_tools.get_image_images()
+
+ # Verify empty string
+ assert result == ""
+
+ mock_conn.image.images.assert_called_once()
+
+ def test_get_image_images_with_empty_name(
+ self,
+ mock_get_openstack_conn_image,
+ ):
+ """Test images with empty or None names."""
+ mock_conn = mock_get_openstack_conn_image
+
+ # Images with empty name (edge case)
+ mock_image1 = Mock()
+ mock_image1.name = "normal-image"
+ mock_image1.id = "img-normal"
+ mock_image1.status = "active"
+
+ mock_image2 = Mock()
+ mock_image2.name = "" # Empty name
+ mock_image2.id = "img-empty-name"
+ mock_image2.status = "active"
+
+ mock_conn.image.images.return_value = [mock_image1, mock_image2]
+
+ image_tools = ImageTools()
+ result = image_tools.get_image_images()
+
+ assert "normal-image (img-normal) - Status: active" in result
+ assert " (img-empty-name) - Status: active" in result # Empty name
+
+ mock_conn.image.images.assert_called_once()
+
+ def test_create_image_success_with_volume_id(
+ self,
+ mock_get_openstack_conn_image,
+ ):
+ """Test creating an image from a volume ID."""
+ volume_id = "6cf57d8d-00ca-43ff-ae6f-56912b69528a" # Example volume ID
+
+ mock_image = self.image_factory()
+ mock_get_openstack_conn_image.block_storage.create_image.return_value = Mock(
+ id=mock_image["id"],
+ )
+ mock_get_openstack_conn_image.get_image.return_value = mock_image
+
+ # Create an instance with volume ID
+ image_tools = ImageTools()
+ image_data = CreateImage(
+ name=mock_image["name"],
+ volume=volume_id,
+ allow_duplicates=False,
+ container=mock_image["container_format"],
+ disk_format=mock_image["disk_format"],
+ container_format=mock_image["container_format"],
+ min_disk=mock_image["min_disk"],
+ )
+
+ expected_output = Image(**mock_image)
+
+ created_image = image_tools.create_image(image_data)
+
+ # Verify the created image
+ assert created_image == expected_output
+ assert mock_get_openstack_conn_image.block_storage.create_image.called_once_with(
+ name=mock_image["name"],
+ volume=volume_id,
+ allow_duplicates=False,
+ container=mock_image["container_format"],
+ disk_format=mock_image["disk_format"],
+ wait=False,
+ timeout=3600,
+ )
+
+ assert mock_get_openstack_conn_image.get_image.called_once_with(
+ mock_image["id"],
+ )
+
+ def test_create_image_success_with_import_options(
+ self,
+ mock_get_openstack_conn_image,
+ ):
+ """Test creating an image with import options."""
+ create_image_data = CreateImage(
+ name="example_image",
+ container="bare",
+ disk_format="qcow2",
+ container_format="bare",
+ min_disk=10,
+ min_ram=512,
+ tags=["example", "test"],
+ import_options=CreateImage.ImportOptions(
+ import_method="web-download",
+ uri="https://example.com/image.qcow2",
+ ),
+ allow_duplicates=False,
+ )
+
+ mock_image = self.image_factory(**create_image_data.__dict__)
+ mock_create_image = Mock(id=mock_image["id"])
+
+ mock_get_openstack_conn_image.image.create_image.return_value = (
+ mock_create_image
+ )
+ mock_get_openstack_conn_image.image.import_image.return_value = None
+ mock_get_openstack_conn_image.get_image.return_value = mock_image
+
+ # Create an instance with import options
+ image_tools = ImageTools()
+
+ expected_output = Image(**mock_image)
+
+ created_image = image_tools.create_image(create_image_data)
+
+ # Verify the created image
+ assert created_image == expected_output
+ assert (
+ mock_get_openstack_conn_image.image.create_image.called_once_with(
+ name=create_image_data.name,
+ container=create_image_data.container,
+ container_format=create_image_data.container_format,
+ disk_format=create_image_data.disk_format,
+ min_disk=create_image_data.min_disk,
+ min_ram=create_image_data.min_ram,
+ tags=create_image_data.tags,
+ protected=create_image_data.protected,
+ visibility=create_image_data.visibility,
+ allow_duplicates=create_image_data.allow_duplicates,
+ )
+ )
+ assert mock_get_openstack_conn_image.image.import_image.called_once_with(
+ image=mock_create_image,
+ method=create_image_data.import_options.import_method,
+ uri=create_image_data.import_options.uri,
+ stores=create_image_data.import_options.stores,
+ remote_region=create_image_data.import_options.glance_region,
+ remote_image_id=create_image_data.import_options.glance_image_id,
+ remote_service_interface=create_image_data.import_options.glance_service_interface,
+ )
+ assert mock_get_openstack_conn_image.get_image.called_once_with(
+ mock_image["id"],
+ )
diff --git a/tests/tools/test_network_tools.py b/tests/tools/test_network_tools.py
new file mode 100644
index 0000000..c41f2e3
--- /dev/null
+++ b/tests/tools/test_network_tools.py
@@ -0,0 +1,1307 @@
+from unittest.mock import Mock
+
+from openstack_mcp_server.tools.network_tools import NetworkTools
+from openstack_mcp_server.tools.response.network import (
+ FloatingIP,
+ Network,
+ Port,
+ Subnet,
+)
+
+
+class TestNetworkTools:
+ """Test cases for NetworkTools class."""
+
+ def get_network_tools(self) -> NetworkTools:
+ """Get an instance of NetworkTools."""
+ return NetworkTools()
+
+ def test_get_networks_success(
+ self,
+ mock_openstack_connect_network,
+ ):
+ """Test getting openstack networks successfully."""
+ mock_conn = mock_openstack_connect_network
+
+ mock_network1 = Mock()
+ mock_network1.id = "net-123-abc-def"
+ mock_network1.name = "private-network"
+ mock_network1.status = "ACTIVE"
+ mock_network1.description = "Private network for project"
+ mock_network1.is_admin_state_up = True
+ mock_network1.is_shared = False
+ mock_network1.mtu = 1500
+ mock_network1.provider_network_type = "vxlan"
+ mock_network1.provider_physical_network = None
+ mock_network1.provider_segmentation_id = 100
+ mock_network1.project_id = "proj-456-ghi-jkl"
+
+ mock_network2 = Mock()
+ mock_network2.id = "net-789-mno-pqr"
+ mock_network2.name = "public-network"
+ mock_network2.status = "ACTIVE"
+ mock_network2.description = "Public shared network"
+ mock_network2.is_admin_state_up = True
+ mock_network2.is_shared = True
+ mock_network2.mtu = 1450
+ mock_network2.provider_network_type = "flat"
+ mock_network2.provider_physical_network = "physnet1"
+ mock_network2.provider_segmentation_id = None
+ mock_network2.project_id = "proj-admin-000"
+
+ mock_conn.list_networks.return_value = [mock_network1, mock_network2]
+
+ network_tools = self.get_network_tools()
+ result = network_tools.get_networks()
+
+ expected_network1 = Network(
+ id="net-123-abc-def",
+ name="private-network",
+ status="ACTIVE",
+ description="Private network for project",
+ is_admin_state_up=True,
+ is_shared=False,
+ mtu=1500,
+ provider_network_type="vxlan",
+ provider_physical_network=None,
+ provider_segmentation_id=100,
+ project_id="proj-456-ghi-jkl",
+ )
+
+ expected_network2 = Network(
+ id="net-789-mno-pqr",
+ name="public-network",
+ status="ACTIVE",
+ description="Public shared network",
+ is_admin_state_up=True,
+ is_shared=True,
+ mtu=1450,
+ provider_network_type="flat",
+ provider_physical_network="physnet1",
+ provider_segmentation_id=None,
+ project_id="proj-admin-000",
+ )
+
+ assert len(result) == 2
+ assert result[0] == expected_network1
+ assert result[1] == expected_network2
+
+ mock_conn.list_networks.assert_called_once_with(filters={})
+
+ def test_get_networks_empty_list(
+ self,
+ mock_openstack_connect_network,
+ ):
+ """Test getting openstack networks when no networks exist."""
+ mock_conn = mock_openstack_connect_network
+
+ mock_conn.list_networks.return_value = []
+
+ network_tools = self.get_network_tools()
+ result = network_tools.get_networks()
+
+ assert result == []
+
+ mock_conn.list_networks.assert_called_once_with(filters={})
+
+ def test_get_networks_with_status_filter(
+ self,
+ mock_openstack_connect_network,
+ ):
+ """Test getting opestack networks with status filter."""
+ mock_conn = mock_openstack_connect_network
+
+ mock_network1 = Mock()
+ mock_network1.id = "net-active"
+ mock_network1.name = "active-network"
+ mock_network1.status = "ACTIVE"
+ mock_network1.description = None
+ mock_network1.is_admin_state_up = True
+ mock_network1.is_shared = False
+ mock_network1.mtu = None
+ mock_network1.provider_network_type = None
+ mock_network1.provider_physical_network = None
+ mock_network1.provider_segmentation_id = None
+ mock_network1.project_id = None
+
+ mock_network2 = Mock()
+ mock_network2.id = "net-down"
+ mock_network2.name = "down-network"
+ mock_network2.status = "DOWN"
+ mock_network2.description = None
+ mock_network2.is_admin_state_up = False
+ mock_network2.is_shared = False
+ mock_network2.mtu = None
+ mock_network2.provider_network_type = None
+ mock_network2.provider_physical_network = None
+ mock_network2.provider_segmentation_id = None
+ mock_network2.project_id = None
+
+ mock_conn.list_networks.return_value = [
+ mock_network1,
+ ] # Only ACTIVE network
+ network_tools = self.get_network_tools()
+ result = network_tools.get_networks(status_filter="ACTIVE")
+
+ assert len(result) == 1
+ assert result[0].id == "net-active"
+ assert result[0].status == "ACTIVE"
+
+ mock_conn.list_networks.assert_called_once_with(
+ filters={"status": "ACTIVE"},
+ )
+
+ def test_get_networks_shared_only(
+ self,
+ mock_openstack_connect_network,
+ ):
+ """Test getting only shared networks."""
+ mock_conn = mock_openstack_connect_network
+
+ mock_network1 = Mock()
+ mock_network1.id = "net-private"
+ mock_network1.name = "private-network"
+ mock_network1.status = "ACTIVE"
+ mock_network1.description = None
+ mock_network1.is_admin_state_up = True
+ mock_network1.is_shared = False
+ mock_network1.mtu = None
+ mock_network1.provider_network_type = None
+ mock_network1.provider_physical_network = None
+ mock_network1.provider_segmentation_id = None
+ mock_network1.project_id = None
+
+ mock_network2 = Mock()
+ mock_network2.id = "net-shared"
+ mock_network2.name = "shared-network"
+ mock_network2.status = "ACTIVE"
+ mock_network2.description = None
+ mock_network2.is_admin_state_up = True
+ mock_network2.is_shared = True
+ mock_network2.mtu = None
+ mock_network2.provider_network_type = None
+ mock_network2.provider_physical_network = None
+ mock_network2.provider_segmentation_id = None
+ mock_network2.project_id = None
+
+ mock_conn.list_networks.return_value = [
+ mock_network2,
+ ] # Only shared network
+
+ network_tools = self.get_network_tools()
+ result = network_tools.get_networks(shared_only=True)
+
+ assert len(result) == 1
+ assert result[0].id == "net-shared"
+ assert result[0].is_shared is True
+
+ mock_conn.list_networks.assert_called_once_with(
+ filters={"shared": True},
+ )
+
+ def test_create_network_success(self, mock_openstack_connect_network):
+ """Test creating a network successfully."""
+ mock_conn = mock_openstack_connect_network
+
+ mock_network = Mock()
+ mock_network.id = "net-new-123"
+ mock_network.name = "new-network"
+ mock_network.status = "ACTIVE"
+ mock_network.description = "A new network"
+ mock_network.is_admin_state_up = True
+ mock_network.is_shared = False
+ mock_network.mtu = 1500
+ mock_network.provider_network_type = "vxlan"
+ mock_network.provider_physical_network = None
+ mock_network.provider_segmentation_id = 200
+ mock_network.project_id = "proj-123"
+
+ mock_conn.network.create_network.return_value = mock_network
+
+ network_tools = self.get_network_tools()
+ result = network_tools.create_network(
+ name="new-network",
+ description="A new network",
+ provider_network_type="vxlan",
+ provider_segmentation_id=200,
+ )
+
+ expected_network = Network(
+ id="net-new-123",
+ name="new-network",
+ status="ACTIVE",
+ description="A new network",
+ is_admin_state_up=True,
+ is_shared=False,
+ mtu=1500,
+ provider_network_type="vxlan",
+ provider_physical_network=None,
+ provider_segmentation_id=200,
+ project_id="proj-123",
+ )
+
+ assert result == expected_network
+
+ expected_args = {
+ "name": "new-network",
+ "admin_state_up": True,
+ "shared": False,
+ "description": "A new network",
+ "provider_network_type": "vxlan",
+ "provider_segmentation_id": 200,
+ }
+ mock_conn.network.create_network.assert_called_once_with(
+ **expected_args,
+ )
+
+ def test_create_network_minimal_args(self, mock_openstack_connect_network):
+ """Test creating a network with minimal arguments."""
+ mock_conn = mock_openstack_connect_network
+
+ mock_network = Mock()
+ mock_network.id = "net-minimal-123"
+ mock_network.name = "minimal-network"
+ mock_network.status = "ACTIVE"
+ mock_network.description = None
+ mock_network.is_admin_state_up = True
+ mock_network.is_shared = False
+ mock_network.mtu = None
+ mock_network.provider_network_type = None
+ mock_network.provider_physical_network = None
+ mock_network.provider_segmentation_id = None
+ mock_network.project_id = None
+
+ mock_conn.network.create_network.return_value = mock_network
+
+ network_tools = self.get_network_tools()
+ result = network_tools.create_network(name="minimal-network")
+
+ expected_network = Network(
+ id="net-minimal-123",
+ name="minimal-network",
+ status="ACTIVE",
+ description=None,
+ is_admin_state_up=True,
+ is_shared=False,
+ mtu=None,
+ provider_network_type=None,
+ provider_physical_network=None,
+ provider_segmentation_id=None,
+ project_id=None,
+ )
+
+ assert result == expected_network
+
+ expected_args = {
+ "name": "minimal-network",
+ "admin_state_up": True,
+ "shared": False,
+ }
+ mock_conn.network.create_network.assert_called_once_with(
+ **expected_args,
+ )
+
+ def test_get_network_detail_success(self, mock_openstack_connect_network):
+ """Test getting network detail successfully."""
+ mock_conn = mock_openstack_connect_network
+
+ mock_network = Mock()
+ mock_network.id = "net-detail-123"
+ mock_network.name = "detail-network"
+ mock_network.status = "ACTIVE"
+ mock_network.description = "Network for detail testing"
+ mock_network.is_admin_state_up = True
+ mock_network.is_shared = True
+ mock_network.mtu = 1500
+ mock_network.provider_network_type = "vlan"
+ mock_network.provider_physical_network = "physnet1"
+ mock_network.provider_segmentation_id = 100
+ mock_network.project_id = "proj-detail-123"
+
+ mock_conn.network.get_network.return_value = mock_network
+
+ network_tools = self.get_network_tools()
+ result = network_tools.get_network_detail("net-detail-123")
+
+ expected_network = Network(
+ id="net-detail-123",
+ name="detail-network",
+ status="ACTIVE",
+ description="Network for detail testing",
+ is_admin_state_up=True,
+ is_shared=True,
+ mtu=1500,
+ provider_network_type="vlan",
+ provider_physical_network="physnet1",
+ provider_segmentation_id=100,
+ project_id="proj-detail-123",
+ )
+
+ assert result == expected_network
+
+ mock_conn.network.get_network.assert_called_once_with("net-detail-123")
+
+ def test_update_network_success(self, mock_openstack_connect_network):
+ """Test updating a network successfully."""
+ mock_conn = mock_openstack_connect_network
+
+ mock_network = Mock()
+ mock_network.id = "net-update-123"
+ mock_network.name = "updated-network"
+ mock_network.status = "ACTIVE"
+ mock_network.description = "Updated description"
+ mock_network.is_admin_state_up = False
+ mock_network.is_shared = True
+ mock_network.mtu = 1400
+ mock_network.provider_network_type = "vxlan"
+ mock_network.provider_physical_network = None
+ mock_network.provider_segmentation_id = 300
+ mock_network.project_id = "proj-update-123"
+
+ mock_conn.network.update_network.return_value = mock_network
+
+ network_tools = self.get_network_tools()
+ result = network_tools.update_network(
+ network_id="net-update-123",
+ name="updated-network",
+ description="Updated description",
+ is_admin_state_up=False,
+ is_shared=True,
+ )
+
+ expected_network = Network(
+ id="net-update-123",
+ name="updated-network",
+ status="ACTIVE",
+ description="Updated description",
+ is_admin_state_up=False,
+ is_shared=True,
+ mtu=1400,
+ provider_network_type="vxlan",
+ provider_physical_network=None,
+ provider_segmentation_id=300,
+ project_id="proj-update-123",
+ )
+
+ assert result == expected_network
+
+ expected_args = {
+ "name": "updated-network",
+ "description": "Updated description",
+ "admin_state_up": False,
+ "shared": True,
+ }
+ mock_conn.network.update_network.assert_called_once_with(
+ "net-update-123",
+ **expected_args,
+ )
+
+ def test_update_network_partial_update(
+ self,
+ mock_openstack_connect_network,
+ ):
+ """Test updating a network with only some parameters."""
+ mock_conn = mock_openstack_connect_network
+
+ mock_network = Mock()
+ mock_network.id = "net-partial-123"
+ mock_network.name = "new-name"
+ mock_network.status = "ACTIVE"
+ mock_network.description = "old description"
+ mock_network.is_admin_state_up = True
+ mock_network.is_shared = False
+ mock_network.mtu = None
+ mock_network.provider_network_type = None
+ mock_network.provider_physical_network = None
+ mock_network.provider_segmentation_id = None
+ mock_network.project_id = None
+
+ mock_conn.network.update_network.return_value = mock_network
+ network_tools = self.get_network_tools()
+ result = network_tools.update_network(
+ network_id="net-partial-123",
+ name="new-name",
+ )
+
+ expected_network = Network(
+ id="net-partial-123",
+ name="new-name",
+ status="ACTIVE",
+ description="old description",
+ is_admin_state_up=True,
+ is_shared=False,
+ mtu=None,
+ provider_network_type=None,
+ provider_physical_network=None,
+ provider_segmentation_id=None,
+ project_id=None,
+ )
+
+ assert result == expected_network
+
+ expected_args = {"name": "new-name"}
+ mock_conn.network.update_network.assert_called_once_with(
+ "net-partial-123",
+ **expected_args,
+ )
+
+ def test_delete_network_success(self, mock_openstack_connect_network):
+ """Test deleting a network successfully."""
+ mock_conn = mock_openstack_connect_network
+
+ mock_network = Mock()
+ mock_network.name = "network-to-delete"
+
+ mock_conn.network.delete_network.return_value = None
+
+ network_tools = self.get_network_tools()
+ result = network_tools.delete_network("net-delete-123")
+
+ assert result is None
+
+ mock_conn.network.delete_network.assert_called_once_with(
+ "net-delete-123",
+ ignore_missing=False,
+ )
+
+ def test_get_ports_with_filters(self, mock_openstack_connect_network):
+ mock_conn = mock_openstack_connect_network
+
+ port = Mock()
+ port.id = "port-1"
+ port.name = "p1"
+ port.status = "ACTIVE"
+ port.description = None
+ port.project_id = "proj-1"
+ port.network_id = "net-1"
+ port.admin_state_up = True
+ port.is_admin_state_up = True
+ port.device_id = "device-1"
+ port.device_owner = "compute:nova"
+ port.mac_address = "fa:16:3e:00:00:01"
+ port.fixed_ips = [{"subnet_id": "subnet-1", "ip_address": "10.0.0.10"}]
+ port.security_group_ids = ["sg-1", "sg-2"]
+
+ mock_conn.list_ports.return_value = [port]
+
+ tools = self.get_network_tools()
+ result = tools.get_ports(
+ status_filter="ACTIVE",
+ device_id="device-1",
+ network_id="net-1",
+ )
+
+ assert result == [
+ Port(
+ id="port-1",
+ name="p1",
+ status="ACTIVE",
+ description=None,
+ project_id="proj-1",
+ network_id="net-1",
+ is_admin_state_up=True,
+ device_id="device-1",
+ device_owner="compute:nova",
+ mac_address="fa:16:3e:00:00:01",
+ fixed_ips=[
+ {"subnet_id": "subnet-1", "ip_address": "10.0.0.10"},
+ ],
+ security_group_ids=["sg-1", "sg-2"],
+ ),
+ ]
+
+ mock_conn.list_ports.assert_called_once_with(
+ filters={
+ "status": "ACTIVE",
+ "device_id": "device-1",
+ "network_id": "net-1",
+ },
+ )
+
+ def test_create_port_success(self, mock_openstack_connect_network):
+ mock_conn = mock_openstack_connect_network
+
+ port = Mock()
+ port.id = "port-1"
+ port.name = "p1"
+ port.status = "DOWN"
+ port.description = "desc"
+ port.project_id = "proj-1"
+ port.network_id = "net-1"
+ port.admin_state_up = True
+ port.is_admin_state_up = True
+ port.device_id = None
+ port.device_owner = None
+ port.mac_address = "fa:16:3e:00:00:02"
+ port.fixed_ips = []
+ port.security_group_ids = ["sg-1"]
+
+ mock_conn.network.create_port.return_value = port
+
+ tools = self.get_network_tools()
+ result = tools.create_port(
+ network_id="net-1",
+ name="p1",
+ description="desc",
+ is_admin_state_up=True,
+ fixed_ips=[],
+ security_group_ids=["sg-1"],
+ )
+
+ assert result == Port(
+ id="port-1",
+ name="p1",
+ status="DOWN",
+ description="desc",
+ project_id="proj-1",
+ network_id="net-1",
+ is_admin_state_up=True,
+ device_id=None,
+ device_owner=None,
+ mac_address="fa:16:3e:00:00:02",
+ fixed_ips=[],
+ security_group_ids=["sg-1"],
+ )
+
+ mock_conn.network.create_port.assert_called_once()
+
+ def test_get_port_detail_success(self, mock_openstack_connect_network):
+ mock_conn = mock_openstack_connect_network
+
+ port = Mock()
+ port.id = "port-1"
+ port.name = "p1"
+ port.status = "ACTIVE"
+ port.description = None
+ port.project_id = None
+ port.network_id = "net-1"
+ port.admin_state_up = True
+ port.is_admin_state_up = True
+ port.device_id = None
+ port.device_owner = None
+ port.mac_address = "fa:16:3e:00:00:03"
+ port.fixed_ips = []
+ port.security_group_ids = None
+
+ mock_conn.network.get_port.return_value = port
+
+ tools = self.get_network_tools()
+ result = tools.get_port_detail("port-1")
+ assert result.id == "port-1"
+ mock_conn.network.get_port.assert_called_once_with("port-1")
+
+ def test_update_port_success(self, mock_openstack_connect_network):
+ mock_conn = mock_openstack_connect_network
+
+ port = Mock()
+ port.id = "port-1"
+ port.name = "p-new"
+ port.status = "ACTIVE"
+ port.description = "d-new"
+ port.project_id = None
+ port.network_id = "net-1"
+ port.admin_state_up = False
+ port.is_admin_state_up = False
+ port.device_id = "dev-2"
+ port.device_owner = None
+ port.mac_address = "fa:16:3e:00:00:04"
+ port.fixed_ips = []
+ port.security_group_ids = ["sg-2"]
+
+ mock_conn.network.update_port.return_value = port
+
+ tools = self.get_network_tools()
+ res = tools.update_port(
+ port_id="port-1",
+ name="p-new",
+ description="d-new",
+ is_admin_state_up=False,
+ device_id="dev-2",
+ security_group_ids=["sg-2"],
+ )
+ assert res.name == "p-new"
+ mock_conn.network.update_port.assert_called_once_with(
+ "port-1",
+ name="p-new",
+ description="d-new",
+ admin_state_up=False,
+ device_id="dev-2",
+ security_groups=["sg-2"],
+ )
+
+ def test_delete_port_success(self, mock_openstack_connect_network):
+ mock_conn = mock_openstack_connect_network
+
+ port = Mock()
+ port.id = "port-1"
+ mock_conn.network.delete_port.return_value = None
+
+ tools = self.get_network_tools()
+ result = tools.delete_port("port-1")
+ assert result is None
+ mock_conn.network.delete_port.assert_called_once_with(
+ "port-1",
+ ignore_missing=False,
+ )
+
+ def test_add_port_fixed_ip(self, mock_openstack_connect_network):
+ mock_conn = mock_openstack_connect_network
+
+ current = Mock()
+ current.fixed_ips = [
+ {"subnet_id": "subnet-1", "ip_address": "10.0.0.10"},
+ ]
+ mock_conn.network.get_port.return_value = current
+
+ updated = Mock()
+ updated.id = "port-1"
+ updated.name = "p1"
+ updated.status = "ACTIVE"
+ updated.description = None
+ updated.project_id = None
+ updated.network_id = "net-1"
+ updated.admin_state_up = True
+ updated.is_admin_state_up = True
+ updated.device_id = None
+ updated.device_owner = None
+ updated.mac_address = "fa:16:3e:00:00:05"
+ updated.fixed_ips = [
+ {"subnet_id": "subnet-1", "ip_address": "10.0.0.10"},
+ {"subnet_id": "subnet-2", "ip_address": "10.0.1.10"},
+ ]
+ updated.security_group_ids = None
+ mock_conn.network.update_port.return_value = updated
+
+ tools = self.get_network_tools()
+ new_fixed = list(current.fixed_ips)
+ new_fixed.append({"subnet_id": "subnet-2", "ip_address": "10.0.1.10"})
+ res = tools.update_port("port-1", fixed_ips=new_fixed)
+ assert len(res.fixed_ips or []) == 2
+
+ def test_remove_port_fixed_ip(self, mock_openstack_connect_network):
+ mock_conn = mock_openstack_connect_network
+
+ current = Mock()
+ current.fixed_ips = [
+ {"subnet_id": "subnet-1", "ip_address": "10.0.0.10"},
+ {"subnet_id": "subnet-2", "ip_address": "10.0.1.10"},
+ ]
+ mock_conn.network.get_port.return_value = current
+
+ updated = Mock()
+ updated.id = "port-1"
+ updated.name = "p1"
+ updated.status = "ACTIVE"
+ updated.description = None
+ updated.project_id = None
+ updated.network_id = "net-1"
+ updated.admin_state_up = True
+ updated.is_admin_state_up = True
+ updated.device_id = None
+ updated.device_owner = None
+ updated.mac_address = "fa:16:3e:00:00:06"
+ updated.fixed_ips = [
+ {"subnet_id": "subnet-1", "ip_address": "10.0.0.10"},
+ ]
+ updated.security_group_ids = None
+ mock_conn.network.update_port.return_value = updated
+
+ tools = self.get_network_tools()
+ filtered = [
+ fi for fi in current.fixed_ips if fi["ip_address"] != "10.0.1.10"
+ ]
+ res = tools.update_port("port-1", fixed_ips=filtered)
+ assert len(res.fixed_ips or []) == 1
+
+ def test_get_and_update_allowed_address_pairs(
+ self,
+ mock_openstack_connect_network,
+ ):
+ mock_conn = mock_openstack_connect_network
+
+ port = Mock()
+ port.allowed_address_pairs = []
+ mock_conn.network.get_port.return_value = port
+
+ tools = self.get_network_tools()
+ lst = tools.get_port_allowed_address_pairs("port-1")
+ assert lst == []
+
+ updated = Mock()
+ updated.id = "port-1"
+ updated.name = "p1"
+ updated.status = "ACTIVE"
+ updated.description = None
+ updated.project_id = None
+ updated.network_id = "net-1"
+ updated.admin_state_up = True
+ updated.is_admin_state_up = True
+ updated.device_id = None
+ updated.device_owner = None
+ updated.mac_address = "fa:16:3e:00:00:07"
+ updated.fixed_ips = []
+ updated.security_group_ids = None
+ mock_conn.network.update_port.return_value = updated
+
+ pairs = []
+ pairs.append(
+ {"ip_address": "192.0.2.5", "mac_address": "aa:bb:cc:dd:ee:ff"}
+ )
+ res_add = tools.update_port("port-1", allowed_address_pairs=pairs)
+ assert isinstance(res_add, Port)
+
+ filtered = [
+ p
+ for p in pairs
+ if not (
+ p["ip_address"] == "192.0.2.5"
+ and p["mac_address"] == "aa:bb:cc:dd:ee:ff"
+ )
+ ]
+ res_remove = tools.update_port(
+ "port-1", allowed_address_pairs=filtered
+ )
+ assert isinstance(res_remove, Port)
+
+ def test_set_port_binding_and_admin_state(
+ self,
+ mock_openstack_connect_network,
+ ):
+ mock_conn = mock_openstack_connect_network
+
+ updated = Mock()
+ updated.id = "port-1"
+ updated.name = "p1"
+ updated.status = "ACTIVE"
+ updated.description = None
+ updated.project_id = None
+ updated.network_id = "net-1"
+ updated.is_admin_state_up = False
+ updated.device_id = None
+ updated.device_owner = None
+ updated.mac_address = "fa:16:3e:00:00:08"
+ updated.fixed_ips = []
+ updated.security_group_ids = None
+ mock_conn.network.update_port.return_value = updated
+
+ tools = self.get_network_tools()
+ res_bind = tools.set_port_binding(
+ "port-1",
+ host_id="host-1",
+ vnic_type="normal",
+ profile={"key": "val"},
+ )
+ assert isinstance(res_bind, Port)
+
+ res_set = tools.update_port("port-1", is_admin_state_up=False)
+ assert res_set.is_admin_state_up is False
+
+ current = Mock()
+ current.is_admin_state_up = False
+ mock_conn.network.get_port.return_value = current
+ updated.is_admin_state_up = True
+ res_toggle = tools.update_port(
+ "port-1", is_admin_state_up=not current.admin_state_up
+ )
+ assert res_toggle.is_admin_state_up is True
+
+ def test_get_subnets_filters_and_has_gateway_true(
+ self,
+ mock_openstack_connect_network,
+ ):
+ mock_conn = mock_openstack_connect_network
+
+ subnet1 = Mock()
+ subnet1.id = "subnet-1"
+ subnet1.name = "s1"
+ subnet1.status = "ACTIVE"
+ subnet1.description = None
+ subnet1.project_id = "proj-1"
+ subnet1.network_id = "net-1"
+ subnet1.cidr = "10.0.0.0/24"
+ subnet1.ip_version = 4
+ subnet1.gateway_ip = "10.0.0.1"
+ subnet1.enable_dhcp = True
+ subnet1.is_dhcp_enabled = True
+ subnet1.allocation_pools = []
+ subnet1.dns_nameservers = []
+ subnet1.host_routes = []
+
+ subnet2 = Mock()
+ subnet2.id = "subnet-2"
+ subnet2.name = "s2"
+ subnet2.status = "ACTIVE"
+ subnet2.description = None
+ subnet2.project_id = "proj-2"
+ subnet2.network_id = "net-1"
+ subnet2.cidr = "10.0.1.0/24"
+ subnet2.ip_version = 4
+ subnet2.gateway_ip = None
+ subnet2.enable_dhcp = False
+ subnet2.is_dhcp_enabled = False
+ subnet2.allocation_pools = []
+ subnet2.dns_nameservers = []
+ subnet2.host_routes = []
+
+ mock_conn.list_subnets.return_value = [subnet1, subnet2]
+
+ tools = self.get_network_tools()
+ result = tools.get_subnets(
+ network_id="net-1",
+ ip_version=4,
+ project_id="proj-1",
+ has_gateway=True,
+ is_dhcp_enabled=True,
+ )
+
+ assert len(result) == 1
+ assert result[0] == Subnet(
+ id="subnet-1",
+ name="s1",
+ status="ACTIVE",
+ description=None,
+ project_id="proj-1",
+ network_id="net-1",
+ cidr="10.0.0.0/24",
+ ip_version=4,
+ gateway_ip="10.0.0.1",
+ is_dhcp_enabled=True,
+ allocation_pools=[],
+ dns_nameservers=[],
+ host_routes=[],
+ )
+
+ mock_conn.list_subnets.assert_called_once_with(
+ filters={
+ "network_id": "net-1",
+ "ip_version": 4,
+ "project_id": "proj-1",
+ "enable_dhcp": True,
+ },
+ )
+
+ def test_get_subnets_has_gateway_false(
+ self,
+ mock_openstack_connect_network,
+ ):
+ mock_conn = mock_openstack_connect_network
+
+ subnet1 = Mock()
+ subnet1.id = "subnet-1"
+ subnet1.name = "s1"
+ subnet1.status = "ACTIVE"
+ subnet1.description = None
+ subnet1.project_id = None
+ subnet1.network_id = "net-1"
+ subnet1.cidr = "10.0.0.0/24"
+ subnet1.ip_version = 4
+ subnet1.gateway_ip = "10.0.0.1"
+ subnet1.enable_dhcp = True
+ subnet1.is_dhcp_enabled = True
+ subnet1.allocation_pools = []
+ subnet1.dns_nameservers = []
+ subnet1.host_routes = []
+
+ subnet2 = Mock()
+ subnet2.id = "subnet-2"
+ subnet2.name = "s2"
+ subnet2.status = "ACTIVE"
+ subnet2.description = None
+ subnet2.project_id = None
+ subnet2.network_id = "net-1"
+ subnet2.cidr = "10.0.1.0/24"
+ subnet2.ip_version = 4
+ subnet2.gateway_ip = None
+ subnet2.enable_dhcp = False
+ subnet2.is_dhcp_enabled = False
+ subnet2.allocation_pools = []
+ subnet2.dns_nameservers = []
+ subnet2.host_routes = []
+
+ mock_conn.list_subnets.return_value = [subnet1, subnet2]
+
+ tools = self.get_network_tools()
+ result = tools.get_subnets(
+ network_id="net-1",
+ has_gateway=False,
+ )
+
+ assert len(result) == 1
+ assert result[0].id == "subnet-2"
+
+ def test_create_subnet_success(
+ self,
+ mock_openstack_connect_network,
+ ):
+ mock_conn = mock_openstack_connect_network
+
+ subnet = Mock()
+ subnet.id = "subnet-new"
+ subnet.name = "s-new"
+ subnet.status = "ACTIVE"
+ subnet.description = "desc"
+ subnet.project_id = "proj-1"
+ subnet.network_id = "net-1"
+ subnet.cidr = "10.0.0.0/24"
+ subnet.ip_version = 4
+ subnet.gateway_ip = "10.0.0.1"
+ subnet.enable_dhcp = True
+ subnet.is_dhcp_enabled = True
+ subnet.allocation_pools = [{"start": "10.0.0.10", "end": "10.0.0.20"}]
+ subnet.dns_nameservers = ["8.8.8.8"]
+ subnet.host_routes = []
+
+ mock_conn.network.create_subnet.return_value = subnet
+
+ tools = self.get_network_tools()
+ result = tools.create_subnet(
+ network_id="net-1",
+ cidr="10.0.0.0/24",
+ name="s-new",
+ gateway_ip="10.0.0.1",
+ is_dhcp_enabled=True,
+ description="desc",
+ dns_nameservers=["8.8.8.8"],
+ allocation_pools=[{"start": "10.0.0.10", "end": "10.0.0.20"}],
+ host_routes=[],
+ )
+
+ assert result == Subnet(
+ id="subnet-new",
+ name="s-new",
+ status="ACTIVE",
+ description="desc",
+ project_id="proj-1",
+ network_id="net-1",
+ cidr="10.0.0.0/24",
+ ip_version=4,
+ gateway_ip="10.0.0.1",
+ is_dhcp_enabled=True,
+ allocation_pools=[{"start": "10.0.0.10", "end": "10.0.0.20"}],
+ dns_nameservers=["8.8.8.8"],
+ host_routes=[],
+ )
+
+ mock_conn.network.create_subnet.assert_called_once()
+
+ def test_get_subnet_detail_success(
+ self,
+ mock_openstack_connect_network,
+ ):
+ mock_conn = mock_openstack_connect_network
+
+ subnet = Mock()
+ subnet.id = "subnet-1"
+ subnet.name = "s1"
+ subnet.status = "ACTIVE"
+ subnet.description = None
+ subnet.project_id = "proj-1"
+ subnet.network_id = "net-1"
+ subnet.cidr = "10.0.0.0/24"
+ subnet.ip_version = 4
+ subnet.gateway_ip = "10.0.0.1"
+ subnet.enable_dhcp = True
+ subnet.is_dhcp_enabled = True
+ subnet.allocation_pools = []
+ subnet.dns_nameservers = []
+ subnet.host_routes = []
+
+ mock_conn.network.get_subnet.return_value = subnet
+
+ tools = self.get_network_tools()
+ result = tools.get_subnet_detail("subnet-1")
+
+ assert result.id == "subnet-1"
+ mock_conn.network.get_subnet.assert_called_once_with("subnet-1")
+
+ def test_update_subnet_success(
+ self,
+ mock_openstack_connect_network,
+ ):
+ mock_conn = mock_openstack_connect_network
+
+ subnet = Mock()
+ subnet.id = "subnet-1"
+ subnet.name = "s1-new"
+ subnet.status = "ACTIVE"
+ subnet.description = "d-new"
+ subnet.project_id = "proj-1"
+ subnet.network_id = "net-1"
+ subnet.cidr = "10.0.0.0/24"
+ subnet.ip_version = 4
+ subnet.gateway_ip = "10.0.0.254"
+ subnet.enable_dhcp = False
+ subnet.is_dhcp_enabled = False
+ subnet.allocation_pools = []
+ subnet.dns_nameservers = []
+ subnet.host_routes = []
+
+ mock_conn.network.update_subnet.return_value = subnet
+
+ tools = self.get_network_tools()
+ result = tools.update_subnet(
+ subnet_id="subnet-1",
+ name="s1-new",
+ description="d-new",
+ gateway_ip="10.0.0.254",
+ is_dhcp_enabled=False,
+ )
+
+ assert result.name == "s1-new"
+ mock_conn.network.update_subnet.assert_called_once_with(
+ "subnet-1",
+ name="s1-new",
+ description="d-new",
+ gateway_ip="10.0.0.254",
+ enable_dhcp=False,
+ )
+
+ def test_delete_subnet_success(
+ self,
+ mock_openstack_connect_network,
+ ):
+ mock_conn = mock_openstack_connect_network
+
+ subnet = Mock()
+ subnet.id = "subnet-1"
+ mock_conn.network.delete_subnet.return_value = None
+
+ tools = self.get_network_tools()
+ result = tools.delete_subnet("subnet-1")
+
+ assert result is None
+ mock_conn.network.delete_subnet.assert_called_once_with(
+ "subnet-1",
+ ignore_missing=False,
+ )
+
+ def test_set_and_clear_subnet_gateway(
+ self,
+ mock_openstack_connect_network,
+ ):
+ mock_conn = mock_openstack_connect_network
+
+ updated = Mock()
+ updated.id = "subnet-1"
+ updated.name = "s1"
+ updated.status = "ACTIVE"
+ updated.description = None
+ updated.project_id = None
+ updated.network_id = "net-1"
+ updated.cidr = "10.0.0.0/24"
+ updated.ip_version = 4
+ updated.gateway_ip = "10.0.0.254"
+ updated.enable_dhcp = True
+ updated.is_dhcp_enabled = True
+ updated.allocation_pools = []
+ updated.dns_nameservers = []
+ updated.host_routes = []
+
+ mock_conn.network.update_subnet.return_value = updated
+
+ tools = self.get_network_tools()
+ res1 = tools.update_subnet("subnet-1", gateway_ip="10.0.0.254")
+ assert res1.gateway_ip == "10.0.0.254"
+
+ updated.gateway_ip = None
+ res2 = tools.update_subnet("subnet-1", clear_gateway=True)
+ assert res2.gateway_ip is None
+
+ def test_set_and_toggle_subnet_dhcp(
+ self,
+ mock_openstack_connect_network,
+ ):
+ mock_conn = mock_openstack_connect_network
+
+ updated = Mock()
+ updated.id = "subnet-1"
+ updated.name = "s1"
+ updated.status = "ACTIVE"
+ updated.description = None
+ updated.project_id = None
+ updated.network_id = "net-1"
+ updated.cidr = "10.0.0.0/24"
+ updated.ip_version = 4
+ updated.gateway_ip = "10.0.0.1"
+ updated.enable_dhcp = True
+ updated.is_dhcp_enabled = True
+ updated.allocation_pools = []
+ updated.dns_nameservers = []
+ updated.host_routes = []
+
+ mock_conn.network.update_subnet.return_value = updated
+
+ tools = self.get_network_tools()
+ res1 = tools.update_subnet("subnet-1", is_dhcp_enabled=True)
+ assert res1.is_dhcp_enabled is True
+
+ updated.enable_dhcp = False
+ updated.is_dhcp_enabled = False
+ res2 = tools.update_subnet("subnet-1", is_dhcp_enabled=False)
+ assert res2.is_dhcp_enabled is False
+
+ def test_get_floating_ips_with_filters_and_unassigned(
+ self,
+ mock_openstack_connect_network,
+ ):
+ mock_conn = mock_openstack_connect_network
+
+ f1 = Mock()
+ f1.id = "fip-1"
+ f1.name = None
+ f1.status = "DOWN"
+ f1.description = None
+ f1.project_id = "proj-1"
+ f1.floating_ip_address = "203.0.113.10"
+ f1.floating_network_id = "ext-net"
+ f1.fixed_ip_address = None
+ f1.port_id = None
+ f1.router_id = None
+
+ f2 = Mock()
+ f2.id = "fip-2"
+ f2.name = None
+ f2.status = "ACTIVE"
+ f2.description = None
+ f2.project_id = "proj-1"
+ f2.floating_ip_address = "203.0.113.11"
+ f2.floating_network_id = "ext-net"
+ f2.fixed_ip_address = "10.0.0.10"
+ f2.port_id = "port-1"
+ f2.router_id = None
+
+ mock_conn.network.ips.return_value = [f1, f2]
+
+ tools = self.get_network_tools()
+ result = tools.get_floating_ips(
+ status_filter="ACTIVE",
+ project_id="proj-1",
+ floating_network_id="ext-net",
+ unassigned_only=True,
+ )
+ assert result == [
+ FloatingIP(
+ id="fip-1",
+ name=None,
+ status="DOWN",
+ description=None,
+ project_id="proj-1",
+ floating_ip_address="203.0.113.10",
+ floating_network_id="ext-net",
+ fixed_ip_address=None,
+ port_id=None,
+ router_id=None,
+ ),
+ ]
+
+ def test_create_attach_detach_delete_floating_ip(
+ self,
+ mock_openstack_connect_network,
+ ):
+ mock_conn = mock_openstack_connect_network
+
+ fip = Mock()
+ fip.id = "fip-1"
+ fip.name = None
+ fip.status = "DOWN"
+ fip.description = "d"
+ fip.project_id = "proj-1"
+ fip.floating_ip_address = "203.0.113.10"
+ fip.floating_network_id = "ext-net"
+ fip.fixed_ip_address = None
+ fip.port_id = None
+ fip.router_id = None
+ mock_conn.network.create_ip.return_value = fip
+
+ tools = self.get_network_tools()
+ created = tools.create_floating_ip("ext-net", description="d")
+ assert created.floating_network_id == "ext-net"
+
+ updated = Mock()
+ updated.id = "fip-1"
+ updated.name = None
+ updated.status = "ACTIVE"
+ updated.description = "d"
+ updated.project_id = "proj-1"
+ updated.floating_ip_address = "203.0.113.10"
+ updated.floating_network_id = "ext-net"
+ updated.fixed_ip_address = "10.0.0.10"
+ updated.port_id = "port-1"
+ updated.router_id = None
+ mock_conn.network.update_ip.return_value = updated
+
+ attached = tools.update_floating_ip(
+ "fip-1",
+ port_id="port-1",
+ fixed_ip_address="10.0.0.10",
+ )
+ assert attached.port_id == "port-1"
+
+ updated.port_id = None
+ detached = tools.update_floating_ip("fip-1", clear_port=True)
+ assert detached.port_id is None
+
+ mock_conn.network.get_ip.return_value = updated
+ tools.delete_floating_ip("fip-1")
+ mock_conn.network.delete_ip.assert_called_once_with(
+ "fip-1",
+ ignore_missing=False,
+ )
+
+ def test_update_reassign_bulk_and_auto_assign_floating_ip(
+ self,
+ mock_openstack_connect_network,
+ ):
+ mock_conn = mock_openstack_connect_network
+
+ updated = Mock()
+ updated.id = "fip-1"
+ updated.name = None
+ updated.status = "DOWN"
+ updated.description = "new desc"
+ updated.project_id = None
+ updated.floating_ip_address = "203.0.113.10"
+ updated.floating_network_id = "ext-net"
+ updated.fixed_ip_address = None
+ updated.port_id = None
+ updated.router_id = None
+ mock_conn.network.update_ip.return_value = updated
+
+ tools = self.get_network_tools()
+ res_desc = tools.update_floating_ip("fip-1", description="new desc")
+ assert res_desc.description == "new desc"
+
+ updated.port_id = "port-2"
+ res_reassign = tools.update_floating_ip("fip-1", port_id="port-2")
+ assert res_reassign.port_id == "port-2"
+
+ f1 = Mock()
+ f1.id = "fip-a"
+ f1.name = None
+ f1.status = "DOWN"
+ f1.description = None
+ f1.project_id = None
+ f1.floating_ip_address = "203.0.113.20"
+ f1.floating_network_id = "ext-net"
+ f1.fixed_ip_address = None
+ f1.port_id = None
+ f1.router_id = None
+ mock_conn.network.create_ip.side_effect = [f1]
+ bulk = tools.create_floating_ips_bulk("ext-net", 1)
+ assert len(bulk) == 1
+
+ exists = Mock()
+ exists.id = "fip-b"
+ exists.name = None
+ exists.status = "DOWN"
+ exists.description = None
+ exists.project_id = None
+ exists.floating_ip_address = "203.0.113.21"
+ exists.floating_network_id = "ext-net"
+ exists.fixed_ip_address = None
+ exists.port_id = None
+ exists.router_id = None
+ mock_conn.network.ips.return_value = [exists]
+ mock_conn.network.update_ip.return_value = exists
+ auto = tools.assign_first_available_floating_ip("ext-net", "port-9")
+ assert isinstance(auto, FloatingIP)
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..d05730c
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,1582 @@
+version = 1
+revision = 2
+requires-python = ">=3.10"
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "idna" },
+ { name = "sniffio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "attrs"
+version = "25.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" },
+]
+
+[[package]]
+name = "authlib"
+version = "1.6.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cryptography" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8e/a1/d8d1c6f8bc922c0b87ae0d933a8ed57be1bef6970894ed79c2852a153cd3/authlib-1.6.1.tar.gz", hash = "sha256:4dffdbb1460ba6ec8c17981a4c67af7d8af131231b5a36a88a1e8c80c111cdfd", size = 159988, upload-time = "2025-07-20T07:38:42.834Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f9/58/cc6a08053f822f98f334d38a27687b69c6655fb05cd74a7a5e70a2aeed95/authlib-1.6.1-py2.py3-none-any.whl", hash = "sha256:e9d2031c34c6309373ab845afc24168fe9e93dc52d252631f52642f21f5ed06e", size = 239299, upload-time = "2025-07-20T07:38:39.259Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.8.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
+]
+
+[[package]]
+name = "cffi"
+version = "1.17.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" },
+ { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" },
+ { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" },
+ { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" },
+ { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" },
+ { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" },
+ { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" },
+ { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" },
+ { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" },
+ { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" },
+ { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" },
+ { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" },
+ { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" },
+ { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" },
+ { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" },
+ { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" },
+ { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" },
+]
+
+[[package]]
+name = "cfgv"
+version = "3.4.0"
+source = { registry = "https://pypi.org/simple" }
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" },
+ { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" },
+ { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" },
+ { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" },
+ { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" },
+ { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" },
+ { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" },
+ { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" },
+ { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" },
+ { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" },
+ { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" },
+ { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" },
+ { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" },
+ { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" },
+ { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" },
+ { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" },
+ { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" },
+ { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
+ { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
+ { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
+ { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
+ { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
+ { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
+ { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
+ { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "cryptography"
+version = "45.0.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/95/1e/49527ac611af559665f71cbb8f92b332b5ec9c6fbc4e88b0f8e92f5e85df/cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a", size = 744903, upload-time = "2025-07-02T13:06:25.941Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f0/fb/09e28bc0c46d2c547085e60897fea96310574c70fb21cd58a730a45f3403/cryptography-45.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:101ee65078f6dd3e5a028d4f19c07ffa4dd22cce6a20eaa160f8b5219911e7d8", size = 7043092, upload-time = "2025-07-02T13:05:01.514Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/05/2194432935e29b91fb649f6149c1a4f9e6d3d9fc880919f4ad1bcc22641e/cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d", size = 4205926, upload-time = "2025-07-02T13:05:04.741Z" },
+ { url = "https://files.pythonhosted.org/packages/07/8b/9ef5da82350175e32de245646b1884fc01124f53eb31164c77f95a08d682/cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5", size = 4429235, upload-time = "2025-07-02T13:05:07.084Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/e1/c809f398adde1994ee53438912192d92a1d0fc0f2d7582659d9ef4c28b0c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57", size = 4209785, upload-time = "2025-07-02T13:05:09.321Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/8b/07eb6bd5acff58406c5e806eff34a124936f41a4fb52909ffa4d00815f8c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0", size = 3893050, upload-time = "2025-07-02T13:05:11.069Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d", size = 4457379, upload-time = "2025-07-02T13:05:13.32Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/9d/44080674dee514dbb82b21d6fa5d1055368f208304e2ab1828d85c9de8f4/cryptography-45.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9", size = 4209355, upload-time = "2025-07-02T13:05:15.017Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d8/0749f7d39f53f8258e5c18a93131919ac465ee1f9dccaf1b3f420235e0b5/cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27", size = 4456087, upload-time = "2025-07-02T13:05:16.945Z" },
+ { url = "https://files.pythonhosted.org/packages/09/d7/92acac187387bf08902b0bf0699816f08553927bdd6ba3654da0010289b4/cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e", size = 4332873, upload-time = "2025-07-02T13:05:18.743Z" },
+ { url = "https://files.pythonhosted.org/packages/03/c2/840e0710da5106a7c3d4153c7215b2736151bba60bf4491bdb421df5056d/cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174", size = 4564651, upload-time = "2025-07-02T13:05:21.382Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/92/cc723dd6d71e9747a887b94eb3827825c6c24b9e6ce2bb33b847d31d5eaa/cryptography-45.0.5-cp311-abi3-win32.whl", hash = "sha256:926c3ea71a6043921050eaa639137e13dbe7b4ab25800932a8498364fc1abec9", size = 2929050, upload-time = "2025-07-02T13:05:23.39Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/10/197da38a5911a48dd5389c043de4aec4b3c94cb836299b01253940788d78/cryptography-45.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:b85980d1e345fe769cfc57c57db2b59cff5464ee0c045d52c0df087e926fbe63", size = 3403224, upload-time = "2025-07-02T13:05:25.202Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/2b/160ce8c2765e7a481ce57d55eba1546148583e7b6f85514472b1d151711d/cryptography-45.0.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3562c2f23c612f2e4a6964a61d942f891d29ee320edb62ff48ffb99f3de9ae8", size = 7017143, upload-time = "2025-07-02T13:05:27.229Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/e7/2187be2f871c0221a81f55ee3105d3cf3e273c0a0853651d7011eada0d7e/cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd", size = 4197780, upload-time = "2025-07-02T13:05:29.299Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/cf/84210c447c06104e6be9122661159ad4ce7a8190011669afceeaea150524/cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e", size = 4420091, upload-time = "2025-07-02T13:05:31.221Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/6a/cb8b5c8bb82fafffa23aeff8d3a39822593cee6e2f16c5ca5c2ecca344f7/cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0", size = 4198711, upload-time = "2025-07-02T13:05:33.062Z" },
+ { url = "https://files.pythonhosted.org/packages/04/f7/36d2d69df69c94cbb2473871926daf0f01ad8e00fe3986ac3c1e8c4ca4b3/cryptography-45.0.5-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135", size = 3883299, upload-time = "2025-07-02T13:05:34.94Z" },
+ { url = "https://files.pythonhosted.org/packages/82/c7/f0ea40f016de72f81288e9fe8d1f6748036cb5ba6118774317a3ffc6022d/cryptography-45.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7", size = 4450558, upload-time = "2025-07-02T13:05:37.288Z" },
+ { url = "https://files.pythonhosted.org/packages/06/ae/94b504dc1a3cdf642d710407c62e86296f7da9e66f27ab12a1ee6fdf005b/cryptography-45.0.5-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42", size = 4198020, upload-time = "2025-07-02T13:05:39.102Z" },
+ { url = "https://files.pythonhosted.org/packages/05/2b/aaf0adb845d5dabb43480f18f7ca72e94f92c280aa983ddbd0bcd6ecd037/cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492", size = 4449759, upload-time = "2025-07-02T13:05:41.398Z" },
+ { url = "https://files.pythonhosted.org/packages/91/e4/f17e02066de63e0100a3a01b56f8f1016973a1d67551beaf585157a86b3f/cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0", size = 4319991, upload-time = "2025-07-02T13:05:43.64Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/2e/e2dbd629481b499b14516eed933f3276eb3239f7cee2dcfa4ee6b44d4711/cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a", size = 4554189, upload-time = "2025-07-02T13:05:46.045Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ea/a78a0c38f4c8736287b71c2ea3799d173d5ce778c7d6e3c163a95a05ad2a/cryptography-45.0.5-cp37-abi3-win32.whl", hash = "sha256:1e1da5accc0c750056c556a93c3e9cb828970206c68867712ca5805e46dc806f", size = 2911769, upload-time = "2025-07-02T13:05:48.329Z" },
+ { url = "https://files.pythonhosted.org/packages/79/b3/28ac139109d9005ad3f6b6f8976ffede6706a6478e21c889ce36c840918e/cryptography-45.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:90cb0a7bb35959f37e23303b7eed0a32280510030daba3f7fdfbb65defde6a97", size = 3390016, upload-time = "2025-07-02T13:05:50.811Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/8b/34394337abe4566848a2bd49b26bcd4b07fd466afd3e8cce4cb79a390869/cryptography-45.0.5-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:206210d03c1193f4e1ff681d22885181d47efa1ab3018766a7b32a7b3d6e6afd", size = 3575762, upload-time = "2025-07-02T13:05:53.166Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/5d/a19441c1e89afb0f173ac13178606ca6fab0d3bd3ebc29e9ed1318b507fc/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c648025b6840fe62e57107e0a25f604db740e728bd67da4f6f060f03017d5097", size = 4140906, upload-time = "2025-07-02T13:05:55.914Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/db/daceb259982a3c2da4e619f45b5bfdec0e922a23de213b2636e78ef0919b/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b8fa8b0a35a9982a3c60ec79905ba5bb090fc0b9addcfd3dc2dd04267e45f25e", size = 4374411, upload-time = "2025-07-02T13:05:57.814Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/35/5d06ad06402fc522c8bf7eab73422d05e789b4e38fe3206a85e3d6966c11/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:14d96584701a887763384f3c47f0ca7c1cce322aa1c31172680eb596b890ec30", size = 4140942, upload-time = "2025-07-02T13:06:00.137Z" },
+ { url = "https://files.pythonhosted.org/packages/65/79/020a5413347e44c382ef1f7f7e7a66817cd6273e3e6b5a72d18177b08b2f/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57c816dfbd1659a367831baca4b775b2a5b43c003daf52e9d57e1d30bc2e1b0e", size = 4374079, upload-time = "2025-07-02T13:06:02.043Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/c5/c0e07d84a9a2a8a0ed4f865e58f37c71af3eab7d5e094ff1b21f3f3af3bc/cryptography-45.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b9e38e0a83cd51e07f5a48ff9691cae95a79bea28fe4ded168a8e5c6c77e819d", size = 3321362, upload-time = "2025-07-02T13:06:04.463Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/71/9bdbcfd58d6ff5084687fe722c58ac718ebedbc98b9f8f93781354e6d286/cryptography-45.0.5-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8c4a6ff8a30e9e3d38ac0539e9a9e02540ab3f827a3394f8852432f6b0ea152e", size = 3587878, upload-time = "2025-07-02T13:06:06.339Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/63/83516cfb87f4a8756eaa4203f93b283fda23d210fc14e1e594bd5f20edb6/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bd4c45986472694e5121084c6ebbd112aa919a25e783b87eb95953c9573906d6", size = 4152447, upload-time = "2025-07-02T13:06:08.345Z" },
+ { url = "https://files.pythonhosted.org/packages/22/11/d2823d2a5a0bd5802b3565437add16f5c8ce1f0778bf3822f89ad2740a38/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:982518cd64c54fcada9d7e5cf28eabd3ee76bd03ab18e08a48cad7e8b6f31b18", size = 4386778, upload-time = "2025-07-02T13:06:10.263Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/38/6bf177ca6bce4fe14704ab3e93627c5b0ca05242261a2e43ef3168472540/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:12e55281d993a793b0e883066f590c1ae1e802e3acb67f8b442e721e475e6463", size = 4151627, upload-time = "2025-07-02T13:06:13.097Z" },
+ { url = "https://files.pythonhosted.org/packages/38/6a/69fc67e5266bff68a91bcb81dff8fb0aba4d79a78521a08812048913e16f/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:5aa1e32983d4443e310f726ee4b071ab7569f58eedfdd65e9675484a4eb67bd1", size = 4385593, upload-time = "2025-07-02T13:06:15.689Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/34/31a1604c9a9ade0fdab61eb48570e09a796f4d9836121266447b0eaf7feb/cryptography-45.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e357286c1b76403dd384d938f93c46b2b058ed4dfcdce64a770f0537ed3feb6f", size = 3331106, upload-time = "2025-07-02T13:06:18.058Z" },
+]
+
+[[package]]
+name = "cyclopts"
+version = "3.22.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "docstring-parser", marker = "python_full_version < '4.0'" },
+ { name = "rich" },
+ { name = "rich-rst" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a3/d5/24c6c894f3833bc93d4944c2064309dfd633c0becf93e16fc79d76edd388/cyclopts-3.22.5.tar.gz", hash = "sha256:fa2450b9840abc41c6aa37af5eaeafc7a1264e08054e3a2fe39d49aa154f592a", size = 74890, upload-time = "2025-07-31T18:18:37.336Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/e5/a7b6db64f08cfe065e531ec6b508fa7dac704fab70d05adb5bc0c2c1d1b6/cyclopts-3.22.5-py3-none-any.whl", hash = "sha256:92efb4a094d9812718d7efe0bffa319a19cb661f230dbf24406c18cd8809fb82", size = 84994, upload-time = "2025-07-31T18:18:35.939Z" },
+]
+
+[[package]]
+name = "decorator"
+version = "5.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" },
+]
+
+[[package]]
+name = "distlib"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
+]
+
+[[package]]
+name = "dnspython"
+version = "2.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" },
+]
+
+[[package]]
+name = "docstring-parser"
+version = "0.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" },
+]
+
+[[package]]
+name = "docutils"
+version = "0.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e9/86/5b41c32ecedcfdb4c77b28b6cb14234f252075f8cdb254531727a35547dd/docutils-0.22.tar.gz", hash = "sha256:ba9d57750e92331ebe7c08a1bbf7a7f8143b86c476acd51528b042216a6aad0f", size = 2277984, upload-time = "2025-07-29T15:20:31.06Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/44/57/8db39bc5f98f042e0153b1de9fb88e1a409a33cda4dd7f723c2ed71e01f6/docutils-0.22-py3-none-any.whl", hash = "sha256:4ed966a0e96a0477d852f7af31bdcb3adc049fbb35ccba358c2ea8a03287615e", size = 630709, upload-time = "2025-07-29T15:20:28.335Z" },
+]
+
+[[package]]
+name = "dogpile-cache"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "decorator" },
+ { name = "stevedore" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e8/07/2257f13f9cd77e71f62076d220b7b59e1f11a70b90eb1e3ef8bdf0f14b34/dogpile_cache-1.4.0.tar.gz", hash = "sha256:b00a9e2f409cf9bf48c2e7a3e3e68dac5fa75913acbf1a62f827c812d35f3d09", size = 937468, upload-time = "2025-04-26T17:44:30.768Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fb/91/6191ee1b821a03ed2487f234b11c58b0390c305452cf31e1e33b4a53064d/dogpile_cache-1.4.0-py3-none-any.whl", hash = "sha256:f1581953afefd5f55743178694bf3b3ffb2782bba3d9537566a09db6daa48a63", size = 62881, upload-time = "2025-04-26T17:45:44.804Z" },
+]
+
+[[package]]
+name = "email-validator"
+version = "2.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "dnspython" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967, upload-time = "2024-06-20T11:30:30.034Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521, upload-time = "2024-06-20T11:30:28.248Z" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "fastmcp"
+version = "2.11.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "authlib" },
+ { name = "cyclopts" },
+ { name = "exceptiongroup" },
+ { name = "httpx" },
+ { name = "mcp" },
+ { name = "openapi-core" },
+ { name = "openapi-pydantic" },
+ { name = "pydantic", extra = ["email"] },
+ { name = "pyperclip" },
+ { name = "python-dotenv" },
+ { name = "rich" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/80/13aec687ec21727b0fe6d26c6fe2febb33ae24e24c980929a706db3a8bc2/fastmcp-2.11.3.tar.gz", hash = "sha256:e8e3834a3e0b513712b8e63a6f0d4cbe19093459a1da3f7fbf8ef2810cfd34e3", size = 2692092, upload-time = "2025-08-11T21:38:46.493Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/61/05/63f63ad5b6789a730d94b8cb3910679c5da1ed5b4e38c957140ac9edcf0e/fastmcp-2.11.3-py3-none-any.whl", hash = "sha256:28f22126c90fd36e5de9cc68b9c271b6d832dcf322256f23d220b68afb3352cc", size = 260231, upload-time = "2025-08-11T21:38:44.746Z" },
+]
+
+[[package]]
+name = "filelock"
+version = "3.18.0"
+source = { registry = "https://pypi.org/simple" }
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "httpx-sse"
+version = "0.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload-time = "2025-06-24T13:21:05.71Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" },
+]
+
+[[package]]
+name = "identify"
+version = "2.6.12"
+source = { registry = "https://pypi.org/simple" }
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "iso8601"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b9/f3/ef59cee614d5e0accf6fd0cbba025b93b272e626ca89fb70a3e9187c5d15/iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df", size = 6522, upload-time = "2023-10-03T00:25:39.317Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6c/0c/f37b6a241f0759b7653ffa7213889d89ad49a2b76eb2ddf3b57b2738c347/iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242", size = 7545, upload-time = "2023-10-03T00:25:32.304Z" },
+]
+
+[[package]]
+name = "isodate"
+version = "0.7.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" },
+]
+
+[[package]]
+name = "jmespath"
+version = "1.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" },
+]
+
+[[package]]
+name = "jsonpatch"
+version = "1.33"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonpointer" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" },
+]
+
+[[package]]
+name = "jsonpointer"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.25.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d5/00/a297a868e9d0784450faa7365c2172a7d6110c763e30ba861867c32ae6a9/jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f", size = 356830, upload-time = "2025-07-18T15:39:45.11Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fe/54/c86cd8e011fe98803d7e382fd67c0df5ceab8d2b7ad8c5a81524f791551c/jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", size = 89184, upload-time = "2025-07-18T15:39:42.956Z" },
+]
+
+[[package]]
+name = "jsonschema-path"
+version = "0.3.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pathable" },
+ { name = "pyyaml" },
+ { name = "referencing" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159, upload-time = "2025-01-24T14:33:16.547Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/58/3485da8cb93d2f393bce453adeef16896751f14ba3e2024bc21dc9597646/jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8", size = 14810, upload-time = "2025-01-24T14:33:14.652Z" },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" },
+]
+
+[[package]]
+name = "keystoneauth1"
+version = "5.11.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "iso8601" },
+ { name = "os-service-types" },
+ { name = "pbr" },
+ { name = "requests" },
+ { name = "stevedore" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8f/ba/faa527d4db6ce2d2840c2a04d26152fa9fa47808299ebd23ff8e716503c8/keystoneauth1-5.11.1.tar.gz", hash = "sha256:806f12c49b7f4b2cad3f5a460f7bdd81e4247c81b6042596a7fea8575f6591f3", size = 288713, upload-time = "2025-06-12T00:37:10.971Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/73/be/2c02cc89ec0c2532c12034976dae2e19baa3f5798a42706ba3f1ea0f1473/keystoneauth1-5.11.1-py3-none-any.whl", hash = "sha256:4525adf03b6e591f4b9b8a72c3b14f6510a04816dd5a7aca6ebaa6dfc90b69e6", size = 344533, upload-time = "2025-06-12T00:37:09.219Z" },
+]
+
+[[package]]
+name = "lazy-object-proxy"
+version = "1.11.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/57/f9/1f56571ed82fb324f293661690635cf42c41deb8a70a6c9e6edc3e9bb3c8/lazy_object_proxy-1.11.0.tar.gz", hash = "sha256:18874411864c9fbbbaa47f9fc1dd7aea754c86cfde21278ef427639d1dd78e9c", size = 44736, upload-time = "2025-04-16T16:53:48.482Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/21/c8/457f1555f066f5bacc44337141294153dc993b5e9132272ab54a64ee98a2/lazy_object_proxy-1.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:132bc8a34f2f2d662a851acfd1b93df769992ed1b81e2b1fda7db3e73b0d5a18", size = 28045, upload-time = "2025-04-16T16:53:32.314Z" },
+ { url = "https://files.pythonhosted.org/packages/18/33/3260b4f8de6f0942008479fee6950b2b40af11fc37dba23aa3672b0ce8a6/lazy_object_proxy-1.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:01261a3afd8621a1accb5682df2593dc7ec7d21d38f411011a5712dcd418fbed", size = 28441, upload-time = "2025-04-16T16:53:33.636Z" },
+ { url = "https://files.pythonhosted.org/packages/51/f6/eb645ca1ff7408bb69e9b1fe692cce1d74394efdbb40d6207096c0cd8381/lazy_object_proxy-1.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:090935756cc041e191f22f4f9c7fd4fe9a454717067adf5b1bbd2ce3046b556e", size = 28047, upload-time = "2025-04-16T16:53:34.679Z" },
+ { url = "https://files.pythonhosted.org/packages/13/9c/aabbe1e8b99b8b0edb846b49a517edd636355ac97364419d9ba05b8fa19f/lazy_object_proxy-1.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:76ec715017f06410f57df442c1a8d66e6b5f7035077785b129817f5ae58810a4", size = 28440, upload-time = "2025-04-16T16:53:36.113Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/24/dae4759469e9cd318fef145f7cfac7318261b47b23a4701aa477b0c3b42c/lazy_object_proxy-1.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9a9f39098e93a63618a79eef2889ae3cf0605f676cd4797fdfd49fcd7ddc318b", size = 28142, upload-time = "2025-04-16T16:53:37.663Z" },
+ { url = "https://files.pythonhosted.org/packages/de/0c/645a881f5f27952a02f24584d96f9f326748be06ded2cee25f8f8d1cd196/lazy_object_proxy-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ee13f67f4fcd044ef27bfccb1c93d39c100046fec1fad6e9a1fcdfd17492aeb3", size = 28380, upload-time = "2025-04-16T16:53:39.07Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/0f/6e004f928f7ff5abae2b8e1f68835a3870252f886e006267702e1efc5c7b/lazy_object_proxy-1.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4c84eafd8dd15ea16f7d580758bc5c2ce1f752faec877bb2b1f9f827c329cd", size = 28149, upload-time = "2025-04-16T16:53:40.135Z" },
+ { url = "https://files.pythonhosted.org/packages/63/cb/b8363110e32cc1fd82dc91296315f775d37a39df1c1cfa976ec1803dac89/lazy_object_proxy-1.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:d2503427bda552d3aefcac92f81d9e7ca631e680a2268cbe62cd6a58de6409b7", size = 28389, upload-time = "2025-04-16T16:53:43.612Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/89/68c50fcfd81e11480cd8ee7f654c9bd790a9053b9a0efe9983d46106f6a9/lazy_object_proxy-1.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0613116156801ab3fccb9e2b05ed83b08ea08c2517fdc6c6bc0d4697a1a376e3", size = 28777, upload-time = "2025-04-16T16:53:41.371Z" },
+ { url = "https://files.pythonhosted.org/packages/39/d0/7e967689e24de8ea6368ec33295f9abc94b9f3f0cd4571bfe148dc432190/lazy_object_proxy-1.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bb03c507d96b65f617a6337dedd604399d35face2cdf01526b913fb50c4cb6e8", size = 29598, upload-time = "2025-04-16T16:53:42.513Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/1e/fb441c07b6662ec1fc92b249225ba6e6e5221b05623cb0131d082f782edc/lazy_object_proxy-1.11.0-py3-none-any.whl", hash = "sha256:a56a5093d433341ff7da0e89f9b486031ccd222ec8e52ec84d0ec1cdc819674b", size = 16635, upload-time = "2025-04-16T16:53:47.198Z" },
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mdurl" },
+]
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" },
+ { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" },
+ { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" },
+ { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" },
+ { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" },
+ { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" },
+ { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" },
+ { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" },
+ { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" },
+ { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" },
+ { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" },
+ { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" },
+ { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" },
+ { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
+ { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
+ { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
+ { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
+ { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
+ { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
+]
+
+[[package]]
+name = "mcp"
+version = "1.13.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "httpx" },
+ { name = "httpx-sse" },
+ { name = "jsonschema" },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+ { name = "python-multipart" },
+ { name = "pywin32", marker = "sys_platform == 'win32'" },
+ { name = "sse-starlette" },
+ { name = "starlette" },
+ { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d3/a8/564c094de5d6199f727f5d9f5672dbec3b00dfafd0f67bf52d995eaa5951/mcp-1.13.0.tar.gz", hash = "sha256:70452f56f74662a94eb72ac5feb93997b35995e389b3a3a574e078bed2aa9ab3", size = 434709, upload-time = "2025-08-14T15:03:58.58Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8b/6b/46b8bcefc2ee9e2d2e8d2bd25f1c2512f5a879fac4619d716b194d6e7ccc/mcp-1.13.0-py3-none-any.whl", hash = "sha256:8b1a002ebe6e17e894ec74d1943cc09aa9d23cb931bf58d49ab2e9fa6bb17e4b", size = 160226, upload-time = "2025-08-14T15:03:56.641Z" },
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "more-itertools"
+version = "10.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ce/a0/834b0cebabbfc7e311f30b46c8188790a37f89fc8d756660346fe5abfd09/more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3", size = 127671, upload-time = "2025-04-22T14:17:41.838Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e", size = 65278, upload-time = "2025-04-22T14:17:40.49Z" },
+]
+
+[[package]]
+name = "nodeenv"
+version = "1.9.1"
+source = { registry = "https://pypi.org/simple" }
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "openapi-core"
+version = "0.19.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "isodate" },
+ { name = "jsonschema" },
+ { name = "jsonschema-path" },
+ { name = "more-itertools" },
+ { name = "openapi-schema-validator" },
+ { name = "openapi-spec-validator" },
+ { name = "parse" },
+ { name = "typing-extensions" },
+ { name = "werkzeug" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/35/1acaa5f2fcc6e54eded34a2ec74b479439c4e469fc4e8d0e803fda0234db/openapi_core-0.19.5.tar.gz", hash = "sha256:421e753da56c391704454e66afe4803a290108590ac8fa6f4a4487f4ec11f2d3", size = 103264, upload-time = "2025-03-20T20:17:28.193Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/27/6f/83ead0e2e30a90445ee4fc0135f43741aebc30cca5b43f20968b603e30b6/openapi_core-0.19.5-py3-none-any.whl", hash = "sha256:ef7210e83a59394f46ce282639d8d26ad6fc8094aa904c9c16eb1bac8908911f", size = 106595, upload-time = "2025-03-20T20:17:26.77Z" },
+]
+
+[[package]]
+name = "openapi-pydantic"
+version = "0.5.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+]
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "openapi-schema-validator"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonschema" },
+ { name = "jsonschema-specifications" },
+ { name = "rfc3339-validator" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/5507ad3325169347cd8ced61c232ff3df70e2b250c49f0fe140edb4973c6/openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee", size = 11550, upload-time = "2025-01-10T18:08:22.268Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/21/c6/ad0fba32775ae749016829dace42ed80f4407b171da41313d1a3a5f102e4/openapi_schema_validator-0.6.3-py3-none-any.whl", hash = "sha256:f3b9870f4e556b5a62a1c39da72a6b4b16f3ad9c73dc80084b1b11e74ba148a3", size = 8755, upload-time = "2025-01-10T18:08:19.758Z" },
+]
+
+[[package]]
+name = "openapi-spec-validator"
+version = "0.7.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonschema" },
+ { name = "jsonschema-path" },
+ { name = "lazy-object-proxy" },
+ { name = "openapi-schema-validator" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/82/af/fe2d7618d6eae6fb3a82766a44ed87cd8d6d82b4564ed1c7cfb0f6378e91/openapi_spec_validator-0.7.2.tar.gz", hash = "sha256:cc029309b5c5dbc7859df0372d55e9d1ff43e96d678b9ba087f7c56fc586f734", size = 36855, upload-time = "2025-06-07T14:48:56.299Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/27/dd/b3fd642260cb17532f66cc1e8250f3507d1e580483e209dc1e9d13bd980d/openapi_spec_validator-0.7.2-py3-none-any.whl", hash = "sha256:4bbdc0894ec85f1d1bea1d6d9c8b2c3c8d7ccaa13577ef40da9c006c9fd0eb60", size = 39713, upload-time = "2025-06-07T14:48:54.077Z" },
+]
+
+[[package]]
+name = "openstacksdk"
+version = "4.6.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cryptography" },
+ { name = "decorator" },
+ { name = "dogpile-cache" },
+ { name = "iso8601" },
+ { name = "jmespath" },
+ { name = "jsonpatch" },
+ { name = "keystoneauth1" },
+ { name = "os-service-types" },
+ { name = "pbr" },
+ { name = "platformdirs" },
+ { name = "psutil" },
+ { name = "pyyaml" },
+ { name = "requestsexceptions" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/52/7a/07813f7501792e6bd7e79a75cd94a5bbce20c7fd2679822d44397201b00a/openstacksdk-4.6.0.tar.gz", hash = "sha256:e47e166c4732e9aea65228e618d490e4be5df06526a1b95e2d5995d7d0977d3d", size = 1287222, upload-time = "2025-06-03T14:08:01.177Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/56/e2/a4813d785c621eb9a61ef95874ac22833f88e5307dfb15532119c10a09a8/openstacksdk-4.6.0-py3-none-any.whl", hash = "sha256:0ea54ce3005d48c5134f77dce8df7dd6b4c52d2a103472abc99db19cd4382638", size = 1812803, upload-time = "2025-06-03T14:07:59.474Z" },
+]
+
+[[package]]
+name = "os-service-types"
+version = "1.8.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pbr" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9d/e9/1725288a94496d7780cd1624d16b86b7ed596960595d5742f051c4b90df5/os_service_types-1.8.0.tar.gz", hash = "sha256:890ce74f132ca334c2b23f0025112b47c6926da6d28c2f75bcfc0a83dea3603e", size = 27279, upload-time = "2025-07-08T09:03:43.252Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/77/ef/d24a7c6772d9ec554d12b97275ee5c8461c90dd73ccd1b364cf586018bb1/os_service_types-1.8.0-py3-none-any.whl", hash = "sha256:bc0418bf826de1639c7f54b2c752827ea9aa91cbde560d0b0bf6339d97270b3b", size = 24717, upload-time = "2025-07-08T09:03:42.457Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "25.0"
+source = { registry = "https://pypi.org/simple" }
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "parse"
+version = "1.20.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4f/78/d9b09ba24bb36ef8b83b71be547e118d46214735b6dfb39e4bfde0e9b9dd/parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce", size = 29391, upload-time = "2024-06-11T04:41:57.34Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d0/31/ba45bf0b2aa7898d81cbbfac0e88c267befb59ad91a19e36e1bc5578ddb1/parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558", size = 20126, upload-time = "2024-06-11T04:41:55.057Z" },
+]
+
+[[package]]
+name = "pathable"
+version = "0.4.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124, upload-time = "2025-01-10T18:43:13.247Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" },
+]
+
+[[package]]
+name = "pbr"
+version = "6.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "setuptools" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/01/d2/510cc0d218e753ba62a1bc1434651db3cd797a9716a0a66cc714cb4f0935/pbr-6.1.1.tar.gz", hash = "sha256:93ea72ce6989eb2eed99d0f75721474f69ad88128afdef5ac377eb797c4bf76b", size = 125702, upload-time = "2025-02-04T14:28:06.514Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/47/ac/684d71315abc7b1214d59304e23a982472967f6bf4bde5a98f1503f648dc/pbr-6.1.1-py2.py3-none-any.whl", hash = "sha256:38d4daea5d9fa63b3f626131b9d34947fd0c8be9b05a29276870580050a25a76", size = 108997, upload-time = "2025-02-04T14:28:03.168Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.3.8"
+source = { registry = "https://pypi.org/simple" }
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "pre-commit"
+version = "4.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cfgv" },
+ { name = "identify" },
+ { name = "nodeenv" },
+ { name = "pyyaml" },
+ { name = "virtualenv" },
+]
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "psutil"
+version = "7.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" },
+ { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" },
+ { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" },
+]
+
+[[package]]
+name = "pycparser"
+version = "2.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.11.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" },
+]
+
+[package.optional-dependencies]
+email = [
+ { name = "email-validator" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.33.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+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" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" },
+ { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" },
+ { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" },
+ { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" },
+ { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" },
+ { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" },
+ { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" },
+ { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" },
+ { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+]
+
+[[package]]
+name = "pydantic-settings"
+version = "2.10.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "pyperclip"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/30/23/2f0a3efc4d6a32f3b63cdff36cd398d9701d26cda58e3ab97ac79fb5e60d/pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310", size = 20961, upload-time = "2024-06-18T20:38:48.401Z" }
+
+[[package]]
+name = "pytest"
+version = "8.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
+]
+
+[[package]]
+name = "python-multipart"
+version = "0.0.20"
+source = { registry = "https://pypi.org/simple" }
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "python-openstackmcp-server"
+source = { editable = "." }
+dependencies = [
+ { name = "fastmcp" },
+ { name = "openstacksdk" },
+ { name = "pydantic" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "pre-commit" },
+ { name = "ruff" },
+ { name = "setuptools-scm" },
+]
+test = [
+ { name = "pytest" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "fastmcp", specifier = ">=2.11.3" },
+ { name = "openstacksdk", specifier = ">=4.6.0" },
+ { name = "pydantic", specifier = ">=2.11.7" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "pre-commit", specifier = ">=4.2.0" },
+ { name = "ruff", specifier = ">=0.12.5" },
+ { name = "setuptools-scm", specifier = ">=9.2.0" },
+]
+test = [{ name = "pytest", specifier = ">=8.4.1" }]
+
+[[package]]
+name = "pywin32"
+version = "311"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" },
+ { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" },
+ { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" },
+ { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" },
+ { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
+ { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.2"
+source = { registry = "https://pypi.org/simple" }
+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" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" },
+ { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" },
+ { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+ { 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" },
+]
+
+[[package]]
+name = "referencing"
+version = "0.36.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
+]
+
+[[package]]
+name = "requestsexceptions"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/ed/61b9652d3256503c99b0b8f145d9c8aa24c514caff6efc229989505937c1/requestsexceptions-1.4.0.tar.gz", hash = "sha256:b095cbc77618f066d459a02b137b020c37da9f46d9b057704019c9f77dba3065", size = 6880, upload-time = "2018-02-01T17:04:45.294Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/01/8c/49ca60ea8c907260da4662582c434bec98716177674e88df3fd340acf06d/requestsexceptions-1.4.0-py2.py3-none-any.whl", hash = "sha256:3083d872b6e07dc5c323563ef37671d992214ad9a32b0ca4a3d7f5500bf38ce3", size = 3802, upload-time = "2018-02-01T17:04:39.07Z" },
+]
+
+[[package]]
+name = "rfc3339-validator"
+version = "0.1.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" },
+]
+
+[[package]]
+name = "rich"
+version = "14.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" },
+]
+
+[[package]]
+name = "rich-rst"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "docutils" },
+ { name = "rich" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b0/69/5514c3a87b5f10f09a34bb011bc0927bc12c596c8dae5915604e71abc386/rich_rst-1.3.1.tar.gz", hash = "sha256:fad46e3ba42785ea8c1785e2ceaa56e0ffa32dbe5410dec432f37e4107c4f383", size = 13839, upload-time = "2024-04-30T04:40:38.125Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/bc/cc4e3dbc5e7992398dcb7a8eda0cbcf4fb792a0cdb93f857b478bf3cf884/rich_rst-1.3.1-py3-none-any.whl", hash = "sha256:498a74e3896507ab04492d326e794c3ef76e7cda078703aa592d1853d91098c1", size = 11621, upload-time = "2024-04-30T04:40:32.619Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "0.26.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a5/aa/4456d84bbb54adc6a916fb10c9b374f78ac840337644e4a5eda229c81275/rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0", size = 27385, upload-time = "2025-07-01T15:57:13.958Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b9/31/1459645f036c3dfeacef89e8e5825e430c77dde8489f3b99eaafcd4a60f5/rpds_py-0.26.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4c70c70f9169692b36307a95f3d8c0a9fcd79f7b4a383aad5eaa0e9718b79b37", size = 372466, upload-time = "2025-07-01T15:53:40.55Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/ff/3d0727f35836cc8773d3eeb9a46c40cc405854e36a8d2e951f3a8391c976/rpds_py-0.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:777c62479d12395bfb932944e61e915741e364c843afc3196b694db3d669fcd0", size = 357825, upload-time = "2025-07-01T15:53:42.247Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/ce/badc5e06120a54099ae287fa96d82cbb650a5f85cf247ffe19c7b157fd1f/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec671691e72dff75817386aa02d81e708b5a7ec0dec6669ec05213ff6b77e1bd", size = 381530, upload-time = "2025-07-01T15:53:43.585Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/a5/fa5d96a66c95d06c62d7a30707b6a4cfec696ab8ae280ee7be14e961e118/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a1cb5d6ce81379401bbb7f6dbe3d56de537fb8235979843f0d53bc2e9815a79", size = 396933, upload-time = "2025-07-01T15:53:45.78Z" },
+ { url = "https://files.pythonhosted.org/packages/00/a7/7049d66750f18605c591a9db47d4a059e112a0c9ff8de8daf8fa0f446bba/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f789e32fa1fb6a7bf890e0124e7b42d1e60d28ebff57fe806719abb75f0e9a3", size = 513973, upload-time = "2025-07-01T15:53:47.085Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/f1/528d02c7d6b29d29fac8fd784b354d3571cc2153f33f842599ef0cf20dd2/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c55b0a669976cf258afd718de3d9ad1b7d1fe0a91cd1ab36f38b03d4d4aeaaf", size = 402293, upload-time = "2025-07-01T15:53:48.117Z" },
+ { url = "https://files.pythonhosted.org/packages/15/93/fde36cd6e4685df2cd08508f6c45a841e82f5bb98c8d5ecf05649522acb5/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70d9ec912802ecfd6cd390dadb34a9578b04f9bcb8e863d0a7598ba5e9e7ccc", size = 383787, upload-time = "2025-07-01T15:53:50.874Z" },
+ { url = "https://files.pythonhosted.org/packages/69/f2/5007553aaba1dcae5d663143683c3dfd03d9395289f495f0aebc93e90f24/rpds_py-0.26.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3021933c2cb7def39d927b9862292e0f4c75a13d7de70eb0ab06efed4c508c19", size = 416312, upload-time = "2025-07-01T15:53:52.046Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/a7/ce52c75c1e624a79e48a69e611f1c08844564e44c85db2b6f711d76d10ce/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a7898b6ca3b7d6659e55cdac825a2e58c638cbf335cde41f4619e290dd0ad11", size = 558403, upload-time = "2025-07-01T15:53:53.192Z" },
+ { url = "https://files.pythonhosted.org/packages/79/d5/e119db99341cc75b538bf4cb80504129fa22ce216672fb2c28e4a101f4d9/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:12bff2ad9447188377f1b2794772f91fe68bb4bbfa5a39d7941fbebdbf8c500f", size = 588323, upload-time = "2025-07-01T15:53:54.336Z" },
+ { url = "https://files.pythonhosted.org/packages/93/94/d28272a0b02f5fe24c78c20e13bbcb95f03dc1451b68e7830ca040c60bd6/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:191aa858f7d4902e975d4cf2f2d9243816c91e9605070aeb09c0a800d187e323", size = 554541, upload-time = "2025-07-01T15:53:55.469Z" },
+ { url = "https://files.pythonhosted.org/packages/93/e0/8c41166602f1b791da892d976057eba30685486d2e2c061ce234679c922b/rpds_py-0.26.0-cp310-cp310-win32.whl", hash = "sha256:b37a04d9f52cb76b6b78f35109b513f6519efb481d8ca4c321f6a3b9580b3f45", size = 220442, upload-time = "2025-07-01T15:53:56.524Z" },
+ { url = "https://files.pythonhosted.org/packages/87/f0/509736bb752a7ab50fb0270c2a4134d671a7b3038030837e5536c3de0e0b/rpds_py-0.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:38721d4c9edd3eb6670437d8d5e2070063f305bfa2d5aa4278c51cedcd508a84", size = 231314, upload-time = "2025-07-01T15:53:57.842Z" },
+ { url = "https://files.pythonhosted.org/packages/09/4c/4ee8f7e512030ff79fda1df3243c88d70fc874634e2dbe5df13ba4210078/rpds_py-0.26.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9e8cb77286025bdb21be2941d64ac6ca016130bfdcd228739e8ab137eb4406ed", size = 372610, upload-time = "2025-07-01T15:53:58.844Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/9d/3dc16be00f14fc1f03c71b1d67c8df98263ab2710a2fbd65a6193214a527/rpds_py-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e09330b21d98adc8ccb2dbb9fc6cb434e8908d4c119aeaa772cb1caab5440a0", size = 358032, upload-time = "2025-07-01T15:53:59.985Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/5a/7f1bf8f045da2866324a08ae80af63e64e7bfaf83bd31f865a7b91a58601/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9c1b92b774b2e68d11193dc39620d62fd8ab33f0a3c77ecdabe19c179cdbc1", size = 381525, upload-time = "2025-07-01T15:54:01.162Z" },
+ { url = "https://files.pythonhosted.org/packages/45/8a/04479398c755a066ace10e3d158866beb600867cacae194c50ffa783abd0/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:824e6d3503ab990d7090768e4dfd9e840837bae057f212ff9f4f05ec6d1975e7", size = 397089, upload-time = "2025-07-01T15:54:02.319Z" },
+ { url = "https://files.pythonhosted.org/packages/72/88/9203f47268db488a1b6d469d69c12201ede776bb728b9d9f29dbfd7df406/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ad7fd2258228bf288f2331f0a6148ad0186b2e3643055ed0db30990e59817a6", size = 514255, upload-time = "2025-07-01T15:54:03.38Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/b4/01ce5d1e853ddf81fbbd4311ab1eff0b3cf162d559288d10fd127e2588b5/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dc23bbb3e06ec1ea72d515fb572c1fea59695aefbffb106501138762e1e915e", size = 402283, upload-time = "2025-07-01T15:54:04.923Z" },
+ { url = "https://files.pythonhosted.org/packages/34/a2/004c99936997bfc644d590a9defd9e9c93f8286568f9c16cdaf3e14429a7/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d80bf832ac7b1920ee29a426cdca335f96a2b5caa839811803e999b41ba9030d", size = 383881, upload-time = "2025-07-01T15:54:06.482Z" },
+ { url = "https://files.pythonhosted.org/packages/05/1b/ef5fba4a8f81ce04c427bfd96223f92f05e6cd72291ce9d7523db3b03a6c/rpds_py-0.26.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0919f38f5542c0a87e7b4afcafab6fd2c15386632d249e9a087498571250abe3", size = 415822, upload-time = "2025-07-01T15:54:07.605Z" },
+ { url = "https://files.pythonhosted.org/packages/16/80/5c54195aec456b292f7bd8aa61741c8232964063fd8a75fdde9c1e982328/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d422b945683e409000c888e384546dbab9009bb92f7c0b456e217988cf316107", size = 558347, upload-time = "2025-07-01T15:54:08.591Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/1c/1845c1b1fd6d827187c43afe1841d91678d7241cbdb5420a4c6de180a538/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a7711fa562ba2da1aa757e11024ad6d93bad6ad7ede5afb9af144623e5f76a", size = 587956, upload-time = "2025-07-01T15:54:09.963Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/ff/9e979329dd131aa73a438c077252ddabd7df6d1a7ad7b9aacf6261f10faa/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238e8c8610cb7c29460e37184f6799547f7e09e6a9bdbdab4e8edb90986a2318", size = 554363, upload-time = "2025-07-01T15:54:11.073Z" },
+ { url = "https://files.pythonhosted.org/packages/00/8b/d78cfe034b71ffbe72873a136e71acc7a831a03e37771cfe59f33f6de8a2/rpds_py-0.26.0-cp311-cp311-win32.whl", hash = "sha256:893b022bfbdf26d7bedb083efeea624e8550ca6eb98bf7fea30211ce95b9201a", size = 220123, upload-time = "2025-07-01T15:54:12.382Z" },
+ { url = "https://files.pythonhosted.org/packages/94/c1/3c8c94c7dd3905dbfde768381ce98778500a80db9924731d87ddcdb117e9/rpds_py-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:87a5531de9f71aceb8af041d72fc4cab4943648d91875ed56d2e629bef6d4c03", size = 231732, upload-time = "2025-07-01T15:54:13.434Z" },
+ { url = "https://files.pythonhosted.org/packages/67/93/e936fbed1b734eabf36ccb5d93c6a2e9246fbb13c1da011624b7286fae3e/rpds_py-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:de2713f48c1ad57f89ac25b3cb7daed2156d8e822cf0eca9b96a6f990718cc41", size = 221917, upload-time = "2025-07-01T15:54:14.559Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/86/90eb87c6f87085868bd077c7a9938006eb1ce19ed4d06944a90d3560fce2/rpds_py-0.26.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:894514d47e012e794f1350f076c427d2347ebf82f9b958d554d12819849a369d", size = 363933, upload-time = "2025-07-01T15:54:15.734Z" },
+ { url = "https://files.pythonhosted.org/packages/63/78/4469f24d34636242c924626082b9586f064ada0b5dbb1e9d096ee7a8e0c6/rpds_py-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc921b96fa95a097add244da36a1d9e4f3039160d1d30f1b35837bf108c21136", size = 350447, upload-time = "2025-07-01T15:54:16.922Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/91/c448ed45efdfdade82348d5e7995e15612754826ea640afc20915119734f/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e1157659470aa42a75448b6e943c895be8c70531c43cb78b9ba990778955582", size = 384711, upload-time = "2025-07-01T15:54:18.101Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/43/e5c86fef4be7f49828bdd4ecc8931f0287b1152c0bb0163049b3218740e7/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:521ccf56f45bb3a791182dc6b88ae5f8fa079dd705ee42138c76deb1238e554e", size = 400865, upload-time = "2025-07-01T15:54:19.295Z" },
+ { url = "https://files.pythonhosted.org/packages/55/34/e00f726a4d44f22d5c5fe2e5ddd3ac3d7fd3f74a175607781fbdd06fe375/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9def736773fd56b305c0eef698be5192c77bfa30d55a0e5885f80126c4831a15", size = 517763, upload-time = "2025-07-01T15:54:20.858Z" },
+ { url = "https://files.pythonhosted.org/packages/52/1c/52dc20c31b147af724b16104500fba13e60123ea0334beba7b40e33354b4/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdad4ea3b4513b475e027be79e5a0ceac8ee1c113a1a11e5edc3c30c29f964d8", size = 406651, upload-time = "2025-07-01T15:54:22.508Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/77/87d7bfabfc4e821caa35481a2ff6ae0b73e6a391bb6b343db2c91c2b9844/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82b165b07f416bdccf5c84546a484cc8f15137ca38325403864bfdf2b5b72f6a", size = 386079, upload-time = "2025-07-01T15:54:23.987Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/d4/7f2200c2d3ee145b65b3cddc4310d51f7da6a26634f3ac87125fd789152a/rpds_py-0.26.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d04cab0a54b9dba4d278fe955a1390da3cf71f57feb78ddc7cb67cbe0bd30323", size = 421379, upload-time = "2025-07-01T15:54:25.073Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/13/9fdd428b9c820869924ab62236b8688b122baa22d23efdd1c566938a39ba/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:79061ba1a11b6a12743a2b0f72a46aa2758613d454aa6ba4f5a265cc48850158", size = 562033, upload-time = "2025-07-01T15:54:26.225Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/e1/b69686c3bcbe775abac3a4c1c30a164a2076d28df7926041f6c0eb5e8d28/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f405c93675d8d4c5ac87364bb38d06c988e11028a64b52a47158a355079661f3", size = 591639, upload-time = "2025-07-01T15:54:27.424Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/c9/1e3d8c8863c84a90197ac577bbc3d796a92502124c27092413426f670990/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dafd4c44b74aa4bed4b250f1aed165b8ef5de743bcca3b88fc9619b6087093d2", size = 557105, upload-time = "2025-07-01T15:54:29.93Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/c5/90c569649057622959f6dcc40f7b516539608a414dfd54b8d77e3b201ac0/rpds_py-0.26.0-cp312-cp312-win32.whl", hash = "sha256:3da5852aad63fa0c6f836f3359647870e21ea96cf433eb393ffa45263a170d44", size = 223272, upload-time = "2025-07-01T15:54:31.128Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/16/19f5d9f2a556cfed454eebe4d354c38d51c20f3db69e7b4ce6cff904905d/rpds_py-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf47cfdabc2194a669dcf7a8dbba62e37a04c5041d2125fae0233b720da6f05c", size = 234995, upload-time = "2025-07-01T15:54:32.195Z" },
+ { url = "https://files.pythonhosted.org/packages/83/f0/7935e40b529c0e752dfaa7880224771b51175fce08b41ab4a92eb2fbdc7f/rpds_py-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:20ab1ae4fa534f73647aad289003f1104092890849e0266271351922ed5574f8", size = 223198, upload-time = "2025-07-01T15:54:33.271Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/67/bb62d0109493b12b1c6ab00de7a5566aa84c0e44217c2d94bee1bd370da9/rpds_py-0.26.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:696764a5be111b036256c0b18cd29783fab22154690fc698062fc1b0084b511d", size = 363917, upload-time = "2025-07-01T15:54:34.755Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/f3/34e6ae1925a5706c0f002a8d2d7f172373b855768149796af87bd65dcdb9/rpds_py-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6c15d2080a63aaed876e228efe4f814bc7889c63b1e112ad46fdc8b368b9e1", size = 350073, upload-time = "2025-07-01T15:54:36.292Z" },
+ { url = "https://files.pythonhosted.org/packages/75/83/1953a9d4f4e4de7fd0533733e041c28135f3c21485faaef56a8aadbd96b5/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390e3170babf42462739a93321e657444f0862c6d722a291accc46f9d21ed04e", size = 384214, upload-time = "2025-07-01T15:54:37.469Z" },
+ { url = "https://files.pythonhosted.org/packages/48/0e/983ed1b792b3322ea1d065e67f4b230f3b96025f5ce3878cc40af09b7533/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7da84c2c74c0f5bc97d853d9e17bb83e2dcafcff0dc48286916001cc114379a1", size = 400113, upload-time = "2025-07-01T15:54:38.954Z" },
+ { url = "https://files.pythonhosted.org/packages/69/7f/36c0925fff6f660a80be259c5b4f5e53a16851f946eb080351d057698528/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c5fe114a6dd480a510b6d3661d09d67d1622c4bf20660a474507aaee7eeeee9", size = 515189, upload-time = "2025-07-01T15:54:40.57Z" },
+ { url = "https://files.pythonhosted.org/packages/13/45/cbf07fc03ba7a9b54662c9badb58294ecfb24f828b9732970bd1a431ed5c/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3100b3090269f3a7ea727b06a6080d4eb7439dca4c0e91a07c5d133bb1727ea7", size = 406998, upload-time = "2025-07-01T15:54:43.025Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/b0/8fa5e36e58657997873fd6a1cf621285ca822ca75b4b3434ead047daa307/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c03c9b0c64afd0320ae57de4c982801271c0c211aa2d37f3003ff5feb75bb04", size = 385903, upload-time = "2025-07-01T15:54:44.752Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/f7/b25437772f9f57d7a9fbd73ed86d0dcd76b4c7c6998348c070d90f23e315/rpds_py-0.26.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5963b72ccd199ade6ee493723d18a3f21ba7d5b957017607f815788cef50eaf1", size = 419785, upload-time = "2025-07-01T15:54:46.043Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/6b/63ffa55743dfcb4baf2e9e77a0b11f7f97ed96a54558fcb5717a4b2cd732/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da4e873860ad5bab3291438525cae80169daecbfafe5657f7f5fb4d6b3f96b9", size = 561329, upload-time = "2025-07-01T15:54:47.64Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/07/1f4f5e2886c480a2346b1e6759c00278b8a69e697ae952d82ae2e6ee5db0/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5afaddaa8e8c7f1f7b4c5c725c0070b6eed0228f705b90a1732a48e84350f4e9", size = 590875, upload-time = "2025-07-01T15:54:48.9Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/bc/e6639f1b91c3a55f8c41b47d73e6307051b6e246254a827ede730624c0f8/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4916dc96489616a6f9667e7526af8fa693c0fdb4f3acb0e5d9f4400eb06a47ba", size = 556636, upload-time = "2025-07-01T15:54:50.619Z" },
+ { url = "https://files.pythonhosted.org/packages/05/4c/b3917c45566f9f9a209d38d9b54a1833f2bb1032a3e04c66f75726f28876/rpds_py-0.26.0-cp313-cp313-win32.whl", hash = "sha256:2a343f91b17097c546b93f7999976fd6c9d5900617aa848c81d794e062ab302b", size = 222663, upload-time = "2025-07-01T15:54:52.023Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/0b/0851bdd6025775aaa2365bb8de0697ee2558184c800bfef8d7aef5ccde58/rpds_py-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:0a0b60701f2300c81b2ac88a5fb893ccfa408e1c4a555a77f908a2596eb875a5", size = 234428, upload-time = "2025-07-01T15:54:53.692Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/e8/a47c64ed53149c75fb581e14a237b7b7cd18217e969c30d474d335105622/rpds_py-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:257d011919f133a4746958257f2c75238e3ff54255acd5e3e11f3ff41fd14256", size = 222571, upload-time = "2025-07-01T15:54:54.822Z" },
+ { url = "https://files.pythonhosted.org/packages/89/bf/3d970ba2e2bcd17d2912cb42874107390f72873e38e79267224110de5e61/rpds_py-0.26.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:529c8156d7506fba5740e05da8795688f87119cce330c244519cf706a4a3d618", size = 360475, upload-time = "2025-07-01T15:54:56.228Z" },
+ { url = "https://files.pythonhosted.org/packages/82/9f/283e7e2979fc4ec2d8ecee506d5a3675fce5ed9b4b7cb387ea5d37c2f18d/rpds_py-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f53ec51f9d24e9638a40cabb95078ade8c99251945dad8d57bf4aabe86ecee35", size = 346692, upload-time = "2025-07-01T15:54:58.561Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/03/7e50423c04d78daf391da3cc4330bdb97042fc192a58b186f2d5deb7befd/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab504c4d654e4a29558eaa5bb8cea5fdc1703ea60a8099ffd9c758472cf913f", size = 379415, upload-time = "2025-07-01T15:54:59.751Z" },
+ { url = "https://files.pythonhosted.org/packages/57/00/d11ee60d4d3b16808432417951c63df803afb0e0fc672b5e8d07e9edaaae/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd0641abca296bc1a00183fe44f7fced8807ed49d501f188faa642d0e4975b83", size = 391783, upload-time = "2025-07-01T15:55:00.898Z" },
+ { url = "https://files.pythonhosted.org/packages/08/b3/1069c394d9c0d6d23c5b522e1f6546b65793a22950f6e0210adcc6f97c3e/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b312fecc1d017b5327afa81d4da1480f51c68810963a7336d92203dbb3d4f1", size = 512844, upload-time = "2025-07-01T15:55:02.201Z" },
+ { url = "https://files.pythonhosted.org/packages/08/3b/c4fbf0926800ed70b2c245ceca99c49f066456755f5d6eb8863c2c51e6d0/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c741107203954f6fc34d3066d213d0a0c40f7bb5aafd698fb39888af277c70d8", size = 402105, upload-time = "2025-07-01T15:55:03.698Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/b0/db69b52ca07413e568dae9dc674627a22297abb144c4d6022c6d78f1e5cc/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3e55a7db08dc9a6ed5fb7103019d2c1a38a349ac41901f9f66d7f95750942f", size = 383440, upload-time = "2025-07-01T15:55:05.398Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/e1/c65255ad5b63903e56b3bb3ff9dcc3f4f5c3badde5d08c741ee03903e951/rpds_py-0.26.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e851920caab2dbcae311fd28f4313c6953993893eb5c1bb367ec69d9a39e7ed", size = 412759, upload-time = "2025-07-01T15:55:08.316Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/22/bb731077872377a93c6e93b8a9487d0406c70208985831034ccdeed39c8e/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dfbf280da5f876d0b00c81f26bedce274e72a678c28845453885a9b3c22ae632", size = 556032, upload-time = "2025-07-01T15:55:09.52Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/8b/393322ce7bac5c4530fb96fc79cc9ea2f83e968ff5f6e873f905c493e1c4/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1cc81d14ddfa53d7f3906694d35d54d9d3f850ef8e4e99ee68bc0d1e5fed9a9c", size = 585416, upload-time = "2025-07-01T15:55:11.216Z" },
+ { url = "https://files.pythonhosted.org/packages/49/ae/769dc372211835bf759319a7aae70525c6eb523e3371842c65b7ef41c9c6/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dca83c498b4650a91efcf7b88d669b170256bf8017a5db6f3e06c2bf031f57e0", size = 554049, upload-time = "2025-07-01T15:55:13.004Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/f9/4c43f9cc203d6ba44ce3146246cdc38619d92c7bd7bad4946a3491bd5b70/rpds_py-0.26.0-cp313-cp313t-win32.whl", hash = "sha256:4d11382bcaf12f80b51d790dee295c56a159633a8e81e6323b16e55d81ae37e9", size = 218428, upload-time = "2025-07-01T15:55:14.486Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/8b/9286b7e822036a4a977f2f1e851c7345c20528dbd56b687bb67ed68a8ede/rpds_py-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff110acded3c22c033e637dd8896e411c7d3a11289b2edf041f86663dbc791e9", size = 231524, upload-time = "2025-07-01T15:55:15.745Z" },
+ { url = "https://files.pythonhosted.org/packages/55/07/029b7c45db910c74e182de626dfdae0ad489a949d84a468465cd0ca36355/rpds_py-0.26.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:da619979df60a940cd434084355c514c25cf8eb4cf9a508510682f6c851a4f7a", size = 364292, upload-time = "2025-07-01T15:55:17.001Z" },
+ { url = "https://files.pythonhosted.org/packages/13/d1/9b3d3f986216b4d1f584878dca15ce4797aaf5d372d738974ba737bf68d6/rpds_py-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ea89a2458a1a75f87caabefe789c87539ea4e43b40f18cff526052e35bbb4fdf", size = 350334, upload-time = "2025-07-01T15:55:18.922Z" },
+ { url = "https://files.pythonhosted.org/packages/18/98/16d5e7bc9ec715fa9668731d0cf97f6b032724e61696e2db3d47aeb89214/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feac1045b3327a45944e7dcbeb57530339f6b17baff154df51ef8b0da34c8c12", size = 384875, upload-time = "2025-07-01T15:55:20.399Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/13/aa5e2b1ec5ab0e86a5c464d53514c0467bec6ba2507027d35fc81818358e/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b818a592bd69bfe437ee8368603d4a2d928c34cffcdf77c2e761a759ffd17d20", size = 399993, upload-time = "2025-07-01T15:55:21.729Z" },
+ { url = "https://files.pythonhosted.org/packages/17/03/8021810b0e97923abdbab6474c8b77c69bcb4b2c58330777df9ff69dc559/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a8b0dd8648709b62d9372fc00a57466f5fdeefed666afe3fea5a6c9539a0331", size = 516683, upload-time = "2025-07-01T15:55:22.918Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/b1/da8e61c87c2f3d836954239fdbbfb477bb7b54d74974d8f6fcb34342d166/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d3498ad0df07d81112aa6ec6c95a7e7b1ae00929fb73e7ebee0f3faaeabad2f", size = 408825, upload-time = "2025-07-01T15:55:24.207Z" },
+ { url = "https://files.pythonhosted.org/packages/38/bc/1fc173edaaa0e52c94b02a655db20697cb5fa954ad5a8e15a2c784c5cbdd/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4146ccb15be237fdef10f331c568e1b0e505f8c8c9ed5d67759dac58ac246", size = 387292, upload-time = "2025-07-01T15:55:25.554Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/eb/3a9bb4bd90867d21916f253caf4f0d0be7098671b6715ad1cead9fe7bab9/rpds_py-0.26.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a9a63785467b2d73635957d32a4f6e73d5e4df497a16a6392fa066b753e87387", size = 420435, upload-time = "2025-07-01T15:55:27.798Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/16/e066dcdb56f5632713445271a3f8d3d0b426d51ae9c0cca387799df58b02/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:de4ed93a8c91debfd5a047be327b7cc8b0cc6afe32a716bbbc4aedca9e2a83af", size = 562410, upload-time = "2025-07-01T15:55:29.057Z" },
+ { url = "https://files.pythonhosted.org/packages/60/22/ddbdec7eb82a0dc2e455be44c97c71c232983e21349836ce9f272e8a3c29/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:caf51943715b12af827696ec395bfa68f090a4c1a1d2509eb4e2cb69abbbdb33", size = 590724, upload-time = "2025-07-01T15:55:30.719Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/b4/95744085e65b7187d83f2fcb0bef70716a1ea0a9e5d8f7f39a86e5d83424/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4a59e5bc386de021f56337f757301b337d7ab58baa40174fb150accd480bc953", size = 558285, upload-time = "2025-07-01T15:55:31.981Z" },
+ { url = "https://files.pythonhosted.org/packages/37/37/6309a75e464d1da2559446f9c811aa4d16343cebe3dbb73701e63f760caa/rpds_py-0.26.0-cp314-cp314-win32.whl", hash = "sha256:92c8db839367ef16a662478f0a2fe13e15f2227da3c1430a782ad0f6ee009ec9", size = 223459, upload-time = "2025-07-01T15:55:33.312Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/6f/8e9c11214c46098b1d1391b7e02b70bb689ab963db3b19540cba17315291/rpds_py-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:b0afb8cdd034150d4d9f53926226ed27ad15b7f465e93d7468caaf5eafae0d37", size = 236083, upload-time = "2025-07-01T15:55:34.933Z" },
+ { url = "https://files.pythonhosted.org/packages/47/af/9c4638994dd623d51c39892edd9d08e8be8220a4b7e874fa02c2d6e91955/rpds_py-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:ca3f059f4ba485d90c8dc75cb5ca897e15325e4e609812ce57f896607c1c0867", size = 223291, upload-time = "2025-07-01T15:55:36.202Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/db/669a241144460474aab03e254326b32c42def83eb23458a10d163cb9b5ce/rpds_py-0.26.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5afea17ab3a126006dc2f293b14ffc7ef3c85336cf451564a0515ed7648033da", size = 361445, upload-time = "2025-07-01T15:55:37.483Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/2d/133f61cc5807c6c2fd086a46df0eb8f63a23f5df8306ff9f6d0fd168fecc/rpds_py-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:69f0c0a3df7fd3a7eec50a00396104bb9a843ea6d45fcc31c2d5243446ffd7a7", size = 347206, upload-time = "2025-07-01T15:55:38.828Z" },
+ { url = "https://files.pythonhosted.org/packages/05/bf/0e8fb4c05f70273469eecf82f6ccf37248558526a45321644826555db31b/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:801a71f70f9813e82d2513c9a96532551fce1e278ec0c64610992c49c04c2dad", size = 380330, upload-time = "2025-07-01T15:55:40.175Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/a8/060d24185d8b24d3923322f8d0ede16df4ade226a74e747b8c7c978e3dd3/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df52098cde6d5e02fa75c1f6244f07971773adb4a26625edd5c18fee906fa84d", size = 392254, upload-time = "2025-07-01T15:55:42.015Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/7b/7c2e8a9ee3e6bc0bae26bf29f5219955ca2fbb761dca996a83f5d2f773fe/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bc596b30f86dc6f0929499c9e574601679d0341a0108c25b9b358a042f51bca", size = 516094, upload-time = "2025-07-01T15:55:43.603Z" },
+ { url = "https://files.pythonhosted.org/packages/75/d6/f61cafbed8ba1499b9af9f1777a2a199cd888f74a96133d8833ce5eaa9c5/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9dfbe56b299cf5875b68eb6f0ebaadc9cac520a1989cac0db0765abfb3709c19", size = 402889, upload-time = "2025-07-01T15:55:45.275Z" },
+ { url = "https://files.pythonhosted.org/packages/92/19/c8ac0a8a8df2dd30cdec27f69298a5c13e9029500d6d76718130f5e5be10/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac64f4b2bdb4ea622175c9ab7cf09444e412e22c0e02e906978b3b488af5fde8", size = 384301, upload-time = "2025-07-01T15:55:47.098Z" },
+ { url = "https://files.pythonhosted.org/packages/41/e1/6b1859898bc292a9ce5776016c7312b672da00e25cec74d7beced1027286/rpds_py-0.26.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:181ef9b6bbf9845a264f9aa45c31836e9f3c1f13be565d0d010e964c661d1e2b", size = 412891, upload-time = "2025-07-01T15:55:48.412Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/b9/ceb39af29913c07966a61367b3c08b4f71fad841e32c6b59a129d5974698/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:49028aa684c144ea502a8e847d23aed5e4c2ef7cadfa7d5eaafcb40864844b7a", size = 557044, upload-time = "2025-07-01T15:55:49.816Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/27/35637b98380731a521f8ec4f3fd94e477964f04f6b2f8f7af8a2d889a4af/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e5d524d68a474a9688336045bbf76cb0def88549c1b2ad9dbfec1fb7cfbe9170", size = 585774, upload-time = "2025-07-01T15:55:51.192Z" },
+ { url = "https://files.pythonhosted.org/packages/52/d9/3f0f105420fecd18551b678c9a6ce60bd23986098b252a56d35781b3e7e9/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1851f429b822831bd2edcbe0cfd12ee9ea77868f8d3daf267b189371671c80e", size = 554886, upload-time = "2025-07-01T15:55:52.541Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/c5/347c056a90dc8dd9bc240a08c527315008e1b5042e7a4cf4ac027be9d38a/rpds_py-0.26.0-cp314-cp314t-win32.whl", hash = "sha256:7bdb17009696214c3b66bb3590c6d62e14ac5935e53e929bcdbc5a495987a84f", size = 219027, upload-time = "2025-07-01T15:55:53.874Z" },
+ { url = "https://files.pythonhosted.org/packages/75/04/5302cea1aa26d886d34cadbf2dc77d90d7737e576c0065f357b96dc7a1a6/rpds_py-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f14440b9573a6f76b4ee4770c13f0b5921f71dde3b6fcb8dabbefd13b7fe05d7", size = 232821, upload-time = "2025-07-01T15:55:55.167Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/9a/1f033b0b31253d03d785b0cd905bc127e555ab496ea6b4c7c2e1f951f2fd/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3c0909c5234543ada2515c05dc08595b08d621ba919629e94427e8e03539c958", size = 373226, upload-time = "2025-07-01T15:56:16.578Z" },
+ { url = "https://files.pythonhosted.org/packages/58/29/5f88023fd6aaaa8ca3c4a6357ebb23f6f07da6079093ccf27c99efce87db/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c1fb0cda2abcc0ac62f64e2ea4b4e64c57dfd6b885e693095460c61bde7bb18e", size = 359230, upload-time = "2025-07-01T15:56:17.978Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/6c/13eaebd28b439da6964dde22712b52e53fe2824af0223b8e403249d10405/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84d142d2d6cf9b31c12aa4878d82ed3b2324226270b89b676ac62ccd7df52d08", size = 382363, upload-time = "2025-07-01T15:56:19.977Z" },
+ { url = "https://files.pythonhosted.org/packages/55/fc/3bb9c486b06da19448646f96147796de23c5811ef77cbfc26f17307b6a9d/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a547e21c5610b7e9093d870be50682a6a6cf180d6da0f42c47c306073bfdbbf6", size = 397146, upload-time = "2025-07-01T15:56:21.39Z" },
+ { url = "https://files.pythonhosted.org/packages/15/18/9d1b79eb4d18e64ba8bba9e7dec6f9d6920b639f22f07ee9368ca35d4673/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35e9a70a0f335371275cdcd08bc5b8051ac494dd58bff3bbfb421038220dc871", size = 514804, upload-time = "2025-07-01T15:56:22.78Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/5a/175ad7191bdbcd28785204621b225ad70e85cdfd1e09cc414cb554633b21/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dfa6115c6def37905344d56fb54c03afc49104e2ca473d5dedec0f6606913b4", size = 402820, upload-time = "2025-07-01T15:56:24.584Z" },
+ { url = "https://files.pythonhosted.org/packages/11/45/6a67ecf6d61c4d4aff4bc056e864eec4b2447787e11d1c2c9a0242c6e92a/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:313cfcd6af1a55a286a3c9a25f64af6d0e46cf60bc5798f1db152d97a216ff6f", size = 384567, upload-time = "2025-07-01T15:56:26.064Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/ba/16589da828732b46454c61858950a78fe4c931ea4bf95f17432ffe64b241/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7bf2496fa563c046d05e4d232d7b7fd61346e2402052064b773e5c378bf6f73", size = 416520, upload-time = "2025-07-01T15:56:27.608Z" },
+ { url = "https://files.pythonhosted.org/packages/81/4b/00092999fc7c0c266045e984d56b7314734cc400a6c6dc4d61a35f135a9d/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:aa81873e2c8c5aa616ab8e017a481a96742fdf9313c40f14338ca7dbf50cb55f", size = 559362, upload-time = "2025-07-01T15:56:29.078Z" },
+ { url = "https://files.pythonhosted.org/packages/96/0c/43737053cde1f93ac4945157f7be1428724ab943e2132a0d235a7e161d4e/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:68ffcf982715f5b5b7686bdd349ff75d422e8f22551000c24b30eaa1b7f7ae84", size = 588113, upload-time = "2025-07-01T15:56:30.485Z" },
+ { url = "https://files.pythonhosted.org/packages/46/46/8e38f6161466e60a997ed7e9951ae5de131dedc3cf778ad35994b4af823d/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6188de70e190847bb6db3dc3981cbadff87d27d6fe9b4f0e18726d55795cee9b", size = 555429, upload-time = "2025-07-01T15:56:31.956Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/ac/65da605e9f1dd643ebe615d5bbd11b6efa1d69644fc4bf623ea5ae385a82/rpds_py-0.26.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1c962145c7473723df9722ba4c058de12eb5ebedcb4e27e7d902920aa3831ee8", size = 231950, upload-time = "2025-07-01T15:56:33.337Z" },
+ { url = "https://files.pythonhosted.org/packages/51/f2/b5c85b758a00c513bb0389f8fc8e61eb5423050c91c958cdd21843faa3e6/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f61a9326f80ca59214d1cceb0a09bb2ece5b2563d4e0cd37bfd5515c28510674", size = 373505, upload-time = "2025-07-01T15:56:34.716Z" },
+ { url = "https://files.pythonhosted.org/packages/23/e0/25db45e391251118e915e541995bb5f5ac5691a3b98fb233020ba53afc9b/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:183f857a53bcf4b1b42ef0f57ca553ab56bdd170e49d8091e96c51c3d69ca696", size = 359468, upload-time = "2025-07-01T15:56:36.219Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/73/dd5ee6075bb6491be3a646b301dfd814f9486d924137a5098e61f0487e16/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:941c1cfdf4799d623cf3aa1d326a6b4fdb7a5799ee2687f3516738216d2262fb", size = 382680, upload-time = "2025-07-01T15:56:37.644Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/10/84b522ff58763a5c443f5bcedc1820240e454ce4e620e88520f04589e2ea/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72a8d9564a717ee291f554eeb4bfeafe2309d5ec0aa6c475170bdab0f9ee8e88", size = 397035, upload-time = "2025-07-01T15:56:39.241Z" },
+ { url = "https://files.pythonhosted.org/packages/06/ea/8667604229a10a520fcbf78b30ccc278977dcc0627beb7ea2c96b3becef0/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:511d15193cbe013619dd05414c35a7dedf2088fcee93c6bbb7c77859765bd4e8", size = 514922, upload-time = "2025-07-01T15:56:40.645Z" },
+ { url = "https://files.pythonhosted.org/packages/24/e6/9ed5b625c0661c4882fc8cdf302bf8e96c73c40de99c31e0b95ed37d508c/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aea1f9741b603a8d8fedb0ed5502c2bc0accbc51f43e2ad1337fe7259c2b77a5", size = 402822, upload-time = "2025-07-01T15:56:42.137Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/58/212c7b6fd51946047fb45d3733da27e2fa8f7384a13457c874186af691b1/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4019a9d473c708cf2f16415688ef0b4639e07abaa569d72f74745bbeffafa2c7", size = 384336, upload-time = "2025-07-01T15:56:44.239Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/f5/a40ba78748ae8ebf4934d4b88e77b98497378bc2c24ba55ebe87a4e87057/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:093d63b4b0f52d98ebae33b8c50900d3d67e0666094b1be7a12fffd7f65de74b", size = 416871, upload-time = "2025-07-01T15:56:46.284Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/a6/33b1fc0c9f7dcfcfc4a4353daa6308b3ece22496ceece348b3e7a7559a09/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2abe21d8ba64cded53a2a677e149ceb76dcf44284202d737178afe7ba540c1eb", size = 559439, upload-time = "2025-07-01T15:56:48.549Z" },
+ { url = "https://files.pythonhosted.org/packages/71/2d/ceb3f9c12f8cfa56d34995097f6cd99da1325642c60d1b6680dd9df03ed8/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:4feb7511c29f8442cbbc28149a92093d32e815a28aa2c50d333826ad2a20fdf0", size = 588380, upload-time = "2025-07-01T15:56:50.086Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/ed/9de62c2150ca8e2e5858acf3f4f4d0d180a38feef9fdab4078bea63d8dba/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e99685fc95d386da368013e7fb4269dd39c30d99f812a8372d62f244f662709c", size = 555334, upload-time = "2025-07-01T15:56:51.703Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.12.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/81/0bd3594fa0f690466e41bd033bdcdf86cba8288345ac77ad4afbe5ec743a/ruff-0.12.7.tar.gz", hash = "sha256:1fc3193f238bc2d7968772c82831a4ff69252f673be371fb49663f0068b7ec71", size = 5197814, upload-time = "2025-07-29T22:32:35.877Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e1/d2/6cb35e9c85e7a91e8d22ab32ae07ac39cc34a71f1009a6f9e4a2a019e602/ruff-0.12.7-py3-none-linux_armv6l.whl", hash = "sha256:76e4f31529899b8c434c3c1dede98c4483b89590e15fb49f2d46183801565303", size = 11852189, upload-time = "2025-07-29T22:31:41.281Z" },
+ { url = "https://files.pythonhosted.org/packages/63/5b/a4136b9921aa84638f1a6be7fb086f8cad0fde538ba76bda3682f2599a2f/ruff-0.12.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:789b7a03e72507c54fb3ba6209e4bb36517b90f1a3569ea17084e3fd295500fb", size = 12519389, upload-time = "2025-07-29T22:31:54.265Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/c9/3e24a8472484269b6b1821794141f879c54645a111ded4b6f58f9ab0705f/ruff-0.12.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e1c2a3b8626339bb6369116e7030a4cf194ea48f49b64bb505732a7fce4f4e3", size = 11743384, upload-time = "2025-07-29T22:31:59.575Z" },
+ { url = "https://files.pythonhosted.org/packages/26/7c/458dd25deeb3452c43eaee853c0b17a1e84169f8021a26d500ead77964fd/ruff-0.12.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32dec41817623d388e645612ec70d5757a6d9c035f3744a52c7b195a57e03860", size = 11943759, upload-time = "2025-07-29T22:32:01.95Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/8b/658798472ef260ca050e400ab96ef7e85c366c39cf3dfbef4d0a46a528b6/ruff-0.12.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47ef751f722053a5df5fa48d412dbb54d41ab9b17875c6840a58ec63ff0c247c", size = 11654028, upload-time = "2025-07-29T22:32:04.367Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/86/9c2336f13b2a3326d06d39178fd3448dcc7025f82514d1b15816fe42bfe8/ruff-0.12.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a828a5fc25a3efd3e1ff7b241fd392686c9386f20e5ac90aa9234a5faa12c423", size = 13225209, upload-time = "2025-07-29T22:32:06.952Z" },
+ { url = "https://files.pythonhosted.org/packages/76/69/df73f65f53d6c463b19b6b312fd2391dc36425d926ec237a7ed028a90fc1/ruff-0.12.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5726f59b171111fa6a69d82aef48f00b56598b03a22f0f4170664ff4d8298efb", size = 14182353, upload-time = "2025-07-29T22:32:10.053Z" },
+ { url = "https://files.pythonhosted.org/packages/58/1e/de6cda406d99fea84b66811c189b5ea139814b98125b052424b55d28a41c/ruff-0.12.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74e6f5c04c4dd4aba223f4fe6e7104f79e0eebf7d307e4f9b18c18362124bccd", size = 13631555, upload-time = "2025-07-29T22:32:12.644Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/ae/625d46d5164a6cc9261945a5e89df24457dc8262539ace3ac36c40f0b51e/ruff-0.12.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d0bfe4e77fba61bf2ccadf8cf005d6133e3ce08793bbe870dd1c734f2699a3e", size = 12667556, upload-time = "2025-07-29T22:32:15.312Z" },
+ { url = "https://files.pythonhosted.org/packages/55/bf/9cb1ea5e3066779e42ade8d0cd3d3b0582a5720a814ae1586f85014656b6/ruff-0.12.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06bfb01e1623bf7f59ea749a841da56f8f653d641bfd046edee32ede7ff6c606", size = 12939784, upload-time = "2025-07-29T22:32:17.69Z" },
+ { url = "https://files.pythonhosted.org/packages/55/7f/7ead2663be5627c04be83754c4f3096603bf5e99ed856c7cd29618c691bd/ruff-0.12.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e41df94a957d50083fd09b916d6e89e497246698c3f3d5c681c8b3e7b9bb4ac8", size = 11771356, upload-time = "2025-07-29T22:32:20.134Z" },
+ { url = "https://files.pythonhosted.org/packages/17/40/a95352ea16edf78cd3a938085dccc55df692a4d8ba1b3af7accbe2c806b0/ruff-0.12.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4000623300563c709458d0ce170c3d0d788c23a058912f28bbadc6f905d67afa", size = 11612124, upload-time = "2025-07-29T22:32:22.645Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/74/633b04871c669e23b8917877e812376827c06df866e1677f15abfadc95cb/ruff-0.12.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:69ffe0e5f9b2cf2b8e289a3f8945b402a1b19eff24ec389f45f23c42a3dd6fb5", size = 12479945, upload-time = "2025-07-29T22:32:24.765Z" },
+ { url = "https://files.pythonhosted.org/packages/be/34/c3ef2d7799c9778b835a76189c6f53c179d3bdebc8c65288c29032e03613/ruff-0.12.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a07a5c8ffa2611a52732bdc67bf88e243abd84fe2d7f6daef3826b59abbfeda4", size = 12998677, upload-time = "2025-07-29T22:32:27.022Z" },
+ { url = "https://files.pythonhosted.org/packages/77/ab/aca2e756ad7b09b3d662a41773f3edcbd262872a4fc81f920dc1ffa44541/ruff-0.12.7-py3-none-win32.whl", hash = "sha256:c928f1b2ec59fb77dfdf70e0419408898b63998789cc98197e15f560b9e77f77", size = 11756687, upload-time = "2025-07-29T22:32:29.381Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/71/26d45a5042bc71db22ddd8252ca9d01e9ca454f230e2996bb04f16d72799/ruff-0.12.7-py3-none-win_amd64.whl", hash = "sha256:9c18f3d707ee9edf89da76131956aba1270c6348bfee8f6c647de841eac7194f", size = 12912365, upload-time = "2025-07-29T22:32:31.517Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/9b/0b8aa09817b63e78d94b4977f18b1fcaead3165a5ee49251c5d5c245bb2d/ruff-0.12.7-py3-none-win_arm64.whl", hash = "sha256:dfce05101dbd11833a0776716d5d1578641b7fddb537fe7fa956ab85d1769b69", size = 11982083, upload-time = "2025-07-29T22:32:33.881Z" },
+]
+
+[[package]]
+name = "setuptools"
+version = "80.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
+]
+
+[[package]]
+name = "setuptools-scm"
+version = "9.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "packaging" },
+ { name = "setuptools" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8f/8d/ffdcace33d0480d591057a30285b7c33f8dc431fed3fff7dbadf5f9f128f/setuptools_scm-9.2.0.tar.gz", hash = "sha256:6662c9b9497b6c9bf13bead9d7a9084756f68238302c5ed089fb4dbd29d102d7", size = 201229, upload-time = "2025-08-16T12:56:39.477Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f7/14/dd3a6053325e882fe191fb4b42289bbdfabf5f44307c302903a8a3236a0a/setuptools_scm-9.2.0-py3-none-any.whl", hash = "sha256:c551ef54e2270727ee17067881c9687ca2aedf179fa5b8f3fab9e8d73bdc421f", size = 62099, upload-time = "2025-08-16T12:56:37.912Z" },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "sse-starlette"
+version = "3.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/6f/22ed6e33f8a9e76ca0a412405f31abb844b779d52c5f96660766edcd737c/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a", size = 20985, upload-time = "2025-07-27T09:07:44.565Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297, upload-time = "2025-07-27T09:07:43.268Z" },
+]
+
+[[package]]
+name = "starlette"
+version = "0.47.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", size = 2583948, upload-time = "2025-07-20T17:31:58.522Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload-time = "2025-07-20T17:31:56.738Z" },
+]
+
+[[package]]
+name = "stevedore"
+version = "5.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pbr" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/28/3f/13cacea96900bbd31bb05c6b74135f85d15564fc583802be56976c940470/stevedore-5.4.1.tar.gz", hash = "sha256:3135b5ae50fe12816ef291baff420acb727fcd356106e3e9cbfa9e5985cd6f4b", size = 513858, upload-time = "2025-02-20T14:03:57.285Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f7/45/8c4ebc0c460e6ec38e62ab245ad3c7fc10b210116cea7c16d61602aa9558/stevedore-5.4.1-py3-none-any.whl", hash = "sha256:d10a31c7b86cba16c1f6e8d15416955fc797052351a56af15e608ad20811fcfe", size = 49533, upload-time = "2025-02-20T14:03:55.849Z" },
+]
+
+[[package]]
+name = "tomli"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" },
+ { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" },
+ { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" },
+ { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" },
+ { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" },
+ { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" },
+ { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" },
+ { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" },
+ { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" },
+ { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" },
+ { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" },
+ { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.14.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+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" }
+wheels = [
+ { 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" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.35.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" },
+]
+
+[[package]]
+name = "virtualenv"
+version = "20.33.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "distlib" },
+ { name = "filelock" },
+ { name = "platformdirs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/db/2e/8a70dcbe8bf15213a08f9b0325ede04faca5d362922ae0d62ef0fa4b069d/virtualenv-20.33.0.tar.gz", hash = "sha256:47e0c0d2ef1801fce721708ccdf2a28b9403fa2307c3268aebd03225976f61d2", size = 6082069, upload-time = "2025-08-03T08:09:19.014Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/87/b22cf40cdf7e2b2bf83f38a94d2c90c5ad6c304896e5a12d0c08a602eb59/virtualenv-20.33.0-py3-none-any.whl", hash = "sha256:106b6baa8ab1b526d5a9b71165c85c456fbd49b16976c88e2bc9352ee3bc5d3f", size = 6060205, upload-time = "2025-08-03T08:09:16.674Z" },
+]
+
+[[package]]
+name = "werkzeug"
+version = "3.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/32/af/d4502dc713b4ccea7175d764718d5183caf8d0867a4f0190d5d4a45cea49/werkzeug-3.1.1.tar.gz", hash = "sha256:8cd39dfbdfc1e051965f156163e2974e52c210f130810e9ad36858f0fd3edad4", size = 806453, upload-time = "2024-11-01T16:40:45.462Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ee/ea/c67e1dee1ba208ed22c06d1d547ae5e293374bfc43e0eb0ef5e262b68561/werkzeug-3.1.1-py3-none-any.whl", hash = "sha256:a71124d1ef06008baafa3d266c02f56e1836a5984afd6dd6c9230669d60d9fb5", size = 224371, upload-time = "2024-11-01T16:40:43.994Z" },
+]