Initial commit

main
CupOfOwls 2 months ago
commit 9a19218bc3

@ -0,0 +1,22 @@
# Kroger API Configuration
# Get these from https://developer.kroger.com/
# Your Kroger API client ID (required)
KROGER_CLIENT_ID=your_client_id_here
# Your Kroger API client secret (required)
KROGER_CLIENT_SECRET=your_client_secret_here
# OAuth redirect URI - must match what's registered in Kroger Developer Portal
# Default for local development:
KROGER_REDIRECT_URI=http://localhost:8000/callback
# Optional: Default zip code for location searches
# This will be used when no zip code is specified in searches
KROGER_USER_ZIP_CODE=90274
# Example of other common zip codes:
# KROGER_USER_ZIP_CODE=10001 # Manhattan, NY
# KROGER_USER_ZIP_CODE=90210 # Beverly Hills, CA
# KROGER_USER_ZIP_CODE=60601 # Chicago, IL
# KROGER_USER_ZIP_CODE=30309 # Atlanta, GA

37
.gitignore vendored

@ -0,0 +1,37 @@
# Virtual environment
venv/
.venv/
# Local Storage / Memory
*.json
# Token files
.kroger_token*
# Python cache files
__pycache__/
*.py[cod]
*$py.class
# Distribution / packaging
dist/
build/
*.egg-info/
# Environment variables
.env
# IDE files
.idea/
.vscode/
# Test coverage
.coverage
coverage/
htmlcov/
# Logs
*.log
PUBLISHING.md

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Stephen Thoemmes
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,281 @@
# 🛒 Kroger MCP Server 🛍️ -- FastMCP for Kroger Shopping
A [FastMCP](https://github.com/jlowin/fastmcp) server that provides AI assistants like Claude with seamless access to Kroger's grocery shopping functionality through the Model Context Protocol ([MCP](https://docs.anthropic.com/en/docs/agents-and-tools/mcp)). This server enables AI assistants to find stores, search products, manage shopping carts, and access Kroger's comprehensive grocery data via the [kroger-api](https://github.com/CupOfOwls/kroger-api) python library.
## 📺 Demo
Using Claude with this MCP server to search for stores, find products, and add items to your cart:
![Kroger MCP Demo](https://github.com/user-attachments/assets/your-demo-image-here)
## 🚀 Quick Start
### Prerequisites
You will need Kroger API credentials (free from [Kroger Developer Portal](https://developer.kroger.com/)).
Visit the [Kroger Developer Portal](https://developer.kroger.com/manage/apps/register) to:
1. Create a developer account
2. Register your application
3. Get your `CLIENT_ID`, `CLIENT_SECRET`, and set your `REDIRECT_URI`
The first time you run a tool requiring user authentication, you'll be prompted to authorize your app through your web browser. You're granting permission to **your own registered app**, not to any third party.
### Installation
#### Option 1: Using uvx with Claude Desktop (Recommended)
Once published to PyPI, you can use uvx to run the package directly without cloning the repository:
Edit Claude Desktop's configuration file:
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
**Linux**: `~/.config/Claude/claude_desktop_config.json`
**Windows**: `%APPDATA%/Claude/claude_desktop_config.json`
```json
{
"mcpServers": {
"kroger": {
"command": "uvx",
"args": [
"kroger-mcp"
],
"env": {
"KROGER_CLIENT_ID": "your_client_id",
"KROGER_CLIENT_SECRET": "your_client_secret",
"KROGER_REDIRECT_URI": "http://localhost:8000/callback",
"KROGER_USER_ZIP_CODE": "10001"
}
}
}
}
```
Benefits of this method:
- Automatically installs the package from PyPI if needed
- Creates an isolated environment for running the server
- Makes it easy to stay updated with the latest version
- Doesn't require maintaining a local repository clone
#### Option 2: Using uv with a Local Clone
First, clone locally:
```bash
git clone https://github.com/CupOfOwls/kroger-mcp
```
Then, edit Claude Desktop's configuration file:
```json
{
"mcpServers": {
"kroger": {
"command": "uv",
"args": [
"--directory",
"/path/to/cloned/kroger-mcp",
"run",
"kroger-mcp"
],
"env": {
"KROGER_CLIENT_ID": "your_client_id",
"KROGER_CLIENT_SECRET": "your_client_secret",
"KROGER_REDIRECT_URI": "http://localhost:8000/callback",
"KROGER_USER_ZIP_CODE": "10001"
}
}
}
}
```
#### Option 3: Installing From PyPI
```bash
# Install with uv (recommended)
uv pip install kroger-mcp
# Or install with pip
pip install kroger-mcp
```
#### Option 4: Installing From Source
```bash
# Clone the repository
git clone https://github.com/CupOfOwls/kroger-mcp
cd kroger-mcp
# Install with uv (recommended)
uv sync
# Or install with pip
pip install -e .
```
### Configuration
Create a `.env` file in your project root or pass in env values via the JSON config:
```bash
# Required: Your Kroger API credentials
KROGER_CLIENT_ID=your_client_id_here
KROGER_CLIENT_SECRET=your_client_secret_here
KROGER_REDIRECT_URI=http://localhost:8000/callback
# Optional: Default zip code for location searches
KROGER_USER_ZIP_CODE=90274
```
### Running the Server
```bash
# With uv (recommended)
uv run kroger-mcp
# With uvx (directly from PyPI without installation)
uvx kroger-mcp
# Or with Python directly
python server.py
# With FastMCP CLI for development
fastmcp dev server.py --with-editable .
```
## 🛠️ Features
### 💬 Built-In MCP Prompts
- **Shopping Path**: Find optimal path through store for a grocery list
- **Pharmacy Check**: Check if pharmacy at preferred location is open
- **Store Selection**: Help user set their preferred Kroger store
- **Recipe Shopping**: Find recipes and add ingredients to cart
### 📚 Available Tools
#### Location Tools
| Tool | Description | Auth Required |
|------|-------------|---------------|
| `search_locations` | Find Kroger stores near a zip code | No |
| `get_location_details` | Get detailed information about a specific store | No |
| `set_preferred_location` | Set a preferred store for future operations | No |
| `get_preferred_location` | Get the currently set preferred store | No |
| `check_location_exists` | Verify if a location ID is valid | No |
#### Product Tools
| Tool | Description | Auth Required |
|------|-------------|---------------|
| `search_products` | Search for products by name, brand, or other criteria | No |
| `get_product_details` | Get detailed product information including pricing | No |
| `search_products_by_id` | Find products by their specific product ID | No |
| `get_product_images` | Get product images from specific perspective (front, back, etc.) | No |
#### Cart Tools
| Tool | Description | Auth Required |
|------|-------------|---------------|
| `add_items_to_cart` | Add a single item to cart | Yes |
| `bulk_add_to_cart` | Add multiple items to cart in one operation | Yes |
| `view_current_cart` | View items currently in your local cart tracking | No |
| `remove_from_cart` | Remove items from local cart tracking | No |
| `clear_current_cart` | Clear all items from local cart tracking | No |
| `mark_order_placed` | Move current cart to order history | No |
| `view_order_history` | View history of placed orders | No |
#### Information Tools
| Tool | Description | Auth Required |
|------|-------------|---------------|
| `list_chains` | Get all Kroger-owned chains | No |
| `get_chain_details` | Get details about a specific chain | No |
| `check_chain_exists` | Check if a chain exists | No |
| `list_departments` | Get all store departments | No |
| `get_department_details` | Get details about a specific department | No |
| `check_department_exists` | Check if a department exists | No |
#### Profile Tools
| Tool | Description | Auth Required |
|------|-------------|---------------|
| `get_user_profile` | Get authenticated user's profile information | Yes |
| `test_authentication` | Test if authentication token is valid | Yes |
| `get_authentication_info` | Get detailed authentication status | Yes |
| `force_reauthenticate` | Clear tokens and force re-authentication | No |
#### Utility Tools
| Tool | Description | Auth Required |
|------|-------------|---------------|
| `get_current_datetime` | Get current system date and time | No |
### 🧰 Local-Only Cart Tracking
Since the Kroger API doesn't provide cart viewing functionality, this server maintains local tracking:
#### Local Cart Storage
- **File**: `kroger_cart.json`
- **Contents**: Current cart items with timestamps
- **Automatic**: Created and updated automatically
#### Order History
- **File**: `kroger_order_history.json`
- **Contents**: Historical orders with placement timestamps
- **Usage**: Move completed carts to history with `mark_order_placed`
### 🚧 Kroger Public API Limitations
- **View Only**: The `remove_from_cart` and `clear_current_cart` tools ONLY affect local tracking, not the actual Kroger cart
- **Local Sync**: Use these tools only when the user has already removed items from their cart in the Kroger app/website
- **One-Way**: Items can be added to the Kroger cart but not removed through the Public API. The Partner API would allow these things, but that requires entering a contract with Kroger.
| API | Version | Rate Limit | Notes |
|-----|---------|------------|-------|
| **Authorization** | 1.0.13 | No specific limit | Token management |
| **Products** | 1.2.4 | 10,000 calls/day | Search and product details |
| **Locations** | 1.2.2 | 1,600 calls/day per endpoint | Store locations and details |
| **Cart** | 1.2.3 | 5,000 calls/day | Add/manage cart items |
| **Identity** | 1.2.3 | 5,000 calls/day | User profile information |
**Note:** Rate limits are enforced per endpoint, not per operation. You can distribute calls across operations using the same endpoint as needed.
## 🏫 Basic Workflow
1. **Set up a preferred location**:
```
User: "Find Kroger stores near 90274"
Assistant: [Uses search_locations tool]
User: "Set the first one as my preferred location"
Assistant: [Uses set_preferred_location tool]
```
2. **Search and add products**:
```
User: "Add milk to my cart"
Assistant: [Uses search_products, then add_items_to_cart]
User: "Add bread, eggs, and cheese to my cart"
Assistant: [Uses search_products for each, then bulk_add_to_cart]
```
3. **Manage cart and orders**:
```
User: "What's in my cart?"
Assistant: [Uses view_current_cart tool to see local memory]
User: "I placed the order on the Kroger website"
Assistant: [Uses mark_order_placed tool, moving current cart to the order history]
```
## 🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
## 📄 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## ⚠️ Disclaimer
This is an unofficial MCP server for the Kroger Public API. It is not affiliated with, endorsed by, or sponsored by Kroger.
For questions about the Kroger API, visit the [Kroger Developer Portal](https://developer.kroger.com/) or read the [kroger-api](https://github.com/CupOfOwls/kroger-api) package documentation.

@ -0,0 +1,64 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "kroger-mcp"
version = "0.1.0"
description = "FastMCP server for Kroger API integration"
readme = "README.md"
license = {file = "LICENSE"}
authors = [
{name = "Stephen Thoemmes", email = "thoemmes.stephen@gmail.com"},
]
readme = "README.md"
requires-python = ">=3.10"
keywords = ["kroger", "mcp", "grocery", "shopping", "retail"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Topic :: Office/Business",
]
dependencies = [
"fastmcp>=2.0.0",
"kroger-api",
"requests",
"pydantic>=2.0.0",
"python-dotenv",
]
[project.optional-dependencies]
dev = [
"pytest",
"pytest-asyncio",
"ruff",
"black",
]
[project.scripts]
kroger-mcp = "kroger_mcp.server:main"
kroger-mcp-cli = "kroger_mcp.cli:main"
[project.urls]
Homepage = "https://github.com/CupOfOwls/kroger-mcp"
Repository = "https://github.com/CupOfOwls/kroger-mcp"
Issues = "https://github.com/CupOfOwls/kroger-mcp/issues"
Kroger Documentation = "https://developer.kroger.com/documentation/public/"
[tool.hatch.build.targets.wheel]
packages = ["src/kroger_mcp"]
[tool.hatch.build.targets.sdist]
include = [
"/src",
"/README.md",
"/pyproject.toml",
]

@ -0,0 +1,10 @@
# FastMCP framework
fastmcp
# Kroger API client
kroger-api
# Additional dependencies (if not already included in kroger-api)
requests
pydantic
python-dotenv

@ -0,0 +1,8 @@
#!/usr/bin/env python3
"""
Standalone script to run Kroger MCP server with uv
"""
if __name__ == "__main__":
from kroger_mcp.server import main
main()

@ -0,0 +1,15 @@
#!/usr/bin/env python3
"""
Backward compatibility entry point for Kroger MCP server
"""
import sys
import os
# Add src directory to path for development
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from kroger_mcp.server import main
if __name__ == "__main__":
main()

@ -0,0 +1,9 @@
"""
Kroger MCP Server Package
A FastMCP server that provides access to Kroger's API for grocery shopping functionality.
"""
__version__ = "0.1.0"
__author__ = "Your Name"
__email__ = "your.email@example.com"

@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""
CLI entry point for Kroger MCP server
"""
import argparse
import os
import sys
from pathlib import Path
def main():
"""CLI entry point with argument parsing"""
parser = argparse.ArgumentParser(
description="Kroger MCP Server - FastMCP server for Kroger API integration"
)
parser.add_argument(
"--client-id",
help="Kroger API client ID (can also use KROGER_CLIENT_ID env var)"
)
parser.add_argument(
"--client-secret",
help="Kroger API client secret (can also use KROGER_CLIENT_SECRET env var)"
)
parser.add_argument(
"--redirect-uri",
default="http://localhost:8000/callback",
help="OAuth redirect URI (default: http://localhost:8000/callback)"
)
parser.add_argument(
"--zip-code",
help="Default zip code for location searches"
)
parser.add_argument(
"--transport",
choices=["stdio", "streamable-http", "sse"],
default="stdio",
help="Transport protocol (default: stdio)"
)
parser.add_argument(
"--host",
default="127.0.0.1",
help="Host for HTTP transports (default: 127.0.0.1)"
)
parser.add_argument(
"--port",
type=int,
default=8000,
help="Port for HTTP transports (default: 8000)"
)
args = parser.parse_args()
# Set environment variables from CLI args if provided
if args.client_id:
os.environ["KROGER_CLIENT_ID"] = args.client_id
if args.client_secret:
os.environ["KROGER_CLIENT_SECRET"] = args.client_secret
if args.redirect_uri:
os.environ["KROGER_REDIRECT_URI"] = args.redirect_uri
if args.zip_code:
os.environ["KROGER_USER_ZIP_CODE"] = args.zip_code
# Import and create server
from kroger_mcp.server import create_server
server = create_server()
# Run with specified transport
if args.transport == "stdio":
server.run()
elif args.transport == "streamable-http":
server.run(transport="streamable-http", host=args.host, port=args.port)
elif args.transport == "sse":
server.run(transport="sse", host=args.host, port=args.port)
if __name__ == "__main__":
main()

@ -0,0 +1,97 @@
"""
MCP prompts for the Kroger MCP server
"""
from typing import List, Dict, Any, Optional
from fastmcp import Context
def register_prompts(mcp):
"""Register prompts with the FastMCP server"""
@mcp.prompt()
async def grocery_list_store_path(grocery_list: str, ctx: Context = None) -> str:
"""
Generate a prompt asking for the optimal path through a store based on a grocery list.
Args:
grocery_list: A list of grocery items the user wants to purchase
Returns:
A prompt asking for the optimal shopping path
"""
return f"""I'm planning to go grocery shopping at Kroger with this list:
{grocery_list}
Can you help me find the most efficient path through the store? Please search for these products to determine their aisle locations, then arrange them in a logical shopping order.
If you can't find exact matches for items, please suggest similar products that are available.
IMPORTANT: Please only organize my shopping path - DO NOT add any items to my cart.
"""
@mcp.prompt()
async def pharmacy_open_check(ctx: Context = None) -> str:
"""
Generate a prompt asking whether a pharmacy at the preferred Kroger location is open.
Returns:
A prompt asking about pharmacy status
"""
return """Can you tell me if the pharmacy at my preferred Kroger store is currently open?
Please check the department information for the pharmacy department and let me know:
1. If there is a pharmacy at my preferred store
2. If it's currently open
3. What the hours are for today
4. What services are available at this pharmacy
Please use the get_location_details tool to find this information for my preferred location.
"""
@mcp.prompt()
async def set_preferred_store(zip_code: Optional[str] = None, ctx: Context = None) -> str:
"""
Generate a prompt to help the user set their preferred Kroger store.
Args:
zip_code: Optional zip code to search near
Returns:
A prompt asking for help setting a preferred store
"""
zip_phrase = f" near zip code {zip_code}" if zip_code else ""
return f"""I'd like to set my preferred Kroger store{zip_phrase}. Can you help me with this process?
Please:
1. Search for nearby Kroger stores{zip_phrase}
2. Show me a list of the closest options with their addresses
3. Let me choose one from the list
4. Set that as my preferred location
For each store, please show the full address, distance, and any special features or departments.
"""
@mcp.prompt()
async def add_recipe_to_cart(recipe_type: str = "classic apple pie", ctx: Context = None) -> str:
"""
Generate a prompt to find a specific recipe and add ingredients to cart. (default: classic apple pie)
Args:
recipe_type: The type of recipe to search for (e.g., "chicken curry", "vegetarian lasagna")
Returns:
A prompt asking for a recipe and to add ingredients to cart
"""
return f"""I'd like to make a recipe: {recipe_type}. Can you help me with the following:
1. Search the web for a good {recipe_type} recipe
2. Present the recipe with ingredients and instructions
3. Look up each ingredient in my local Kroger store
4. Add all the ingredients I'll need to my cart using bulk_add_to_cart
5. If any ingredients aren't available, suggest alternatives
Before adding items to cart, please ask me if I prefer pickup or delivery for these items.
"""

@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""
FastMCP Server for Kroger API
This server provides MCP tools for interacting with the Kroger API, including:
- Location management (search stores, get details, set preferred location)
- Product search and details
- Cart management (add items, bulk operations, tracking)
- Chain and department information
- User profile and authentication
Environment Variables Required:
- KROGER_CLIENT_ID: Your Kroger API client ID
- KROGER_CLIENT_SECRET: Your Kroger API client secret
- KROGER_REDIRECT_URI: Redirect URI for OAuth2 flow (default: http://localhost:8000/callback)
- KROGER_USER_ZIP_CODE: Default zip code for location searches (optional)
"""
import sys
from fastmcp import FastMCP
# Import all tool modules
from .tools import location_tools
from .tools import product_tools
from .tools import cart_tools
from .tools import info_tools
from .tools import profile_tools
from .tools import utility_tools
# Import prompts
from . import prompts
def create_server() -> FastMCP:
"""Create and configure the FastMCP server instance"""
# Initialize the FastMCP server
mcp = FastMCP(
name="Kroger API Server",
instructions="""
This MCP server provides access to Kroger's API for grocery shopping functionality.
Key Features:
- Search and manage store locations
- Find and search products
- Add items to shopping cart with local tracking
- Access chain and department information
- User profile management
Common workflows:
1. Set a preferred location with set_preferred_location
2. Search for products with search_products
3. Add items to cart with add_items_to_cart
4. Use bulk_add_to_cart for multiple items at once
5. View current cart with view_current_cart
6. Mark order as placed with mark_order_placed
Authentication is handled automatically - OAuth flows will open in your browser when needed.
Cart Tracking:
This server maintains a local record of items added to your cart since the Kroger API
doesn't provide cart viewing functionality. When you place an order through the Kroger
website/app, use mark_order_placed to move the current cart to order history.
"""
)
# Register all tools from the modules
location_tools.register_tools(mcp)
product_tools.register_tools(mcp)
cart_tools.register_tools(mcp)
info_tools.register_tools(mcp)
profile_tools.register_tools(mcp)
utility_tools.register_tools(mcp)
# Register prompts
prompts.register_prompts(mcp)
return mcp
def main():
"""Main entry point for the Kroger MCP server"""
mcp = create_server()
mcp.run()
if __name__ == "__main__":
main()

@ -0,0 +1,11 @@
"""
Tools package for Kroger MCP server
This package contains all the tool modules organized by functionality:
- location_tools: Store location search and management
- product_tools: Product search and details
- cart_tools: Shopping cart management with local tracking
- info_tools: Chain and department information
- profile_tools: User profile and authentication
- shared: Common utilities and client management
"""

@ -0,0 +1,504 @@
"""
Cart tracking and management functionality
"""
import json
import os
from datetime import datetime
from typing import Dict, Any, List
from fastmcp import Context
from .shared import get_authenticated_client
# Cart storage file
CART_FILE = "kroger_cart.json"
ORDER_HISTORY_FILE = "kroger_order_history.json"
def _load_cart_data() -> Dict[str, Any]:
"""Load cart data from file"""
try:
if os.path.exists(CART_FILE):
with open(CART_FILE, 'r') as f:
return json.load(f)
except Exception:
pass
return {"current_cart": [], "last_updated": None, "preferred_location_id": None}
def _save_cart_data(cart_data: Dict[str, Any]) -> None:
"""Save cart data to file"""
try:
with open(CART_FILE, 'w') as f:
json.dump(cart_data, f, indent=2)
except Exception as e:
print(f"Warning: Could not save cart data: {e}")
def _load_order_history() -> List[Dict[str, Any]]:
"""Load order history from file"""
try:
if os.path.exists(ORDER_HISTORY_FILE):
with open(ORDER_HISTORY_FILE, 'r') as f:
return json.load(f)
except Exception:
pass
return []
def _save_order_history(history: List[Dict[str, Any]]) -> None:
"""Save order history to file"""
try:
with open(ORDER_HISTORY_FILE, 'w') as f:
json.dump(history, f, indent=2)
except Exception as e:
print(f"Warning: Could not save order history: {e}")
def _add_item_to_local_cart(product_id: str, quantity: int, modality: str, product_details: Dict[str, Any] = None) -> None:
"""Add an item to the local cart tracking"""
cart_data = _load_cart_data()
current_cart = cart_data.get("current_cart", [])
# Check if item already exists in cart
existing_item = None
for item in current_cart:
if item.get("product_id") == product_id and item.get("modality") == modality:
existing_item = item
break
if existing_item:
# Update existing item quantity
existing_item["quantity"] = existing_item.get("quantity", 0) + quantity
existing_item["last_updated"] = datetime.now().isoformat()
else:
# Add new item
new_item = {
"product_id": product_id,
"quantity": quantity,
"modality": modality,
"added_at": datetime.now().isoformat(),
"last_updated": datetime.now().isoformat()
}
# Add product details if provided
if product_details:
new_item.update(product_details)
current_cart.append(new_item)
cart_data["current_cart"] = current_cart
cart_data["last_updated"] = datetime.now().isoformat()
_save_cart_data(cart_data)
def register_tools(mcp):
"""Register cart-related tools with the FastMCP server"""
@mcp.tool()
async def add_items_to_cart(
product_id: str,
quantity: int = 1,
modality: str = "PICKUP",
ctx: Context = None
) -> Dict[str, Any]:
"""
Add a single item to the user's Kroger cart and track it locally.
If the user doesn't specifically indicate a preference for pickup or delivery,
you should ask them which modality they prefer before calling this tool.
Args:
product_id: The product ID or UPC to add to cart
quantity: Quantity to add (default: 1)
modality: Fulfillment method - PICKUP or DELIVERY
Returns:
Dictionary confirming the item was added to cart
"""
try:
if ctx:
await ctx.info(f"Adding {quantity}x {product_id} to cart with {modality} modality")
# Get authenticated client
client = get_authenticated_client()
# Format the item for the API
cart_item = {
"upc": product_id,
"quantity": quantity,
"modality": modality
}
if ctx:
await ctx.info(f"Calling Kroger API to add item: {cart_item}")
# Add the item to the actual Kroger cart
# Note: add_to_cart returns None on success, raises exception on failure
client.cart.add_to_cart([cart_item])
if ctx:
await ctx.info("Successfully added item to Kroger cart")
# Add to local cart tracking
_add_item_to_local_cart(product_id, quantity, modality)
if ctx:
await ctx.info("Item added to local cart tracking")
return {
"success": True,
"message": f"Successfully added {quantity}x {product_id} to cart",
"product_id": product_id,
"quantity": quantity,
"modality": modality,
"timestamp": datetime.now().isoformat()
}
except Exception as e:
if ctx:
await ctx.error(f"Failed to add item to cart: {str(e)}")
# Provide helpful error message for authentication issues
error_message = str(e)
if "401" in error_message or "Unauthorized" in error_message:
return {
"success": False,
"error": "Authentication failed. Please run force_reauthenticate and try again.",
"details": error_message
}
elif "400" in error_message or "Bad Request" in error_message:
return {
"success": False,
"error": f"Invalid request. Please check the product ID and try again.",
"details": error_message
}
else:
return {
"success": False,
"error": f"Failed to add item to cart: {error_message}",
"product_id": product_id,
"quantity": quantity,
"modality": modality
}
@mcp.tool()
async def bulk_add_to_cart(
items: List[Dict[str, Any]],
ctx: Context = None
) -> Dict[str, Any]:
"""
Add multiple items to the user's Kroger cart in a single operation.
If the user doesn't specifically indicate a preference for pickup or delivery,
you should ask them which modality they prefer before calling this tool.
Args:
items: List of items to add. Each item should have:
- product_id: The product ID or UPC
- quantity: Quantity to add (default: 1)
- modality: PICKUP or DELIVERY (default: PICKUP)
Returns:
Dictionary with results for each item
"""
try:
if ctx:
await ctx.info(f"Adding {len(items)} items to cart in bulk")
client = get_authenticated_client()
# Format items for the API
cart_items = []
for item in items:
cart_item = {
"upc": item["product_id"],
"quantity": item.get("quantity", 1),
"modality": item.get("modality", "PICKUP")
}
cart_items.append(cart_item)
if ctx:
await ctx.info(f"Calling Kroger API to add {len(cart_items)} items")
# Add all items to the actual Kroger cart
client.cart.add_to_cart(cart_items)
if ctx:
await ctx.info("Successfully added all items to Kroger cart")
# Add all items to local cart tracking
for item in items:
_add_item_to_local_cart(
item["product_id"],
item.get("quantity", 1),
item.get("modality", "PICKUP")
)
if ctx:
await ctx.info("All items added to local cart tracking")
return {
"success": True,
"message": f"Successfully added {len(items)} items to cart",
"items_added": len(items),
"timestamp": datetime.now().isoformat()
}
except Exception as e:
if ctx:
await ctx.error(f"Failed to bulk add items to cart: {str(e)}")
error_message = str(e)
if "401" in error_message or "Unauthorized" in error_message:
return {
"success": False,
"error": "Authentication failed. Please run force_reauthenticate and try again.",
"details": error_message
}
else:
return {
"success": False,
"error": f"Failed to add items to cart: {error_message}",
"items_attempted": len(items)
}
@mcp.tool()
async def view_current_cart(ctx: Context = None) -> Dict[str, Any]:
"""
View the current cart contents tracked locally.
Note: This tool can only see items that were added via this MCP server.
The Kroger API does not provide permission to query the actual user cart contents.
Returns:
Dictionary containing current cart items and summary
"""
try:
cart_data = _load_cart_data()
current_cart = cart_data.get("current_cart", [])
# Calculate summary
total_quantity = sum(item.get("quantity", 0) for item in current_cart)
pickup_items = [item for item in current_cart if item.get("modality") == "PICKUP"]
delivery_items = [item for item in current_cart if item.get("modality") == "DELIVERY"]
return {
"success": True,
"current_cart": current_cart,
"summary": {
"total_items": len(current_cart),
"total_quantity": total_quantity,
"pickup_items": len(pickup_items),
"delivery_items": len(delivery_items),
"last_updated": cart_data.get("last_updated")
}
}
except Exception as e:
return {
"success": False,
"error": f"Failed to view cart: {str(e)}"
}
@mcp.tool()
async def remove_from_cart(
product_id: str,
modality: str = None,
ctx: Context = None
) -> Dict[str, Any]:
"""
Remove an item from the local cart tracking only.
IMPORTANT: This tool CANNOT remove items from the actual Kroger cart in the app/website.
It only updates our local tracking to stay in sync. The user must remove the item from
their actual cart through the Kroger app or website themselves.
Use this tool only when:
1. The user has already removed an item from their Kroger cart through the app/website
2. You need to update the local tracking to reflect that change
Args:
product_id: The product ID to remove
modality: Specific modality to remove (if None, removes all instances)
Returns:
Dictionary confirming the removal from local tracking
"""
try:
cart_data = _load_cart_data()
current_cart = cart_data.get("current_cart", [])
original_count = len(current_cart)
if modality:
# Remove specific modality
cart_data["current_cart"] = [
item for item in current_cart
if not (item.get("product_id") == product_id and item.get("modality") == modality)
]
else:
# Remove all instances
cart_data["current_cart"] = [
item for item in current_cart
if item.get("product_id") != product_id
]
items_removed = original_count - len(cart_data["current_cart"])
if items_removed > 0:
cart_data["last_updated"] = datetime.now().isoformat()
_save_cart_data(cart_data)
return {
"success": True,
"message": f"Removed {items_removed} items from local cart tracking",
"items_removed": items_removed,
"product_id": product_id,
"modality": modality
}
except Exception as e:
return {
"success": False,
"error": f"Failed to remove from cart: {str(e)}"
}
@mcp.tool()
async def clear_current_cart(ctx: Context = None) -> Dict[str, Any]:
"""
Clear all items from the local cart tracking only.
IMPORTANT: This tool CANNOT remove items from the actual Kroger cart in the app/website.
It only clears our local tracking. The user must remove items from their actual cart
through the Kroger app or website themselves.
Use this tool only when:
1. The user has already cleared their Kroger cart through the app/website
2. You need to update the local tracking to reflect that change
3. Or when the local tracking is out of sync with the actual cart
Returns:
Dictionary confirming the local cart tracking was cleared
"""
try:
cart_data = _load_cart_data()
items_count = len(cart_data.get("current_cart", []))
cart_data["current_cart"] = []
cart_data["last_updated"] = datetime.now().isoformat()
_save_cart_data(cart_data)
return {
"success": True,
"message": f"Cleared {items_count} items from local cart tracking",
"items_cleared": items_count
}
except Exception as e:
return {
"success": False,
"error": f"Failed to clear cart: {str(e)}"
}
@mcp.tool()
async def mark_order_placed(
order_notes: str = None,
ctx: Context = None
) -> Dict[str, Any]:
"""
Mark the current cart as an order that has been placed and move it to order history.
Use this after you've completed checkout on the Kroger website/app.
Args:
order_notes: Optional notes about the order
Returns:
Dictionary confirming the order was recorded
"""
try:
cart_data = _load_cart_data()
current_cart = cart_data.get("current_cart", [])
if not current_cart:
return {
"success": False,
"error": "No items in current cart to mark as placed"
}
# Create order record
order_record = {
"items": current_cart.copy(),
"placed_at": datetime.now().isoformat(),
"item_count": len(current_cart),
"total_quantity": sum(item.get("quantity", 0) for item in current_cart),
"notes": order_notes
}
# Load and update order history
order_history = _load_order_history()
order_history.append(order_record)
_save_order_history(order_history)
# Clear current cart
cart_data["current_cart"] = []
cart_data["last_updated"] = datetime.now().isoformat()
_save_cart_data(cart_data)
return {
"success": True,
"message": f"Marked order with {order_record['item_count']} items as placed",
"order_id": len(order_history), # Simple order ID based on history length
"items_placed": order_record["item_count"],
"total_quantity": order_record["total_quantity"],
"placed_at": order_record["placed_at"]
}
except Exception as e:
return {
"success": False,
"error": f"Failed to mark order as placed: {str(e)}"
}
@mcp.tool()
async def view_order_history(
limit: int = 10,
ctx: Context = None
) -> Dict[str, Any]:
"""
View the history of placed orders.
Note: This tool can only see orders that were explicitly marked as placed via this MCP server.
The Kroger API does not provide permission to query the actual order history from Kroger's systems.
Args:
limit: Number of recent orders to show (1-50)
Returns:
Dictionary containing order history
"""
try:
# Ensure limit is within bounds
limit = max(1, min(50, limit))
order_history = _load_order_history()
# Sort by placed_at date (most recent first) and limit
sorted_orders = sorted(order_history, key=lambda x: x.get("placed_at", ""), reverse=True)
limited_orders = sorted_orders[:limit]
# Calculate summary stats
total_orders = len(order_history)
total_items_all_time = sum(order.get("item_count", 0) for order in order_history)
total_quantity_all_time = sum(order.get("total_quantity", 0) for order in order_history)
return {
"success": True,
"orders": limited_orders,
"showing": len(limited_orders),
"summary": {
"total_orders": total_orders,
"total_items_all_time": total_items_all_time,
"total_quantity_all_time": total_quantity_all_time
}
}
except Exception as e:
return {
"success": False,
"error": f"Failed to view order history: {str(e)}"
}

@ -0,0 +1,274 @@
"""
Chain and department information tools for Kroger MCP server
"""
from typing import Dict, List, Any, Optional
from fastmcp import Context
from .shared import get_client_credentials_client
def register_tools(mcp):
"""Register information-related tools with the FastMCP server"""
@mcp.tool()
async def list_chains(ctx: Context = None) -> Dict[str, Any]:
"""
Get a list of all Kroger-owned chains.
Returns:
Dictionary containing chain information
"""
if ctx:
await ctx.info("Getting list of Kroger chains")
client = get_client_credentials_client()
try:
chains = client.location.list_chains()
if not chains or "data" not in chains or not chains["data"]:
return {
"success": False,
"message": "No chains found",
"data": []
}
# Format chain data
formatted_chains = [
{
"name": chain.get("name"),
"division_numbers": chain.get("divisionNumbers", [])
}
for chain in chains["data"]
]
if ctx:
await ctx.info(f"Found {len(formatted_chains)} chains")
return {
"success": True,
"count": len(formatted_chains),
"data": formatted_chains
}
except Exception as e:
if ctx:
await ctx.error(f"Error listing chains: {str(e)}")
return {
"success": False,
"error": str(e),
"data": []
}
@mcp.tool()
async def get_chain_details(
chain_name: str,
ctx: Context = None
) -> Dict[str, Any]:
"""
Get detailed information about a specific Kroger chain.
Args:
chain_name: Name of the chain to get details for
Returns:
Dictionary containing chain details
"""
if ctx:
await ctx.info(f"Getting details for chain: {chain_name}")
client = get_client_credentials_client()
try:
chain_details = client.location.get_chain(chain_name)
if not chain_details or "data" not in chain_details:
return {
"success": False,
"message": f"Chain '{chain_name}' not found"
}
chain = chain_details["data"]
return {
"success": True,
"name": chain.get("name"),
"division_numbers": chain.get("divisionNumbers", [])
}
except Exception as e:
if ctx:
await ctx.error(f"Error getting chain details: {str(e)}")
return {
"success": False,
"error": str(e)
}
@mcp.tool()
async def check_chain_exists(
chain_name: str,
ctx: Context = None
) -> Dict[str, Any]:
"""
Check if a chain exists in the Kroger system.
Args:
chain_name: Name of the chain to check
Returns:
Dictionary indicating whether the chain exists
"""
if ctx:
await ctx.info(f"Checking if chain '{chain_name}' exists")
client = get_client_credentials_client()
try:
exists = client.location.chain_exists(chain_name)
return {
"success": True,
"chain_name": chain_name,
"exists": exists,
"message": f"Chain '{chain_name}' {'exists' if exists else 'does not exist'}"
}
except Exception as e:
if ctx:
await ctx.error(f"Error checking chain existence: {str(e)}")
return {
"success": False,
"error": str(e)
}
@mcp.tool()
async def list_departments(ctx: Context = None) -> Dict[str, Any]:
"""
Get a list of all available departments in Kroger stores.
Returns:
Dictionary containing department information
"""
if ctx:
await ctx.info("Getting list of departments")
client = get_client_credentials_client()
try:
departments = client.location.list_departments()
if not departments or "data" not in departments or not departments["data"]:
return {
"success": False,
"message": "No departments found",
"data": []
}
# Format department data
formatted_departments = [
{
"department_id": dept.get("departmentId"),
"name": dept.get("name")
}
for dept in departments["data"]
]
if ctx:
await ctx.info(f"Found {len(formatted_departments)} departments")
return {
"success": True,
"count": len(formatted_departments),
"data": formatted_departments
}
except Exception as e:
if ctx:
await ctx.error(f"Error listing departments: {str(e)}")
return {
"success": False,
"error": str(e),
"data": []
}
@mcp.tool()
async def get_department_details(
department_id: str,
ctx: Context = None
) -> Dict[str, Any]:
"""
Get detailed information about a specific department.
Args:
department_id: The unique identifier for the department
Returns:
Dictionary containing department details
"""
if ctx:
await ctx.info(f"Getting details for department: {department_id}")
client = get_client_credentials_client()
try:
dept_details = client.location.get_department(department_id)
if not dept_details or "data" not in dept_details:
return {
"success": False,
"message": f"Department '{department_id}' not found"
}
dept = dept_details["data"]
return {
"success": True,
"department_id": dept.get("departmentId"),
"name": dept.get("name")
}
except Exception as e:
if ctx:
await ctx.error(f"Error getting department details: {str(e)}")
return {
"success": False,
"error": str(e)
}
@mcp.tool()
async def check_department_exists(
department_id: str,
ctx: Context = None
) -> Dict[str, Any]:
"""
Check if a department exists in the Kroger system.
Args:
department_id: The department ID to check
Returns:
Dictionary indicating whether the department exists
"""
if ctx:
await ctx.info(f"Checking if department '{department_id}' exists")
client = get_client_credentials_client()
try:
exists = client.location.department_exists(department_id)
return {
"success": True,
"department_id": department_id,
"exists": exists,
"message": f"Department '{department_id}' {'exists' if exists else 'does not exist'}"
}
except Exception as e:
if ctx:
await ctx.error(f"Error checking department existence: {str(e)}")
return {
"success": False,
"error": str(e)
}

@ -0,0 +1,332 @@
"""
Location management tools for Kroger MCP server
"""
from typing import Dict, List, Any, Optional
from pydantic import Field
from fastmcp import Context
from .shared import (
get_client_credentials_client,
get_preferred_location_id,
set_preferred_location_id,
get_default_zip_code
)
def register_tools(mcp):
"""Register location-related tools with the FastMCP server"""
@mcp.tool()
async def search_locations(
zip_code: Optional[str] = None,
radius_in_miles: int = Field(default=10, ge=1, le=100, description="Search radius in miles (1-100)"),
limit: int = Field(default=10, ge=1, le=200, description="Number of results to return (1-200)"),
chain: Optional[str] = None,
ctx: Context = None
) -> Dict[str, Any]:
"""
Search for Kroger store locations near a zip code.
Args:
zip_code: Zip code to search near (uses environment default if not provided)
radius_in_miles: Search radius in miles (1-100)
limit: Number of results to return (1-200)
chain: Filter by specific chain name
Returns:
Dictionary containing location search results
"""
if ctx:
await ctx.info(f"Searching for Kroger locations near {zip_code or 'default zip code'}")
if not zip_code:
zip_code = get_default_zip_code()
client = get_client_credentials_client()
try:
locations = client.location.search_locations(
zip_code=zip_code,
radius_in_miles=radius_in_miles,
limit=limit,
chain=chain
)
if not locations or "data" not in locations or not locations["data"]:
return {
"success": False,
"message": f"No locations found near zip code {zip_code}",
"data": []
}
# Format location data for easier consumption
formatted_locations = []
for loc in locations["data"]:
address = loc.get("address", {})
formatted_loc = {
"location_id": loc.get("locationId"),
"name": loc.get("name"),
"chain": loc.get("chain"),
"phone": loc.get("phone"),
"address": {
"street": address.get("addressLine1", ""),
"city": address.get("city", ""),
"state": address.get("state", ""),
"zip_code": address.get("zipCode", "")
},
"full_address": f"{address.get('addressLine1', '')}, {address.get('city', '')}, {address.get('state', '')} {address.get('zipCode', '')}",
"coordinates": loc.get("geolocation", {}),
"departments": [dept.get("name") for dept in loc.get("departments", [])],
"department_count": len(loc.get("departments", []))
}
# Add hours info if available
if "hours" in loc and "monday" in loc["hours"]:
monday = loc["hours"]["monday"]
if monday.get("open24", False):
formatted_loc["hours_monday"] = "Open 24 hours"
elif "open" in monday and "close" in monday:
formatted_loc["hours_monday"] = f"{monday['open']} - {monday['close']}"
else:
formatted_loc["hours_monday"] = "Hours not available"
formatted_locations.append(formatted_loc)
if ctx:
await ctx.info(f"Found {len(formatted_locations)} locations")
return {
"success": True,
"search_params": {
"zip_code": zip_code,
"radius_miles": radius_in_miles,
"limit": limit,
"chain": chain
},
"count": len(formatted_locations),
"data": formatted_locations
}
except Exception as e:
if ctx:
await ctx.error(f"Error searching locations: {str(e)}")
return {
"success": False,
"error": str(e),
"data": []
}
@mcp.tool()
async def get_location_details(
location_id: str,
ctx: Context = None
) -> Dict[str, Any]:
"""
Get detailed information about a specific Kroger store location.
Args:
location_id: The unique identifier for the store location
Returns:
Dictionary containing detailed location information
"""
if ctx:
await ctx.info(f"Getting details for location {location_id}")
client = get_client_credentials_client()
try:
location_details = client.location.get_location(location_id)
if not location_details or "data" not in location_details:
return {
"success": False,
"message": f"Location {location_id} not found"
}
loc = location_details["data"]
# Format department information
departments = []
for dept in loc.get("departments", []):
dept_info = {
"department_id": dept.get("departmentId"),
"name": dept.get("name"),
"phone": dept.get("phone")
}
# Add department hours
if "hours" in dept and "monday" in dept["hours"]:
monday = dept["hours"]["monday"]
if monday.get("open24", False):
dept_info["hours_monday"] = "Open 24 hours"
elif "open" in monday and "close" in monday:
dept_info["hours_monday"] = f"{monday['open']} - {monday['close']}"
departments.append(dept_info)
# Format the response
address = loc.get("address", {})
result = {
"success": True,
"location_id": loc.get("locationId"),
"name": loc.get("name"),
"chain": loc.get("chain"),
"phone": loc.get("phone"),
"address": {
"street": address.get("addressLine1", ""),
"street2": address.get("addressLine2", ""),
"city": address.get("city", ""),
"state": address.get("state", ""),
"zip_code": address.get("zipCode", "")
},
"coordinates": loc.get("geolocation", {}),
"departments": departments,
"department_count": len(departments)
}
return result
except Exception as e:
if ctx:
await ctx.error(f"Error getting location details: {str(e)}")
return {
"success": False,
"error": str(e)
}
@mcp.tool()
async def set_preferred_location(
location_id: str,
ctx: Context = None
) -> Dict[str, Any]:
"""
Set a preferred store location for future operations.
Args:
location_id: The unique identifier for the store location
Returns:
Dictionary confirming the preferred location has been set
"""
if ctx:
await ctx.info(f"Setting preferred location to {location_id}")
# Verify the location exists
client = get_client_credentials_client()
try:
exists = client.location.location_exists(location_id)
if not exists:
return {
"success": False,
"error": f"Location {location_id} does not exist"
}
# Get location details for confirmation
location_details = client.location.get_location(location_id)
loc_data = location_details.get("data", {})
set_preferred_location_id(location_id)
if ctx:
await ctx.info(f"Preferred location set to {loc_data.get('name', location_id)}")
return {
"success": True,
"preferred_location_id": location_id,
"location_name": loc_data.get("name"),
"message": f"Preferred location set to {loc_data.get('name', location_id)}"
}
except Exception as e:
if ctx:
await ctx.error(f"Error setting preferred location: {str(e)}")
return {
"success": False,
"error": str(e)
}
@mcp.tool()
async def get_preferred_location(ctx: Context = None) -> Dict[str, Any]:
"""
Get the currently set preferred store location.
Returns:
Dictionary containing the preferred location information
"""
preferred_location_id = get_preferred_location_id()
if not preferred_location_id:
return {
"success": False,
"message": "No preferred location set. Use set_preferred_location to set one."
}
if ctx:
await ctx.info(f"Getting preferred location details for {preferred_location_id}")
# Get location details
client = get_client_credentials_client()
try:
location_details = client.location.get_location(preferred_location_id)
loc_data = location_details.get("data", {})
return {
"success": True,
"preferred_location_id": preferred_location_id,
"location_details": {
"name": loc_data.get("name"),
"chain": loc_data.get("chain"),
"phone": loc_data.get("phone"),
"address": loc_data.get("address", {})
}
}
except Exception as e:
if ctx:
await ctx.error(f"Error getting preferred location details: {str(e)}")
return {
"success": False,
"error": str(e),
"preferred_location_id": preferred_location_id
}
@mcp.tool()
async def check_location_exists(
location_id: str,
ctx: Context = None
) -> Dict[str, Any]:
"""
Check if a location exists in the Kroger system.
Args:
location_id: The unique identifier for the store location
Returns:
Dictionary indicating whether the location exists
"""
if ctx:
await ctx.info(f"Checking if location {location_id} exists")
client = get_client_credentials_client()
try:
exists = client.location.location_exists(location_id)
return {
"success": True,
"location_id": location_id,
"exists": exists,
"message": f"Location {location_id} {'exists' if exists else 'does not exist'}"
}
except Exception as e:
if ctx:
await ctx.error(f"Error checking location existence: {str(e)}")
return {
"success": False,
"error": str(e)
}

@ -0,0 +1,484 @@
"""
Product search and management tools for Kroger MCP server
"""
from typing import Dict, List, Any, Optional, Literal
from pydantic import Field
from fastmcp import Context, Image
import requests
from io import BytesIO
from .shared import (
get_client_credentials_client,
get_preferred_location_id,
format_currency
)
def register_tools(mcp):
"""Register product-related tools with the FastMCP server"""
@mcp.tool()
async def get_product_images(
product_id: str,
perspective: str = "front",
location_id: Optional[str] = None,
ctx: Context = None
) -> Image:
"""
Get an image for a specific product from the requested perspective.
Use get_product_details first to see what perspectives are available (typically "front", "back", "left", "right").
Args:
product_id: The unique product identifier
perspective: The image perspective to retrieve (default: "front")
location_id: Store location ID (uses preferred if not provided)
Returns:
The product image from the requested perspective
"""
# Use preferred location if none provided
if not location_id:
location_id = get_preferred_location_id()
if not location_id:
return {
"success": False,
"error": "No location_id provided and no preferred location set. Use set_preferred_location first."
}
if ctx:
await ctx.info(f"Fetching images for product {product_id} at location {location_id}")
client = get_client_credentials_client()
try:
# Get product details to extract image URLs
product_details = client.product.get_product(
product_id=product_id,
location_id=location_id
)
if not product_details or "data" not in product_details:
return {
"success": False,
"message": f"Product {product_id} not found"
}
product = product_details["data"]
# Check if images are available
if "images" not in product or not product["images"]:
return {
"success": False,
"message": f"No images available for product {product_id}"
}
# Find the requested perspective image
perspective_image = None
available_perspectives = []
for img_data in product["images"]:
img_perspective = img_data.get("perspective", "unknown")
available_perspectives.append(img_perspective)
# Skip if not the requested perspective
if img_perspective != perspective:
continue
if not img_data.get("sizes"):
continue
# Find the best image size (prefer large, fallback to xlarge or other available)
img_url = None
size_preference = ["large", "xlarge", "medium", "small", "thumbnail"]
# Create a map of available sizes for quick lookup
available_sizes = {size.get("size"): size.get("url") for size in img_data.get("sizes", []) if size.get("size") and size.get("url")}
# Select best size based on preference order
for size in size_preference:
if size in available_sizes:
img_url = available_sizes[size]
break
if img_url:
try:
if ctx:
await ctx.info(f"Downloading {perspective} image from {img_url}")
# Download image
response = requests.get(img_url)
response.raise_for_status()
# Create Image object
perspective_image = Image(
data=response.content,
format="jpeg" # Kroger images are typically JPEG
)
break
except Exception as e:
if ctx:
await ctx.warning(f"Failed to download {perspective} image: {str(e)}")
# If the requested perspective wasn't found
if not perspective_image:
available_str = ", ".join(available_perspectives) if available_perspectives else "none"
return {
"success": False,
"message": f"No image found for perspective '{perspective}'. Available perspectives: {available_str}"
}
return perspective_image
except Exception as e:
if ctx:
await ctx.error(f"Error getting product images: {str(e)}")
return {
"success": False,
"error": str(e)
}
@mcp.tool()
async def search_products(
search_term: str,
location_id: Optional[str] = None,
limit: int = Field(default=10, ge=1, le=50, description="Number of results to return (1-50)"),
fulfillment: Optional[Literal["csp", "delivery", "pickup"]] = None,
brand: Optional[str] = None,
ctx: Context = None
) -> Dict[str, Any]:
"""
Search for products at a Kroger store.
Args:
search_term: Product search term (e.g., "milk", "bread", "organic apples")
location_id: Store location ID (uses preferred location if not provided)
limit: Number of results to return (1-50)
fulfillment: Filter by fulfillment method (csp=curbside pickup, delivery, pickup)
brand: Filter by brand name
Returns:
Dictionary containing product search results
"""
# Use preferred location if none provided
if not location_id:
location_id = get_preferred_location_id()
if not location_id:
return {
"success": False,
"error": "No location_id provided and no preferred location set. Use set_preferred_location first."
}
if ctx:
await ctx.info(f"Searching for '{search_term}' at location {location_id}")
client = get_client_credentials_client()
try:
products = client.product.search_products(
term=search_term,
location_id=location_id,
limit=limit,
fulfillment=fulfillment,
brand=brand
)
if not products or "data" not in products or not products["data"]:
return {
"success": False,
"message": f"No products found matching '{search_term}'",
"data": []
}
# Format product data
formatted_products = []
for product in products["data"]:
formatted_product = {
"product_id": product.get("productId"),
"upc": product.get("upc"),
"description": product.get("description"),
"brand": product.get("brand"),
"categories": product.get("categories", []),
"country_origin": product.get("countryOrigin"),
"temperature": product.get("temperature", {})
}
# Add item information (size, price, etc.)
if "items" in product and product["items"]:
item = product["items"][0]
formatted_product["item"] = {
"size": item.get("size"),
"sold_by": item.get("soldBy"),
"inventory": item.get("inventory", {}),
"fulfillment": item.get("fulfillment", {})
}
# Add pricing information
if "price" in item:
price = item["price"]
formatted_product["pricing"] = {
"regular_price": price.get("regular"),
"sale_price": price.get("promo"),
"regular_per_unit": price.get("regularPerUnitEstimate"),
"formatted_regular": format_currency(price.get("regular")),
"formatted_sale": format_currency(price.get("promo")),
"on_sale": price.get("promo") is not None and price.get("promo") < price.get("regular", float('inf'))
}
# Add aisle information
if "aisleLocations" in product:
formatted_product["aisle_locations"] = [
{
"description": aisle.get("description"),
"number": aisle.get("number"),
"side": aisle.get("side"),
"shelf_number": aisle.get("shelfNumber")
}
for aisle in product["aisleLocations"]
]
# Add image information
if "images" in product and product["images"]:
formatted_product["images"] = [
{
"perspective": img.get("perspective"),
"url": img["sizes"][0].get("url") if img.get("sizes") else None,
"size": img["sizes"][0].get("size") if img.get("sizes") else None
}
for img in product["images"]
if img.get("sizes")
]
formatted_products.append(formatted_product)
if ctx:
await ctx.info(f"Found {len(formatted_products)} products")
return {
"success": True,
"search_params": {
"search_term": search_term,
"location_id": location_id,
"limit": limit,
"fulfillment": fulfillment,
"brand": brand
},
"count": len(formatted_products),
"data": formatted_products
}
except Exception as e:
if ctx:
await ctx.error(f"Error searching products: {str(e)}")
return {
"success": False,
"error": str(e),
"data": []
}
@mcp.tool()
async def get_product_details(
product_id: str,
location_id: Optional[str] = None,
ctx: Context = None
) -> Dict[str, Any]:
"""
Get detailed information about a specific product.
Args:
product_id: The unique product identifier
location_id: Store location ID for pricing/availability (uses preferred if not provided)
Returns:
Dictionary containing detailed product information
"""
# Use preferred location if none provided
if not location_id:
location_id = get_preferred_location_id()
if not location_id:
return {
"success": False,
"error": "No location_id provided and no preferred location set. Use set_preferred_location first."
}
if ctx:
await ctx.info(f"Getting details for product {product_id} at location {location_id}")
client = get_client_credentials_client()
try:
product_details = client.product.get_product(
product_id=product_id,
location_id=location_id
)
if not product_details or "data" not in product_details:
return {
"success": False,
"message": f"Product {product_id} not found"
}
product = product_details["data"]
# Format the detailed product information
result = {
"success": True,
"product_id": product.get("productId"),
"upc": product.get("upc"),
"description": product.get("description"),
"brand": product.get("brand"),
"categories": product.get("categories", []),
"country_origin": product.get("countryOrigin"),
"temperature": product.get("temperature", {}),
"location_id": location_id
}
# Add detailed item information
if "items" in product and product["items"]:
item = product["items"][0]
result["item_details"] = {
"size": item.get("size"),
"sold_by": item.get("soldBy"),
"inventory": item.get("inventory", {}),
"fulfillment": item.get("fulfillment", {})
}
# Add detailed pricing
if "price" in item:
price = item["price"]
result["pricing"] = {
"regular_price": price.get("regular"),
"sale_price": price.get("promo"),
"regular_per_unit": price.get("regularPerUnitEstimate"),
"formatted_regular": format_currency(price.get("regular")),
"formatted_sale": format_currency(price.get("promo")),
"on_sale": price.get("promo") is not None and price.get("promo") < price.get("regular", float('inf')),
"savings": price.get("regular", 0) - price.get("promo", price.get("regular", 0)) if price.get("promo") else 0
}
# Add aisle locations
if "aisleLocations" in product:
result["aisle_locations"] = [
{
"description": aisle.get("description"),
"aisle_number": aisle.get("number"),
"side": aisle.get("side"),
"shelf_number": aisle.get("shelfNumber")
}
for aisle in product["aisleLocations"]
]
# Add images
if "images" in product and product["images"]:
result["images"] = [
{
"perspective": img.get("perspective"),
"sizes": [
{
"size": size.get("size"),
"url": size.get("url")
}
for size in img.get("sizes", [])
]
}
for img in product["images"]
]
return result
except Exception as e:
if ctx:
await ctx.error(f"Error getting product details: {str(e)}")
return {
"success": False,
"error": str(e)
}
@mcp.tool()
async def search_products_by_id(
product_id: str,
location_id: Optional[str] = None,
ctx: Context = None
) -> Dict[str, Any]:
"""
Search for products by their specific product ID.
Args:
product_id: The product ID to search for
location_id: Store location ID (uses preferred location if not provided)
Returns:
Dictionary containing matching products
"""
# Use preferred location if none provided
if not location_id:
location_id = get_preferred_location_id()
if not location_id:
return {
"success": False,
"error": "No location_id provided and no preferred location set. Use set_preferred_location first."
}
if ctx:
await ctx.info(f"Searching for products with ID '{product_id}' at location {location_id}")
client = get_client_credentials_client()
try:
products = client.product.search_products(
product_id=product_id,
location_id=location_id
)
if not products or "data" not in products or not products["data"]:
return {
"success": False,
"message": f"No products found with ID '{product_id}'",
"data": []
}
# Format product data (similar to search_products but simpler)
formatted_products = []
for product in products["data"]:
formatted_product = {
"product_id": product.get("productId"),
"upc": product.get("upc"),
"description": product.get("description"),
"brand": product.get("brand"),
"categories": product.get("categories", [])
}
# Add basic pricing if available
if "items" in product and product["items"] and "price" in product["items"][0]:
price = product["items"][0]["price"]
formatted_product["pricing"] = {
"regular_price": price.get("regular"),
"sale_price": price.get("promo"),
"formatted_regular": format_currency(price.get("regular")),
"formatted_sale": format_currency(price.get("promo"))
}
formatted_products.append(formatted_product)
if ctx:
await ctx.info(f"Found {len(formatted_products)} products with ID '{product_id}'")
return {
"success": True,
"search_params": {
"product_id": product_id,
"location_id": location_id
},
"count": len(formatted_products),
"data": formatted_products
}
except Exception as e:
if ctx:
await ctx.error(f"Error searching products by ID: {str(e)}")
return {
"success": False,
"error": str(e),
"data": []
}

@ -0,0 +1,184 @@
"""
User profile and authentication tools for Kroger MCP server
"""
from typing import Dict, List, Any, Optional
from fastmcp import Context
from .shared import get_authenticated_client, invalidate_authenticated_client
def register_tools(mcp):
"""Register profile-related tools with the FastMCP server"""
@mcp.tool()
async def get_user_profile(ctx: Context = None) -> Dict[str, Any]:
"""
Get the authenticated user's Kroger profile information.
Returns:
Dictionary containing user profile data
"""
if ctx:
await ctx.info("Getting user profile information")
try:
client = get_authenticated_client()
profile = client.identity.get_profile()
if profile and "data" in profile:
profile_id = profile["data"].get("id", "N/A")
if ctx:
await ctx.info(f"Retrieved profile for user ID: {profile_id}")
return {
"success": True,
"profile_id": profile_id,
"message": "User profile retrieved successfully",
"note": "The Kroger Identity API only provides the profile ID for privacy reasons."
}
else:
return {
"success": False,
"message": "Failed to retrieve user profile"
}
except Exception as e:
if ctx:
await ctx.error(f"Error getting user profile: {str(e)}")
return {
"success": False,
"error": str(e)
}
@mcp.tool()
async def test_authentication(ctx: Context = None) -> Dict[str, Any]:
"""
Test if the current authentication token is valid.
Returns:
Dictionary indicating authentication status
"""
if ctx:
await ctx.info("Testing authentication token validity")
try:
client = get_authenticated_client()
is_valid = client.test_current_token()
if ctx:
await ctx.info(f"Authentication test result: {'valid' if is_valid else 'invalid'}")
result = {
"success": True,
"token_valid": is_valid,
"message": f"Authentication token is {'valid' if is_valid else 'invalid'}"
}
# Check for refresh token availability
if hasattr(client.client, 'token_info') and client.client.token_info:
has_refresh_token = "refresh_token" in client.client.token_info
result["has_refresh_token"] = has_refresh_token
result["can_auto_refresh"] = has_refresh_token
if has_refresh_token:
result["message"] += ". Token can be automatically refreshed when it expires."
else:
result["message"] += ". No refresh token available - will need to re-authenticate when token expires."
return result
except Exception as e:
if ctx:
await ctx.error(f"Error testing authentication: {str(e)}")
return {
"success": False,
"error": str(e),
"token_valid": False
}
@mcp.tool()
async def get_authentication_info(ctx: Context = None) -> Dict[str, Any]:
"""
Get information about the current authentication state and token.
Returns:
Dictionary containing authentication information
"""
if ctx:
await ctx.info("Getting authentication information")
try:
client = get_authenticated_client()
result = {
"success": True,
"authenticated": True,
"message": "User is authenticated"
}
# Get token information if available
if hasattr(client.client, 'token_info') and client.client.token_info:
token_info = client.client.token_info
result.update({
"token_type": token_info.get("token_type", "Unknown"),
"has_refresh_token": "refresh_token" in token_info,
"expires_in": token_info.get("expires_in"),
"scope": token_info.get("scope", "Unknown")
})
# Don't expose the actual tokens for security
result["access_token_preview"] = f"{token_info.get('access_token', '')[:10]}..." if token_info.get('access_token') else "N/A"
if "refresh_token" in token_info:
result["refresh_token_preview"] = f"{token_info['refresh_token'][:10]}..."
# Get token file information if available
if hasattr(client.client, 'token_file') and client.client.token_file:
result["token_file"] = client.client.token_file
return result
except Exception as e:
if ctx:
await ctx.error(f"Error getting authentication info: {str(e)}")
return {
"success": False,
"error": str(e),
"authenticated": False
}
@mcp.tool()
async def force_reauthenticate(ctx: Context = None) -> Dict[str, Any]:
"""
Force re-authentication by clearing the current authentication token.
Use this if you're having authentication issues or need to log in as a different user.
Returns:
Dictionary indicating the re-authentication was initiated
"""
if ctx:
await ctx.info("Forcing re-authentication by clearing current token")
try:
# Clear the current authenticated client
invalidate_authenticated_client()
if ctx:
await ctx.info("Authentication token cleared. Next cart operation will trigger re-authentication.")
return {
"success": True,
"message": "Authentication token cleared. The next cart operation will open your browser for re-authentication.",
"note": "You will need to log in again when you next use cart-related tools."
}
except Exception as e:
if ctx:
await ctx.error(f"Error clearing authentication: {str(e)}")
return {
"success": False,
"error": str(e)
}

@ -0,0 +1,307 @@
"""
Shared utilities and client management for Kroger MCP server
"""
import os
import socket
import threading
import time
import webbrowser
import json
from typing import Optional
from kroger_api.kroger_api import KrogerAPI
from kroger_api.auth import authenticate_user
from kroger_api.utils.env import load_and_validate_env, get_zip_code, get_redirect_uri
from kroger_api.utils.oauth import start_oauth_server, generate_random_state, extract_port_from_redirect_uri
from kroger_api.token_storage import load_token, save_token
# Global state for clients and preferred location
_authenticated_client: Optional[KrogerAPI] = None
_client_credentials_client: Optional[KrogerAPI] = None
# JSON files for configuration storage
PREFERENCES_FILE = "kroger_preferences.json"
def get_client_credentials_client() -> KrogerAPI:
"""Get or create a client credentials authenticated client for public data"""
global _client_credentials_client
if _client_credentials_client is None:
try:
load_and_validate_env(["KROGER_CLIENT_ID", "KROGER_CLIENT_SECRET"])
_client_credentials_client = KrogerAPI()
# Try to load existing token first
token_file = ".kroger_token_client_product.compact.json"
token_info = load_token(token_file)
if token_info:
# Test if the token is still valid
_client_credentials_client.client.token_info = token_info
if _client_credentials_client.test_current_token():
# Token is valid, use it
pass
else:
# Token is invalid, get a new one
token_info = _client_credentials_client.authorization.get_token_with_client_credentials("product.compact")
else:
# No existing token, get a new one
token_info = _client_credentials_client.authorization.get_token_with_client_credentials("product.compact")
except Exception as e:
raise Exception(f"Failed to get client credentials: {str(e)}")
return _client_credentials_client
def get_authenticated_client() -> KrogerAPI:
"""Get or create a user-authenticated client for cart operations with browser-based OAuth"""
global _authenticated_client
if _authenticated_client is None:
try:
load_and_validate_env(["KROGER_CLIENT_ID", "KROGER_CLIENT_SECRET", "KROGER_REDIRECT_URI"])
# Try to load existing user token first
token_file = ".kroger_token_user.json"
token_info = load_token(token_file)
if token_info:
# Test if the token is still valid
_authenticated_client = KrogerAPI()
_authenticated_client.client.token_info = token_info
_authenticated_client.client.token_file = token_file
if _authenticated_client.test_current_token():
# Token is valid, use it
return _authenticated_client
else:
# Token is invalid, try to refresh it
if "refresh_token" in token_info:
try:
new_token_info = _authenticated_client.authorization.refresh_token(token_info["refresh_token"])
# Token refreshed successfully
return _authenticated_client
except Exception as e:
print(f"Token refresh failed: {str(e)}")
# Refresh failed, need to re-authenticate
_authenticated_client = None
# No valid token available, need to authenticate with browser
if _authenticated_client is None:
_authenticated_client = _authenticate_with_browser()
except Exception as e:
raise Exception(f"Authentication failed: {str(e)}")
return _authenticated_client
def _authenticate_with_browser() -> KrogerAPI:
"""Authenticate user by opening browser and handling OAuth flow"""
server = None
try:
redirect_uri = get_redirect_uri()
port = extract_port_from_redirect_uri(redirect_uri)
# Check if port is already in use and try alternative ports
original_port = port
max_attempts = 5
for attempt in range(max_attempts):
try:
# Test if port is available
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('127.0.0.1', port))
# Port is available, break out of loop
break
except OSError:
if attempt < max_attempts - 1:
port += 1
print(f"Port {port - 1} is in use, trying port {port}...")
else:
raise Exception(f"Ports {original_port}-{port} are all in use. Please free up port {original_port} or restart your system.")
# Update redirect URI if we had to change the port
if port != original_port:
redirect_uri = redirect_uri.replace(f":{original_port}", f":{port}")
print(f"Using alternative port {port} for OAuth callback.")
# Create the API client
kroger = KrogerAPI()
# Generate a random state value for security
state = generate_random_state()
# Variables to store the authorization code
auth_code = None
auth_state = None
auth_event = threading.Event()
auth_error = None
# Callback for when the authorization code is received
def on_code_received(code, received_state):
nonlocal auth_code, auth_state, auth_error
try:
auth_code = code
auth_state = received_state
auth_event.set()
except Exception as e:
auth_error = str(e)
auth_event.set()
# Start the server to handle the OAuth2 redirect
try:
server, server_thread = start_oauth_server(port, on_code_received)
except Exception as e:
raise Exception(f"Failed to start OAuth server on port {port}: {str(e)}")
try:
# Get the authorization URL with the potentially updated redirect URI
if port != original_port:
# Temporarily override the redirect URI for this authentication
original_redirect = os.environ.get('KROGER_REDIRECT_URI')
os.environ['KROGER_REDIRECT_URI'] = redirect_uri
auth_url = kroger.authorization.get_authorization_url(
scope="cart.basic:write profile.compact",
state=state
)
# Restore original redirect URI if we changed it
if port != original_port and original_redirect:
os.environ['KROGER_REDIRECT_URI'] = original_redirect
print("\n" + "="*60)
print("KROGER AUTHENTICATION REQUIRED")
print("="*60)
print("Opening your browser for Kroger login...")
print("Please log in and authorize the application.")
if port != original_port:
print(f"Note: Using port {port} instead of {original_port} due to port conflict.")
print("This window will close automatically after authentication.")
print("If the browser doesn't open, copy this URL:")
print(f" {auth_url}")
print("="*60)
# Open the authorization URL in the default browser
try:
webbrowser.open(auth_url)
except Exception as e:
print(f"Could not open browser automatically: {e}")
print("Please manually open the URL above in your browser.")
# Wait for the authorization code (timeout after 5 minutes)
if not auth_event.wait(timeout=300):
raise Exception("Authentication timed out after 5 minutes. Please try again.")
if auth_error:
raise Exception(f"OAuth callback error: {auth_error}")
if not auth_code:
raise Exception("No authorization code received. Authentication may have been cancelled.")
# Verify the state parameter to prevent CSRF attacks
if auth_state != state:
raise Exception(f"State mismatch. Expected {state}, got {auth_state}. This could be a security issue.")
# Exchange the authorization code for an access token
try:
token_info = kroger.authorization.get_token_with_authorization_code(auth_code)
except Exception as e:
raise Exception(f"Failed to exchange authorization code for token: {str(e)}")
print("\n" + "="*60)
print("AUTHENTICATION SUCCESSFUL!")
print("="*60)
print("You can now use cart operations and user-specific features.")
print("Your authentication token has been saved for future use.")
print("="*60 + "\n")
return kroger
finally:
# Ensure the server is shut down properly
if server:
try:
server.shutdown()
# Give it a moment to fully shut down
time.sleep(0.5)
except Exception as e:
print(f"Note: OAuth server cleanup had an issue: {e}")
except Exception as e:
error_msg = str(e)
print(f"\nAuthentication failed: {error_msg}")
print("\nTo resolve this issue:")
print("1. Make sure KROGER_REDIRECT_URI is set correctly in your .env file")
print("2. Ensure the redirect URI matches what's registered in Kroger Developer Portal")
print("3. If port issues persist, restart Claude Desktop or try a different port")
print("4. You can change the port by updating KROGER_REDIRECT_URI to http://localhost:8001/callback")
print("5. Make sure you have a stable internet connection")
# Re-raise with a cleaner error message
if "timed out" in error_msg.lower():
raise Exception("Authentication timed out. Please try again and complete the login process more quickly.")
elif "port" in error_msg.lower():
raise Exception(f"Port conflict: {error_msg}")
elif "connection" in error_msg.lower():
raise Exception(f"Connection error: {error_msg}")
else:
raise Exception(f"Authentication failed: {error_msg}")
def invalidate_authenticated_client():
"""Invalidate the authenticated client to force re-authentication"""
global _authenticated_client
_authenticated_client = None
def invalidate_client_credentials_client():
"""Invalidate the client credentials client to force re-authentication"""
global _client_credentials_client
_client_credentials_client = None
def _load_preferences() -> dict:
"""Load preferences from file"""
try:
if os.path.exists(PREFERENCES_FILE):
with open(PREFERENCES_FILE, 'r') as f:
return json.load(f)
except Exception as e:
print(f"Warning: Could not load preferences: {e}")
return {"preferred_location_id": None}
def _save_preferences(preferences: dict) -> None:
"""Save preferences to file"""
try:
with open(PREFERENCES_FILE, 'w') as f:
json.dump(preferences, f, indent=2)
except Exception as e:
print(f"Warning: Could not save preferences: {e}")
def get_preferred_location_id() -> Optional[str]:
"""Get the current preferred location ID from preferences file"""
preferences = _load_preferences()
return preferences.get("preferred_location_id")
def set_preferred_location_id(location_id: str) -> None:
"""Set the preferred location ID in preferences file"""
preferences = _load_preferences()
preferences["preferred_location_id"] = location_id
_save_preferences(preferences)
def format_currency(value: Optional[float]) -> str:
"""Format a value as currency"""
if value is None:
return "N/A"
return f"${value:.2f}"
def get_default_zip_code() -> str:
"""Get the default zip code from environment or fallback"""
return get_zip_code(default="10001")

@ -0,0 +1,33 @@
"""
Utility tools for the Kroger MCP server
"""
from typing import Dict, Any
from datetime import datetime
from fastmcp import Context
def register_tools(mcp):
"""Register utility tools with the FastMCP server"""
@mcp.tool()
async def get_current_datetime(ctx: Context = None) -> Dict[str, Any]:
"""
Get the current system date and time.
This tool is useful for comparing with cart checkout dates, order history,
or any other time-sensitive operations.
Returns:
Dictionary containing current date and time information
"""
now = datetime.now()
return {
"success": True,
"datetime": now.isoformat(),
"date": now.date().isoformat(),
"time": now.time().isoformat(),
"timestamp": int(now.timestamp()),
"formatted": now.strftime("%A, %B %d, %Y at %I:%M:%S %p")
}

@ -0,0 +1,820 @@
version = 1
revision = 1
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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
]
[[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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 },
]
[[package]]
name = "black"
version = "25.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "mypy-extensions" },
{ name = "packaging" },
{ name = "pathspec" },
{ name = "platformdirs" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419 },
{ url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080 },
{ url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886 },
{ url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404 },
{ url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372 },
{ url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865 },
{ url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699 },
{ url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028 },
{ url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988 },
{ url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985 },
{ url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816 },
{ url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860 },
{ url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673 },
{ url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190 },
{ url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926 },
{ url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613 },
{ url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 },
]
[[package]]
name = "certifi"
version = "2025.4.26"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 },
]
[[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 }
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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445 },
{ url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106 },
{ url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357 },
{ url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064 },
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641 },
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 },
]
[[package]]
name = "click"
version = "8.1.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
]
[[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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
]
[[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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 },
]
[[package]]
name = "fastmcp"
version = "2.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "exceptiongroup" },
{ name = "httpx" },
{ name = "mcp" },
{ name = "openapi-pydantic" },
{ name = "python-dotenv" },
{ name = "rich" },
{ name = "typer" },
{ name = "websockets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5d/cc/37ff3a96338234a697df31d2c70b50a1d0f5e20f045d9b7cbba052be36af/fastmcp-2.5.1.tar.gz", hash = "sha256:0d10ec65a362ae4f78bdf3b639faf35b36cc0a1c8f5461a54fac906fe821b84d", size = 1035613 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/4f/e7ec7b63eadcd5b10978dbc472fc3c36de3fc8c91f60ad7642192ed78836/fastmcp-2.5.1-py3-none-any.whl", hash = "sha256:a6fe50693954a6aed89fc6e43f227dcd66e112e3d3a1d633ee22b4f435ee8aed", size = 105789 },
]
[[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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 },
]
[[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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 },
]
[[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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
]
[[package]]
name = "httpx-sse"
version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 },
]
[[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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 },
]
[[package]]
name = "kroger-api"
version = "0.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "python-dotenv" },
{ name = "requests" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a7/57/904b454000e89077e2dde18fbce60e7d7521d7467863da9def9ec50da1f9/kroger_api-0.1.0.tar.gz", hash = "sha256:f846a6ad6b9c5bf7cea49408e900f0e7f4f4143cbb686e2e57b157e1a5054eaf", size = 687441 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/34/27981d9a1b53cbf8a4240ced328e81d0bd90b267250b805e3d704c7277d2/kroger_api-0.1.0-py3-none-any.whl", hash = "sha256:52975031506bdb9e465f340fb5d1f30a7b522df393be16dc1a5b3389dde6247f", size = 20643 },
]
[[package]]
name = "kroger-mcp"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "fastmcp" },
{ name = "kroger-api" },
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "requests" },
]
[package.optional-dependencies]
dev = [
{ name = "black" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "black", marker = "extra == 'dev'" },
{ name = "fastmcp", specifier = ">=2.0.0" },
{ name = "kroger-api" },
{ name = "pydantic", specifier = ">=2.0.0" },
{ name = "pytest", marker = "extra == 'dev'" },
{ name = "pytest-asyncio", marker = "extra == 'dev'" },
{ name = "python-dotenv" },
{ name = "requests" },
{ name = "ruff", marker = "extra == 'dev'" },
]
provides-extras = ["dev"]
[[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 }
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 },
]
[[package]]
name = "mcp"
version = "1.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "httpx" },
{ name = "httpx-sse" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
{ name = "python-multipart" },
{ name = "sse-starlette" },
{ name = "starlette" },
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e7/bc/54aec2c334698cc575ca3b3481eed627125fb66544152fa1af927b1a495c/mcp-1.9.1.tar.gz", hash = "sha256:19879cd6dde3d763297617242888c2f695a95dfa854386a6a68676a646ce75e4", size = 316247 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/c0/4ac795585a22a0a2d09cd2b1187b0252d2afcdebd01e10a68bbac4d34890/mcp-1.9.1-py3-none-any.whl", hash = "sha256:2900ded8ffafc3c8a7bfcfe8bc5204037e988e753ec398f371663e6a06ecd9a9", size = 130261 },
]
[[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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 },
]
[[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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381 },
]
[[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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
]
[[package]]
name = "pathspec"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
]
[[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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 },
]
[[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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 },
]
[[package]]
name = "pydantic"
version = "2.11.5"
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/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229 },
]
[[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 }
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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496 },
{ url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538 },
{ url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909 },
{ url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628 },
{ url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866 },
{ url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473 },
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269 },
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921 },
{ 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 },
{ 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 },
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
]
[[package]]
name = "pydantic-settings"
version = "2.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356 },
]
[[package]]
name = "pygments"
version = "2.19.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
]
[[package]]
name = "pytest"
version = "8.3.5"
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 = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 },
]
[[package]]
name = "pytest-asyncio"
version = "0.26.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694 },
]
[[package]]
name = "python-dotenv"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 },
]
[[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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 },
]
[[package]]
name = "requests"
version = "2.32.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
]
[[package]]
name = "rich"
version = "14.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 },
]
[[package]]
name = "ruff"
version = "0.11.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/53/ae4857030d59286924a8bdb30d213d6ff22d8f0957e738d0289990091dd8/ruff-0.11.11.tar.gz", hash = "sha256:7774173cc7c1980e6bf67569ebb7085989a78a103922fb83ef3dfe230cd0687d", size = 4186707 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/14/f2326676197bab099e2a24473158c21656fbf6a207c65f596ae15acb32b9/ruff-0.11.11-py3-none-linux_armv6l.whl", hash = "sha256:9924e5ae54125ed8958a4f7de320dab7380f6e9fa3195e3dc3b137c6842a0092", size = 10229049 },
{ url = "https://files.pythonhosted.org/packages/9a/f3/bff7c92dd66c959e711688b2e0768e486bbca46b2f35ac319bb6cce04447/ruff-0.11.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c8a93276393d91e952f790148eb226658dd275cddfde96c6ca304873f11d2ae4", size = 11053601 },
{ url = "https://files.pythonhosted.org/packages/e2/38/8e1a3efd0ef9d8259346f986b77de0f62c7a5ff4a76563b6b39b68f793b9/ruff-0.11.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6e333dbe2e6ae84cdedefa943dfd6434753ad321764fd937eef9d6b62022bcd", size = 10367421 },
{ url = "https://files.pythonhosted.org/packages/b4/50/557ad9dd4fb9d0bf524ec83a090a3932d284d1a8b48b5906b13b72800e5f/ruff-0.11.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7885d9a5e4c77b24e8c88aba8c80be9255fa22ab326019dac2356cff42089fc6", size = 10581980 },
{ url = "https://files.pythonhosted.org/packages/c4/b2/e2ed82d6e2739ece94f1bdbbd1d81b712d3cdaf69f0a1d1f1a116b33f9ad/ruff-0.11.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b5ab797fcc09121ed82e9b12b6f27e34859e4227080a42d090881be888755d4", size = 10089241 },
{ url = "https://files.pythonhosted.org/packages/3d/9f/b4539f037a5302c450d7c695c82f80e98e48d0d667ecc250e6bdeb49b5c3/ruff-0.11.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e231ff3132c1119ece836487a02785f099a43992b95c2f62847d29bace3c75ac", size = 11699398 },
{ url = "https://files.pythonhosted.org/packages/61/fb/32e029d2c0b17df65e6eaa5ce7aea5fbeaed22dddd9fcfbbf5fe37c6e44e/ruff-0.11.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a97c9babe1d4081037a90289986925726b802d180cca784ac8da2bbbc335f709", size = 12427955 },
{ url = "https://files.pythonhosted.org/packages/6e/e3/160488dbb11f18c8121cfd588e38095ba779ae208292765972f7732bfd95/ruff-0.11.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8c4ddcbe8a19f59f57fd814b8b117d4fcea9bee7c0492e6cf5fdc22cfa563c8", size = 12069803 },
{ url = "https://files.pythonhosted.org/packages/ff/16/3b006a875f84b3d0bff24bef26b8b3591454903f6f754b3f0a318589dcc3/ruff-0.11.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6224076c344a7694c6fbbb70d4f2a7b730f6d47d2a9dc1e7f9d9bb583faf390b", size = 11242630 },
{ url = "https://files.pythonhosted.org/packages/65/0d/0338bb8ac0b97175c2d533e9c8cdc127166de7eb16d028a43c5ab9e75abd/ruff-0.11.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:882821fcdf7ae8db7a951df1903d9cb032bbe838852e5fc3c2b6c3ab54e39875", size = 11507310 },
{ url = "https://files.pythonhosted.org/packages/6f/bf/d7130eb26174ce9b02348b9f86d5874eafbf9f68e5152e15e8e0a392e4a3/ruff-0.11.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:dcec2d50756463d9df075a26a85a6affbc1b0148873da3997286caf1ce03cae1", size = 10441144 },
{ url = "https://files.pythonhosted.org/packages/b3/f3/4be2453b258c092ff7b1761987cf0749e70ca1340cd1bfb4def08a70e8d8/ruff-0.11.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:99c28505ecbaeb6594701a74e395b187ee083ee26478c1a795d35084d53ebd81", size = 10081987 },
{ url = "https://files.pythonhosted.org/packages/6c/6e/dfa4d2030c5b5c13db158219f2ec67bf333e8a7748dccf34cfa2a6ab9ebc/ruff-0.11.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9263f9e5aa4ff1dec765e99810f1cc53f0c868c5329b69f13845f699fe74f639", size = 11073922 },
{ url = "https://files.pythonhosted.org/packages/ff/f4/f7b0b0c3d32b593a20ed8010fa2c1a01f2ce91e79dda6119fcc51d26c67b/ruff-0.11.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:64ac6f885e3ecb2fdbb71de2701d4e34526651f1e8503af8fb30d4915a3fe345", size = 11568537 },
{ url = "https://files.pythonhosted.org/packages/d2/46/0e892064d0adc18bcc81deed9aaa9942a27fd2cd9b1b7791111ce468c25f/ruff-0.11.11-py3-none-win32.whl", hash = "sha256:1adcb9a18802268aaa891ffb67b1c94cd70578f126637118e8099b8e4adcf112", size = 10536492 },
{ url = "https://files.pythonhosted.org/packages/1b/d9/232e79459850b9f327e9f1dc9c047a2a38a6f9689e1ec30024841fc4416c/ruff-0.11.11-py3-none-win_amd64.whl", hash = "sha256:748b4bb245f11e91a04a4ff0f96e386711df0a30412b9fe0c74d5bdc0e4a531f", size = 11612562 },
{ url = "https://files.pythonhosted.org/packages/ce/eb/09c132cff3cc30b2e7244191dcce69437352d6d6709c0adf374f3e6f476e/ruff-0.11.11-py3-none-win_arm64.whl", hash = "sha256:6c51f136c0364ab1b774767aa8b86331bd8e9d414e2d107db7a2189f35ea1f7b", size = 10735951 },
]
[[package]]
name = "shellingham"
version = "1.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
]
[[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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
[[package]]
name = "sse-starlette"
version = "2.3.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "starlette" },
]
sdist = { url = "https://files.pythonhosted.org/packages/10/5f/28f45b1ff14bee871bacafd0a97213f7ec70e389939a80c60c0fb72a9fc9/sse_starlette-2.3.5.tar.gz", hash = "sha256:228357b6e42dcc73a427990e2b4a03c023e2495ecee82e14f07ba15077e334b2", size = 17511 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/48/3e49cf0f64961656402c0023edbc51844fe17afe53ab50e958a6dbbbd499/sse_starlette-2.3.5-py3-none-any.whl", hash = "sha256:251708539a335570f10eaaa21d1848a10c42ee6dc3a9cf37ef42266cdb1c52a8", size = 10233 },
]
[[package]]
name = "starlette"
version = "0.46.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037 },
]
[[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 }
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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 },
{ url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 },
{ url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ 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 },
{ url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 },
{ url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 },
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
]
[[package]]
name = "typer"
version = "0.15.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "rich" },
{ name = "shellingham" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6c/89/c527e6c848739be8ceb5c44eb8208c52ea3515c6cf6406aa61932887bf58/typer-0.15.4.tar.gz", hash = "sha256:89507b104f9b6a0730354f27c39fae5b63ccd0c95b1ce1f1a6ba0cfd329997c3", size = 101559 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c9/62/d4ba7afe2096d5659ec3db8b15d8665bdcb92a3c6ff0b95e99895b335a9c/typer-0.15.4-py3-none-any.whl", hash = "sha256:eb0651654dcdea706780c466cf06d8f174405a659ffff8f163cfbfee98c0e173", size = 45258 },
]
[[package]]
name = "typing-extensions"
version = "4.13.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 },
]
[[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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 },
]
[[package]]
name = "urllib3"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 },
]
[[package]]
name = "uvicorn"
version = "0.34.2"
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/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483 },
]
[[package]]
name = "websockets"
version = "15.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423 },
{ url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080 },
{ url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329 },
{ url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312 },
{ url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319 },
{ url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631 },
{ url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016 },
{ url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426 },
{ url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360 },
{ url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388 },
{ url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830 },
{ url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423 },
{ url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082 },
{ url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330 },
{ url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878 },
{ url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883 },
{ url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252 },
{ url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521 },
{ url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958 },
{ url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918 },
{ url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388 },
{ url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828 },
{ url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437 },
{ url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096 },
{ url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332 },
{ url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152 },
{ url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096 },
{ url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523 },
{ url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790 },
{ url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165 },
{ url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160 },
{ url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395 },
{ url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841 },
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 },
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 },
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 },
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 },
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 },
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 },
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 },
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 },
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 },
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 },
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 },
{ url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109 },
{ url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343 },
{ url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599 },
{ url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207 },
{ url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155 },
{ url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884 },
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 },
]
Loading…
Cancel
Save