Initial commit
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
|
@ -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:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 🚀 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…
Reference in New Issue