diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml
index 32452a1a..4f237041 100644
--- a/.github/workflows/docker-build.yml
+++ b/.github/workflows/docker-build.yml
@@ -85,6 +85,8 @@ jobs:
dockerfile: src/envs/textarena_env/server/Dockerfile
- name: browsergym-env
dockerfile: src/envs/browsergym_env/server/Dockerfile
+ - name: warehouse-env
+ dockerfile: src/envs/warehouse_env/server/Dockerfile
steps:
- name: Checkout code
diff --git a/examples/warehouse_simple.py b/examples/warehouse_simple.py
new file mode 100644
index 00000000..ef6c6750
--- /dev/null
+++ b/examples/warehouse_simple.py
@@ -0,0 +1,216 @@
+"""
+Simple example script demonstrating the Warehouse Environment.
+
+This script shows basic usage of the warehouse environment with both
+random and greedy agents.
+"""
+
+import os
+import random
+import sys
+
+# Add src directory to path for imports
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
+
+from envs.warehouse_env import WarehouseAction, WarehouseEnv
+
+
+def random_agent_example():
+ """Run a simple random agent in the warehouse."""
+ print("=" * 60)
+ print("RANDOM AGENT EXAMPLE")
+ print("=" * 60)
+
+ # Connect to server (assumes server is running on localhost:8000)
+ # Or use from_docker_image() to automatically start a container
+ try:
+ env = WarehouseEnv(base_url="http://localhost:8000")
+ except Exception as e:
+ print(f"Error connecting to server: {e}")
+ print("\nPlease start the server first:")
+ print(" docker run -p 8000:8000 warehouse-env:latest")
+ print("Or run the server locally:")
+ print(
+ " python -m uvicorn envs.warehouse_env.server.app:app --host 0.0.0.0 --port 8000"
+ )
+ return
+
+ # Run one episode
+ result = env.reset()
+ print(f"\nStarting episode...")
+
+ # Debug: print what we got
+ print(f"Grid data: {result.observation.grid}")
+ print(f"Robot position: {result.observation.robot_position}")
+
+ if result.observation.grid and len(result.observation.grid) > 0:
+ print(
+ f"Grid size: {len(result.observation.grid)}x{len(result.observation.grid[0])}"
+ )
+ else:
+ print("Warning: Grid is empty!")
+
+ print(f"Packages to deliver: {result.observation.total_packages}")
+ print(f"Max steps: {result.observation.time_remaining}")
+
+ step = 0
+ done = False
+
+ while not done and step < 50: # Limit to 50 steps for demo
+ # Random action
+ action = WarehouseAction(action_id=random.randint(0, 5))
+ result = env.step(action)
+
+ step += 1
+ if result.reward != -0.1: # Only print interesting events
+ print(
+ f"Step {step}: {action.action_name} -> {result.observation.message} (reward: {result.reward:.1f})"
+ )
+
+ done = result.done
+
+ # Print final results
+ print(f"\nEpisode finished!")
+ print(f"Steps taken: {step}")
+ print(
+ f"Packages delivered: {result.observation.packages_delivered}/{result.observation.total_packages}"
+ )
+ print(f"Total reward: {env.state().cum_reward:.2f}")
+
+ env.close()
+
+
+def greedy_agent_example():
+ """Run a simple greedy agent that moves toward targets."""
+ print("\n" + "=" * 60)
+ print("GREEDY AGENT EXAMPLE")
+ print("=" * 60)
+
+ def get_greedy_action(obs):
+ """Simple greedy policy: move toward nearest target."""
+ robot_x, robot_y = obs.robot_position
+
+ # Determine target location
+ if obs.robot_carrying is None:
+ # Not carrying: move toward nearest waiting package
+ for pkg in obs.packages:
+ if pkg["status"] == "waiting":
+ target_x, target_y = pkg["pickup_location"]
+ break
+ else:
+ # No packages waiting, try to pick up
+ return 4
+ else:
+ # Carrying: move toward dropoff zone
+ pkg = next((p for p in obs.packages if p["id"] == obs.robot_carrying), None)
+ if pkg:
+ target_x, target_y = pkg["dropoff_location"]
+ else:
+ return 4 # Try action
+
+ # Simple pathfinding: move closer on one axis at a time
+ if robot_x < target_x:
+ return 3 # RIGHT
+ elif robot_x > target_x:
+ return 2 # LEFT
+ elif robot_y < target_y:
+ return 1 # DOWN
+ elif robot_y > target_y:
+ return 0 # UP
+ else:
+ # At target location
+ return 4 if obs.robot_carrying is None else 5
+
+ try:
+ env = WarehouseEnv(base_url="http://localhost:8000")
+ except Exception as e:
+ print(f"Error connecting to server: {e}")
+ return
+
+ # Run 3 episodes
+ for episode in range(3):
+ result = env.reset()
+ print(f"\nEpisode {episode + 1}")
+ print(f"Packages: {result.observation.total_packages}")
+
+ done = False
+ steps = 0
+
+ while not done and steps < 200:
+ action_id = get_greedy_action(result.observation)
+ action = WarehouseAction(action_id=action_id)
+ result = env.step(action)
+ steps += 1
+
+ # Print delivery events
+ if "delivered" in result.observation.message.lower():
+ print(f" Step {steps}: {result.observation.message}")
+
+ done = result.done
+
+ state = env.state()
+ print(
+ f" Result: {state.packages_delivered}/{state.total_packages} delivered, "
+ f"reward: {state.cum_reward:.2f}, steps: {steps}"
+ )
+
+ env.close()
+
+
+def visualization_example():
+ """Demonstrate ASCII visualization."""
+ print("\n" + "=" * 60)
+ print("VISUALIZATION EXAMPLE")
+ print("=" * 60)
+
+ try:
+ env = WarehouseEnv(base_url="http://localhost:8000")
+ except Exception as e:
+ print(f"Error connecting to server: {e}")
+ return
+
+ # Reset and show initial state
+ result = env.reset()
+ print("\nInitial warehouse state:")
+ print(env.render_ascii())
+
+ # Take a few actions and show updates
+ actions = [3, 3, 1, 1, 4] # RIGHT, RIGHT, DOWN, DOWN, PICKUP
+ for i, action_id in enumerate(actions):
+ action = WarehouseAction(action_id=action_id)
+ result = env.step(action)
+ print(f"\nAfter action {i+1} ({action.action_name}):")
+ print(env.render_ascii())
+
+ if result.done:
+ break
+
+ env.close()
+
+
+if __name__ == "__main__":
+ print("Warehouse Environment - Example Script")
+ print("=" * 60)
+ print("\nThis script demonstrates the warehouse environment.")
+ print("Make sure the server is running on http://localhost:8000")
+ print("\nTo start the server:")
+ print(" docker run -p 8000:8000 warehouse-env:latest")
+ print("\n" + "=" * 60)
+
+ # Run examples
+ try:
+ random_agent_example()
+ greedy_agent_example()
+ visualization_example()
+
+ print("\n" + "=" * 60)
+ print("All examples completed successfully!")
+ print("=" * 60)
+
+ except KeyboardInterrupt:
+ print("\n\nInterrupted by user")
+ except Exception as e:
+ print(f"\n\nError: {e}")
+ import traceback
+
+ traceback.print_exc()
diff --git a/src/envs/warehouse_env/README.md b/src/envs/warehouse_env/README.md
new file mode 100644
index 00000000..e4fef0ef
--- /dev/null
+++ b/src/envs/warehouse_env/README.md
@@ -0,0 +1,532 @@
+---
+title: Warehouse Env Environment Server
+emoji: π
+colorFrom: blue
+colorTo: indigo
+sdk: docker
+pinned: false
+app_port: 8000
+base_path: /demo
+tags:
+ - openenv
+ - reinforcement-learning
+ - logistics
+ - warehouse
+ - robotics
+---
+
+# Warehouse Optimization Environment
+
+A grid-based warehouse logistics optimization environment for reinforcement learning. This environment simulates a warehouse robot that must navigate through obstacles, pick up packages from pickup zones, and deliver them to designated dropoff zones while optimizing for time and efficiency.
+
+## Overview
+
+The Warehouse Environment is designed for training reinforcement learning agents on logistics and pathfinding tasks. It features:
+
+- **Grid-based navigation** with walls and obstacles
+- **Package pickup and delivery** mechanics
+- **Multi-objective optimization** (speed, deliveries, efficiency)
+- **Scalable difficulty** levels (1-5)
+- **Dense reward signals** for effective learning
+- **ASCII visualization** for debugging
+
+## Quick Start
+
+### Using Docker (Recommended)
+
+```bash
+# Build the Docker image (from OpenEnv root)
+cd /path/to/OpenEnv
+docker build -f src/envs/warehouse_env/server/Dockerfile -t warehouse-env:latest .
+
+# Run with default settings (difficulty level 2)
+docker run -p 8000:8000 warehouse-env:latest
+
+# Run with custom difficulty
+docker run -p 8000:8000 -e DIFFICULTY_LEVEL=3 warehouse-env:latest
+```
+
+### Using Python Client
+
+```python
+from envs.warehouse_env import WarehouseEnv, WarehouseAction
+
+# Connect to server (or start from Docker)
+env = WarehouseEnv.from_docker_image(
+ "warehouse-env:latest",
+ environment={"DIFFICULTY_LEVEL": "2"}
+)
+
+# Reset environment
+result = env.reset()
+print(f"Warehouse size: {len(result.observation.grid)}x{len(result.observation.grid[0])}")
+print(f"Packages to deliver: {result.observation.total_packages}")
+
+# Run episode
+done = False
+while not done:
+ # Simple policy: move toward pickup if not carrying, else toward dropoff
+ if result.observation.robot_carrying is None:
+ action = WarehouseAction(action_id=4) # Try to pick up
+ else:
+ action = WarehouseAction(action_id=5) # Try to drop off
+
+ result = env.step(action)
+ print(f"Step {result.observation.step_count}: {result.observation.message}")
+ print(f"Reward: {result.reward:.2f}")
+
+ done = result.done
+
+print(f"\nEpisode finished!")
+print(f"Delivered: {result.observation.packages_delivered}/{result.observation.total_packages}")
+print(f"Total reward: {env.state().cum_reward:.2f}")
+
+env.close()
+```
+
+## Environment Specification
+
+### State Space
+
+The environment provides rich observations including:
+
+- **Grid layout**: 2D array with cell types (empty, wall, shelf, pickup zone, dropoff zone)
+- **Robot state**: Position, carrying status
+- **Package information**: Locations, status (waiting/picked/delivered), priorities
+- **Episode metrics**: Step count, deliveries, time remaining
+
+### Action Space
+
+6 discrete actions:
+
+| Action ID | Action Name | Description |
+|-----------|-------------|-------------|
+| 0 | MOVE_UP | Move robot one cell up |
+| 1 | MOVE_DOWN | Move robot one cell down |
+| 2 | MOVE_LEFT | Move robot one cell left |
+| 3 | MOVE_RIGHT | Move robot one cell right |
+| 4 | PICK_UP | Pick up package at current location |
+| 5 | DROP_OFF | Drop off package at current location |
+
+### Reward Structure
+
+Multi-component reward function:
+
+- **+100**: Successful package delivery
+- **+10**: Successful package pickup
+- **+0.1 Γ time_remaining**: Time bonus for fast deliveries
+- **+200**: Completion bonus (all packages delivered)
+- **-0.1**: Small step penalty (encourages efficiency)
+- **-1**: Invalid action penalty
+
+### Episode Termination
+
+Episodes end when:
+- All packages are delivered (success!)
+- Maximum steps reached (timeout)
+
+## Difficulty Levels
+
+### Level 1: Simple
+- Grid: 5Γ5
+- Packages: 1
+- Obstacles: 0
+- Max steps: 50
+- **Best for**: Testing, debugging, quick validation
+
+### Level 2: Easy (Default)
+- Grid: 8Γ8
+- Packages: 2
+- Obstacles: 3
+- Max steps: 100
+- **Best for**: Initial training, curriculum learning start
+
+### Level 3: Medium
+- Grid: 10Γ10
+- Packages: 3
+- Obstacles: 8
+- Max steps: 150
+- **Best for**: Intermediate training, testing learned policies
+
+### Level 4: Hard
+- Grid: 15Γ15
+- Packages: 5
+- Obstacles: 20
+- Max steps: 250
+- **Best for**: Advanced training, evaluation
+
+### Level 5: Expert
+- Grid: 20Γ20
+- Packages: 8
+- Obstacles: 40
+- Max steps: 400
+- **Best for**: Final evaluation, research benchmarks
+
+## Configuration
+
+### Environment Variables
+
+Configure the warehouse via environment variables:
+
+```bash
+# Difficulty level (1-5)
+DIFFICULTY_LEVEL=2
+
+# Custom grid size (overrides difficulty)
+GRID_WIDTH=12
+GRID_HEIGHT=12
+
+# Custom package count (overrides difficulty)
+NUM_PACKAGES=4
+
+# Custom step limit (overrides difficulty)
+MAX_STEPS=200
+
+# Random seed for reproducibility
+RANDOM_SEED=42
+```
+
+### Docker Example
+
+```bash
+docker run -p 8000:8000 \
+ -e DIFFICULTY_LEVEL=3 \
+ -e RANDOM_SEED=42 \
+ warehouse-env:latest
+```
+
+### Python Client Example
+
+```python
+env = WarehouseEnv.from_docker_image(
+ "warehouse-env:latest",
+ environment={
+ "DIFFICULTY_LEVEL": "3",
+ "GRID_WIDTH": "12",
+ "GRID_HEIGHT": "12",
+ "NUM_PACKAGES": "4",
+ "MAX_STEPS": "200",
+ "RANDOM_SEED": "42"
+ }
+)
+```
+
+## Visualization
+
+### ASCII Rendering
+
+Get a visual representation of the warehouse state:
+
+```python
+# Get ASCII visualization
+ascii_art = env.render_ascii()
+print(ascii_art)
+```
+
+Example output:
+```
+=================================
+Step: 15/100 | Delivered: 1/2 | Reward: 109.9
+=================================
+β β β β β β β β
+β P . . . # . β
+β . # . . . . β
+β . . R . # . β
+β . # . . . . β
+β . . . . D . β
+β . . . . . . β
+β β β β β β β β
+=================================
+Robot at (3, 3), carrying: 1
+β Package #0: delivered (P(1,1)βD(5,5))
+β» Package #1: picked (P(1,1)βD(5,5))
+=================================
+Legend: r/R=Robot(empty/carrying), P=Pickup, D=Dropoff, #=Shelf, β=Wall
+```
+
+## Training Examples
+
+### Random Agent
+
+```python
+import random
+from envs.warehouse_env import WarehouseEnv, WarehouseAction
+
+env = WarehouseEnv.from_docker_image("warehouse-env:latest")
+
+for episode in range(100):
+ result = env.reset()
+ done = False
+
+ while not done:
+ # Random action
+ action = WarehouseAction(action_id=random.randint(0, 5))
+ result = env.step(action)
+ done = result.done
+
+ print(f"Episode {episode}: Delivered {result.observation.packages_delivered}")
+
+env.close()
+```
+
+### Greedy Agent (Move toward target)
+
+```python
+from envs.warehouse_env import WarehouseEnv, WarehouseAction
+
+def get_greedy_action(obs):
+ """Simple greedy policy: move toward nearest target."""
+ robot_x, robot_y = obs.robot_position
+
+ # If not carrying, move toward nearest waiting package
+ if obs.robot_carrying is None:
+ for pkg in obs.packages:
+ if pkg["status"] == "waiting":
+ target_x, target_y = pkg["pickup_location"]
+ break
+ else:
+ return 4 # Try to pick up if at location
+ else:
+ # Move toward dropoff zone
+ pkg = next(p for p in obs.packages if p["id"] == obs.robot_carrying)
+ target_x, target_y = pkg["dropoff_location"]
+
+ # Simple pathfinding: move closer on one axis
+ if robot_x < target_x:
+ return 3 # RIGHT
+ elif robot_x > target_x:
+ return 2 # LEFT
+ elif robot_y < target_y:
+ return 1 # DOWN
+ elif robot_y > target_y:
+ return 0 # UP
+ else:
+ # At target location
+ return 4 if obs.robot_carrying is None else 5
+
+env = WarehouseEnv.from_docker_image("warehouse-env:latest")
+
+for episode in range(10):
+ result = env.reset()
+ done = False
+
+ while not done:
+ action_id = get_greedy_action(result.observation)
+ action = WarehouseAction(action_id=action_id)
+ result = env.step(action)
+ done = result.done
+
+ state = env.state()
+ print(f"Episode {episode}: {state.packages_delivered}/{state.total_packages} delivered, "
+ f"reward: {state.cum_reward:.2f}")
+
+env.close()
+```
+
+### Integration with RL Libraries
+
+#### Stable Baselines 3
+
+```python
+import gymnasium as gym
+import numpy as np
+from stable_baselines3 import PPO
+from envs.warehouse_env import WarehouseEnv, WarehouseAction
+
+class WarehouseGymWrapper(gym.Env):
+ """Gymnasium wrapper for Warehouse environment."""
+
+ def __init__(self, base_url="http://localhost:8000"):
+ super().__init__()
+ self.env = WarehouseEnv(base_url=base_url)
+
+ # Define spaces (simplified)
+ self.action_space = gym.spaces.Discrete(6)
+
+ # Observation: grid + robot state + package info
+ # For simplicity, use flattened representation
+ self.observation_space = gym.spaces.Box(
+ low=0, high=255,
+ shape=(200,), # Adjust based on grid size
+ dtype=np.float32
+ )
+
+ def reset(self, **kwargs):
+ result = self.env.reset()
+ obs = self._process_obs(result.observation)
+ return obs, {}
+
+ def step(self, action):
+ result = self.env.step(WarehouseAction(action_id=int(action)))
+ obs = self._process_obs(result.observation)
+ return obs, result.reward, result.done, False, {}
+
+ def _process_obs(self, observation):
+ # Flatten grid and add robot/package info
+ grid_flat = np.array(observation.grid).flatten()
+ robot_pos = np.array(observation.robot_position)
+ carrying = np.array([1 if observation.robot_carrying else 0])
+
+ # Pad or truncate to fixed size
+ obs = np.concatenate([
+ grid_flat[:196], # Grid (max 14x14)
+ robot_pos, # Robot position (2)
+ carrying, # Carrying status (1)
+ [observation.packages_delivered] # Progress (1)
+ ])
+ return obs.astype(np.float32)
+
+ def close(self):
+ self.env.close()
+
+# Train with PPO
+env = WarehouseGymWrapper()
+model = PPO("MlpPolicy", env, verbose=1)
+model.learn(total_timesteps=10000)
+model.save("warehouse_ppo")
+
+env.close()
+```
+
+## API Reference
+
+### WarehouseAction
+
+```python
+@dataclass
+class WarehouseAction(Action):
+ action_id: int # 0-5
+```
+
+### WarehouseObservation
+
+```python
+@dataclass
+class WarehouseObservation(Observation):
+ grid: List[List[int]] # Warehouse layout
+ robot_position: tuple[int, int] # Robot (x, y)
+ robot_carrying: Optional[int] # Package ID or None
+ packages: List[Dict[str, Any]] # Package states
+ step_count: int # Current step
+ packages_delivered: int # Successful deliveries
+ total_packages: int # Total packages
+ time_remaining: int # Steps left
+ action_success: bool # Last action valid
+ message: str # Status message
+```
+
+### WarehouseState
+
+```python
+@dataclass
+class WarehouseState(State):
+ episode_id: str # Unique episode ID
+ step_count: int # Steps taken
+ packages_delivered: int # Deliveries
+ total_packages: int # Total packages
+ difficulty_level: int # Difficulty (1-5)
+ grid_size: tuple[int, int] # Grid dimensions
+ cum_reward: float # Cumulative reward
+ is_done: bool # Episode finished
+```
+
+## Development
+
+### Local Setup (without Docker)
+
+```bash
+# Install dependencies
+cd OpenEnv/src/envs/warehouse_env
+pip install -r server/requirements.txt
+
+# Run server
+python -m uvicorn envs.warehouse_env.server.app:app --host 0.0.0.0 --port 8000
+```
+
+### Running Tests
+
+```bash
+# Run basic test
+python examples/warehouse_simple.py
+```
+
+## Architecture
+
+```
+βββββββββββββββββββββββββββββββββββββββ
+β RL Training Framework (Client) β
+β ββββββββββββββββββββββββββββββββ β
+β β Agent Policy (PPO/DQN/etc) β β
+β ββββββββββββ¬ββββββββββββββββββββ β
+β β β
+β ββββββββββββΌββββββββββββββββββββ β
+β β WarehouseEnv (HTTPEnvClient) β β
+β ββββββββββββ¬ββββββββββββββββββββ β
+βββββββββββββββΌββββββββββββββββββββββββ
+ β HTTP/JSON
+βββββββββββββββΌββββββββββββββββββββββββ
+β Docker Container β
+β βββββββββββββββββββββββββββββββ β
+β β FastAPI Server β β
+β ββββββββββββ¬βββββββββββββββββββ β
+β ββββββββββββΌβββββββββββββββββββ β
+β β WarehouseEnvironment β β
+β β - Grid generation β β
+β β - Collision detection β β
+β β - Reward calculation β β
+β β - Package management β β
+β βββββββββββββββββββββββββββββββ β
+βββββββββββββββββββββββββββββββββββββββ
+```
+
+## Real-World Applications
+
+This environment simulates real warehouse optimization problems:
+
+- **Amazon fulfillment centers**: Robot pathfinding and package routing
+- **Manufacturing warehouses**: Material handling optimization
+- **Distribution centers**: Inventory management and delivery sequencing
+- **Automated storage**: Efficient retrieval systems
+
+## Research & Benchmarking
+
+The warehouse environment is suitable for research on:
+
+- **Pathfinding algorithms**: A*, Dijkstra, learned policies
+- **Multi-objective RL**: Balancing speed, safety, and coverage
+- **Curriculum learning**: Progressive difficulty scaling
+- **Transfer learning**: Generalization across warehouse layouts
+- **Hierarchical RL**: High-level planning + low-level control
+
+## Contributing
+
+We welcome contributions! Areas for enhancement:
+
+- **Multi-robot coordination**: Multiple robots working together
+- **Dynamic obstacles**: Moving shelves or other robots
+- **Battery management**: Energy constraints and charging stations
+- **Priority queuing**: Handling different package urgencies
+- **3D visualization**: Enhanced rendering
+
+## License
+
+BSD 3-Clause License (see LICENSE file)
+
+## Citation
+
+If you use this environment in your research, please cite:
+
+```bibtex
+@software{warehouse_env_openenv,
+ title = {Warehouse Optimization Environment for OpenEnv},
+ author = {OpenEnv Contributors},
+ year = {2024},
+ url = {https://github.com/meta-pytorch/OpenEnv}
+}
+```
+
+## References
+
+- [OpenEnv Documentation](https://github.com/meta-pytorch/OpenEnv)
+- [Gymnasium API](https://gymnasium.farama.org/)
+- [Warehouse Robotics Research](https://arxiv.org/abs/2006.14876)
diff --git a/src/envs/warehouse_env/__init__.py b/src/envs/warehouse_env/__init__.py
new file mode 100644
index 00000000..aac2275b
--- /dev/null
+++ b/src/envs/warehouse_env/__init__.py
@@ -0,0 +1,23 @@
+"""
+Warehouse Optimization Environment for OpenEnv.
+
+A grid-based warehouse logistics optimization environment for training
+RL agents on pathfinding, package pickup/delivery, and multi-objective
+optimization tasks.
+"""
+
+from envs.warehouse_env.client import WarehouseEnv
+from envs.warehouse_env.models import (
+ Package,
+ WarehouseAction,
+ WarehouseObservation,
+ WarehouseState,
+)
+
+__all__ = [
+ "WarehouseAction",
+ "WarehouseObservation",
+ "WarehouseState",
+ "Package",
+ "WarehouseEnv",
+]
diff --git a/src/envs/warehouse_env/client.py b/src/envs/warehouse_env/client.py
new file mode 100644
index 00000000..bfbbc422
--- /dev/null
+++ b/src/envs/warehouse_env/client.py
@@ -0,0 +1,134 @@
+"""
+HTTP client for Warehouse Optimization Environment.
+
+This module provides the client-side interface for interacting with
+the warehouse environment server.
+"""
+
+from typing import Optional
+
+import requests
+
+from core.client_types import StepResult
+from core.http_env_client import HTTPEnvClient
+from envs.warehouse_env.models import (
+ WarehouseAction,
+ WarehouseObservation,
+ WarehouseState,
+)
+
+
+class WarehouseEnv(HTTPEnvClient[WarehouseAction, WarehouseObservation]):
+ """
+ HTTP client for the Warehouse Optimization environment.
+
+ This environment simulates a warehouse robot that must pick up and deliver
+ packages while navigating a grid-based warehouse with obstacles.
+
+ Example usage:
+ ```python
+ from envs.warehouse_env import WarehouseEnv, WarehouseAction
+
+ # Start environment from Docker image
+ env = WarehouseEnv.from_docker_image(
+ "warehouse-env:latest",
+ environment={"DIFFICULTY_LEVEL": "2"}
+ )
+
+ # Reset environment
+ result = env.reset()
+ print(f"Grid size: {len(result.observation.grid)}x{len(result.observation.grid[0])}")
+ print(f"Packages: {result.observation.total_packages}")
+
+ # Take actions
+ for step in range(100):
+ # Example: move randomly or pick/drop based on state
+ if result.observation.robot_carrying is None:
+ action = WarehouseAction(action_id=4) # Try to pick up
+ else:
+ action = WarehouseAction(action_id=5) # Try to drop off
+
+ result = env.step(action)
+ print(f"Step {step}: {result.observation.message}")
+
+ if result.done:
+ print(f"Episode finished! Delivered {result.observation.packages_delivered} packages")
+ break
+
+ env.close()
+ ```
+
+ Configuration (via environment variables):
+ - DIFFICULTY_LEVEL: 1-5 (default: 2)
+ 1 = Simple (5x5, 1 package)
+ 2 = Easy (8x8, 2 packages)
+ 3 = Medium (10x10, 3 packages)
+ 4 = Hard (15x15, 5 packages)
+ 5 = Expert (20x20, 8 packages)
+
+ - GRID_WIDTH: Custom grid width (overrides difficulty)
+ - GRID_HEIGHT: Custom grid height (overrides difficulty)
+ - NUM_PACKAGES: Custom number of packages (overrides difficulty)
+ - MAX_STEPS: Maximum steps per episode (default: based on difficulty)
+ - RANDOM_SEED: Random seed for reproducibility (default: None)
+ """
+
+ def __init__(self, base_url: str = "http://localhost:8000", **kwargs):
+ """Initialize warehouse environment client."""
+ super().__init__(base_url=base_url, **kwargs)
+ self.base_url = base_url # Store for render_ascii() method
+
+ def _step_payload(self, action: WarehouseAction) -> dict:
+ """Convert WarehouseAction to JSON payload for step request."""
+ return {"action_id": action.action_id}
+
+ def _parse_result(self, payload: dict) -> StepResult[WarehouseObservation]:
+ """Parse server response into StepResult[WarehouseObservation]."""
+ obs_data = payload.get("observation", {})
+
+ observation = WarehouseObservation(
+ grid=obs_data.get("grid", []),
+ robot_position=tuple(obs_data.get("robot_position", [0, 0])),
+ robot_carrying=obs_data.get("robot_carrying"),
+ packages=obs_data.get("packages", []),
+ step_count=obs_data.get("step_count", 0),
+ packages_delivered=obs_data.get("packages_delivered", 0),
+ total_packages=obs_data.get("total_packages", 0),
+ time_remaining=obs_data.get("time_remaining", 0),
+ action_success=obs_data.get("action_success", False),
+ message=obs_data.get("message", ""),
+ metadata=obs_data.get("metadata", {}),
+ )
+
+ return StepResult(
+ observation=observation,
+ reward=payload.get("reward", 0.0),
+ done=payload.get("done", False),
+ )
+
+ def _parse_state(self, payload: dict) -> WarehouseState:
+ """Parse server response into WarehouseState object."""
+ return WarehouseState(
+ episode_id=payload.get("episode_id", ""),
+ step_count=payload.get("step_count", 0),
+ packages_delivered=payload.get("packages_delivered", 0),
+ total_packages=payload.get("total_packages", 0),
+ difficulty_level=payload.get("difficulty_level", 2),
+ grid_size=tuple(payload.get("grid_size", [0, 0])),
+ cum_reward=payload.get("cum_reward", 0.0),
+ is_done=payload.get("is_done", False),
+ )
+
+ def render_ascii(self) -> str:
+ """
+ Get ASCII visualization of the current warehouse state.
+
+ Returns:
+ String representation of the warehouse grid
+ """
+ try:
+ response = requests.get(f"{self.base_url}/render")
+ response.raise_for_status()
+ return response.json()["ascii"]
+ except Exception as e:
+ return f"Error rendering: {str(e)}"
diff --git a/src/envs/warehouse_env/models.py b/src/envs/warehouse_env/models.py
new file mode 100644
index 00000000..82b4afae
--- /dev/null
+++ b/src/envs/warehouse_env/models.py
@@ -0,0 +1,124 @@
+"""
+Data models for Warehouse Optimization Environment.
+
+This module defines the Action, Observation, and State dataclasses
+for the warehouse logistics optimization environment.
+"""
+
+from dataclasses import dataclass, field
+from typing import Any, Dict, List, Optional
+
+# Support both in-repo and standalone imports
+try:
+ # In-repo imports (when running from OpenEnv repository)
+ from core.env_server import Action, Observation, State
+except ImportError:
+ # Standalone imports (when environment is standalone with openenv-core from pip)
+ from openenv_core.env_server import Action, Observation, State
+
+
+@dataclass
+class WarehouseAction(Action):
+ """
+ Action for the warehouse robot.
+
+ Actions:
+ 0: MOVE_UP - Move robot one cell up
+ 1: MOVE_DOWN - Move robot one cell down
+ 2: MOVE_LEFT - Move robot one cell left
+ 3: MOVE_RIGHT - Move robot one cell right
+ 4: PICK_UP - Pick up package at current location
+ 5: DROP_OFF - Drop off package at current location
+ """
+
+ action_id: int # 0-5
+
+ # Action names for reference
+ ACTION_NAMES = {
+ 0: "MOVE_UP",
+ 1: "MOVE_DOWN",
+ 2: "MOVE_LEFT",
+ 3: "MOVE_RIGHT",
+ 4: "PICK_UP",
+ 5: "DROP_OFF",
+ }
+
+ def __post_init__(self):
+ """Validate action ID."""
+ if self.action_id not in range(6):
+ raise ValueError(f"action_id must be 0-5, got {self.action_id}")
+
+ @property
+ def action_name(self) -> str:
+ """Get human-readable action name."""
+ return self.ACTION_NAMES.get(self.action_id, "UNKNOWN")
+
+
+@dataclass
+class Package:
+ """Represents a package in the warehouse."""
+
+ id: int
+ status: str # "waiting", "picked", "delivered"
+ pickup_location: tuple[int, int]
+ dropoff_location: tuple[int, int]
+ priority: int # 1 (low), 2 (medium), 3 (high)
+ time_waiting: int # Steps since created
+
+
+@dataclass(kw_only=True)
+class WarehouseObservation(Observation):
+ """
+ Observation returned after each step in the warehouse environment.
+
+ Attributes:
+ grid: 2D list representing the warehouse layout
+ 0=empty, 1=wall, 2=shelf, 3=pickup_zone, 4=dropoff_zone
+ robot_position: Current (x, y) position of the robot
+ robot_carrying: Package ID if carrying, None otherwise
+ packages: List of all packages and their states
+ step_count: Current step number in episode
+ packages_delivered: Number of packages successfully delivered
+ total_packages: Total number of packages in episode
+ time_remaining: Steps remaining before timeout
+ action_success: Whether the last action was successful
+ message: Human-readable message about last action
+ """
+
+ grid: List[List[int]]
+ robot_position: tuple[int, int]
+ robot_carrying: Optional[int]
+ packages: List[Dict[str, Any]]
+ step_count: int
+ packages_delivered: int
+ total_packages: int
+ time_remaining: int
+ action_success: bool
+ message: str
+ metadata: Dict[str, Any] = field(default_factory=dict)
+
+
+@dataclass(kw_only=True)
+class WarehouseState(State):
+ """
+ Episode state tracking for the warehouse environment.
+
+ Attributes:
+ episode_id: Unique identifier for this episode
+ step_count: Number of steps taken
+ packages_delivered: Packages successfully delivered
+ total_packages: Total packages in episode
+ difficulty_level: Difficulty setting (1-5)
+ grid_size: (width, height) of warehouse
+ cum_reward: Cumulative reward for episode
+ is_done: Whether episode has ended
+ """
+
+ episode_id: str
+ step_count: int
+ packages_delivered: int
+ total_packages: int
+ difficulty_level: int
+ grid_size: tuple[int, int]
+ cum_reward: float
+ is_done: bool
diff --git a/src/envs/warehouse_env/openenv.yaml b/src/envs/warehouse_env/openenv.yaml
new file mode 100644
index 00000000..b28ab042
--- /dev/null
+++ b/src/envs/warehouse_env/openenv.yaml
@@ -0,0 +1,57 @@
+---
+name: warehouse_env
+version: "1.0.0"
+description: "Grid-based warehouse logistics optimization environment for reinforcement learning"
+author: "OpenEnv Contributors"
+tags:
+ - reinforcement-learning
+ - logistics
+ - pathfinding
+ - grid-world
+ - warehouse
+ - robotics
+
+environment:
+ type: "discrete"
+ action_space:
+ type: "Discrete"
+ n: 6
+ observation_space:
+ type: "Dict"
+ spaces:
+ grid: "Box"
+ robot_position: "Tuple"
+ packages: "List"
+
+difficulty_levels:
+ - level: 1
+ name: "Simple"
+ description: "5x5 grid, 1 package, no obstacles"
+ - level: 2
+ name: "Easy"
+ description: "8x8 grid, 2 packages, 3 obstacles"
+ - level: 3
+ name: "Medium"
+ description: "10x10 grid, 3 packages, 8 obstacles"
+ - level: 4
+ name: "Hard"
+ description: "15x15 grid, 5 packages, 20 obstacles"
+ - level: 5
+ name: "Expert"
+ description: "20x20 grid, 8 packages, 40 obstacles"
+
+configuration:
+ default_difficulty: 2
+ environment_variables:
+ - DIFFICULTY_LEVEL
+ - GRID_WIDTH
+ - GRID_HEIGHT
+ - NUM_PACKAGES
+ - MAX_STEPS
+ - RANDOM_SEED
+
+docker:
+ image_name: "warehouse-env"
+ default_port: 8000
+ build_context: "../../../"
+ dockerfile: "src/envs/warehouse_env/server/Dockerfile"
diff --git a/src/envs/warehouse_env/pyproject.toml b/src/envs/warehouse_env/pyproject.toml
new file mode 100644
index 00000000..4dbd2fec
--- /dev/null
+++ b/src/envs/warehouse_env/pyproject.toml
@@ -0,0 +1,63 @@
+[project]
+name = "warehouse-env"
+version = "1.0.0"
+description = "Grid-based warehouse logistics optimization environment for reinforcement learning"
+authors = [
+ {name = "OpenEnv Contributors"}
+]
+readme = "README.md"
+requires-python = ">=3.11"
+license = {text = "BSD-3-Clause"}
+keywords = [
+ "reinforcement-learning",
+ "openenv",
+ "warehouse",
+ "logistics",
+ "pathfinding",
+ "robotics"
+]
+classifiers = [
+ "Development Status :: 4 - Beta",
+ "Intended Audience :: Developers",
+ "Intended Audience :: Science/Research",
+ "License :: OSI Approved :: BSD License",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
+]
+
+dependencies = [
+ "openenv-core",
+ "fastapi>=0.104.1",
+ "uvicorn>=0.24.0",
+ "requests>=2.31.0",
+ "pydantic>=2.5.0",
+]
+
+[project.scripts]
+warehouse-server = "envs.warehouse_env.server.app:main"
+
+[project.optional-dependencies]
+dev = [
+ "pytest>=7.0.0",
+ "pytest-asyncio>=0.21.0",
+ "black>=23.0.0",
+ "ruff>=0.1.0",
+]
+
+[build-system]
+requires = ["setuptools>=61.0", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools]
+packages = ["warehouse_env"]
+package-dir = {"" = "."}
+
+[tool.black]
+line-length = 100
+target-version = ['py311']
+
+[tool.ruff]
+line-length = 100
+target-version = "py311"
diff --git a/src/envs/warehouse_env/server/Dockerfile b/src/envs/warehouse_env/server/Dockerfile
new file mode 100644
index 00000000..d24251a4
--- /dev/null
+++ b/src/envs/warehouse_env/server/Dockerfile
@@ -0,0 +1,30 @@
+FROM python:3.11-slim
+
+WORKDIR /app
+
+# Copy all warehouse environment files (for HF Spaces deployment)
+COPY . /app/
+
+# Install Python dependencies
+RUN pip install --no-cache-dir \
+ fastapi==0.104.1 \
+ uvicorn==0.24.0 \
+ pydantic==2.5.0 \
+ requests==2.31.0
+
+# Expose port
+EXPOSE 8000
+
+# Environment variables with defaults
+ENV DIFFICULTY_LEVEL=2
+ENV GRID_WIDTH=0
+ENV GRID_HEIGHT=0
+ENV NUM_PACKAGES=0
+ENV MAX_STEPS=0
+ENV RANDOM_SEED=0
+
+# Set Python path to include current directory
+ENV PYTHONPATH=/app
+
+# Run the server
+CMD ["python", "-m", "uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "8000"]
diff --git a/src/envs/warehouse_env/server/__init__.py b/src/envs/warehouse_env/server/__init__.py
new file mode 100644
index 00000000..b497ea31
--- /dev/null
+++ b/src/envs/warehouse_env/server/__init__.py
@@ -0,0 +1 @@
+"""Server module for Warehouse Optimization Environment."""
diff --git a/src/envs/warehouse_env/server/app.py b/src/envs/warehouse_env/server/app.py
new file mode 100644
index 00000000..eafa45ba
--- /dev/null
+++ b/src/envs/warehouse_env/server/app.py
@@ -0,0 +1,274 @@
+"""
+FastAPI server for Warehouse Optimization Environment.
+
+This module creates the HTTP server that exposes the warehouse environment
+via REST API endpoints.
+"""
+
+import os
+
+from core.env_server import create_app
+from envs.warehouse_env.models import WarehouseAction, WarehouseObservation
+from envs.warehouse_env.server.warehouse_environment import WarehouseEnvironment
+from fastapi import FastAPI
+from fastapi.responses import JSONResponse, HTMLResponse, FileResponse
+
+
+# Get configuration from environment variables
+DIFFICULTY_LEVEL = int(os.getenv("DIFFICULTY_LEVEL", "2"))
+GRID_WIDTH = int(os.getenv("GRID_WIDTH", "0")) or None
+GRID_HEIGHT = int(os.getenv("GRID_HEIGHT", "0")) or None
+NUM_PACKAGES = int(os.getenv("NUM_PACKAGES", "0")) or None
+MAX_STEPS = int(os.getenv("MAX_STEPS", "0")) or None
+RANDOM_SEED = int(os.getenv("RANDOM_SEED", "0")) or None
+
+
+# Create the warehouse environment instance
+warehouse_env = WarehouseEnvironment(
+ difficulty_level=DIFFICULTY_LEVEL,
+ grid_width=GRID_WIDTH,
+ grid_height=GRID_HEIGHT,
+ num_packages=NUM_PACKAGES,
+ max_steps=MAX_STEPS,
+ random_seed=RANDOM_SEED,
+)
+
+
+# Create FastAPI app using OpenEnv's helper (with web interface if enabled)
+app = create_app(warehouse_env, WarehouseAction, WarehouseObservation, env_name="warehouse_env")
+
+
+# Add custom render endpoints
+@app.post("/set-difficulty")
+async def set_difficulty(request: dict):
+ """Change the difficulty level and reset the environment."""
+ try:
+ difficulty = int(request.get("difficulty", 2))
+ if difficulty < 1 or difficulty > 5:
+ return JSONResponse(
+ status_code=400,
+ content={"error": "Difficulty must be between 1 and 5"}
+ )
+
+ # Recreate the warehouse environment with new difficulty
+ global warehouse_env
+ warehouse_env = WarehouseEnvironment(
+ difficulty_level=difficulty,
+ grid_width=None,
+ grid_height=None,
+ num_packages=None,
+ max_steps=None,
+ random_seed=None,
+ )
+
+ # Reset the environment
+ observation = warehouse_env.reset()
+
+ return JSONResponse(content={
+ "success": True,
+ "difficulty": difficulty,
+ "grid_size": (warehouse_env.grid_width, warehouse_env.grid_height),
+ "num_packages": warehouse_env.num_packages,
+ "max_steps": warehouse_env.max_steps,
+ "observation": {
+ "step_count": observation.step_count,
+ "packages_delivered": observation.packages_delivered,
+ "total_packages": observation.total_packages,
+ "robot_position": observation.robot_position,
+ }
+ })
+ except Exception as e:
+ return JSONResponse(
+ status_code=500,
+ content={"error": f"Failed to set difficulty: {str(e)}"}
+ )
+
+
+@app.get("/render")
+async def render():
+ """Get ASCII visualization of warehouse state."""
+ try:
+ ascii_art = warehouse_env.render_ascii()
+ return JSONResponse(content={"ascii": ascii_art})
+ except Exception as e:
+ return JSONResponse(
+ status_code=500, content={"error": f"Failed to render: {str(e)}"}
+ )
+
+@app.get("/render/html")
+async def render_html():
+ """Get HTML visualization of warehouse state."""
+ try:
+ html_content = warehouse_env.render_html()
+ return HTMLResponse(content=html_content)
+ except Exception as e:
+ return JSONResponse(
+ status_code=500, content={"error": f"Failed to render HTML: {str(e)}"}
+ )
+
+@app.post("/auto-step")
+async def auto_step():
+ """Execute one step using a greedy agent."""
+ try:
+ # Get current observation
+ if warehouse_env.is_done:
+ return JSONResponse(content={
+ "done": True,
+ "message": "Episode finished. Reset to start a new episode."
+ })
+
+ # Simple greedy policy
+ action_id = _get_greedy_action()
+ action = WarehouseAction(action_id=action_id)
+
+ # Execute step
+ result = warehouse_env.step(action)
+
+ return JSONResponse(content={
+ "action": action.action_name,
+ "message": result.message,
+ "reward": result.reward,
+ "done": result.done,
+ "step_count": result.step_count,
+ "packages_delivered": result.packages_delivered,
+ "robot_position": result.robot_position,
+ })
+ except Exception as e:
+ return JSONResponse(
+ status_code=500, content={"error": f"Failed to execute auto-step: {str(e)}"}
+ )
+
+def _get_greedy_action() -> int:
+ """Simple greedy policy with obstacle avoidance."""
+ robot_x, robot_y = warehouse_env.robot_position
+
+ # Determine target location
+ if warehouse_env.robot_carrying is None:
+ # Not carrying: move toward nearest waiting package
+ target = None
+ min_dist = float('inf')
+
+ for package in warehouse_env.packages:
+ if package.status == "waiting":
+ px, py = package.pickup_location
+ dist = abs(robot_x - px) + abs(robot_y - py)
+ if dist < min_dist:
+ min_dist = dist
+ target = (px, py)
+
+ if target is None:
+ return 4 # Try to pick up if at location
+
+ target_x, target_y = target
+ else:
+ # Carrying: move toward dropoff zone
+ package = next((p for p in warehouse_env.packages if p.id == warehouse_env.robot_carrying), None)
+ if package:
+ target_x, target_y = package.dropoff_location
+ else:
+ return 5 # Try to drop off
+
+ # Check if at target location
+ if robot_x == target_x and robot_y == target_y:
+ return 4 if warehouse_env.robot_carrying is None else 5
+
+ # Try to move toward target, checking for obstacles
+ # Priority: move on axis with larger distance first
+ dx = target_x - robot_x
+ dy = target_y - robot_y
+
+ # List of possible moves in order of preference
+ moves = []
+
+ if abs(dx) > abs(dy):
+ # Prioritize horizontal movement
+ if dx > 0:
+ moves.append((3, robot_x + 1, robot_y)) # RIGHT
+ elif dx < 0:
+ moves.append((2, robot_x - 1, robot_y)) # LEFT
+
+ if dy > 0:
+ moves.append((1, robot_x, robot_y + 1)) # DOWN
+ elif dy < 0:
+ moves.append((0, robot_x, robot_y - 1)) # UP
+ else:
+ # Prioritize vertical movement
+ if dy > 0:
+ moves.append((1, robot_x, robot_y + 1)) # DOWN
+ elif dy < 0:
+ moves.append((0, robot_x, robot_y - 1)) # UP
+
+ if dx > 0:
+ moves.append((3, robot_x + 1, robot_y)) # RIGHT
+ elif dx < 0:
+ moves.append((2, robot_x - 1, robot_y)) # LEFT
+
+ # Add perpendicular moves as fallback
+ if dx == 0 and dy != 0:
+ moves.append((3, robot_x + 1, robot_y)) # RIGHT
+ moves.append((2, robot_x - 1, robot_y)) # LEFT
+ elif dy == 0 and dx != 0:
+ moves.append((1, robot_x, robot_y + 1)) # DOWN
+ moves.append((0, robot_x, robot_y - 1)) # UP
+
+ # Try moves in order until we find a valid one
+ WALL = 1
+ SHELF = 2
+
+ for action_id, new_x, new_y in moves:
+ # Check bounds
+ if 0 <= new_x < warehouse_env.grid_width and 0 <= new_y < warehouse_env.grid_height:
+ # Check if cell is passable
+ if warehouse_env.grid[new_y][new_x] not in [WALL, SHELF]:
+ return action_id
+
+ # If no valid move toward target, try any valid move
+ for action_id, dx, dy in [(0, 0, -1), (1, 0, 1), (2, -1, 0), (3, 1, 0)]:
+ new_x, new_y = robot_x + dx, robot_y + dy
+ if 0 <= new_x < warehouse_env.grid_width and 0 <= new_y < warehouse_env.grid_height:
+ if warehouse_env.grid[new_y][new_x] not in [WALL, SHELF]:
+ return action_id
+
+ # Last resort: try pickup/dropoff
+ return 4 if warehouse_env.robot_carrying is None else 5
+
+
+# Add health check endpoint
+@app.get("/health")
+async def health():
+ """Health check endpoint."""
+ return {
+ "status": "healthy",
+ "environment": "warehouse_env",
+ "difficulty_level": DIFFICULTY_LEVEL,
+ "grid_size": (warehouse_env.grid_width, warehouse_env.grid_height),
+ "num_packages": warehouse_env.num_packages,
+ "max_steps": warehouse_env.max_steps,
+ }
+
+
+@app.get("/demo")
+async def demo():
+ """Serve the interactive demo page."""
+ import pathlib
+ demo_path = pathlib.Path(__file__).parent / "demo.html"
+ if demo_path.exists():
+ return FileResponse(demo_path)
+ else:
+ return HTMLResponse(content="
Demo page not found
Please check the server configuration.
")
+
+
+if __name__ == "__main__":
+ import uvicorn
+
+ uvicorn.run(app, host="0.0.0.0", port=8000)
+
+def main():
+ """Entry point for warehouse-server command."""
+ import uvicorn
+ import os
+
+ port = int(os.getenv("PORT", "8000"))
+ host = os.getenv("HOST", "0.0.0.0")
+
+ uvicorn.run(app, host=host, port=port)
diff --git a/src/envs/warehouse_env/server/demo.html b/src/envs/warehouse_env/server/demo.html
new file mode 100644
index 00000000..13449e45
--- /dev/null
+++ b/src/envs/warehouse_env/server/demo.html
@@ -0,0 +1,456 @@
+
+
+
+
+
+ Warehouse Optimization Demo
+
+
+
+
+
π Warehouse Optimization Environment
+
Watch an AI agent navigate a warehouse to pick up and deliver packages!
+
+
+
+
+
+
+
+
+
+
+
Packages Delivered
+
0 / 0
+
+
+
Cumulative Reward
+
0.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 500ms
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Click "Start Auto-Play" or "Reset" to begin!
+
+
+
+
+
+
diff --git a/src/envs/warehouse_env/server/requirements.txt b/src/envs/warehouse_env/server/requirements.txt
new file mode 100644
index 00000000..228bccad
--- /dev/null
+++ b/src/envs/warehouse_env/server/requirements.txt
@@ -0,0 +1,4 @@
+fastapi>=0.104.1
+uvicorn>=0.24.0
+pydantic>=2.5.0
+requests>=2.31.0
diff --git a/src/envs/warehouse_env/server/warehouse_environment.py b/src/envs/warehouse_env/server/warehouse_environment.py
new file mode 100644
index 00000000..c87438d0
--- /dev/null
+++ b/src/envs/warehouse_env/server/warehouse_environment.py
@@ -0,0 +1,571 @@
+"""
+Core warehouse environment implementation.
+
+This module implements the warehouse logistics optimization environment
+with grid-based navigation, package pickup/delivery, and reward calculation.
+"""
+
+import random
+import uuid
+from dataclasses import asdict
+from typing import Any, Dict, List, Optional, Tuple
+
+from core.client_types import StepResult
+
+from core.env_server import Environment
+from envs.warehouse_env.models import (
+ Package,
+ WarehouseAction,
+ WarehouseObservation,
+ WarehouseState,
+)
+
+
+# Cell types
+EMPTY = 0
+WALL = 1
+SHELF = 2
+PICKUP_ZONE = 3
+DROPOFF_ZONE = 4
+
+
+# Difficulty configurations
+DIFFICULTY_CONFIGS = {
+ 1: {"grid_size": (5, 5), "num_packages": 1, "num_obstacles": 0, "max_steps": 50},
+ 2: {"grid_size": (8, 8), "num_packages": 2, "num_obstacles": 3, "max_steps": 100},
+ 3: {"grid_size": (10, 10), "num_packages": 3, "num_obstacles": 8, "max_steps": 150},
+ 4: {
+ "grid_size": (15, 15),
+ "num_packages": 5,
+ "num_obstacles": 20,
+ "max_steps": 250,
+ },
+ 5: {
+ "grid_size": (20, 20),
+ "num_packages": 8,
+ "num_obstacles": 40,
+ "max_steps": 400,
+ },
+}
+
+
+class WarehouseEnvironment(Environment):
+ """
+ Warehouse optimization environment.
+
+ A grid-based environment where a robot must navigate a warehouse,
+ pick up packages from pickup zones, and deliver them to dropoff zones
+ while avoiding obstacles.
+ """
+
+ def __init__(
+ self,
+ difficulty_level: int = 2,
+ grid_width: Optional[int] = None,
+ grid_height: Optional[int] = None,
+ num_packages: Optional[int] = None,
+ max_steps: Optional[int] = None,
+ random_seed: Optional[int] = None,
+ ):
+ """
+ Initialize the warehouse environment.
+
+ Args:
+ difficulty_level: Preset difficulty (1-5)
+ grid_width: Custom grid width (overrides difficulty)
+ grid_height: Custom grid height (overrides difficulty)
+ num_packages: Custom package count (overrides difficulty)
+ max_steps: Custom step limit (overrides difficulty)
+ random_seed: Random seed for reproducibility
+ """
+ super().__init__()
+
+ # Get config from difficulty or use custom values
+ config = DIFFICULTY_CONFIGS.get(difficulty_level, DIFFICULTY_CONFIGS[2])
+
+ self.difficulty_level = difficulty_level
+ self.grid_width = grid_width or config["grid_size"][0]
+ self.grid_height = grid_height or config["grid_size"][1]
+ self.num_packages = num_packages or config["num_packages"]
+ self.max_steps = max_steps or config["max_steps"]
+ self.num_obstacles = config["num_obstacles"]
+
+ if random_seed is not None:
+ random.seed(random_seed)
+
+ # Episode state
+ self.episode_id: str = ""
+ self.step_count: int = 0
+ self.grid: List[List[int]] = []
+ self.robot_position: Tuple[int, int] = (0, 0)
+ self.robot_carrying: Optional[int] = None
+ self.packages: List[Package] = []
+ self.packages_delivered: int = 0
+ self.cum_reward: float = 0.0
+ self.is_done: bool = False
+
+ # Pickup and dropoff zones
+ self.pickup_zones: List[Tuple[int, int]] = []
+ self.dropoff_zones: List[Tuple[int, int]] = []
+
+ def reset(self) -> WarehouseObservation:
+ """Reset the environment for a new episode."""
+ self.episode_id = str(uuid.uuid4())
+ self.step_count = 0
+ self.packages_delivered = 0
+ self.cum_reward = 0.0
+ self.is_done = False
+ self.robot_carrying = None
+
+ # Generate warehouse layout
+ self._generate_warehouse()
+
+ # Place robot at start position (usually near center)
+ self.robot_position = (self.grid_width // 2, self.grid_height // 2)
+
+ # Generate packages
+ self._generate_packages()
+
+ observation = self._get_observation(
+ action_success=True,
+ message="Warehouse environment ready! Navigate to pickup zones to collect packages.",
+ )
+
+ return observation
+
+ def step(self, action: WarehouseAction) -> WarehouseObservation:
+ """Execute an action and return the result."""
+ if self.is_done:
+ return self._get_observation(False, "Episode already finished")
+
+ self.step_count += 1
+ reward = 0.0
+ action_success = False
+ message = ""
+
+ # Track state before action
+ packages_delivered_before = self.packages_delivered
+
+ # Execute action
+ if action.action_id in [0, 1, 2, 3]: # Movement actions
+ action_success, message = self._move_robot(action.action_id)
+ reward = -0.1 # Small step penalty
+ if not action_success:
+ reward = -1.0 # Penalty for invalid move
+
+ elif action.action_id == 4: # PICK_UP
+ action_success, message = self._pickup_package()
+ if action_success:
+ reward = 10.0 # Reward for successful pickup
+ else:
+ reward = -1.0 # Penalty for invalid pickup
+
+ elif action.action_id == 5: # DROP_OFF
+ action_success, message = self._dropoff_package()
+ if action_success:
+ # Major reward for delivery
+ reward = 100.0
+
+ # Time bonus
+ time_bonus = (self.max_steps - self.step_count) * 0.1
+ reward += time_bonus
+
+ # Check if all packages delivered
+ if self.packages_delivered == self.num_packages:
+ reward += 200.0 # Completion bonus
+ self.is_done = True
+ message += " All packages delivered! Episode complete!"
+ else:
+ reward = -1.0
+
+ # Update package waiting times
+ for package in self.packages:
+ if package.status == "waiting":
+ package.time_waiting += 1
+
+ # Check timeout
+ if self.step_count >= self.max_steps:
+ self.is_done = True
+ message += " Maximum steps reached. Episode terminated."
+
+ self.cum_reward += reward
+
+ observation = self._get_observation(action_success, message)
+ # Set reward and done in observation (these are expected by Observation base class)
+ observation.reward = reward
+ observation.done = self.is_done
+
+ return observation
+
+ @property
+ def state(self) -> WarehouseState:
+ """Get current episode state."""
+ return WarehouseState(
+ episode_id=self.episode_id,
+ step_count=self.step_count,
+ packages_delivered=self.packages_delivered,
+ total_packages=self.num_packages,
+ difficulty_level=self.difficulty_level,
+ grid_size=(self.grid_width, self.grid_height),
+ cum_reward=self.cum_reward,
+ is_done=self.is_done,
+ )
+
+ def _generate_warehouse(self):
+ """Generate the warehouse grid layout."""
+ # Initialize empty grid
+ self.grid = [
+ [EMPTY for _ in range(self.grid_width)] for _ in range(self.grid_height)
+ ]
+
+ # Add walls around perimeter
+ for x in range(self.grid_width):
+ self.grid[0][x] = WALL
+ self.grid[self.grid_height - 1][x] = WALL
+ for y in range(self.grid_height):
+ self.grid[y][0] = WALL
+ self.grid[y][self.grid_width - 1] = WALL
+
+ # Add random shelves/obstacles
+ obstacles_placed = 0
+ attempts = 0
+ while (
+ obstacles_placed < self.num_obstacles and attempts < self.num_obstacles * 10
+ ):
+ x = random.randint(2, self.grid_width - 3)
+ y = random.randint(2, self.grid_height - 3)
+
+ # Don't place near center (robot start)
+ if abs(x - self.grid_width // 2) < 2 and abs(y - self.grid_height // 2) < 2:
+ attempts += 1
+ continue
+
+ if self.grid[y][x] == EMPTY:
+ self.grid[y][x] = SHELF
+ obstacles_placed += 1
+
+ attempts += 1
+
+ # Create pickup zones (top-left area)
+ self.pickup_zones = []
+ for _ in range(min(3, self.num_packages)):
+ x = random.randint(1, self.grid_width // 3)
+ y = random.randint(1, self.grid_height // 3)
+ if self.grid[y][x] == EMPTY:
+ self.grid[y][x] = PICKUP_ZONE
+ self.pickup_zones.append((x, y))
+
+ # Create dropoff zones (bottom-right area)
+ self.dropoff_zones = []
+ for _ in range(min(3, self.num_packages)):
+ x = random.randint(2 * self.grid_width // 3, self.grid_width - 2)
+ y = random.randint(2 * self.grid_height // 3, self.grid_height - 2)
+ if self.grid[y][x] == EMPTY:
+ self.grid[y][x] = DROPOFF_ZONE
+ self.dropoff_zones.append((x, y))
+
+ def _generate_packages(self):
+ """Generate packages with random pickup/dropoff locations."""
+ self.packages = []
+ for i in range(self.num_packages):
+ pickup_loc = (
+ random.choice(self.pickup_zones) if self.pickup_zones else (1, 1)
+ )
+ dropoff_loc = (
+ random.choice(self.dropoff_zones)
+ if self.dropoff_zones
+ else (self.grid_width - 2, self.grid_height - 2)
+ )
+
+ package = Package(
+ id=i,
+ status="waiting",
+ pickup_location=pickup_loc,
+ dropoff_location=dropoff_loc,
+ priority=random.randint(1, 3),
+ time_waiting=0,
+ )
+ self.packages.append(package)
+
+ def _move_robot(self, direction: int) -> Tuple[bool, str]:
+ """
+ Move robot in specified direction.
+
+ Args:
+ direction: 0=UP, 1=DOWN, 2=LEFT, 3=RIGHT
+
+ Returns:
+ (success, message)
+ """
+ x, y = self.robot_position
+
+ if direction == 0: # UP
+ new_pos = (x, y - 1)
+ elif direction == 1: # DOWN
+ new_pos = (x, y + 1)
+ elif direction == 2: # LEFT
+ new_pos = (x - 1, y)
+ elif direction == 3: # RIGHT
+ new_pos = (x + 1, y)
+ else:
+ return False, "Invalid direction"
+
+ new_x, new_y = new_pos
+
+ # Check bounds
+ if (
+ new_x < 0
+ or new_x >= self.grid_width
+ or new_y < 0
+ or new_y >= self.grid_height
+ ):
+ return False, "Cannot move outside warehouse bounds"
+
+ # Check collision with walls/shelves
+ if self.grid[new_y][new_x] in [WALL, SHELF]:
+ return False, "Cannot move into obstacle"
+
+ self.robot_position = new_pos
+ return True, f"Moved {WarehouseAction.ACTION_NAMES[direction]}"
+
+ def _pickup_package(self) -> Tuple[bool, str]:
+ """Attempt to pick up a package."""
+ if self.robot_carrying is not None:
+ return False, "Robot already carrying a package"
+
+ # Check if at pickup zone
+ x, y = self.robot_position
+ if self.grid[y][x] != PICKUP_ZONE:
+ return False, "Not at a pickup zone"
+
+ # Find available package at this location
+ for package in self.packages:
+ if package.status == "waiting" and package.pickup_location == (x, y):
+ package.status = "picked"
+ self.robot_carrying = package.id
+ return True, f"Picked up package #{package.id}"
+
+ return False, "No packages available at this location"
+
+ def _dropoff_package(self) -> Tuple[bool, str]:
+ """Attempt to drop off a package."""
+ if self.robot_carrying is None:
+ return False, "Not carrying any package"
+
+ # Check if at dropoff zone
+ x, y = self.robot_position
+ if self.grid[y][x] != DROPOFF_ZONE:
+ return False, "Not at a dropoff zone"
+
+ # Find the package being carried
+ package = next((p for p in self.packages if p.id == self.robot_carrying), None)
+ if package is None:
+ return False, "Package not found"
+
+ # Check if correct dropoff location
+ if package.dropoff_location == (x, y):
+ package.status = "delivered"
+ self.packages_delivered += 1
+ self.robot_carrying = None
+ return True, f"Successfully delivered package #{package.id}!"
+ else:
+ return False, f"Wrong dropoff zone for package #{package.id}"
+
+ def _get_observation(
+ self, action_success: bool, message: str
+ ) -> WarehouseObservation:
+ """Create observation object."""
+ packages_data = [
+ {
+ "id": p.id,
+ "status": p.status,
+ "pickup_location": p.pickup_location,
+ "dropoff_location": p.dropoff_location,
+ "priority": p.priority,
+ "time_waiting": p.time_waiting,
+ }
+ for p in self.packages
+ ]
+
+ return WarehouseObservation(
+ grid=self.grid,
+ robot_position=self.robot_position,
+ robot_carrying=self.robot_carrying,
+ packages=packages_data,
+ step_count=self.step_count,
+ packages_delivered=self.packages_delivered,
+ total_packages=self.num_packages,
+ time_remaining=self.max_steps - self.step_count,
+ action_success=action_success,
+ message=message,
+ )
+
+ def render_ascii(self) -> str:
+ """Render warehouse as ASCII art."""
+ symbols = {
+ EMPTY: ".",
+ WALL: "β",
+ SHELF: "#",
+ PICKUP_ZONE: "P",
+ DROPOFF_ZONE: "D",
+ }
+
+ lines = []
+ lines.append("=" * (self.grid_width * 2 + 1))
+ lines.append(
+ f"Step: {self.step_count}/{self.max_steps} | Delivered: {self.packages_delivered}/{self.num_packages} | Reward: {self.cum_reward:.1f}"
+ )
+ lines.append("=" * (self.grid_width * 2 + 1))
+
+ for y in range(self.grid_height):
+ row = ""
+ for x in range(self.grid_width):
+ if (x, y) == self.robot_position:
+ if self.robot_carrying is not None:
+ row += "R " # Robot carrying package
+ else:
+ row += "r " # Robot empty
+ else:
+ row += symbols[self.grid[y][x]] + " "
+ lines.append(row)
+
+ lines.append("=" * (self.grid_width * 2 + 1))
+ lines.append(f"Robot at {self.robot_position}, carrying: {self.robot_carrying}")
+
+ # Show package info
+ for package in self.packages:
+ status_icon = (
+ "β"
+ if package.status == "delivered"
+ else ("β»" if package.status == "picked" else "β")
+ )
+ lines.append(
+ f"{status_icon} Package #{package.id}: {package.status} (P{package.pickup_location}βD{package.dropoff_location})"
+ )
+
+ lines.append("=" * (self.grid_width * 2 + 1))
+ lines.append(
+ "Legend: r/R=Robot(empty/carrying), P=Pickup, D=Dropoff, #=Shelf, β=Wall"
+ )
+
+ return "\n".join(lines)
+
+ def render_html(self) -> str:
+ """Render warehouse as HTML with CSS styling for web interface."""
+ cell_size = 40 # pixels
+ colors = {
+ EMPTY: "#f0f0f0",
+ WALL: "#333333",
+ SHELF: "#8B4513",
+ PICKUP_ZONE: "#4CAF50",
+ DROPOFF_ZONE: "#2196F3",
+ }
+
+ html_parts = []
+ html_parts.append('')
+ html_parts.append(f'
Warehouse Environment - Step {self.step_count}/{self.max_steps}
')
+ html_parts.append(f'
Delivered: {self.packages_delivered}/{self.num_packages} | ')
+ html_parts.append(f'Reward: {self.cum_reward:.1f}
')
+
+ # Grid visualization
+ html_parts.append(f'
')
+ html_parts.append(f'')
+ html_parts.append('
')
+
+ # Legend
+ html_parts.append('
')
+ html_parts.append('
Legend:
')
+ html_parts.append('
')
+
+ legend_items = [
+ ("r", "#FFC107", "Robot (empty)"),
+ ("R", "#FF5722", "Robot (carrying)"),
+ ("P", colors[PICKUP_ZONE], "Pickup Zone"),
+ ("D", colors[DROPOFF_ZONE], "Dropoff Zone"),
+ ("#", colors[SHELF], "Shelf"),
+ ("β", colors[WALL], "Wall"),
+ ]
+
+ for symbol, color, label in legend_items:
+ html_parts.append(f'
')
+ html_parts.append(f'
{symbol}
')
+ html_parts.append(f'
{label}')
+ html_parts.append('
')
+
+ html_parts.append('
')
+ html_parts.append('
')
+
+ # Package status
+ html_parts.append('
')
+ html_parts.append('
Packages:
')
+ for package in self.packages:
+ status_color = {
+ "waiting": "#FFA726",
+ "picked": "#42A5F5",
+ "delivered": "#66BB6A"
+ }.get(package.status, "#999")
+
+ status_icon = {
+ "waiting": "β",
+ "picked": "β»",
+ "delivered": "β"
+ }.get(package.status, "")
+
+ html_parts.append(
+ f'
'
+ )
+ html_parts.append(
+ f'{status_icon} Package #{package.id}: {package.status} '
+ f'(Pickup: {package.pickup_location} β Dropoff: {package.dropoff_location})'
+ )
+ html_parts.append('
')
+
+ html_parts.append('
')
+ html_parts.append('
')
+
+ return "".join(html_parts)
diff --git a/src/envs/warehouse_env/uv.lock b/src/envs/warehouse_env/uv.lock
new file mode 100644
index 00000000..aed24c81
--- /dev/null
+++ b/src/envs/warehouse_env/uv.lock
@@ -0,0 +1,571 @@
+version = 1
+revision = 2
+requires-python = ">=3.11"
+
+[[package]]
+name = "annotated-doc"
+version = "0.0.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.11.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "sniffio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
+]
+
+[[package]]
+name = "black"
+version = "25.11.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "mypy-extensions" },
+ { name = "packaging" },
+ { name = "pathspec" },
+ { name = "platformdirs" },
+ { name = "pytokens" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8c/ad/33adf4708633d047950ff2dfdea2e215d84ac50ef95aff14a614e4b6e9b2/black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08", size = 655669, upload-time = "2025-11-10T01:53:50.558Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c4/62/d304786b75ab0c530b833a89ce7d997924579fb7484ecd9266394903e394/black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a", size = 1727891, upload-time = "2025-11-10T02:01:40.507Z" },
+ { url = "https://files.pythonhosted.org/packages/82/5d/ffe8a006aa522c9e3f430e7b93568a7b2163f4b3f16e8feb6d8c3552761a/black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170", size = 1581875, upload-time = "2025-11-10T01:57:51.192Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/c8/7c8bda3108d0bb57387ac41b4abb5c08782b26da9f9c4421ef6694dac01a/black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc", size = 1642716, upload-time = "2025-11-10T01:56:51.589Z" },
+ { url = "https://files.pythonhosted.org/packages/34/b9/f17dea34eecb7cc2609a89627d480fb6caea7b86190708eaa7eb15ed25e7/black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e", size = 1352904, upload-time = "2025-11-10T01:59:26.252Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/12/5c35e600b515f35ffd737da7febdb2ab66bb8c24d88560d5e3ef3d28c3fd/black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac", size = 1772831, upload-time = "2025-11-10T02:03:47Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/75/b3896bec5a2bb9ed2f989a970ea40e7062f8936f95425879bbe162746fe5/black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96", size = 1608520, upload-time = "2025-11-10T01:58:46.895Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/b5/2bfc18330eddbcfb5aab8d2d720663cd410f51b2ed01375f5be3751595b0/black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd", size = 1682719, upload-time = "2025-11-10T01:56:55.24Z" },
+ { url = "https://files.pythonhosted.org/packages/96/fb/f7dc2793a22cdf74a72114b5ed77fe3349a2e09ef34565857a2f917abdf2/black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409", size = 1362684, upload-time = "2025-11-10T01:57:07.639Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/47/3378d6a2ddefe18553d1115e36aea98f4a90de53b6a3017ed861ba1bd3bc/black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b", size = 1772446, upload-time = "2025-11-10T02:02:16.181Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/4b/0f00bfb3d1f7e05e25bfc7c363f54dc523bb6ba502f98f4ad3acf01ab2e4/black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd", size = 1607983, upload-time = "2025-11-10T02:02:52.502Z" },
+ { url = "https://files.pythonhosted.org/packages/99/fe/49b0768f8c9ae57eb74cc10a1f87b4c70453551d8ad498959721cc345cb7/black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993", size = 1682481, upload-time = "2025-11-10T01:57:12.35Z" },
+ { url = "https://files.pythonhosted.org/packages/55/17/7e10ff1267bfa950cc16f0a411d457cdff79678fbb77a6c73b73a5317904/black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c", size = 1363869, upload-time = "2025-11-10T01:58:24.608Z" },
+ { url = "https://files.pythonhosted.org/packages/67/c0/cc865ce594d09e4cd4dfca5e11994ebb51604328489f3ca3ae7bb38a7db5/black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170", size = 1771358, upload-time = "2025-11-10T02:03:33.331Z" },
+ { url = "https://files.pythonhosted.org/packages/37/77/4297114d9e2fd2fc8ab0ab87192643cd49409eb059e2940391e7d2340e57/black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545", size = 1612902, upload-time = "2025-11-10T01:59:33.382Z" },
+ { url = "https://files.pythonhosted.org/packages/de/63/d45ef97ada84111e330b2b2d45e1dd163e90bd116f00ac55927fb6bf8adb/black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda", size = 1680571, upload-time = "2025-11-10T01:57:04.239Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/4b/5604710d61cdff613584028b4cb4607e56e148801ed9b38ee7970799dab6/black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664", size = 1382599, upload-time = "2025-11-10T01:57:57.427Z" },
+ { url = "https://files.pythonhosted.org/packages/00/5d/aed32636ed30a6e7f9efd6ad14e2a0b0d687ae7c8c7ec4e4a557174b895c/black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b", size = 204918, upload-time = "2025-11-10T01:53:48.917Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.11.12"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
+ { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
+ { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
+ { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
+ { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
+ { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
+ { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
+ { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
+ { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
+ { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
+ { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
+ { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
+ { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
+ { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
+ { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
+ { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
+ { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
+ { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
+ { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
+ { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
+ { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
+ { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
+ { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
+ { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
+ { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
+ { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "fastapi"
+version = "0.121.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-doc" },
+ { name = "pydantic" },
+ { name = "starlette" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fb/48/f08f264da34cf160db82c62ffb335e838b1fc16cbcc905f474c7d4c815db/fastapi-0.121.2.tar.gz", hash = "sha256:ca8e932b2b823ec1721c641e3669472c855ad9564a2854c9899d904c2848b8b9", size = 342944, upload-time = "2025-11-13T17:05:54.692Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/23/dfb161e91db7c92727db505dc72a384ee79681fe0603f706f9f9f52c2901/fastapi-0.121.2-py3-none-any.whl", hash = "sha256:f2d80b49a86a846b70cc3a03eb5ea6ad2939298bf6a7fe377aa9cd3dd079d358", size = 109201, upload-time = "2025-11-13T17:05:52.718Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[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, upload-time = "2025-04-22T14:54:24.164Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
+]
+
+[[package]]
+name = "openenv-core"
+version = "0.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "fastapi" },
+ { name = "requests" },
+ { name = "uvicorn" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7f/18/74d2aedbf099a86de772364260827a12b4b4a56711db4caa3caa078588d7/openenv_core-0.1.0.tar.gz", hash = "sha256:3a4e8bf4f2f3b7eba1c3a212e6e2dc7d980b8350015ae6c250a3ce93000f1d7c", size = 26512, upload-time = "2025-10-21T20:00:24.29Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3a/48/85afcd090eeaadf00e6f88ac92a866cb9238eaf6246820d1bc6564f5bc97/openenv_core-0.1.0-py3-none-any.whl", hash = "sha256:8d02513f26518f98ab1f35a875f7493d2983cf87f8b0e4b0af6634ec63edfd4b", size = 30607, upload-time = "2025-10-21T20:00:22.183Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
+]
+
+[[package]]
+name = "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, upload-time = "2023-12-10T22:30:45Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.12.4"
+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/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.41.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
+ { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
+ { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
+ { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
+ { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
+ { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
+ { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
+ { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
+ { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
+ { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
+ { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
+ { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
+ { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
+ { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
+ { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
+ { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
+ { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
+ { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
+ { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
+ { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
+ { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
+ { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
+ { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
+ { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
+ { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
+ { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
+ { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
+ { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
+ { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
+ { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
+ { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
+ { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
+ { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
+ { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
+ { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
+ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "9.0.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" },
+]
+
+[[package]]
+name = "pytest-asyncio"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
+]
+
+[[package]]
+name = "pytokens"
+version = "0.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4e/8d/a762be14dae1c3bf280202ba3172020b2b0b4c537f94427435f19c413b72/pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a", size = 17644, upload-time = "2025-11-05T13:36:35.34Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/84/25/d9db8be44e205a124f6c98bc0324b2bb149b7431c53877fc6d1038dddaf5/pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3", size = 12195, upload-time = "2025-11-05T13:36:33.183Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.14.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/fa/fbb67a5780ae0f704876cb8ac92d6d76da41da4dc72b7ed3565ab18f2f52/ruff-0.14.5.tar.gz", hash = "sha256:8d3b48d7d8aad423d3137af7ab6c8b1e38e4de104800f0d596990f6ada1a9fc1", size = 5615944, upload-time = "2025-11-13T19:58:51.155Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/68/31/c07e9c535248d10836a94e4f4e8c5a31a1beed6f169b31405b227872d4f4/ruff-0.14.5-py3-none-linux_armv6l.whl", hash = "sha256:f3b8248123b586de44a8018bcc9fefe31d23dda57a34e6f0e1e53bd51fd63594", size = 13171630, upload-time = "2025-11-13T19:57:54.894Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/5c/283c62516dca697cd604c2796d1487396b7a436b2f0ecc3fd412aca470e0/ruff-0.14.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f7a75236570318c7a30edd7f5491945f0169de738d945ca8784500b517163a72", size = 13413925, upload-time = "2025-11-13T19:57:59.181Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/f3/aa319f4afc22cb6fcba2b9cdfc0f03bbf747e59ab7a8c5e90173857a1361/ruff-0.14.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d146132d1ee115f8802356a2dc9a634dbf58184c51bff21f313e8cd1c74899a", size = 12574040, upload-time = "2025-11-13T19:58:02.056Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/7f/cb5845fcc7c7e88ed57f58670189fc2ff517fe2134c3821e77e29fd3b0c8/ruff-0.14.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2380596653dcd20b057794d55681571a257a42327da8894b93bbd6111aa801f", size = 13009755, upload-time = "2025-11-13T19:58:05.172Z" },
+ { url = "https://files.pythonhosted.org/packages/21/d2/bcbedbb6bcb9253085981730687ddc0cc7b2e18e8dc13cf4453de905d7a0/ruff-0.14.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2d1fa985a42b1f075a098fa1ab9d472b712bdb17ad87a8ec86e45e7fa6273e68", size = 12937641, upload-time = "2025-11-13T19:58:08.345Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/58/e25de28a572bdd60ffc6bb71fc7fd25a94ec6a076942e372437649cbb02a/ruff-0.14.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88f0770d42b7fa02bbefddde15d235ca3aa24e2f0137388cc15b2dcbb1f7c7a7", size = 13610854, upload-time = "2025-11-13T19:58:11.419Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/24/43bb3fd23ecee9861970978ea1a7a63e12a204d319248a7e8af539984280/ruff-0.14.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3676cb02b9061fee7294661071c4709fa21419ea9176087cb77e64410926eb78", size = 15061088, upload-time = "2025-11-13T19:58:14.551Z" },
+ { url = "https://files.pythonhosted.org/packages/23/44/a022f288d61c2f8c8645b24c364b719aee293ffc7d633a2ca4d116b9c716/ruff-0.14.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b595bedf6bc9cab647c4a173a61acf4f1ac5f2b545203ba82f30fcb10b0318fb", size = 14734717, upload-time = "2025-11-13T19:58:17.518Z" },
+ { url = "https://files.pythonhosted.org/packages/58/81/5c6ba44de7e44c91f68073e0658109d8373b0590940efe5bd7753a2585a3/ruff-0.14.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f55382725ad0bdb2e8ee2babcbbfb16f124f5a59496a2f6a46f1d9d99d93e6e2", size = 14028812, upload-time = "2025-11-13T19:58:20.533Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/ef/41a8b60f8462cb320f68615b00299ebb12660097c952c600c762078420f8/ruff-0.14.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7497d19dce23976bdaca24345ae131a1d38dcfe1b0850ad8e9e6e4fa321a6e19", size = 13825656, upload-time = "2025-11-13T19:58:23.345Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/00/207e5de737fdb59b39eb1fac806904fe05681981b46d6a6db9468501062e/ruff-0.14.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:410e781f1122d6be4f446981dd479470af86537fb0b8857f27a6e872f65a38e4", size = 13959922, upload-time = "2025-11-13T19:58:26.537Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/7e/fa1f5c2776db4be405040293618846a2dece5c70b050874c2d1f10f24776/ruff-0.14.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c01be527ef4c91a6d55e53b337bfe2c0f82af024cc1a33c44792d6844e2331e1", size = 12932501, upload-time = "2025-11-13T19:58:29.822Z" },
+ { url = "https://files.pythonhosted.org/packages/67/d8/d86bf784d693a764b59479a6bbdc9515ae42c340a5dc5ab1dabef847bfaa/ruff-0.14.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f66e9bb762e68d66e48550b59c74314168ebb46199886c5c5aa0b0fbcc81b151", size = 12927319, upload-time = "2025-11-13T19:58:32.923Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/de/ee0b304d450ae007ce0cb3e455fe24fbcaaedae4ebaad6c23831c6663651/ruff-0.14.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d93be8f1fa01022337f1f8f3bcaa7ffee2d0b03f00922c45c2207954f351f465", size = 13206209, upload-time = "2025-11-13T19:58:35.952Z" },
+ { url = "https://files.pythonhosted.org/packages/33/aa/193ca7e3a92d74f17d9d5771a765965d2cf42c86e6f0fd95b13969115723/ruff-0.14.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c135d4b681f7401fe0e7312017e41aba9b3160861105726b76cfa14bc25aa367", size = 13953709, upload-time = "2025-11-13T19:58:39.002Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/f1/7119e42aa1d3bf036ffc9478885c2e248812b7de9abea4eae89163d2929d/ruff-0.14.5-py3-none-win32.whl", hash = "sha256:c83642e6fccfb6dea8b785eb9f456800dcd6a63f362238af5fc0c83d027dd08b", size = 12925808, upload-time = "2025-11-13T19:58:42.779Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/9d/7c0a255d21e0912114784e4a96bf62af0618e2190cae468cd82b13625ad2/ruff-0.14.5-py3-none-win_amd64.whl", hash = "sha256:9d55d7af7166f143c94eae1db3312f9ea8f95a4defef1979ed516dbb38c27621", size = 14331546, upload-time = "2025-11-13T19:58:45.691Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/80/69756670caedcf3b9be597a6e12276a6cf6197076eb62aad0c608f8efce0/ruff-0.14.5-py3-none-win_arm64.whl", hash = "sha256:4b700459d4649e2594b31f20a9de33bc7c19976d4746d8d0798ad959621d64a4", size = 13433331, upload-time = "2025-11-13T19:58:48.434Z" },
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
+]
+
+[[package]]
+name = "starlette"
+version = "0.49.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/de/1a/608df0b10b53b0beb96a37854ee05864d182ddd4b1156a22f1ad3860425a/starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284", size = 2655031, upload-time = "2025-11-01T15:12:26.13Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a3/e0/021c772d6a662f43b63044ab481dc6ac7592447605b5b35a957785363122/starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f", size = 74340, upload-time = "2025-11-01T15:12:24.387Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.38.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" },
+]
+
+[[package]]
+name = "warehouse-env"
+version = "1.0.0"
+source = { editable = "." }
+dependencies = [
+ { name = "fastapi" },
+ { name = "openenv-core" },
+ { name = "pydantic" },
+ { name = "requests" },
+ { name = "uvicorn" },
+]
+
+[package.optional-dependencies]
+dev = [
+ { name = "black" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "ruff" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" },
+ { name = "fastapi", specifier = ">=0.104.1" },
+ { name = "openenv-core" },
+ { name = "pydantic", specifier = ">=2.5.0" },
+ { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" },
+ { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" },
+ { name = "requests", specifier = ">=2.31.0" },
+ { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" },
+ { name = "uvicorn", specifier = ">=0.24.0" },
+]
+provides-extras = ["dev"]