diff --git a/CLAUDE.md b/CLAUDE.md index e387313..2c15617 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,15 +1,15 @@ -# Monadical AI Development Guide +# Monadical Container Development Guide ## Build Commands ```bash # Install dependencies using uv (Astral) uv sync -# Run MAI service -uv run -m monadical_ai.service +# Run MC service +uv run -m mcontainer.service -# Run MAI CLI -uv run -m monadical_ai.cli +# Run MC CLI +uv run -m mcontainer.cli ``` ## Lint/Test Commands diff --git a/README.md b/README.md index e69de29..b452e06 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,85 @@ +# MC - Monadical Container Tool + +MC (Monadical Container) is a command-line tool for managing ephemeral containers that run AI tools and development environments. It works with both local Docker and a dedicated remote web service that manages containers in a Docker-in-Docker (DinD) environment. + +## Installation + +```bash +# Clone the repository +git clone https://github.com/monadical/mc.git +cd mc + +# Install with uv +uv sync +``` + +## Basic Usage + +```bash +# Create a new session with the default driver +mc session create + +# List all active sessions +mc session list + +# Connect to a specific session +mc session connect SESSION_ID + +# Close a session when done +mc session close SESSION_ID + +# Create a session with a specific driver +mc session create --driver goose + +# Create a session with environment variables +mc session create -e VAR1=value1 -e VAR2=value2 + +# Shorthand for creating a session with a project repository +mc github.com/username/repo +``` + +## Driver Management + +MC includes a driver management system that allows you to build, manage, and use Docker images for different AI tools: + +```bash +# List available drivers +mc driver list + +# Get detailed information about a driver +mc driver info goose + +# Build a driver image +mc driver build goose + +# Build and push a driver image +mc driver build goose --push +``` + +Drivers are defined in the `drivers/` directory, with each subdirectory containing: + +- `Dockerfile`: Docker image definition +- `entrypoint.sh`: Container entrypoint script +- `mai-init.sh`: Standardized initialization script +- `mai-driver.yaml`: Driver metadata and configuration +- `README.md`: Driver documentation + +## Development + +```bash +# Run the tests +uv run -m pytest + +# Run linting +uv run --with=ruff ruff check . + +# Run type checking +uv run --with=mypy mypy . + +# Format code +uv run --with=ruff ruff format . +``` + +## License + +See LICENSE file for details. \ No newline at end of file diff --git a/drivers/goose/Dockerfile b/drivers/goose/Dockerfile new file mode 100644 index 0000000..44b231b --- /dev/null +++ b/drivers/goose/Dockerfile @@ -0,0 +1,42 @@ +FROM python:3.12-slim + +LABEL maintainer="team@monadical.com" +LABEL description="Goose with MCP servers" + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + git \ + openssh-server \ + bash \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Set up SSH server +RUN mkdir /var/run/sshd +RUN echo 'root:root' | chpasswd +RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config +RUN sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config + +# Create app directory +WORKDIR /app + +# Copy initialization scripts +COPY mai-init.sh /mai-init.sh +COPY entrypoint.sh /entrypoint.sh +COPY mai-driver.yaml /mai-driver.yaml + +# Make scripts executable +RUN chmod +x /mai-init.sh /entrypoint.sh + +# Install python dependencies +RUN pip install --no-cache-dir goose-ai langfuse + +# Set up environment +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 + +# Expose ports +EXPOSE 8000 22 + +# Set entrypoint +ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/drivers/goose/README.md b/drivers/goose/README.md new file mode 100644 index 0000000..7f68e79 --- /dev/null +++ b/drivers/goose/README.md @@ -0,0 +1,47 @@ +# Goose Driver for MC + +This driver provides a containerized environment for running [Goose](https://goose.ai) with MCP servers. + +## Features + +- Pre-configured environment for Goose AI +- MCP server integration +- SSH access +- Git repository integration +- Langfuse logging support + +## Environment Variables + +| Variable | Description | Required | +|----------|-------------|----------| +| `MCP_HOST` | MCP server host | Yes | +| `GOOSE_API_KEY` | Goose API key | Yes | +| `GOOSE_ID` | Goose instance ID | No | +| `LANGFUSE_PUBLIC_KEY` | Langfuse public key | No | +| `LANGFUSE_SECRET_KEY` | Langfuse secret key | No | +| `LANGFUSE_HOST` | Langfuse API host | No | +| `MC_PROJECT_URL` | Project repository URL | No | +| `MC_GIT_SSH_KEY` | SSH key for Git authentication | No | +| `MC_GIT_TOKEN` | Token for Git authentication | No | + +## Build + +To build this driver: + +```bash +cd drivers/goose +docker build -t monadical/mc-goose:latest . +``` + +## Usage + +```bash +# Create a new session with this driver +mc session create --driver goose + +# Create with specific MCP server +mc session create --driver goose -e MCP_HOST=http://mcp.example.com:8000 + +# Create with project repository +mc session create --driver goose --project github.com/username/repo +``` \ No newline at end of file diff --git a/drivers/goose/entrypoint.sh b/drivers/goose/entrypoint.sh new file mode 100755 index 0000000..eeee879 --- /dev/null +++ b/drivers/goose/entrypoint.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Entrypoint script for Goose driver + +# Run the standard initialization script +/mai-init.sh + +# Start SSH server in the background +/usr/sbin/sshd + +# Print welcome message +echo "===============================================" +echo "Goose driver container started" +echo "SSH server running on port 22" +echo "===============================================" + +# Keep container running +exec tail -f /dev/null \ No newline at end of file diff --git a/drivers/goose/mai-driver.yaml b/drivers/goose/mai-driver.yaml new file mode 100644 index 0000000..d554dd3 --- /dev/null +++ b/drivers/goose/mai-driver.yaml @@ -0,0 +1,66 @@ +name: goose +description: Goose with MCP servers +version: 1.0.0 +maintainer: team@monadical.com + +init: + pre_command: /mai-init.sh + command: /entrypoint.sh + +environment: + - name: MCP_HOST + description: MCP server host + required: true + default: http://localhost:8000 + + - name: GOOSE_API_KEY + description: Goose API key + required: true + sensitive: true + + - name: GOOSE_ID + description: Goose instance ID + required: false + + - name: LANGFUSE_PUBLIC_KEY + description: Langfuse public key + required: false + sensitive: true + + - name: LANGFUSE_SECRET_KEY + description: Langfuse secret key + required: false + sensitive: true + + - name: LANGFUSE_HOST + description: Langfuse API host + required: false + default: https://api.langfuse.com + + # Project environment variables + - name: MC_PROJECT_URL + description: Project repository URL + required: false + + - name: MC_PROJECT_TYPE + description: Project repository type (git, svn, etc.) + required: false + default: git + + - name: MC_GIT_SSH_KEY + description: SSH key for Git authentication + required: false + sensitive: true + + - name: MC_GIT_TOKEN + description: Token for Git authentication + required: false + sensitive: true + +ports: + - 8000 # Main application + - 22 # SSH server + +volumes: + - mountPath: /app + description: Application directory \ No newline at end of file diff --git a/drivers/goose/mai-init.sh b/drivers/goose/mai-init.sh new file mode 100755 index 0000000..48330e7 --- /dev/null +++ b/drivers/goose/mai-init.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Standardized initialization script for MC drivers + +# Project initialization +if [ -n "$MC_PROJECT_URL" ]; then + echo "Initializing project: $MC_PROJECT_URL" + + # Set up SSH key if provided + if [ -n "$MC_GIT_SSH_KEY" ]; then + mkdir -p ~/.ssh + echo "$MC_GIT_SSH_KEY" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null + ssh-keyscan gitlab.com >> ~/.ssh/known_hosts 2>/dev/null + ssh-keyscan bitbucket.org >> ~/.ssh/known_hosts 2>/dev/null + fi + + # Set up token if provided + if [ -n "$MC_GIT_TOKEN" ]; then + git config --global credential.helper store + echo "https://$MC_GIT_TOKEN:x-oauth-basic@github.com" > ~/.git-credentials + fi + + # Clone repository + git clone $MC_PROJECT_URL /app + cd /app + + # Run project-specific initialization if present + if [ -f "/app/.mc/init.sh" ]; then + bash /app/.mc/init.sh + fi +fi + +# Set up Goose API key if provided +if [ -n "$GOOSE_API_KEY" ]; then + echo "Setting up Goose API key" + export GOOSE_API_KEY="$GOOSE_API_KEY" +fi + +# Set up MCP connection if provided +if [ -n "$MCP_HOST" ]; then + echo "Setting up MCP connection to $MCP_HOST" + export MCP_HOST="$MCP_HOST" +fi + +# Set up Langfuse logging if credentials are provided +if [ -n "$LANGFUSE_SECRET_KEY" ] && [ -n "$LANGFUSE_PUBLIC_KEY" ]; then + echo "Setting up Langfuse logging" + export LANGFUSE_SECRET_KEY="$LANGFUSE_SECRET_KEY" + export LANGFUSE_PUBLIC_KEY="$LANGFUSE_PUBLIC_KEY" + export LANGFUSE_HOST="${LANGFUSE_HOST:-https://api.langfuse.com}" +fi + +echo "MC driver initialization complete" \ No newline at end of file diff --git a/mcontainer/__init__.py b/mcontainer/__init__.py new file mode 100644 index 0000000..dae98c0 --- /dev/null +++ b/mcontainer/__init__.py @@ -0,0 +1,5 @@ +""" +MC - Monadical Container Tool +""" + +__version__ = "0.1.0" diff --git a/mcontainer/__main__.py b/mcontainer/__main__.py new file mode 100644 index 0000000..d5cf0fd --- /dev/null +++ b/mcontainer/__main__.py @@ -0,0 +1,4 @@ +from .cli import app + +if __name__ == "__main__": + app() diff --git a/mcontainer/cli.py b/mcontainer/cli.py new file mode 100644 index 0000000..d2c6135 --- /dev/null +++ b/mcontainer/cli.py @@ -0,0 +1,314 @@ +import os +import sys +from typing import List, Optional +import typer +from rich.console import Console +from rich.table import Table + +from .config import ConfigManager +from .container import ContainerManager +from .models import SessionStatus + +app = typer.Typer(help="Monadical Container Tool") +session_app = typer.Typer(help="Manage MC sessions") +driver_app = typer.Typer(help="Manage MC drivers", no_args_is_help=True) +app.add_typer(session_app, name="session", no_args_is_help=True) +app.add_typer(driver_app, name="driver", no_args_is_help=True) + +console = Console() +config_manager = ConfigManager() +container_manager = ContainerManager(config_manager) + + +@app.callback(invoke_without_command=True) +def main(ctx: typer.Context) -> None: + """Monadical Container Tool""" + # If no command is specified, create a session + if ctx.invoked_subcommand is None: + create_session(driver=None, project=None, env=[], name=None) + + +@app.command() +def version() -> None: + """Show MC version information""" + from importlib.metadata import version as get_version + + try: + version_str = get_version("mcontainer") + console.print(f"MC - Monadical Container Tool v{version_str}") + except Exception: + console.print("MC - Monadical Container Tool (development version)") + + +@session_app.command("list") +def list_sessions() -> None: + """List active MC sessions""" + sessions = container_manager.list_sessions() + + if not sessions: + console.print("No active sessions found") + return + + table = Table(show_header=True, header_style="bold") + table.add_column("ID") + table.add_column("Name") + table.add_column("Driver") + table.add_column("Status") + table.add_column("Ports") + table.add_column("Project") + + for session in sessions: + ports_str = ", ".join( + [ + f"{container_port}:{host_port}" + for container_port, host_port in session.ports.items() + ] + ) + + status_color = { + SessionStatus.RUNNING: "green", + SessionStatus.STOPPED: "red", + SessionStatus.CREATING: "yellow", + SessionStatus.FAILED: "red", + }.get(session.status, "white") + + table.add_row( + session.id, + session.name, + session.driver, + f"[{status_color}]{session.status}[/{status_color}]", + ports_str, + session.project or "", + ) + + console.print(table) + + +@session_app.command("create") +def create_session( + driver: Optional[str] = typer.Option(None, "--driver", "-d", help="Driver to use"), + project: Optional[str] = typer.Option(None, "--project", "-p", help="Project repository URL"), + env: List[str] = typer.Option( + [], "--env", "-e", help="Environment variables (KEY=VALUE)" + ), + name: Optional[str] = typer.Option(None, "--name", "-n", help="Session name"), +) -> None: + """Create a new MC session""" + # Use default driver if not specified + if not driver: + driver = config_manager.config.defaults.get("driver", "goose") + + # Parse environment variables + environment = {} + for var in env: + if "=" in var: + key, value = var.split("=", 1) + environment[key] = value + else: + console.print( + f"[yellow]Warning: Ignoring invalid environment variable format: {var}[/yellow]" + ) + + with console.status(f"Creating session with driver '{driver}'..."): + session = container_manager.create_session( + driver_name=driver, + project=project, + environment=environment, + session_name=name, + ) + + if session: + console.print("[green]Session created successfully![/green]") + console.print(f"Session ID: {session.id}") + console.print(f"Driver: {session.driver}") + + if session.ports: + console.print("Ports:") + for container_port, host_port in session.ports.items(): + console.print(f" {container_port} -> {host_port}") + + console.print( + f"\nConnect to the session with:\n mc session connect {session.id}" + ) + else: + console.print("[red]Failed to create session[/red]") + + +@session_app.command("close") +def close_session( + session_id: str = typer.Argument(..., help="Session ID to close"), +) -> None: + """Close a MC session""" + with console.status(f"Closing session {session_id}..."): + success = container_manager.close_session(session_id) + + if success: + console.print(f"[green]Session {session_id} closed successfully[/green]") + else: + console.print(f"[red]Failed to close session {session_id}[/red]") + + +@session_app.command("connect") +def connect_session( + session_id: str = typer.Argument(..., help="Session ID to connect to"), +) -> None: + """Connect to a MC session""" + console.print(f"Connecting to session {session_id}...") + success = container_manager.connect_session(session_id) + + if not success: + console.print(f"[red]Failed to connect to session {session_id}[/red]") + + +@session_app.command("logs") +def session_logs( + session_id: str = typer.Argument(..., help="Session ID to get logs from"), + follow: bool = typer.Option(False, "--follow", "-f", help="Follow log output"), +) -> None: + """Stream logs from a MC session""" + if follow: + console.print(f"Streaming logs from session {session_id}... (Ctrl+C to exit)") + container_manager.get_session_logs(session_id, follow=True) + else: + logs = container_manager.get_session_logs(session_id) + if logs: + console.print(logs) + + +@app.command() +def stop() -> None: + """Stop the current MC session (from inside the container)""" + # Check if running inside a container + if not os.path.exists("/.dockerenv"): + console.print( + "[red]This command can only be run from inside a MC container[/red]" + ) + return + + # Stop the container from inside + console.print("Stopping the current session...") + os.system("kill 1") # Send SIGTERM to PID 1 (container's init process) + + +# Main CLI entry point that handles project repository URLs +@app.command(name="") +def quick_create( + project: Optional[str] = typer.Argument(..., help="Project repository URL"), + driver: Optional[str] = typer.Option(None, "--driver", "-d", help="Driver to use"), + env: List[str] = typer.Option( + [], "--env", "-e", help="Environment variables (KEY=VALUE)" + ), + name: Optional[str] = typer.Option(None, "--name", "-n", help="Session name"), +) -> None: + """Create a new MC session with a project repository""" + create_session(driver=driver, project=project, env=env, name=name) + + +@driver_app.command("list") +def list_drivers() -> None: + """List available MC drivers""" + drivers = config_manager.list_drivers() + + if not drivers: + console.print("No drivers found") + return + + table = Table(show_header=True, header_style="bold") + table.add_column("Name") + table.add_column("Description") + table.add_column("Version") + table.add_column("Maintainer") + table.add_column("Image") + + for name, driver in drivers.items(): + table.add_row( + driver.name, + driver.description, + driver.version, + driver.maintainer, + driver.image, + ) + + console.print(table) + + +@driver_app.command("build") +def build_driver( + driver_name: str = typer.Argument(..., help="Driver name to build"), + tag: str = typer.Option("latest", "--tag", "-t", help="Image tag"), + push: bool = typer.Option(False, "--push", "-p", help="Push image to registry after building"), +) -> None: + """Build a driver Docker image""" + # Get driver path + driver_path = config_manager.get_driver_path(driver_name) + if not driver_path: + console.print(f"[red]Driver '{driver_name}' not found[/red]") + return + + # Check if Dockerfile exists + dockerfile_path = driver_path / "Dockerfile" + if not dockerfile_path.exists(): + console.print(f"[red]Dockerfile not found in {driver_path}[/red]") + return + + # Build image name + image_name = f"monadical/mc-{driver_name}:{tag}" + + # Build the image + with console.status(f"Building image {image_name}..."): + result = os.system(f"cd {driver_path} && docker build -t {image_name} .") + + if result != 0: + console.print("[red]Failed to build driver image[/red]") + return + + console.print(f"[green]Successfully built image: {image_name}[/green]") + + # Push if requested + if push: + with console.status(f"Pushing image {image_name}..."): + result = os.system(f"docker push {image_name}") + + if result != 0: + console.print("[red]Failed to push driver image[/red]") + return + + console.print(f"[green]Successfully pushed image: {image_name}[/green]") + + +@driver_app.command("info") +def driver_info( + driver_name: str = typer.Argument(..., help="Driver name to get info for"), +) -> None: + """Show detailed information about a driver""" + driver = config_manager.get_driver(driver_name) + if not driver: + console.print(f"[red]Driver '{driver_name}' not found[/red]") + return + + console.print(f"[bold]Driver: {driver.name}[/bold]") + console.print(f"Description: {driver.description}") + console.print(f"Version: {driver.version}") + console.print(f"Maintainer: {driver.maintainer}") + console.print(f"Image: {driver.image}") + + if driver.ports: + console.print("\n[bold]Ports:[/bold]") + for port in driver.ports: + console.print(f" {port}") + + # Get driver path + driver_path = config_manager.get_driver_path(driver_name) + if driver_path: + console.print(f"\n[bold]Path:[/bold] {driver_path}") + + # Check for README + readme_path = driver_path / "README.md" + if readme_path.exists(): + console.print("\n[bold]README:[/bold]") + with open(readme_path, "r") as f: + console.print(f.read()) + + +if __name__ == "__main__": + app() diff --git a/mcontainer/config.py b/mcontainer/config.py new file mode 100644 index 0000000..a8ed0f7 --- /dev/null +++ b/mcontainer/config.py @@ -0,0 +1,199 @@ +import yaml +from pathlib import Path +from typing import Dict, Optional + +from .models import Config, Driver + +DEFAULT_CONFIG_DIR = Path.home() / ".config" / "mc" +DEFAULT_CONFIG_FILE = DEFAULT_CONFIG_DIR / "config.yaml" +DEFAULT_DRIVERS_DIR = Path.home() / ".config" / "mc" / "drivers" +PROJECT_ROOT = Path(__file__).parent.parent +BUILTIN_DRIVERS_DIR = PROJECT_ROOT / "drivers" + +# Default built-in driver configurations +DEFAULT_DRIVERS = { + "goose": Driver( + name="goose", + description="Goose with MCP servers", + version="1.0.0", + maintainer="team@monadical.com", + image="monadical/mc-goose:latest", + ports=[8000, 22], + ), + "aider": Driver( + name="aider", + description="Aider coding assistant", + version="1.0.0", + maintainer="team@monadical.com", + image="monadical/mc-aider:latest", + ports=[22], + ), + "claude-code": Driver( + name="claude-code", + description="Claude Code environment", + version="1.0.0", + maintainer="team@monadical.com", + image="monadical/mc-claude-code:latest", + ports=[22], + ), +} + + +class ConfigManager: + def __init__(self, config_path: Optional[Path] = None): + self.config_path = config_path or DEFAULT_CONFIG_FILE + self.config_dir = self.config_path.parent + self.drivers_dir = DEFAULT_DRIVERS_DIR + self.config = self._load_or_create_config() + + def _load_or_create_config(self) -> Config: + """Load existing config or create a new one with defaults""" + if self.config_path.exists(): + try: + with open(self.config_path, "r") as f: + config_data = yaml.safe_load(f) or {} + + # Create a new config from scratch, then update with data from file + config = Config( + docker=config_data.get('docker', {}), + defaults=config_data.get('defaults', {}) + ) + + # Add drivers + if 'drivers' in config_data: + for driver_name, driver_data in config_data['drivers'].items(): + config.drivers[driver_name] = Driver.model_validate(driver_data) + + # Add sessions (stored as simple dictionaries) + if 'sessions' in config_data: + config.sessions = config_data['sessions'] + + return config + except Exception as e: + print(f"Error loading config: {e}") + return self._create_default_config() + else: + return self._create_default_config() + + def _create_default_config(self) -> Config: + """Create a default configuration""" + self.config_dir.mkdir(parents=True, exist_ok=True) + self.drivers_dir.mkdir(parents=True, exist_ok=True) + + # Load built-in drivers from directories + builtin_drivers = self.load_builtin_drivers() + + # Merge with default drivers, with directory drivers taking precedence + drivers = {**DEFAULT_DRIVERS, **builtin_drivers} + + config = Config( + docker={ + "socket": "/var/run/docker.sock", + "network": "mc-network", + }, + drivers=drivers, + defaults={ + "driver": "goose", + }, + ) + + self.save_config(config) + return config + + def save_config(self, config: Optional[Config] = None) -> None: + """Save the current config to disk""" + if config: + self.config = config + + self.config_dir.mkdir(parents=True, exist_ok=True) + + # Use model_dump with mode="json" for proper serialization of enums + config_dict = self.config.model_dump(mode="json") + + # Write to file + with open(self.config_path, "w") as f: + yaml.dump(config_dict, f) + + def get_driver(self, name: str) -> Optional[Driver]: + """Get a driver by name""" + return self.config.drivers.get(name) + + def list_drivers(self) -> Dict[str, Driver]: + """List all available drivers""" + return self.config.drivers + + def add_session(self, session_id: str, session_data: dict) -> None: + """Add a session to the config""" + # Store session data as a dictionary in the config + self.config.sessions[session_id] = session_data + self.save_config() + + def remove_session(self, session_id: str) -> None: + """Remove a session from the config""" + if session_id in self.config.sessions: + del self.config.sessions[session_id] + self.save_config() + + def list_sessions(self) -> Dict: + """List all sessions in the config""" + return self.config.sessions + + def load_driver_from_dir(self, driver_dir: Path) -> Optional[Driver]: + """Load a driver configuration from a directory""" + yaml_path = driver_dir / "mai-driver.yaml" # Keep this name for backward compatibility + + if not yaml_path.exists(): + return None + + try: + with open(yaml_path, "r") as f: + driver_data = yaml.safe_load(f) + + # Extract required fields + if not all(k in driver_data for k in ["name", "description", "version", "maintainer"]): + print(f"Driver config {yaml_path} missing required fields") + return None + + # Create driver object + driver = Driver( + name=driver_data["name"], + description=driver_data["description"], + version=driver_data["version"], + maintainer=driver_data["maintainer"], + image=f"monadical/mc-{driver_data['name']}:latest", + ports=driver_data.get("ports", []), + ) + + return driver + except Exception as e: + print(f"Error loading driver from {yaml_path}: {e}") + return None + + def load_builtin_drivers(self) -> Dict[str, Driver]: + """Load all built-in drivers from the drivers directory""" + drivers = {} + + if not BUILTIN_DRIVERS_DIR.exists(): + return drivers + + for driver_dir in BUILTIN_DRIVERS_DIR.iterdir(): + if driver_dir.is_dir(): + driver = self.load_driver_from_dir(driver_dir) + if driver: + drivers[driver.name] = driver + + return drivers + + def get_driver_path(self, driver_name: str) -> Optional[Path]: + """Get the directory path for a driver""" + # Check built-in drivers first + builtin_path = BUILTIN_DRIVERS_DIR / driver_name + if builtin_path.exists() and builtin_path.is_dir(): + return builtin_path + + # Then check user drivers + user_path = self.drivers_dir / driver_name + if user_path.exists() and user_path.is_dir(): + return user_path + + return None diff --git a/mcontainer/container.py b/mcontainer/container.py new file mode 100644 index 0000000..a13a484 --- /dev/null +++ b/mcontainer/container.py @@ -0,0 +1,235 @@ +import os +import sys +import uuid +import docker +from typing import Dict, List, Optional +from docker.errors import DockerException, ImageNotFound + +from .models import Session, SessionStatus +from .config import ConfigManager + + +class ContainerManager: + def __init__(self, config_manager: Optional[ConfigManager] = None): + self.config_manager = config_manager or ConfigManager() + try: + self.client = docker.from_env() + # Test connection + self.client.ping() + except DockerException as e: + print(f"Error connecting to Docker: {e}") + sys.exit(1) + + def _ensure_network(self) -> None: + """Ensure the MC network exists""" + network_name = self.config_manager.config.docker.get("network", "mc-network") + networks = self.client.networks.list(names=[network_name]) + if not networks: + self.client.networks.create(network_name, driver="bridge") + + def _generate_session_id(self) -> str: + """Generate a unique session ID""" + return str(uuid.uuid4())[:8] + + def list_sessions(self) -> List[Session]: + """List all active MC sessions""" + sessions = [] + try: + containers = self.client.containers.list( + all=True, filters={"label": "mc.session"} + ) + + for container in containers: + container_id = container.id + labels = container.labels + + session_id = labels.get("mc.session.id") + if not session_id: + continue + + status = SessionStatus.RUNNING + if container.status == "exited": + status = SessionStatus.STOPPED + elif container.status == "created": + status = SessionStatus.CREATING + + session = Session( + id=session_id, + name=labels.get("mc.session.name", f"mc-{session_id}"), + driver=labels.get("mc.driver", "unknown"), + status=status, + container_id=container_id, + created_at=container.attrs["Created"], + project=labels.get("mc.project"), + ) + + # Get port mappings + if container.attrs.get("NetworkSettings", {}).get("Ports"): + ports = {} + for container_port, host_ports in container.attrs[ + "NetworkSettings" + ]["Ports"].items(): + if host_ports: + # Strip /tcp or /udp suffix and convert to int + container_port_num = int(container_port.split("/")[0]) + host_port = int(host_ports[0]["HostPort"]) + ports[container_port_num] = host_port + session.ports = ports + + sessions.append(session) + + except DockerException as e: + print(f"Error listing sessions: {e}") + + return sessions + + def create_session( + self, + driver_name: str, + project: Optional[str] = None, + environment: Optional[Dict[str, str]] = None, + session_name: Optional[str] = None, + ) -> Optional[Session]: + """Create a new MC session""" + try: + # Validate driver exists + driver = self.config_manager.get_driver(driver_name) + if not driver: + print(f"Driver '{driver_name}' not found") + return None + + # Generate session ID and name + session_id = self._generate_session_id() + if not session_name: + session_name = f"mc-{session_id}" + + # Ensure network exists + self._ensure_network() + + # Prepare environment variables + env_vars = environment or {} + + # Pull image if needed + try: + self.client.images.get(driver.image) + except ImageNotFound: + print(f"Pulling image {driver.image}...") + self.client.images.pull(driver.image) + + # Create container + container = self.client.containers.create( + image=driver.image, + name=session_name, + hostname=session_name, + detach=True, + tty=True, + stdin_open=True, + environment=env_vars, + labels={ + "mc.session": "true", + "mc.session.id": session_id, + "mc.session.name": session_name, + "mc.driver": driver_name, + "mc.project": project or "", + }, + network=self.config_manager.config.docker.get("network", "mc-network"), + ports={f"{port}/tcp": None for port in driver.ports}, + ) + + # Start container + container.start() + + # Get updated port information + container.reload() + ports = {} + if container.attrs.get("NetworkSettings", {}).get("Ports"): + for container_port, host_ports in container.attrs["NetworkSettings"][ + "Ports" + ].items(): + if host_ports: + container_port_num = int(container_port.split("/")[0]) + host_port = int(host_ports[0]["HostPort"]) + ports[container_port_num] = host_port + + # Create session object + session = Session( + id=session_id, + name=session_name, + driver=driver_name, + status=SessionStatus.RUNNING, + container_id=container.id, + environment=env_vars, + project=project, + created_at=container.attrs["Created"], + ports=ports, + ) + + # Save session to config as JSON-compatible dict + self.config_manager.add_session(session_id, session.model_dump(mode="json")) + + return session + + except DockerException as e: + print(f"Error creating session: {e}") + return None + + def close_session(self, session_id: str) -> bool: + """Close a MC session""" + try: + sessions = self.list_sessions() + for session in sessions: + if session.id == session_id and session.container_id: + container = self.client.containers.get(session.container_id) + container.stop() + container.remove() + self.config_manager.remove_session(session_id) + return True + + print(f"Session '{session_id}' not found") + return False + + except DockerException as e: + print(f"Error closing session: {e}") + return False + + def connect_session(self, session_id: str) -> bool: + """Connect to a running MC session""" + try: + sessions = self.list_sessions() + for session in sessions: + if session.id == session_id and session.container_id: + if session.status != SessionStatus.RUNNING: + print(f"Session '{session_id}' is not running") + return False + + # Execute interactive shell in container + os.system(f"docker exec -it {session.container_id} /bin/bash") + return True + + print(f"Session '{session_id}' not found") + return False + + except DockerException as e: + print(f"Error connecting to session: {e}") + return False + + def get_session_logs(self, session_id: str, follow: bool = False) -> Optional[str]: + """Get logs from a MC session""" + try: + sessions = self.list_sessions() + for session in sessions: + if session.id == session_id and session.container_id: + container = self.client.containers.get(session.container_id) + if follow: + for line in container.logs(stream=True, follow=True): + print(line.decode().strip()) + return None + else: + return container.logs().decode() + + print(f"Session '{session_id}' not found") + return None + + except DockerException as e: + print(f"Error getting session logs: {e}") + return None diff --git a/mcontainer/drivers/__init__.py b/mcontainer/drivers/__init__.py new file mode 100644 index 0000000..3cab3a7 --- /dev/null +++ b/mcontainer/drivers/__init__.py @@ -0,0 +1 @@ +"""Driver definitions for MAI""" diff --git a/mcontainer/drivers/base.py b/mcontainer/drivers/base.py new file mode 100644 index 0000000..1919aec --- /dev/null +++ b/mcontainer/drivers/base.py @@ -0,0 +1,28 @@ +""" +Base driver implementation for MAI +""" + +from typing import Dict, Optional + +from ..models import Driver + + +class DriverManager: + """Manager for MAI drivers""" + + @staticmethod + def get_default_drivers() -> Dict[str, Driver]: + """Get the default built-in drivers""" + from ..config import DEFAULT_DRIVERS + + return DEFAULT_DRIVERS + + @staticmethod + def get_driver_metadata(driver_name: str) -> Optional[Dict]: + """Get metadata for a specific driver""" + from ..config import DEFAULT_DRIVERS + + if driver_name in DEFAULT_DRIVERS: + return DEFAULT_DRIVERS[driver_name].model_dump() + + return None diff --git a/mcontainer/models.py b/mcontainer/models.py new file mode 100644 index 0000000..65fa13c --- /dev/null +++ b/mcontainer/models.py @@ -0,0 +1,48 @@ +from enum import Enum +from typing import Dict, List, Optional +from pydantic import BaseModel, Field + + +class SessionStatus(str, Enum): + CREATING = "creating" + RUNNING = "running" + STOPPED = "stopped" + FAILED = "failed" + + +class DriverEnvironmentVariable(BaseModel): + name: str + description: str + required: bool = False + default: Optional[str] = None + sensitive: bool = False + + +class Driver(BaseModel): + name: str + description: str + version: str + maintainer: str + image: str + environment: List[DriverEnvironmentVariable] = [] + ports: List[int] = [] + volumes: List[Dict[str, str]] = [] + + +class Session(BaseModel): + id: str + name: str + driver: str + status: SessionStatus + container_id: Optional[str] = None + environment: Dict[str, str] = Field(default_factory=dict) + project: Optional[str] = None + created_at: str + ports: Dict[int, int] = Field(default_factory=dict) + + +class Config(BaseModel): + docker: Dict[str, str] = Field(default_factory=dict) + drivers: Dict[str, Driver] = Field(default_factory=dict) + sessions: Dict[str, dict] = Field(default_factory=dict) # Store as dict to avoid serialization issues + defaults: Dict[str, str] = Field(default_factory=dict) diff --git a/mcontainer/service.py b/mcontainer/service.py new file mode 100644 index 0000000..bd64393 --- /dev/null +++ b/mcontainer/service.py @@ -0,0 +1,14 @@ +""" +MC Service - Container Management Web Service +(This is a placeholder for Phase 2) +""" + + +def main() -> None: + """Run the MC service""" + print("MC Service - Container Management Web Service") + print("This feature will be implemented in Phase 2") + + +if __name__ == "__main__": + main() diff --git a/mcontainer/utils/__init__.py b/mcontainer/utils/__init__.py new file mode 100644 index 0000000..04e8b09 --- /dev/null +++ b/mcontainer/utils/__init__.py @@ -0,0 +1 @@ +"""Utility functions for MAI""" diff --git a/mcontainer/utils/git.py b/mcontainer/utils/git.py new file mode 100644 index 0000000..89e905d --- /dev/null +++ b/mcontainer/utils/git.py @@ -0,0 +1,61 @@ +""" +Git repository handling utilities for MAI +""" + +import re +from typing import Optional, Tuple + + +def parse_git_url(url: str) -> Optional[Tuple[str, str, str]]: + """ + Parse a Git URL into its components: hostname, owner, repo + + Supports formats: + - git@github.com:owner/repo.git + - https://github.com/owner/repo.git + - github.com/owner/repo + + Returns: + Tuple of (hostname, owner, repo) or None if invalid + """ + # SSH format: git@github.com:owner/repo.git + ssh_pattern = r"^(?:git@)?([\w\.-]+)(?::)([\w\.-]+)/([\w\.-]+)(?:\.git)?$" + + # HTTPS format: https://github.com/owner/repo.git + https_pattern = r"^(?:https?://)([\w\.-]+)/(?:([\w\.-]+)/([\w\.-]+))(?:\.git)?$" + + # Simple format: github.com/owner/repo + simple_pattern = r"^([\w\.-]+)/(?:([\w\.-]+)/([\w\.-]+))(?:\.git)?$" + + for pattern in [ssh_pattern, https_pattern, simple_pattern]: + match = re.match(pattern, url) + if match: + hostname, owner, repo = match.groups() + return hostname, owner, repo + + return None + + +def get_normalized_url(url: str) -> Optional[str]: + """ + Convert various Git URL formats to a normalized form + + Returns: + Normalized URL (git@hostname:owner/repo.git) or None if invalid + """ + parsed = parse_git_url(url) + if not parsed: + return None + + hostname, owner, repo = parsed + return f"git@{hostname}:{owner}/{repo}.git" + + +def get_repository_name(url: str) -> Optional[str]: + """Get the repository name from a Git URL""" + parsed = parse_git_url(url) + if not parsed: + return None + + _, _, repo = parsed + return repo.replace(".git", "") diff --git a/pyproject.toml b/pyproject.toml index f5c89a3..6165865 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,38 @@ [project] -name = "monadical-ai" +name = "mcontainer" version = "0.1.0" -description = "Add your description here" +description = "Monadical Container Tool" readme = "README.md" -requires-python = ">=3.13" -dependencies = [] +requires-python = ">=3.12" +dependencies = [ + "typer>=0.9.0", + "docker>=7.0.0", + "pyyaml>=6.0.1", + "rich>=13.6.0", + "pydantic>=2.5.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project.optional-dependencies] +dev = [ + "pytest>=7.4.0", + "ruff>=0.1.9", + "mypy>=1.7.0", +] + +[project.scripts] +mc = "mcontainer.cli:app" + +[tool.ruff] +line-length = 88 +target-version = "py312" + +[tool.mypy] +python_version = "3.12" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true +disallow_incomplete_defs = true diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..cda1511 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,27 @@ +from typer.testing import CliRunner +from mcontainer.cli import app + +runner = CliRunner() + + +def test_version() -> None: + """Test version command""" + result = runner.invoke(app, ["version"]) + assert result.exit_code == 0 + assert "MC - Monadical Container Tool" in result.stdout + + +def test_session_list() -> None: + """Test session list command""" + result = runner.invoke(app, ["session", "list"]) + assert result.exit_code == 0 + # Could be either "No active sessions found" or a table of sessions + assert "sessions" in result.stdout.lower() or "no active" in result.stdout.lower() + + +def test_help() -> None: + """Test help command""" + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "Usage" in result.stdout + assert "Monadical Container Tool" in result.stdout \ No newline at end of file diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..b7b3127 --- /dev/null +++ b/uv.lock @@ -0,0 +1,423 @@ +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "docker" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "mcontainer" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "docker" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "typer" }, +] + +[package.optional-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "docker", specifier = ">=7.0.0" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.7.0" }, + { name = "pydantic", specifier = ">=2.5.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" }, + { name = "pyyaml", specifier = ">=6.0.1" }, + { name = "rich", specifier = ">=13.6.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.9" }, + { name = "typer", specifier = ">=0.9.0" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "mypy" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 }, + { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 }, + { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 }, + { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 }, + { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 }, + { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 }, + { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 }, + { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 }, + { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 }, + { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 }, + { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 }, + { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 }, + { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pydantic" +version = "2.10.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, + { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, + { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, + { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, + { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, + { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, + { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, + { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, + { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, + { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, + { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, + { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, + { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, + { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, + { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, + { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, + { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, + { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, + { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, + { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, + { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, + { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, + { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, + { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, + { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, + { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, + { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, +] + +[[package]] +name = "pywin32" +version = "309" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/2c/b0240b14ff3dba7a8a7122dc9bbf7fbd21ed0e8b57c109633675b5d1761f/pywin32-309-cp312-cp312-win32.whl", hash = "sha256:de9acacced5fa82f557298b1fed5fef7bd49beee04190f68e1e4783fbdc19926", size = 8790648 }, + { url = "https://files.pythonhosted.org/packages/dd/11/c36884c732e2b3397deee808b5dac1abbb170ec37f94c6606fcb04d1e9d7/pywin32-309-cp312-cp312-win_amd64.whl", hash = "sha256:6ff9eebb77ffc3d59812c68db33c0a7817e1337e3537859499bd27586330fc9e", size = 9497399 }, + { url = "https://files.pythonhosted.org/packages/18/9f/79703972958f8ba3fd38bc9bf1165810bd75124982419b0cc433a2894d46/pywin32-309-cp312-cp312-win_arm64.whl", hash = "sha256:619f3e0a327b5418d833f44dc87859523635cf339f86071cc65a13c07be3110f", size = 8454122 }, + { url = "https://files.pythonhosted.org/packages/6c/c3/51aca6887cc5e410aa4cdc55662cf8438212440c67335c3f141b02eb8d52/pywin32-309-cp313-cp313-win32.whl", hash = "sha256:008bffd4afd6de8ca46c6486085414cc898263a21a63c7f860d54c9d02b45c8d", size = 8789700 }, + { url = "https://files.pythonhosted.org/packages/dd/66/330f265140fa814b4ed1bf16aea701f9d005f8f4ab57a54feb17f53afe7e/pywin32-309-cp313-cp313-win_amd64.whl", hash = "sha256:bd0724f58492db4cbfbeb1fcd606495205aa119370c0ddc4f70e5771a3ab768d", size = 9496714 }, + { url = "https://files.pythonhosted.org/packages/2c/84/9a51e6949a03f25cd329ece54dbf0846d57fadd2e79046c3b8d140aaa132/pywin32-309-cp313-cp313-win_arm64.whl", hash = "sha256:8fd9669cfd41863b688a1bc9b1d4d2d76fd4ba2128be50a70b0ea66b8d37953b", size = 8453052 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, +] + +[[package]] +name = "ruff" +version = "0.9.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/8e/fafaa6f15c332e73425d9c44ada85360501045d5ab0b81400076aff27cf6/ruff-0.9.10.tar.gz", hash = "sha256:9bacb735d7bada9cfb0f2c227d3658fc443d90a727b47f206fb33f52f3c0eac7", size = 3759776 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/b2/af7c2cc9e438cbc19fafeec4f20bfcd72165460fe75b2b6e9a0958c8c62b/ruff-0.9.10-py3-none-linux_armv6l.whl", hash = "sha256:eb4d25532cfd9fe461acc83498361ec2e2252795b4f40b17e80692814329e42d", size = 10049494 }, + { url = "https://files.pythonhosted.org/packages/6d/12/03f6dfa1b95ddd47e6969f0225d60d9d7437c91938a310835feb27927ca0/ruff-0.9.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:188a6638dab1aa9bb6228a7302387b2c9954e455fb25d6b4470cb0641d16759d", size = 10853584 }, + { url = "https://files.pythonhosted.org/packages/02/49/1c79e0906b6ff551fb0894168763f705bf980864739572b2815ecd3c9df0/ruff-0.9.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5284dcac6b9dbc2fcb71fdfc26a217b2ca4ede6ccd57476f52a587451ebe450d", size = 10155692 }, + { url = "https://files.pythonhosted.org/packages/5b/01/85e8082e41585e0e1ceb11e41c054e9e36fed45f4b210991052d8a75089f/ruff-0.9.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47678f39fa2a3da62724851107f438c8229a3470f533894b5568a39b40029c0c", size = 10369760 }, + { url = "https://files.pythonhosted.org/packages/a1/90/0bc60bd4e5db051f12445046d0c85cc2c617095c0904f1aa81067dc64aea/ruff-0.9.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99713a6e2766b7a17147b309e8c915b32b07a25c9efd12ada79f217c9c778b3e", size = 9912196 }, + { url = "https://files.pythonhosted.org/packages/66/ea/0b7e8c42b1ec608033c4d5a02939c82097ddcb0b3e393e4238584b7054ab/ruff-0.9.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524ee184d92f7c7304aa568e2db20f50c32d1d0caa235d8ddf10497566ea1a12", size = 11434985 }, + { url = "https://files.pythonhosted.org/packages/d5/86/3171d1eff893db4f91755175a6e1163c5887be1f1e2f4f6c0c59527c2bfd/ruff-0.9.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df92aeac30af821f9acf819fc01b4afc3dfb829d2782884f8739fb52a8119a16", size = 12155842 }, + { url = "https://files.pythonhosted.org/packages/89/9e/700ca289f172a38eb0bca752056d0a42637fa17b81649b9331786cb791d7/ruff-0.9.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de42e4edc296f520bb84954eb992a07a0ec5a02fecb834498415908469854a52", size = 11613804 }, + { url = "https://files.pythonhosted.org/packages/f2/92/648020b3b5db180f41a931a68b1c8575cca3e63cec86fd26807422a0dbad/ruff-0.9.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d257f95b65806104b6b1ffca0ea53f4ef98454036df65b1eda3693534813ecd1", size = 13823776 }, + { url = "https://files.pythonhosted.org/packages/5e/a6/cc472161cd04d30a09d5c90698696b70c169eeba2c41030344194242db45/ruff-0.9.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60dec7201c0b10d6d11be00e8f2dbb6f40ef1828ee75ed739923799513db24c", size = 11302673 }, + { url = "https://files.pythonhosted.org/packages/6c/db/d31c361c4025b1b9102b4d032c70a69adb9ee6fde093f6c3bf29f831c85c/ruff-0.9.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d838b60007da7a39c046fcdd317293d10b845001f38bcb55ba766c3875b01e43", size = 10235358 }, + { url = "https://files.pythonhosted.org/packages/d1/86/d6374e24a14d4d93ebe120f45edd82ad7dcf3ef999ffc92b197d81cdc2a5/ruff-0.9.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ccaf903108b899beb8e09a63ffae5869057ab649c1e9231c05ae354ebc62066c", size = 9886177 }, + { url = "https://files.pythonhosted.org/packages/00/62/a61691f6eaaac1e945a1f3f59f1eea9a218513139d5b6c2b8f88b43b5b8f/ruff-0.9.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f9567d135265d46e59d62dc60c0bfad10e9a6822e231f5b24032dba5a55be6b5", size = 10864747 }, + { url = "https://files.pythonhosted.org/packages/ee/94/2c7065e1d92a8a8a46d46d9c3cf07b0aa7e0a1e0153d74baa5e6620b4102/ruff-0.9.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5f202f0d93738c28a89f8ed9eaba01b7be339e5d8d642c994347eaa81c6d75b8", size = 11360441 }, + { url = "https://files.pythonhosted.org/packages/a7/8f/1f545ea6f9fcd7bf4368551fb91d2064d8f0577b3079bb3f0ae5779fb773/ruff-0.9.10-py3-none-win32.whl", hash = "sha256:bfb834e87c916521ce46b1788fbb8484966e5113c02df216680102e9eb960029", size = 10247401 }, + { url = "https://files.pythonhosted.org/packages/4f/18/fb703603ab108e5c165f52f5b86ee2aa9be43bb781703ec87c66a5f5d604/ruff-0.9.10-py3-none-win_amd64.whl", hash = "sha256:f2160eeef3031bf4b17df74e307d4c5fb689a6f3a26a2de3f7ef4044e3c484f1", size = 11366360 }, + { url = "https://files.pythonhosted.org/packages/35/85/338e603dc68e7d9994d5d84f24adbf69bae760ba5efd3e20f5ff2cec18da/ruff-0.9.10-py3-none-win_arm64.whl", hash = "sha256:5fd804c0327a5e5ea26615550e706942f348b197d5475ff34c19733aee4b2e69", size = 10436892 }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, +] + +[[package]] +name = "typer" +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "urllib3" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, +]