diff --git a/.all-contributorsrc b/.all-contributorsrc index 503f0e94..5969251d 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -160,6 +160,24 @@ "contributions": [ "code" ] + }, + { + "login": "jklapacz", + "name": "Jakub Klapacz", + "avatar_url": "https://avatars.githubusercontent.com/u/5343758?v=4", + "profile": "https://github.com/jklapacz", + "contributions": [ + "code" + ] + }, + { + "login": "evnsnclr", + "name": "Evan smith", + "avatar_url": "https://avatars.githubusercontent.com/u/139897548?v=4", + "profile": "https://github.com/evnsnclr", + "contributions": [ + "code" + ] } ] } diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 00000000..ed66c86e --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,62 @@ +# Dev Container Setup + +This repository includes a Dev Container configuration that simplifies the development setup to just 3 steps: + +## Quick Start + +![Clipboard-20250611-180809-459](https://github.com/user-attachments/assets/447eaeeb-0eec-4354-9a82-44446e202e06) + +1. **Install Dev Containers extension** in VS Code +2. **Clone and open in container**: + - Press `Ctrl+Shift+P` (or `Cmd+Shift+P` on Mac) + - Type "Dev Containers: Clone Repository in Container Volume..." + - Paste the repository URL: `https://github.com/trycua/cua.git` +3. **Hit play**: Once the container builds, you're ready to develop! + +## What's Included + +The dev container automatically: + +- ✅ Sets up Python 3.11 environment +- ✅ Installs all system dependencies (build tools, OpenGL, etc.) +- ✅ Configures Python paths for all packages +- ✅ Installs Python extensions (Black, Ruff, Pylance) +- ✅ Forwards port 7860 for the Gradio web UI +- ✅ Mounts your source code for live editing +- ✅ Creates the required `.env.local` file + +## Running Examples + +After the container is built, you can run examples directly: + +```bash +# Run the agent UI (Gradio web interface) +python examples/agent_ui_examples.py + +# Run computer examples +python examples/computer_examples.py + +# Run computer UI examples +python examples/computer_ui_examples.py +``` + +The Gradio UI will be available at `http://localhost:7860` and will automatically forward to your host machine. + +## Environment Variables + +You'll need to add your API keys to `.env.local`: + +```bash +# Required for Anthropic provider +ANTHROPIC_API_KEY=your_anthropic_key_here + +# Required for OpenAI provider +OPENAI_API_KEY=your_openai_key_here +``` + +## Notes + +- The container connects to `host.docker.internal:7777` for Lume server communication +- All Python packages are pre-installed and configured +- Source code changes are reflected immediately (no rebuild needed) +- The container uses the same Dockerfile as the regular Docker development environment diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..4f9ceccc --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,49 @@ +{ + "name": "C/ua - OSS", + "build": { + "dockerfile": "../Dockerfile" + }, + "containerEnv": { + "DISPLAY": "", + "PYTHONPATH": "/app/libs/core:/app/libs/computer:/app/libs/agent:/app/libs/som:/app/libs/pylume:/app/libs/computer-server:/app/libs/mcp-server", + "PYLUME_HOST": "host.docker.internal" + }, + "forwardPorts": [7860], + "portsAttributes": { + "7860": { + "label": "C/ua web client (Gradio)", + "onAutoForward": "silent" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "ms-python.black-formatter", + "charliermarsh.ruff", + "ms-python.vscode-pylance" + ], + "settings": { + "python.defaultInterpreterPath": "/usr/local/bin/python", + "python.terminal.activateEnvironment": false, + "[python]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + } + }, + "python.linting.enabled": true, + "python.linting.ruffEnabled": true, + "python.formatting.provider": "black", + "files.watcherExclude": { + "**/.venv/**": true, + "**/node_modules/**": true, + "**/__pycache__/**": true, + "**/.pytest_cache/**": true + } + } + } + }, + "postCreateCommand": "/bin/bash .devcontainer/post-install.sh" +} diff --git a/.devcontainer/post-install.sh b/.devcontainer/post-install.sh new file mode 100755 index 00000000..1738e635 --- /dev/null +++ b/.devcontainer/post-install.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +WORKSPACE="/workspaces/cua" + +# Setup .env.local +echo "PYTHON_BIN=python" > /workspaces/cua/.env.local + +# Run /scripts/build.sh +./scripts/build.sh + +# --- +# Build is complete. Show user a clear message to open the workspace manually. +# --- + +cat << 'EOM' + +============================================ + 🚀 Build complete! + + 👉 Next steps: + + 1. Open '.vscode/py.code-workspace' + 2. Press 'Open Workspace' + + Happy coding! +============================================ + +EOM diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..3852dc32 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto +*.sh text eol=lf \ No newline at end of file diff --git a/.github/workflows/publish-agent.yml b/.github/workflows/publish-agent.yml index 1566880b..ea03edd6 100644 --- a/.github/workflows/publish-agent.yml +++ b/.github/workflows/publish-agent.yml @@ -56,7 +56,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' - name: Update dependencies to latest versions id: update-deps diff --git a/.github/workflows/publish-computer.yml b/.github/workflows/publish-computer.yml index 9175907e..b2a9fb25 100644 --- a/.github/workflows/publish-computer.yml +++ b/.github/workflows/publish-computer.yml @@ -26,7 +26,6 @@ jobs: runs-on: macos-latest outputs: version: ${{ steps.get-version.outputs.version }} - pylume_version: ${{ steps.update-deps.outputs.pylume_version }} core_version: ${{ steps.update-deps.outputs.core_version }} steps: - uses: actions/checkout@v4 @@ -55,7 +54,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' - name: Update dependencies to latest versions id: update-deps @@ -91,20 +90,16 @@ jobs: return fallback # Get latest versions - print(get_package_version('pylume')) print(get_package_version('cua-core')) EOF # Execute the script to get the versions VERSIONS=($(python get_latest_versions.py)) - LATEST_PYLUME=${VERSIONS[0]} - LATEST_CORE=${VERSIONS[1]} + LATEST_CORE=${VERSIONS[0]} - echo "Latest pylume version: $LATEST_PYLUME" echo "Latest cua-core version: $LATEST_CORE" # Output the versions for the next job - echo "pylume_version=$LATEST_PYLUME" >> $GITHUB_OUTPUT echo "core_version=$LATEST_CORE" >> $GITHUB_OUTPUT # Determine major version for version constraint @@ -114,17 +109,15 @@ jobs: # Update dependencies in pyproject.toml if [[ "$OSTYPE" == "darwin"* ]]; then # macOS version of sed needs an empty string for -i - sed -i '' "s/\"pylume>=.*\"/\"pylume>=$LATEST_PYLUME\"/" pyproject.toml sed -i '' "s/\"cua-core>=.*,<.*\"/\"cua-core>=$LATEST_CORE,<$NEXT_CORE_MAJOR.0.0\"/" pyproject.toml else # Linux version - sed -i "s/\"pylume>=.*\"/\"pylume>=$LATEST_PYLUME\"/" pyproject.toml sed -i "s/\"cua-core>=.*,<.*\"/\"cua-core>=$LATEST_CORE,<$NEXT_CORE_MAJOR.0.0\"/" pyproject.toml fi # Display the updated dependencies echo "Updated dependencies in pyproject.toml:" - grep -E "pylume|cua-core" pyproject.toml + grep -E "cua-core" pyproject.toml publish: needs: prepare @@ -144,5 +137,4 @@ jobs: steps: - name: Set environment variables for use in other jobs run: | - echo "PYLUME_VERSION=${{ needs.prepare.outputs.pylume_version }}" >> $GITHUB_ENV echo "CORE_VERSION=${{ needs.prepare.outputs.core_version }}" >> $GITHUB_ENV \ No newline at end of file diff --git a/.github/workflows/publish-lume.yml b/.github/workflows/publish-lume.yml index 3b2311ee..ec5e7550 100644 --- a/.github/workflows/publish-lume.yml +++ b/.github/workflows/publish-lume.yml @@ -114,9 +114,25 @@ jobs: # Allow codesign to access the certificates (minimal output) security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain > /dev/null 2>&1 - # Verify certificates were imported but only show count, not details - echo "Verifying signing identity (showing count only)..." - security find-identity -v -p codesigning | grep -c "valid identities found" || true + # Verify certificates were imported + echo "Verifying signing identities..." + CERT_COUNT=$(security find-identity -v -p codesigning build.keychain | grep -c "Developer ID Application" || echo "0") + INSTALLER_COUNT=$(security find-identity -v build.keychain | grep -c "Developer ID Installer" || echo "0") + + if [ "$CERT_COUNT" -eq 0 ]; then + echo "Error: No Developer ID Application certificate found" + security find-identity -v -p codesigning build.keychain + exit 1 + fi + + if [ "$INSTALLER_COUNT" -eq 0 ]; then + echo "Error: No Developer ID Installer certificate found" + security find-identity -v build.keychain + exit 1 + fi + + echo "Found $CERT_COUNT Developer ID Application certificate(s) and $INSTALLER_COUNT Developer ID Installer certificate(s)" + echo "All required certificates verified successfully" # Clean up certificate files rm application.p12 installer.p12 diff --git a/.github/workflows/publish-mcp-server.yml b/.github/workflows/publish-mcp-server.yml index d23bf8c0..e6eccd5a 100644 --- a/.github/workflows/publish-mcp-server.yml +++ b/.github/workflows/publish-mcp-server.yml @@ -59,7 +59,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' - name: Update dependencies to latest versions id: update-deps diff --git a/.github/workflows/reusable-publish.yml b/.github/workflows/reusable-publish.yml index 892e3250..8856b60d 100644 --- a/.github/workflows/reusable-publish.yml +++ b/.github/workflows/reusable-publish.yml @@ -52,7 +52,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' - name: Create root pdm.lock file run: | @@ -62,7 +62,7 @@ jobs: - name: Install PDM uses: pdm-project/setup-pdm@v3 with: - python-version: '3.10' + python-version: '3.11' cache: true - name: Set version diff --git a/.gitignore b/.gitignore index 33b01bd8..c5f78692 100644 --- a/.gitignore +++ b/.gitignore @@ -249,4 +249,7 @@ trajectories/ storage/ # Trashes -.Trashes \ No newline at end of file +.Trashes +.Trash-1000/ + +post-provision \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 3ea4279e..eb7f1801 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,12 +1,12 @@ { "configurations": [ { - "name": "Run Computer Examples", + "name": "Agent UI", "type": "debugpy", "request": "launch", - "program": "examples/computer_examples.py", + "program": "examples/agent_ui_examples.py", "console": "integratedTerminal", - "justMyCode": true, + "justMyCode": false, "python": "${workspaceFolder:cua-root}/.venv/bin/python", "cwd": "${workspaceFolder:cua-root}", "env": { @@ -14,10 +14,10 @@ } }, { - "name": "Run Agent Examples", + "name": "Computer UI", "type": "debugpy", "request": "launch", - "program": "examples/agent_examples.py", + "program": "examples/computer_ui_examples.py", "console": "integratedTerminal", "justMyCode": false, "python": "${workspaceFolder:cua-root}/.venv/bin/python", @@ -27,10 +27,23 @@ } }, { - "name": "Run Agent UI Examples", + "name": "Run Computer Examples", "type": "debugpy", "request": "launch", - "program": "examples/agent_ui_examples.py", + "program": "examples/computer_examples.py", + "console": "integratedTerminal", + "justMyCode": true, + "python": "${workspaceFolder:cua-root}/.venv/bin/python", + "cwd": "${workspaceFolder:cua-root}", + "env": { + "PYTHONPATH": "${workspaceFolder:cua-root}/libs/core:${workspaceFolder:cua-root}/libs/computer:${workspaceFolder:cua-root}/libs/agent:${workspaceFolder:cua-root}/libs/som:${workspaceFolder:cua-root}/libs/pylume" + } + }, + { + "name": "Run Agent Examples", + "type": "debugpy", + "request": "launch", + "program": "examples/agent_examples.py", "console": "integratedTerminal", "justMyCode": false, "python": "${workspaceFolder:cua-root}/.venv/bin/python", diff --git a/.vscode/py.code-workspace b/.vscode/py.code-workspace index 6adef38b..11751d53 100644 --- a/.vscode/py.code-workspace +++ b/.vscode/py.code-workspace @@ -105,7 +105,7 @@ "${workspaceFolder:cua-root}/libs/som", "${workspaceFolder:cua-root}/libs/pylume" ], - "python.languageServer": "Pylance", + "python.languageServer": "None", "[python]": { "editor.formatOnSave": true, "editor.defaultFormatter": "ms-python.black-formatter", @@ -148,119 +148,6 @@ } ] }, - "launch": { - "version": "0.2.0", - "configurations": [ - { - "name": "Run Computer Examples", - "type": "debugpy", - "request": "launch", - "program": "examples/computer_examples.py", - "console": "integratedTerminal", - "justMyCode": true, - "python": "${workspaceFolder:cua-root}/.venv/bin/python", - "cwd": "${workspaceFolder:cua-root}", - "env": { - "PYTHONPATH": "${workspaceFolder:cua-root}/libs/core:${workspaceFolder:cua-root}/libs/computer:${workspaceFolder:cua-root}/libs/agent:${workspaceFolder:cua-root}/libs/som:${workspaceFolder:cua-root}/libs/pylume" - } - }, - { - "name": "Run Agent Examples", - "type": "debugpy", - "request": "launch", - "program": "examples/agent_examples.py", - "console": "integratedTerminal", - "justMyCode": false, - "python": "${workspaceFolder:cua-root}/.venv/bin/python", - "cwd": "${workspaceFolder:cua-root}", - "env": { - "PYTHONPATH": "${workspaceFolder:cua-root}/libs/core:${workspaceFolder:cua-root}/libs/computer:${workspaceFolder:cua-root}/libs/agent:${workspaceFolder:cua-root}/libs/som:${workspaceFolder:cua-root}/libs/pylume" - } - }, - { - "name": "Run PyLume Examples", - "type": "debugpy", - "request": "launch", - "program": "examples/pylume_examples.py", - "console": "integratedTerminal", - "justMyCode": true, - "python": "${workspaceFolder:cua-root}/.venv/bin/python", - "cwd": "${workspaceFolder:cua-root}", - "env": { - "PYTHONPATH": "${workspaceFolder:cua-root}/libs/core:${workspaceFolder:cua-root}/libs/computer:${workspaceFolder:cua-root}/libs/agent:${workspaceFolder:cua-root}/libs/som:${workspaceFolder:cua-root}/libs/pylume" - } - }, - { - "name": "SOM: Run Experiments (No OCR)", - "type": "debugpy", - "request": "launch", - "program": "examples/som_examples.py", - "args": [ - "examples/test_data", - "--output-dir", "examples/output", - "--ocr", "none", - "--mode", "experiment" - ], - "console": "integratedTerminal", - "justMyCode": false, - "python": "${workspaceFolder:cua-root}/.venv/bin/python", - "cwd": "${workspaceFolder:cua-root}", - "env": { - "PYTHONPATH": "${workspaceFolder:cua-root}/libs/core:${workspaceFolder:cua-root}/libs/computer:${workspaceFolder:cua-root}/libs/agent:${workspaceFolder:cua-root}/libs/som:${workspaceFolder:cua-root}/libs/pylume" - } - }, - { - "name": "SOM: Run Experiments (EasyOCR)", - "type": "debugpy", - "request": "launch", - "program": "examples/som_examples.py", - "args": [ - "examples/test_data", - "--output-dir", "examples/output", - "--ocr", "easyocr", - "--mode", "experiment" - ], - "console": "integratedTerminal", - "justMyCode": false, - "python": "${workspaceFolder:cua-root}/.venv/bin/python", - "cwd": "${workspaceFolder:cua-root}", - "env": { - "PYTHONPATH": "${workspaceFolder:cua-root}/libs/core:${workspaceFolder:cua-root}/libs/computer:${workspaceFolder:cua-root}/libs/agent:${workspaceFolder:cua-root}/libs/som:${workspaceFolder:cua-root}/libs/pylume" - } - }, - { - "name": "Run Computer Server", - "type": "debugpy", - "request": "launch", - "program": "${workspaceFolder}/libs/computer-server/run_server.py", - "console": "integratedTerminal", - "justMyCode": true, - "python": "${workspaceFolder:cua-root}/.venv/bin/python", - "cwd": "${workspaceFolder:cua-root}", - "env": { - "PYTHONPATH": "${workspaceFolder:cua-root}/libs/core:${workspaceFolder:cua-root}/libs/computer:${workspaceFolder:cua-root}/libs/agent:${workspaceFolder:cua-root}/libs/som:${workspaceFolder:cua-root}/libs/pylume" - } - }, - { - "name": "Run Computer Server with Args", - "type": "debugpy", - "request": "launch", - "program": "${workspaceFolder}/libs/computer-server/run_server.py", - "args": [ - "--host", "0.0.0.0", - "--port", "8000", - "--log-level", "debug" - ], - "console": "integratedTerminal", - "justMyCode": false, - "python": "${workspaceFolder:cua-root}/.venv/bin/python", - "cwd": "${workspaceFolder:cua-root}", - "env": { - "PYTHONPATH": "${workspaceFolder:cua-root}/libs/core:${workspaceFolder:cua-root}/libs/computer-server" - } - } - ] - }, "compounds": [ { "name": "Run Computer Examples + Server", diff --git a/Dockerfile b/Dockerfile index c1e8bbf8..7762b43d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-slim +FROM python:3.12-slim # Set environment variables ENV PYTHONUNBUFFERED=1 \ @@ -21,6 +21,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ iputils-ping \ net-tools \ sed \ + xxd \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* @@ -49,7 +50,7 @@ RUN rm -rf /app/* /app/.??* # Note: This Docker image doesn't contain the lume executable (macOS-specific) # Instead, it relies on connecting to a lume server running on the host machine -# via host.docker.internal:3000 +# via host.docker.internal:7777 # Default command CMD ["bash"] \ No newline at end of file diff --git a/README.md b/README.md index dafef93d..cf1e8004 100644 --- a/README.md +++ b/README.md @@ -9,45 +9,89 @@ [![Swift](https://img.shields.io/badge/Swift-F05138?logo=swift&logoColor=white)](#) [![macOS](https://img.shields.io/badge/macOS-000000?logo=apple&logoColor=F0F0F0)](#) [![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white)](https://discord.com/invite/mVnXXpdE85) +
+ trycua%2Fcua | Trendshift -**c/ua** (pronounced "koo-ah") enables AI agents to control full operating systems in high-performance virtual containers with near-native speed on Apple Silicon. +**c/ua** ("koo-ah") is Docker for [Computer-Use Agents](https://www.oneusefulthing.org/p/when-you-give-a-claude-a-mouse) - it enables AI agents to control full operating systems in virtual containers and deploy them locally or to the cloud. +
+ +
+
+Check out more demos of the Computer-Use Agent in action + +
+MCP Server: Work with Claude Desktop and Tableau +
+
+ +
+
+ +
+AI-Gradio: Multi-app workflow with browser, VS Code and terminal +
+
+ +
+
+
+Notebook: Fix GitHub issue in Cursor +
-
+ + +
+

+ +# 🚀 Quick Start with a Computer-Use Agent UI + +**Need to automate desktop tasks? Launch the Computer-Use Agent UI with a single command.** -# 🚀 Quick Start -Get started with a Computer-Use Agent UI and a VM with a single command: +### Option 1: Fully-managed install (recommended) +*Guided install for quick use* +**macOS/Linux/Windows (via WSL):** ```bash +# Requires Python 3.11+ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/scripts/playground.sh)" ``` +This script will guide you through setup and launch the Computer-Use Agent UI. +--- -This script will: -- Install Lume CLI for VM management (if needed) -- Pull the latest macOS CUA image (if needed) -- Set up Python environment and install/update required packages -- Launch the Computer-Use Agent UI +### Option 2: [Dev Container](./.devcontainer/README.md) +*Best for contributors and development* -#### Supported [Agent Loops](https://github.com/trycua/cua/blob/main/libs/agent/README.md#agent-loops) +This repository includes a [Dev Container](./.devcontainer/README.md) configuration that simplifies setup to a few steps: + +1. **Install the Dev Containers extension ([VS Code](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) or [WindSurf](https://docs.windsurf.com/windsurf/advanced#dev-containers-beta))** +2. **Open the repository in the Dev Container:** + - Press `Ctrl+Shift+P` (or `Cmd+Shift+P` on macOS) + - Select `Dev Containers: Clone Repository in Container Volume...` and paste the repository URL: `https://github.com/trycua/cua.git` (if not cloned) or `Dev Containers: Open Folder in Container...` (if already cloned). **Note**: On WindSurf, the post install hook might not run automatically. If so, run `/bin/bash .devcontainer/post-install.sh` manually. +3. **Run the Agent UI example:** Click ![Run Agent UI](https://github.com/user-attachments/assets/7a61ef34-4b22-4dab-9864-f86bf83e290b) + to start the Gradio UI. If prompted to install **debugpy (Python Debugger)** to enable remote debugging, select 'Yes' to proceed. +4. **Access the Gradio UI:** The Gradio UI will be available at `http://localhost:7860` and will automatically forward to your host machine. + +--- + +*How it works: Computer module provides secure desktops (Lume CLI locally, [C/ua Cloud Containers](https://trycua.com) remotely), Agent module provides local/API agents with OpenAI AgentResponse format and [trajectory tracing](https://trycua.com/trajectory-viewer).* +### Supported [Agent Loops](https://github.com/trycua/cua/blob/main/libs/agent/README.md#agent-loops) - [UITARS-1.5](https://github.com/trycua/cua/blob/main/libs/agent/README.md#agent-loops) - Run locally on Apple Silicon with MLX, or use cloud providers - [OpenAI CUA](https://github.com/trycua/cua/blob/main/libs/agent/README.md#agent-loops) - Use OpenAI's Computer-Use Preview model - [Anthropic CUA](https://github.com/trycua/cua/blob/main/libs/agent/README.md#agent-loops) - Use Anthropic's Computer-Use capabilities -- [OmniParser](https://github.com/trycua/cua/blob/main/libs/agent/README.md#agent-loops) - Control UI with [Set-of-Marks prompting](https://som-gpt4v.github.io/) using any vision model +- [OmniParser-v2.0](https://github.com/trycua/cua/blob/main/libs/agent/README.md#agent-loops) - Control UI with [Set-of-Marks prompting](https://som-gpt4v.github.io/) using any vision model -### System Requirements -- Mac with Apple Silicon (M1/M2/M3/M4 series) -- macOS 15 (Sequoia) or newer -- Disk space for VM images (30GB+ recommended) +# 💻 Developer Guide -# 💻 For Developers +Follow these steps to use C/ua in your own code. See [Developer Guide](./docs/Developer-Guide.md) for building from source. ### Step 1: Install Lume CLI @@ -68,11 +112,9 @@ The macOS CUA image contains the default Mac apps and the Computer Server for ea ### Step 3: Install Python SDK ```bash -pip install cua-computer "cua-agent[all]" +pip install "cua-computer[all]" "cua-agent[all]" ``` -Alternatively, see the [Developer Guide](./docs/Developer-Guide.md) for building from source. - ### Step 4: Use in Your Code ```python @@ -80,23 +122,33 @@ from computer import Computer from agent import ComputerAgent, LLM async def main(): - # Start a local macOS VM with a 1024x768 display - async with Computer(os_type="macos", display="1024x768") as computer: - - # Example: Direct control of a macOS VM with Computer - await computer.interface.left_click(100, 200) - await computer.interface.type_text("Hello, world!") - screenshot_bytes = await computer.interface.screenshot() - - # Example: Create and run an agent locally using mlx-community/UI-TARS-1.5-7B-6bit - agent = ComputerAgent( - computer=computer, - loop="UITARS", - model=LLM(provider="MLXVLM", name="mlx-community/UI-TARS-1.5-7B-6bit") - ) - await agent.run("Find the trycua/cua repository on GitHub and follow the quick start guide") - -main() + # Start a local macOS VM + computer = Computer(os_type="macos") + await computer.run() + + # Or with C/ua Cloud Container + computer = Computer( + os_type="linux", + api_key="your_cua_api_key_here", + name="your_container_name_here" + ) + + # Example: Direct control of a macOS VM with Computer + await computer.interface.left_click(100, 200) + await computer.interface.type_text("Hello, world!") + screenshot_bytes = await computer.interface.screenshot() + + # Example: Create and run an agent locally using mlx-community/UI-TARS-1.5-7B-6bit + agent = ComputerAgent( + computer=computer, + loop="uitars", + model=LLM(provider="mlxvlm", name="mlx-community/UI-TARS-1.5-7B-6bit") + ) + async for result in agent.run("Find the trycua/cua repository on GitHub and follow the quick start guide"): + print(result) + +if __name__ == "__main__": + asyncio.run(main()) ``` For ready-to-use examples, check out our [Notebooks](./notebooks/) collection. @@ -104,7 +156,7 @@ For ready-to-use examples, check out our [Notebooks](./notebooks/) collection. ### Lume CLI Reference ```bash -# Install Lume CLI +# Install Lume CLI and background service curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/lume/scripts/install.sh | bash # List all VMs @@ -126,8 +178,29 @@ lume stop macos-sequoia-cua_latest lume delete macos-sequoia-cua_latest ``` +### Lumier CLI Reference + For advanced container-like virtualization, check out [Lumier](./libs/lumier/README.md) - a Docker interface for macOS and Linux VMs. +```bash +# Install Lume CLI and background service +curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/lume/scripts/install.sh | bash + +# Run macOS in a Docker container +docker run -it --rm \ + --name lumier-vm \ + -p 8006:8006 \ + -v $(pwd)/storage:/storage \ + -v $(pwd)/shared:/shared \ + -e VM_NAME=lumier-vm \ + -e VERSION=ghcr.io/trycua/macos-sequoia-cua:latest \ + -e CPU_CORES=4 \ + -e RAM_SIZE=8192 \ + -e HOST_STORAGE_PATH=$(pwd)/storage \ + -e HOST_SHARED_PATH=$(pwd)/shared \ + trycua/lumier:latest +``` + ## Resources - [How to use the MCP Server with Claude Desktop or other MCP clients](./libs/mcp-server/README.md) - One of the easiest ways to get started with C/ua @@ -141,11 +214,11 @@ For advanced container-like virtualization, check out [Lumier](./libs/lumier/REA | Module | Description | Installation | |--------|-------------|---------------| | [**Lume**](./libs/lume/README.md) | VM management for macOS/Linux using Apple's Virtualization.Framework | `curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/lume/scripts/install.sh \| bash` | -| [**Computer**](./libs/computer/README.md) | Interface for controlling virtual machines | `pip install cua-computer` | -| [**Agent**](./libs/agent/README.md) | AI agent framework for automating tasks | `pip install cua-agent` | +| [**Lumier**](./libs/lumier/README.md) | Docker interface for macOS and Linux VMs | `docker pull trycua/lumier:latest` | +| [**Computer**](./libs/computer/README.md) | Interface for controlling virtual machines | `pip install "cua-computer[all]"` | +| [**Agent**](./libs/agent/README.md) | AI agent framework for automating tasks | `pip install "cua-agent[all]"` | | [**MCP Server**](./libs/mcp-server/README.md) | MCP server for using CUA with Claude Desktop | `pip install cua-mcp-server` | | [**SOM**](./libs/som/README.md) | Self-of-Mark library for Agent | `pip install cua-som` | -| [**PyLume**](./libs/pylume/README.md) | Python bindings for Lume | `pip install pylume` | | [**Computer Server**](./libs/computer-server/README.md) | Server component for Computer | `pip install cua-computer-server` | | [**Core**](./libs/core/README.md) | Core utilities | `pip install cua-core` | @@ -154,6 +227,9 @@ For advanced container-like virtualization, check out [Lumier](./libs/lumier/REA For complete examples, see [computer_examples.py](./examples/computer_examples.py) or [computer_nb.ipynb](./notebooks/computer_nb.ipynb) ```python +# Shell Actions +await computer.interface.run_command(cmd) # Run shell command + # Mouse Actions await computer.interface.left_click(x, y) # Left click at coordinates await computer.interface.right_click(x, y) # Right click at coordinates @@ -161,11 +237,20 @@ await computer.interface.double_click(x, y) # Double click at coordinates await computer.interface.move_cursor(x, y) # Move cursor to coordinates await computer.interface.drag_to(x, y, duration) # Drag to coordinates await computer.interface.get_cursor_position() # Get current cursor position +await computer.interface.mouse_down(x, y, button="left") # Press and hold a mouse button +await computer.interface.mouse_up(x, y, button="left") # Release a mouse button # Keyboard Actions await computer.interface.type_text("Hello") # Type text await computer.interface.press_key("enter") # Press a single key await computer.interface.hotkey("command", "c") # Press key combination +await computer.interface.key_down("command") # Press and hold a key +await computer.interface.key_up("command") # Release a key + +# Scrolling Actions +await computer.interface.scroll(x, y) # Scroll the mouse wheel +await computer.interface.scroll_down(clicks) # Scroll down +await computer.interface.scroll_up(clicks) # Scroll up # Screen Actions await computer.interface.screenshot() # Take a screenshot @@ -178,10 +263,40 @@ await computer.interface.copy_to_clipboard() # Get clipboard content # File System Operations await computer.interface.file_exists(path) # Check if file exists await computer.interface.directory_exists(path) # Check if directory exists -await computer.interface.run_command(cmd) # Run shell command +await computer.interface.read_text(path) # Read file content +await computer.interface.write_text(path, content) # Write file content +await computer.interface.read_bytes(path) # Read file content as bytes +await computer.interface.write_bytes(path, content) # Write file content as bytes +await computer.interface.delete_file(path) # Delete file +await computer.interface.create_dir(path) # Create directory +await computer.interface.delete_dir(path) # Delete directory +await computer.interface.list_dir(path) # List directory contents # Accessibility await computer.interface.get_accessibility_tree() # Get accessibility tree + +# Python Virtual Environment Operations +await computer.venv_install("demo_venv", ["requests", "macos-pyxa"]) # Install packages in a virtual environment +await computer.venv_cmd("demo_venv", "python -c 'import requests; print(requests.get(`https://httpbin.org/ip`).json())'") # Run a shell command in a virtual environment +await computer.venv_exec("demo_venv", python_function_or_code, *args, **kwargs) # Run a Python function in a virtual environment and return the result / raise an exception + +# Example: Use sandboxed functions to execute code in a C/ua Container +from computer.helpers import sandboxed + +@sandboxed("demo_venv") +def greet_and_print(name): + """Get the HTML of the current Safari tab""" + import PyXA + safari = PyXA.Application("Safari") + html = safari.current_document.source() + print(f"Hello from inside the container, {name}!") + return {"greeted": name, "safari_html": html} + +# When a @sandboxed function is called, it will execute in the container +result = await greet_and_print("C/ua") +# Result: {"greeted": "C/ua", "safari_html": "..."} +# stdout and stderr are also captured and printed / raised +print("Result from sandboxed function:", result) ``` ## ComputerAgent Reference @@ -213,33 +328,6 @@ ComputerAgent( ) ``` -## Demos - -Check out these demos of the Computer-Use Agent in action: - -
-MCP Server: Work with Claude Desktop and Tableau -
-
- -
-
- -
-AI-Gradio: Multi-app workflow with browser, VS Code and terminal -
-
- -
-
- -
-Notebook: Fix GitHub issue in Cursor -
-
- -
-
## Community @@ -287,13 +375,7 @@ Thank you to all our supporters! Ricter Zheng
Ricter Zheng

💻 Rahul Karajgikar
Rahul Karajgikar

💻 trospix
trospix

💻 - Ikko Eltociear Ashimine
Ikko Eltociear Ashimine

💻 - 한석호(MilKyo)
한석호(MilKyo)

💻 - - - Rahim Nathwani
Rahim Nathwani

💻 - Matt Speck
Matt Speck

💻 - FinnBorge
FinnBorge

💻 + Evan smith
Evan smith

💻 diff --git a/docs/Developer-Guide.md b/docs/Developer-Guide.md index ad2d7f1c..31cf1c12 100644 --- a/docs/Developer-Guide.md +++ b/docs/Developer-Guide.md @@ -62,7 +62,7 @@ Refer to the [Lume README](../libs/lume/docs/Development.md) for instructions on ## Python Development -There are two ways to instal Lume: +There are two ways to install Lume: ### Run the build script @@ -91,7 +91,7 @@ To install with PDM, simply run: pdm install -G:all ``` -This installs all the dependencies for development, testing, and building the docs. If you'd oly like development dependencies, you can run: +This installs all the dependencies for development, testing, and building the docs. If you'd only like development dependencies, you can run: ```console pdm install -d @@ -122,7 +122,7 @@ As an alternative to installing directly on your host machine, you can use Docke ### Prerequisites - Docker installed on your machine -- Lume server running on your host (port 3000): `lume serve` +- Lume server running on your host (port 7777): `lume serve` ### Setup and Usage @@ -156,10 +156,10 @@ The Docker development environment: - Installs all required Python dependencies in the container - Mounts your source code from the host at runtime -- Automatically configures the connection to use host.docker.internal:3000 for accessing the Lume server on your host machine +- Automatically configures the connection to use host.docker.internal:7777 for accessing the Lume server on your host machine - Preserves your code changes without requiring rebuilds (source code is mounted as a volume) -> **Note**: The Docker container doesn't include the macOS-specific Lume executable. Instead, it connects to the Lume server running on your host machine via host.docker.internal:3000. Make sure to start the Lume server on your host before running examples in the container. +> **Note**: The Docker container doesn't include the macOS-specific Lume executable. Instead, it connects to the Lume server running on your host machine via host.docker.internal:7777. Make sure to start the Lume server on your host before running examples in the container. ## Cleanup and Reset @@ -200,11 +200,11 @@ The formatting configuration is defined in the root `pyproject.toml` file: ```toml [tool.black] line-length = 100 -target-version = ["py310"] +target-version = ["py311"] [tool.ruff] line-length = 100 -target-version = "py310" +target-version = "py311" select = ["E", "F", "B", "I"] fix = true @@ -213,7 +213,7 @@ docstring-code-format = true [tool.mypy] strict = true -python_version = "3.10" +python_version = "3.11" ignore_missing_imports = true disallow_untyped_defs = true check_untyped_defs = true @@ -225,7 +225,7 @@ warn_unused_ignores = false #### Key Formatting Rules - **Line Length**: Maximum of 100 characters -- **Python Version**: Code should be compatible with Python 3.10+ +- **Python Version**: Code should be compatible with Python 3.11+ - **Imports**: Automatically sorted (using Ruff's "I" rule) - **Type Hints**: Required for all function definitions (strict mypy mode) diff --git a/docs/FAQ.md b/docs/FAQ.md index 8b89c3fa..e6d77b70 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -105,13 +105,13 @@ This is typically due to known instability issues with the `lume serve` backgrou ### How do I troubleshoot Computer not connecting to lume daemon? -If you're experiencing connection issues between Computer and the lume daemon, it could be because the port 3000 (used by lume) is already in use by an orphaned process. You can diagnose this issue with: +If you're experiencing connection issues between Computer and the lume daemon, it could be because the port 7777 (used by lume) is already in use by an orphaned process. You can diagnose this issue with: ```bash -sudo lsof -i :3000 +sudo lsof -i :7777 ``` -This command will show all processes using port 3000. If you see a lume process already running, you can terminate it with: +This command will show all processes using port 7777. If you see a lume process already running, you can terminate it with: ```bash kill diff --git a/docs/Telemetry.md b/docs/Telemetry.md index 01731287..8f85e761 100644 --- a/docs/Telemetry.md +++ b/docs/Telemetry.md @@ -10,7 +10,7 @@ CUA libraries collect minimal anonymous usage data to help improve our software. - Basic system information: - Operating system (e.g., 'darwin', 'win32', 'linux') - - Python version (e.g., '3.10.0') + - Python version (e.g., '3.11.0') - Module initialization events: - When a module (like 'computer' or 'agent') is imported - Version of the module being used diff --git a/examples/agent_examples.py b/examples/agent_examples.py index 189ecddd..62573077 100644 --- a/examples/agent_examples.py +++ b/examples/agent_examples.py @@ -5,7 +5,7 @@ import traceback import signal -from computer import Computer +from computer import Computer, VMProviderType # Import the unified agent class and types from agent import ComputerAgent, LLMProvider, LLM, AgentLoop @@ -23,76 +23,88 @@ async def run_agent_example(): print("\n=== Example: ComputerAgent with OpenAI and Omni provider ===") try: + # Create a local macOS computer + computer = Computer( + os_type="macos", + verbosity=logging.DEBUG, + ) + + # Create a remote Linux computer with C/ua + # computer = Computer( + # os_type="linux", + # api_key=os.getenv("CUA_API_KEY"), + # name=os.getenv("CUA_CONTAINER_NAME"), + # provider_type=VMProviderType.CLOUD, + # ) + # Create Computer instance with async context manager - async with Computer(verbosity=logging.DEBUG) as macos_computer: - # Create agent with loop and provider - agent = ComputerAgent( - computer=macos_computer, - # loop=AgentLoop.OPENAI, - # loop=AgentLoop.ANTHROPIC, - # loop=AgentLoop.UITARS, - loop=AgentLoop.OMNI, - # model=LLM(provider=LLMProvider.OPENAI), # No model name for Operator CUA - # model=LLM(provider=LLMProvider.OPENAI, name="gpt-4o"), - # model=LLM(provider=LLMProvider.ANTHROPIC, name="claude-3-7-sonnet-20250219"), - # model=LLM(provider=LLMProvider.OLLAMA, name="gemma3:4b-it-q4_K_M"), - # model=LLM(provider=LLMProvider.MLXVLM, name="mlx-community/UI-TARS-1.5-7B-4bit"), - model=LLM( - provider=LLMProvider.OAICOMPAT, - name="gemma-3-12b-it", - provider_base_url="http://localhost:1234/v1", # LM Studio local endpoint - ), - save_trajectory=True, - only_n_most_recent_images=3, - verbosity=logging.DEBUG, - ) - - tasks = [ - "Look for a repository named trycua/cua on GitHub.", - "Check the open issues, open the most recent one and read it.", - "Clone the repository in users/lume/projects if it doesn't exist yet.", - "Open the repository with an app named Cursor (on the dock, black background and white cube icon).", - "From Cursor, open Composer if not already open.", - "Focus on the Composer text area, then write and submit a task to help resolve the GitHub issue.", - ] - - for i, task in enumerate(tasks): - print(f"\nExecuting task {i}/{len(tasks)}: {task}") - async for result in agent.run(task): - print("Response ID: ", result.get("id")) - - # Print detailed usage information - usage = result.get("usage") - if usage: - print("\nUsage Details:") - print(f" Input Tokens: {usage.get('input_tokens')}") - if "input_tokens_details" in usage: - print(f" Input Tokens Details: {usage.get('input_tokens_details')}") - print(f" Output Tokens: {usage.get('output_tokens')}") - if "output_tokens_details" in usage: - print(f" Output Tokens Details: {usage.get('output_tokens_details')}") - print(f" Total Tokens: {usage.get('total_tokens')}") - - print("Response Text: ", result.get("text")) - - # Print tools information - tools = result.get("tools") - if tools: - print("\nTools:") - print(tools) - - # Print reasoning and tool call outputs - outputs = result.get("output", []) - for output in outputs: - output_type = output.get("type") - if output_type == "reasoning": - print("\nReasoning Output:") - print(output) - elif output_type == "computer_call": - print("\nTool Call Output:") - print(output) - - print(f"\n✅ Task {i+1}/{len(tasks)} completed: {task}") + agent = ComputerAgent( + computer=computer, + loop=AgentLoop.OPENAI, + # loop=AgentLoop.ANTHROPIC, + # loop=AgentLoop.UITARS, + # loop=AgentLoop.OMNI, + model=LLM(provider=LLMProvider.OPENAI), # No model name for Operator CUA + # model=LLM(provider=LLMProvider.OPENAI, name="gpt-4o"), + # model=LLM(provider=LLMProvider.ANTHROPIC, name="claude-3-7-sonnet-20250219"), + # model=LLM(provider=LLMProvider.OLLAMA, name="gemma3:4b-it-q4_K_M"), + # model=LLM(provider=LLMProvider.MLXVLM, name="mlx-community/UI-TARS-1.5-7B-4bit"), + # model=LLM( + # provider=LLMProvider.OAICOMPAT, + # name="gemma-3-12b-it", + # provider_base_url="http://localhost:1234/v1", # LM Studio local endpoint + # ), + save_trajectory=True, + only_n_most_recent_images=3, + verbosity=logging.DEBUG, + ) + + tasks = [ + "Look for a repository named trycua/cua on GitHub.", + "Check the open issues, open the most recent one and read it.", + "Clone the repository in users/lume/projects if it doesn't exist yet.", + "Open the repository with an app named Cursor (on the dock, black background and white cube icon).", + "From Cursor, open Composer if not already open.", + "Focus on the Composer text area, then write and submit a task to help resolve the GitHub issue.", + ] + + for i, task in enumerate(tasks): + print(f"\nExecuting task {i}/{len(tasks)}: {task}") + async for result in agent.run(task): + print("Response ID: ", result.get("id")) + + # Print detailed usage information + usage = result.get("usage") + if usage: + print("\nUsage Details:") + print(f" Input Tokens: {usage.get('input_tokens')}") + if "input_tokens_details" in usage: + print(f" Input Tokens Details: {usage.get('input_tokens_details')}") + print(f" Output Tokens: {usage.get('output_tokens')}") + if "output_tokens_details" in usage: + print(f" Output Tokens Details: {usage.get('output_tokens_details')}") + print(f" Total Tokens: {usage.get('total_tokens')}") + + print("Response Text: ", result.get("text")) + + # Print tools information + tools = result.get("tools") + if tools: + print("\nTools:") + print(tools) + + # Print reasoning and tool call outputs + outputs = result.get("output", []) + for output in outputs: + output_type = output.get("type") + if output_type == "reasoning": + print("\nReasoning Output:") + print(output) + elif output_type == "computer_call": + print("\nTool Call Output:") + print(output) + + print(f"\n✅ Task {i+1}/{len(tasks)} completed: {task}") except Exception as e: logger.error(f"Error in run_agent_example: {e}") diff --git a/examples/agent_ui_examples.py b/examples/agent_ui_examples.py index 17130f5c..d5a37119 100644 --- a/examples/agent_ui_examples.py +++ b/examples/agent_ui_examples.py @@ -18,4 +18,8 @@ if __name__ == "__main__": print("Launching Computer-Use Agent Gradio UI with advanced features...") app = create_gradio_ui() - app.launch(share=False) + app.launch( + share=False, + server_name="0.0.0.0", + server_port=7860, + ) diff --git a/examples/computer_examples.py b/examples/computer_examples.py index d1d7d73e..227beb8c 100644 --- a/examples/computer_examples.py +++ b/examples/computer_examples.py @@ -2,7 +2,6 @@ import asyncio from pathlib import Path import sys -import json import traceback # Load environment variables from .env file @@ -17,31 +16,57 @@ pythonpath = os.environ.get("PYTHONPATH", "") for path in pythonpath.split(":"): if path and path not in sys.path: - sys.path.append(path) + sys.path.insert(0, path) # Insert at beginning to prioritize print(f"Added to sys.path: {path}") from computer.computer import Computer +from computer.providers.base import VMProviderType from computer.logger import LogLevel -from computer.utils import get_image_size - async def main(): try: print("\n=== Using direct initialization ===") - # Create computer with configured host + # Create a local macOS computer computer = Computer( - display="1024x768", # Higher resolution - memory="8GB", # More memory - cpu="4", # More CPU cores + display="1024x768", + memory="8GB", + cpu="4", os_type="macos", - verbosity=LogLevel.NORMAL, # Use QUIET to suppress most logs - use_host_computer_server=False, + name="macos", + verbosity=LogLevel.VERBOSE, + provider_type=VMProviderType.LUME, + storage="/Users//repos/trycua/computer/examples/storage", + shared_directories=[ + "/Users//repos/trycua/computer/examples/shared" + ], + ephemeral=False, ) + + # Create a remote Linux computer with C/ua + # computer = Computer( + # os_type="linux", + # api_key=os.getenv("CUA_API_KEY"), + # name=os.getenv("CONTAINER_NAME"), + # provider_type=VMProviderType.CLOUD, + # ) + try: + # Run the computer with default parameters await computer.run() - - await computer.interface.hotkey("command", "space") + + screenshot = await computer.interface.screenshot() + + # Create output directory if it doesn't exist + output_dir = Path("./output") + output_dir.mkdir(exist_ok=True) + + screenshot_path = output_dir / "screenshot.png" + with open(screenshot_path, "wb") as f: + f.write(screenshot) + print(f"Screenshot saved to: {screenshot_path.absolute()}") + + # await computer.interface.hotkey("command", "space") # res = await computer.interface.run_command("touch ./Downloads/empty_file") # print(f"Run command result: {res}") @@ -88,8 +113,7 @@ async def main(): finally: # Important to clean up resources - pass - # await computer.stop() + await computer.stop() except Exception as e: print(f"Error in main: {e}") traceback.print_exc() diff --git a/examples/computer_ui_examples.py b/examples/computer_ui_examples.py index 4116b41e..0c1d0974 100644 --- a/examples/computer_ui_examples.py +++ b/examples/computer_ui_examples.py @@ -18,7 +18,11 @@ if __name__ == "__main__": print("Launching Computer Interface Gradio UI with advanced features...") app = create_gradio_ui() - app.launch(share=False) + app.launch( + share=False, + server_name="0.0.0.0", + server_port=7860, + ) # Optional: Using the saved dataset # import datasets diff --git a/examples/evals/wikipedia_most_linked.txt b/examples/evals/wikipedia_most_linked.txt new file mode 100644 index 00000000..877909d2 --- /dev/null +++ b/examples/evals/wikipedia_most_linked.txt @@ -0,0 +1,1000 @@ +ISBN (identifier) +United States +Main Page +Tilde +Doi (identifier) +Fair use +Association football +Years +Wayback Machine +ISSN (identifier) +India +Wikimedia Foundation +Wikidata +Animal +Taxonomy (biology) +Australia +France +Eukaryote +IP address +U.S. state +Time zone +City +Copyright +Canada +Town +ASCII +Greek alphabet +Typographic ligature +Diacritical mark +Wikipedia +Germany +Human settlement +Open Tree of Life +IMDb (identifier) +United Kingdom +Catalogue of Life +Insect +Russia +Japan +Italy +Arthropod +Television show +Public domain +INaturalist +Poland +England +PMID (identifier) +Daylight saving time +S2CID (identifier) +China +Encyclopedia of Life +Spain +OCLC (identifier) +Plant +Flickr +Wikispecies +Africa +Song +Record label +Lepidoptera +Iran +English language +Music genre +News aggregator +Web feed +Proxy server +X-Forwarded-For +College football +World War II +Brazil +Sweden +Politics +Olympics +Netherlands +Record producer +California +New York City +Surname +The New York Times +London +New Zealand +PMC (identifier) +Logo +Synonym (taxonomy) +Switzerland +Turkey +Sport +Video game +Architecture +Norway +Bibcode (identifier) +Mexico +Botany +JSTOR (identifier) +Rail transport +Field hockey +Ireland +Scotland +Belgium +South Africa +Common name +Professional sports +Sport governing body +Sport industry +Olympic games +Election +Austria +Ukraine +Anthroponymy +Pakistan +Baseball +Denmark +Christianity +Philippines +Woman +Romania +Czech Republic +Album +Godzilla Minus One +Single (music) +Electoral reform +Nofollow +Basketball +New York (state) +Argentina +Finland +Soviet Union +Greece +Russian language +Historic site +Free content +YouTube +Catholic Church +Hungary +Kingdom Hearts +Beetle +Company +Tetris +Portugal +BioShock +Abandonware +Deus Ex (video game) +4A Engine +Yoshi's New Island +Kaboom! (video game) +Rain World +Juno (Overwatch) +Crash Team Rumble +Vault 101 +Tales of Commons +NHL Hockey +Clutch Gaming +Haseo +Allin Kempthorne +Ilyas El Maliki +Ratalaika Games +3D mousepad +HaptX +Walid Sultan Midani +Rustler (video game) +Look Outside +Ducks Ahoy! +Fusion Engine +Cricket +Geography +Chordate +The Guardian +Israel +Billboard (magazine) +Ice hockey +Given name +Chicago +World War I +Pennsylvania +Indonesia +Alma mater +Vascular plant +Amorphea +Wikimedia Commons +Novel +Village +Visual arts +Film poster +Flowering plant +Opisthokont +Obazoa +County seat +Short story +First-class cricket +Law +Europe +University +Croatia +Sport of athletics +Holozoa +Choanozoa +Filozoa +German language +Tennis +Eumetazoa +Serbia +ParaHoxozoa +Thailand +History +Midfielder +Bilateria +Unincorporated area +French language +AllMusic +Astronomy +Nephrozoa +Novella +Ship +Twitter +Character (arts) +College +Malaysia +Conflict of interest +Higher education +IUCN Red List +Rock music +Gastropoda +Creative Commons +Wales +Bulgaria +UTC+2 +Paris +Species +Illinois +HTML element +South Korea +BBC +Persian language +Moth +Conservation status +Pop music +Colombia +Wicket +American football +Jazz +World Flora Online +Los Angeles +Songwriter +Hong Kong +Hdl (identifier) +Genus +Spanish language +Egypt +Not out +Slovenia +Chile +Korea +Tropicos +Slovakia +Bishop +Family (biology) +Rugby union +Women's history +Nigeria +College basketball +Sports Reference +Washington, D.C. +GFDL +Afghanistan +Sri Lanka +Newspapers.com +UTC+1 +Eudicots +Estonia +Los Angeles Times +Olympedia +Bangladesh +Peru +Singapore +Typographical error +UTC +Virginia +Taiwan +Fast bowling +COVID-19 pandemic +Food +Fish +River +Republic of Ireland +Beer +Caribbean +Michigan +Drink +Chinese language +Business +Leg break +Women's Test cricket +Women's cricket +Innings +New Jersey +Protostome +Spin bowling +Sugar +Underarm bowling +Roger Federer +Googly +Apple +Comics +Cricket Australia XI +Fair and unfair play +Anime +Rafael Nadal +Leander Paes +Kazakhstan +Capital city +Blessed Virgin Mary +Venezuela +Case sensitivity +Arabic language +North America +Texas +Burger King +The Plant List +Justine Henin +Sushi +Angelus +Beef +Sanctification +Cuthbert Tunstall +Bread +Saint Mungo +Incumbent +Americanism (heresy) +Curry +Ensoulment +Associated Press +Adolph John Paschang +French cuisine +Altar Society +UTC-5 +Philadelphia +Bill Mallon +Yogurt +Soy sauce +Open Era (tennis) +Belarus +Manga +English Wikipedia +Islam +Trademark +ISO 4 +Wisconsin +Lithuania +The Washington Post +Agaricus bisporus +Reptile +Sociology +Organizations +Death +Ham and eggs +Asia +Swimming (sport) +South America +Northern Ireland +Observation.org +European Union +Astronomical object +Georgia (U.S. state) +Gmina +Provinces of Iran +Computing +Counties of Iran +Discogs +Mathematics +Powiat +Missouri +Bachelor of Arts +Iran Standard Time +Florida +Bakhsh +Minnesota +Oregon +Nepal +Variety (magazine) +Japanese language +Journalism +Rome +Computer +Ohio +Ontario +Internet Archive +Latvia +Comedy +Azerbaijan +BBC News +Morocco +Ecdysozoa +Print-on-demand +Bengali language +A5 paper +Pedia Press +Education +Mollusca +American Civil War +Berlin +Taxon +Maryland +Panarthropoda +Hebrew language +Toronto +Tactopoda +Episode +Cuba +Country music +Religion +Rotten Tomatoes +Georgia (country) +Classical music +Month +Puerto Rico +GEOnet Names Server +Sydney +The Times +Iraq +Polyphaga +Derivative work +Lisbon +Syria +Ecuador +Uzbekistan +Greek language +Latin +United Nations +Literature +Animation +Physics +Amphibian +Romanize +List of countries +Moscow +Politician +Philosophy +Metacritic +Mammal +Pinyin +Open access +New South Wales +Theatre +Allmusic +Syntax +Women in music +Fly +Colorado +Academic journal +LGBTQ +Seal (emblem) +Rolling Stone +Saudi Arabia +Science fiction +Tweet (social media) +Heavy metal music +Boston +Vietnam +Molecular biology +Facebook +Iceland +Albania +Cycling +Tennessee +Armenia +Massachusetts +Mandibulata +United States Navy +Communes of France +Census +Algeria +United States Army +Wikilink +Pancrustacea +Alternative rock +American English +Radio stations +History of Romania +Endemism +San Francisco +Award +Ghana +Judaism +Alabama +Blog +The Independent +Melbourne +Cantons of France +Lebanon +West Germany +Quotation mark +Regions of France +Chernivtsi Oblast +Tokyo +Italian language +Connecticut +Country +Screenshot +Ghost town +Iran Daylight Time +NatureServe +Mongolia +Cyprus +Northern Bukovina +Rugby league +Northern Bessarabia +State highway +Harvard University +Yorkshire +Pterygota +Slash (punctuation) +Prize +Science +Asian Games +Eastern Time Zone +Myanmar +Nazi Germany +Ottoman Empire +Quebec +Billboard Hot 100 +United Arab Emirates +Neoptera +Hexapoda +Least Concern +Type species +EPPO Code +Wikisource +Kyrgyzstan +Allotriocarida +Volleyball +Geology +Second World War +British Columbia +Socialism +Zoology +The Daily Telegraph +Paleontology +Vienna +Dicondylia +BugGuide +United States Senate +Hermit crab +Paraphrase +CNN +Royal Navy +Indian Standard Time +Billboard 200 +Kenya +DVD +Sipuncula +Tajikistan +National park +Economics +Heterocyathus +Uruguay +Heteropsammia +Road +Spanish name +Luxembourg +Korean language +UK Singles Chart +Queensland +Montreal +New York Times +Bolivia +CP/M +Timestamp +Electronic music +INSEE code +ArXiv (identifier) +PubMed +SVG +USA Today +Omnivore +Tunisia +Psychology +ESPN +UEFA +Hawaii +Gastropod +Aliyah +North Carolina +Russian Empire +Tibet +Fungi +Oklahoma +Fauna Europaea +Turkmenistan +British English +The London Gazette +Civil township +Boxing +Barack Obama +Animal Diversity Web +Reuters +Eumetabola +Voter turnout +Transport +False positive +Donald Trump +Kansas +Antarctica +Lake +Ethiopia +Time (magazine) +Marriage +NBC +Beijing +Vertebrate +Czechoslovakia +Protected area +Energy +Poetry +Archaeology +Columbia University +Poverty line +Alaska +Computing platform +British Empire +University of Oxford +Costa Rica +Dublin +A-side and B-side +ZIP code +Actinopterygii +UTC-6 +Photoperiodism +Mayor +Sphaeriidae +Animal suicide +Atka mackerel +Starling +Arizona +Entertainment Weekly +Sphaerium beckmani +Junqueira cow +Zaniolepis frenata +Campocraspedon +Zimbabwe +Motorsport +Bird flight +Cnemophilidae +Hinduism +Phalarope +Indiana +Museums +Holometabola +Pytilia +North Macedonia +Malta +Cathartiformes +Darter +Saker falcon +Cathartes +Avian malaria +Coal tit +Magpie duck +Video game developer +Bird bath +Vesper sparrow +Gouldian finch +Debeaking +Vector graphics +Semiplumbeous hawk +Scottish crossbill +Bullfinch +Fregata +Nidicolous +Plushcap +Pallid scops owl +Hip-hop +Blyth's frogmouth +Sunda scops owl +Argus (bird) +Operation Migration +Nik Borrow +Per capita income +Guy Oseary +Madrid +Buddhism +Drainage basin +Sephardic Haredim +Rami Kleinstein +Guy Bavli +David Bar-Hayim +Levin Kipnis +Edna Arbel +Prisoner of Zion +Ayala Procaccia +Nachum Heiman +Zman Tel Aviv +CBS +ARIA Charts +Cucujiformia +Away colours +Regex +2019 African Games +1962 Asian Games +1958 Asian Games +Chemistry +Olympic Games +The Middle Ages +Central Asia +Bengalis +Southeast Asia +Find a Grave +Microsoft Windows +Swing (politics) +White (U.S. Census) +Roman Catholic +Maine +The Times of India +Season (sports) +Jamaica +Video game genre +Munich +Asterids +Rosids +Golf +Language +Hangul +Atlanta +Glasgow +UTC+3 +Library of Congress +Deuterostome +COVID-19 +Video game publisher +Montenegro +ESPNcricinfo +Brand +UTC-4 +IGN +Stockholm +Istanbul +NASA +Gnathostomata +Ukrainian language +Human rights +Chicago Tribune +ProQuest +IMDb +River mouth +Hip hop music +Gene +Netflix +Moldova +Barcelona +Paraguay +Olfactores +Labour Party (UK) +United States dollar +Qatar +Photography +Guatemala +Summit +Cold War +Running +First World War +Precipitation +Edinburgh +Amsterdam +Lima +New Eskaton +Computer program +Xinjiang +Women in science +Manhattan +Warsaw +Magazine +Horror film +Deadline Hollywood +Jordan +Aparaglossata +Agriculture +Internet +Prague +The Hindu +Cretaceous +Latino (U.S. Census) +Vietnam War +Music download +Encyclopedia +Chemical compounds +Pittsburgh +Soap opera +Budapest +George W. Bush +Seattle +Extended play +Washington (state) +Listed building +Palestine +LCCN (identifier) +Portland, Oregon +Panama +Plagiarism +Brooklyn +Teleostomi +Manchester +Bird +Mollusk +Automobile +Historic England +Linguistics +Dependent territory +Athens +Civil engineering +Sea snail +Population density +Finance +Disaster management +Tanzania +Jurassic +Districts of Russia +Western Australia +Louisiana +Portuguese language +Anatomy +The Beatles +Tamil language +Milan +Uganda +Natural environment +FIFA +Cameroon +Blu-ray +Mexico City +Chemical formula +Jimmy Wales +Papua New Guinea +Diaphoretickes +UNESCO +Forbes +Technology +Buenos Aires +Vancouver +Dominican Republic +2007 +Species description +East Germany +Folk music +Kentucky +Multimedia +Monocotyledon +Rio de Janeiro +Automated +Hindi +Houston +Google +Devonian +Member of Parliament +Bible +Mumbai +FishBase +African diaspora +Carboniferous +Cambrian +Triassic +Montana +Handball +Ordovician +San Diego +Archive.today +Stanford University +British Army +Middle Ages +Frequency +Ultratop +Permian +Detroit +Earth +Precambrian +Hamburg +Alberta +Tamil Nadu +Madagascar +Lancashire +Guitar +Trade union +Instagram +Engineering +2006 +Silurian +NPR +Railway station +CAS Registry Number +Yemen +Noctuoidea +Fiji +Haiti +Rowing (sport) +New Orleans +NME +Alternative media +North Korea +Microsoft +Jerusalem +Paleogene +Audery Mill Creek +Horse racing +Post town +Piano +Bavaria +Polish language +Horror fiction +Neogene +Kerala +Copenhagen +Google Books +Central Time Zone +Island +Birmingham +Anglicanism +Software +Mountain range +Investment +Brussels +Muhammad Ali +Asian (U.S. Census) +Video game culture +Brisbane +Church of England +Kosovo +Bachelor of Science +Molar mass +Arachnid +Own goal +Yale University +Caenogastropoda +Auckland +World Athletics +Trinidad and Tobago +Hanyu Pinyin +Sound bite +Time +El Salvador +Microbiology +Columbia Records +Seoul +Cerambycidae +Maharashtra +Chelicerata +Fungus +Media influence +South Carolina +Radio +Telenovela +FA Cup +Senegal +Internet trolling +Nashville, Tennessee +Demonym +Standard Chinese +Sculpture +Liverpool +Thesis +Bass guitar +Chess +Women artists +Icon (computing) +PubChem +UK Albums Chart +Head coach +Roman Empire +Grand Slam (tennis) +JSmol +Formula One +Biology +Kent +Ancient Rome +Inner Carniola +Oslo +Dutch language +Wingspan +Archaeplastida +MTV +Edvard Ravnikar +ITunes +Feminism +German Empire +Pacific Ocean +Atlantic Ocean +Pharmacology +Track gauge +ChemSpider +Doctor of Philosophy +Regions of England +Districts of England +Christmas +Pavel Golia +Predjama Castle +Overtime (sports) +Forum +Swiss Hitparade +Stumped +Majority +Male +Shanghai +Siddharta (band) \ No newline at end of file diff --git a/examples/pylume_examples.py b/examples/pylume_examples.py index 31939686..37dead88 100644 --- a/examples/pylume_examples.py +++ b/examples/pylume_examples.py @@ -4,7 +4,7 @@ async def main(): """Example usage of PyLume.""" - async with PyLume(port=3000, use_existing_server=False, debug=True) as pylume: + async with PyLume(port=7777, use_existing_server=False, debug=True) as pylume: # Get latest IPSW URL print("\n=== Getting Latest IPSW URL ===") diff --git a/examples/sandboxed_functions_examples.py b/examples/sandboxed_functions_examples.py new file mode 100644 index 00000000..caa733b9 --- /dev/null +++ b/examples/sandboxed_functions_examples.py @@ -0,0 +1,54 @@ +from pathlib import Path +import os +import sys + +# Load environment variables from .env file +project_root = Path(__file__).parent.parent +env_file = project_root / ".env" +print(f"Loading environment from: {env_file}") +from dotenv import load_dotenv + +load_dotenv(env_file) + +# Add paths to sys.path if needed +pythonpath = os.environ.get("PYTHONPATH", "") +for path in pythonpath.split(":"): + if path and path not in sys.path: + sys.path.insert(0, path) # Insert at beginning to prioritize + print(f"Added to sys.path: {path}") + +import asyncio +from computer.computer import Computer +from computer.helpers import sandboxed + +async def main(): + # Initialize the computer in a C/ua Container + computer = Computer() + await computer.run() + + # Install a package in a virtual environment in the container + await computer.venv_install("demo_venv", ["requests", "macos-pyxa"]) + + # Open Safari + await computer.interface.run_command("open -a Safari") + await asyncio.sleep(2) + + # Define a sandboxed function + # This function will run inside the C/ua Container + @sandboxed("demo_venv") + def greet_and_print(name): + # get .html of the current Safari tab + import PyXA + safari = PyXA.Application("Safari") + current_doc = safari.current_document + html = current_doc.source() + print(f"Hello from inside the container, {name}!") + print("Safari HTML length:", len(html)) + return {"greeted": name, "safari_html_length": len(html), "safari_html_snippet": html[:200]} + + # Call with args and kwargs + result = await greet_and_print("C/ua") + print("Result from sandboxed function:", result) + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/libs/agent/agent/core/__init__.py b/libs/agent/agent/core/__init__.py index 7fa4aae1..7c013c80 100644 --- a/libs/agent/agent/core/__init__.py +++ b/libs/agent/agent/core/__init__.py @@ -2,7 +2,7 @@ from .factory import BaseLoop from .messages import ( - BaseMessageManager, + StandardMessageManager, ImageRetentionConfig, ) from .callbacks import ( @@ -18,7 +18,7 @@ "BaseLoop", "CallbackManager", "CallbackHandler", - "BaseMessageManager", + "StandardMessageManager", "ImageRetentionConfig", "BaseCallbackManager", "ContentCallback", diff --git a/libs/agent/agent/core/base.py b/libs/agent/agent/core/base.py index 702be207..fe0f07ad 100644 --- a/libs/agent/agent/core/base.py +++ b/libs/agent/agent/core/base.py @@ -5,7 +5,6 @@ from abc import ABC, abstractmethod from typing import Any, AsyncGenerator, Dict, List, Optional -from agent.providers.omni.parser import ParseResult from computer import Computer from .messages import StandardMessageManager, ImageRetentionConfig from .types import AgentResponse @@ -131,6 +130,15 @@ def run(self, messages: List[Dict[str, Any]]) -> AsyncGenerator[AgentResponse, N An async generator that yields agent responses """ raise NotImplementedError + + @abstractmethod + async def cancel(self) -> None: + """Cancel the currently running agent loop task. + + This method should stop any ongoing processing in the agent loop + and clean up resources appropriately. + """ + raise NotImplementedError ########################################### # EXPERIMENT AND TRAJECTORY MANAGEMENT @@ -198,7 +206,7 @@ def _save_screenshot(self, img_base64: str, action_type: str = "") -> None: # EVENT HOOKS / CALLBACKS ########################################### - async def handle_screenshot(self, screenshot_base64: str, action_type: str = "", parsed_screen: Optional[ParseResult] = None) -> None: + async def handle_screenshot(self, screenshot_base64: str, action_type: str = "", parsed_screen: Optional[dict] = None) -> None: """Process a screenshot through callback managers Args: diff --git a/libs/agent/agent/core/callbacks.py b/libs/agent/agent/core/callbacks.py index 59cd0e5a..0c5a719e 100644 --- a/libs/agent/agent/core/callbacks.py +++ b/libs/agent/agent/core/callbacks.py @@ -6,8 +6,6 @@ from datetime import datetime from typing import Any, Dict, List, Optional, Protocol -from agent.providers.omni.parser import ParseResult - logger = logging.getLogger(__name__) class ContentCallback(Protocol): @@ -117,7 +115,7 @@ async def on_error(self, error: Exception, **kwargs) -> None: for handler in self.handlers: await handler.on_error(error, **kwargs) - async def on_screenshot(self, screenshot_base64: str, action_type: str = "", parsed_screen: Optional[ParseResult] = None) -> None: + async def on_screenshot(self, screenshot_base64: str, action_type: str = "", parsed_screen: Optional[dict] = None) -> None: """Called when a screenshot is taken. Args: @@ -166,7 +164,7 @@ async def on_error(self, error: Exception, **kwargs) -> None: pass @abstractmethod - async def on_screenshot(self, screenshot_base64: str, action_type: str = "", parsed_screen: Optional[ParseResult] = None) -> None: + async def on_screenshot(self, screenshot_base64: str, action_type: str = "", parsed_screen: Optional[dict] = None) -> None: """Called when a screenshot is taken. Args: diff --git a/libs/agent/agent/core/messages.py b/libs/agent/agent/core/messages.py index 821ca82c..d2c70558 100644 --- a/libs/agent/agent/core/messages.py +++ b/libs/agent/agent/core/messages.py @@ -5,7 +5,6 @@ from typing import Any, Dict, List, Optional, Union, Tuple from dataclasses import dataclass import re -from ..providers.omni.parser import ParseResult logger = logging.getLogger(__name__) @@ -22,106 +21,6 @@ def should_retain_images(self) -> bool: """Check if image retention is enabled.""" return self.num_images_to_keep is not None and self.num_images_to_keep > 0 - -class BaseMessageManager: - """Base class for message preparation and management.""" - - def __init__(self, image_retention_config: Optional[ImageRetentionConfig] = None): - """Initialize the message manager. - - Args: - image_retention_config: Configuration for image retention - """ - self.image_retention_config = image_retention_config or ImageRetentionConfig() - if self.image_retention_config.min_removal_threshold < 1: - raise ValueError("min_removal_threshold must be at least 1") - - # Track provider for message formatting - self.provider = "openai" # Default provider - - def set_provider(self, provider: str) -> None: - """Set the current provider to format messages for. - - Args: - provider: Provider name (e.g., 'openai', 'anthropic') - """ - self.provider = provider.lower() - - def prepare_messages(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """Prepare messages by applying image retention and caching as configured. - - Args: - messages: List of messages to prepare - - Returns: - Prepared messages - """ - if self.image_retention_config.should_retain_images(): - self._filter_images(messages) - if self.image_retention_config.enable_caching: - self._inject_caching(messages) - return messages - - def _filter_images(self, messages: List[Dict[str, Any]]) -> None: - """Filter messages to retain only the specified number of most recent images. - - Args: - messages: Messages to filter - """ - # Find all tool result blocks that contain images - tool_results = [ - item - for message in messages - for item in (message["content"] if isinstance(message["content"], list) else []) - if isinstance(item, dict) and item.get("type") == "tool_result" - ] - - # Count total images - total_images = sum( - 1 - for result in tool_results - for content in result.get("content", []) - if isinstance(content, dict) and content.get("type") == "image" - ) - - # Calculate how many images to remove - images_to_remove = total_images - (self.image_retention_config.num_images_to_keep or 0) - images_to_remove -= images_to_remove % self.image_retention_config.min_removal_threshold - - # Remove oldest images first - for result in tool_results: - if isinstance(result.get("content"), list): - new_content = [] - for content in result["content"]: - if isinstance(content, dict) and content.get("type") == "image": - if images_to_remove > 0: - images_to_remove -= 1 - continue - new_content.append(content) - result["content"] = new_content - - def _inject_caching(self, messages: List[Dict[str, Any]]) -> None: - """Inject caching control for recent message turns. - - Args: - messages: Messages to inject caching into - """ - # Only apply cache_control for Anthropic API, not OpenAI - if self.provider != "anthropic": - return - - # Default to caching last 3 turns - turns_to_cache = 3 - for message in reversed(messages): - if message["role"] == "user" and isinstance(content := message["content"], list): - if turns_to_cache: - turns_to_cache -= 1 - content[-1]["cache_control"] = {"type": "ephemeral"} - else: - content[-1].pop("cache_control", None) - break - - class StandardMessageManager: """Manages messages in a standardized OpenAI format across different providers.""" @@ -160,6 +59,7 @@ def add_system_message(self, content: str) -> None: def get_messages(self) -> List[Dict[str, Any]]: """Get all messages in standard format. + This method applies image retention policy if configured. Returns: List of messages @@ -181,16 +81,27 @@ def _apply_image_retention(self, messages: List[Dict[str, Any]]) -> List[Dict[st if not self.config.num_images_to_keep: return messages - # Find user messages with images + # Find messages with images (both user messages and tool call outputs) image_messages = [] for msg in messages: + has_image = False + + # Check user messages with images if msg["role"] == "user" and isinstance(msg["content"], list): has_image = any( item.get("type") == "image_url" or item.get("type") == "image" for item in msg["content"] ) - if has_image: - image_messages.append(msg) + + # Check assistant messages with tool calls that have images + elif msg["role"] == "assistant" and isinstance(msg["content"], list): + for item in msg["content"]: + if item.get("type") == "tool_result" and "base64_image" in item: + has_image = True + break + + if has_image: + image_messages.append(msg) # If we don't have more images than the limit, return all messages if len(image_messages) <= self.config.num_images_to_keep: @@ -200,13 +111,35 @@ def _apply_image_retention(self, messages: List[Dict[str, Any]]) -> List[Dict[st images_to_keep = image_messages[-self.config.num_images_to_keep :] images_to_remove = image_messages[: -self.config.num_images_to_keep] - # Create a new message list without the older images + # Create a new message list, removing images from older messages result = [] for msg in messages: if msg in images_to_remove: - # Skip this message - continue - result.append(msg) + # Remove images from this message but keep the text content + if msg["role"] == "user" and isinstance(msg["content"], list): + # Keep only text content, remove images + new_content = [ + item for item in msg["content"] + if item.get("type") not in ["image_url", "image"] + ] + if new_content: # Only add if there's still content + result.append({"role": msg["role"], "content": new_content}) + elif msg["role"] == "assistant" and isinstance(msg["content"], list): + # Remove base64_image from tool_result items + new_content = [] + for item in msg["content"]: + if item.get("type") == "tool_result" and "base64_image" in item: + # Create a copy without the base64_image + new_item = {k: v for k, v in item.items() if k != "base64_image"} + new_content.append(new_item) + else: + new_content.append(item) + result.append({"role": msg["role"], "content": new_content}) + else: + # For other message types, keep as is + result.append(msg) + else: + result.append(msg) return result diff --git a/libs/agent/agent/core/tools.py b/libs/agent/agent/core/tools.py index 13b1f8de..0447eaee 100644 --- a/libs/agent/agent/core/tools.py +++ b/libs/agent/agent/core/tools.py @@ -1,10 +1,10 @@ """Tool-related type definitions.""" -from enum import Enum +from enum import StrEnum from typing import Dict, Any, Optional from pydantic import BaseModel, ConfigDict -class ToolInvocationState(str, Enum): +class ToolInvocationState(StrEnum): """States for tool invocation.""" CALL = 'call' PARTIAL_CALL = 'partial-call' diff --git a/libs/agent/agent/core/types.py b/libs/agent/agent/core/types.py index fd337062..8d84cd75 100644 --- a/libs/agent/agent/core/types.py +++ b/libs/agent/agent/core/types.py @@ -1,18 +1,18 @@ """Core type definitions.""" from typing import Any, Dict, List, Optional, TypedDict, Union -from enum import Enum, StrEnum, auto +from enum import StrEnum from dataclasses import dataclass -class AgentLoop(Enum): +class AgentLoop(StrEnum): """Enumeration of available loop types.""" - ANTHROPIC = auto() # Anthropic implementation - OMNI = auto() # OmniLoop implementation - OPENAI = auto() # OpenAI implementation - OLLAMA = auto() # OLLAMA implementation - UITARS = auto() # UI-TARS implementation + ANTHROPIC = "anthropic" # Anthropic implementation + OMNI = "omni" # OmniLoop implementation + OPENAI = "openai" # OpenAI implementation + OLLAMA = "ollama" # OLLAMA implementation + UITARS = "uitars" # UI-TARS implementation # Add more loop types as needed diff --git a/libs/agent/agent/providers/anthropic/loop.py b/libs/agent/agent/providers/anthropic/loop.py index 130a43cb..dfdfbc6c 100644 --- a/libs/agent/agent/providers/anthropic/loop.py +++ b/libs/agent/agent/providers/anthropic/loop.py @@ -101,6 +101,7 @@ def __init__( self.tool_manager = None self.callback_manager = None self.queue = asyncio.Queue() # Initialize queue + self.loop_task = None # Store the loop task for cancellation # Initialize handlers self.api_handler = AnthropicAPIHandler(self) @@ -169,7 +170,7 @@ async def run(self, messages: List[Dict[str, Any]]) -> AsyncGenerator[AgentRespo logger.info("Client initialized successfully") # Start loop in background task - loop_task = asyncio.create_task(self._run_loop(queue, messages)) + self.loop_task = asyncio.create_task(self._run_loop(queue, messages)) # Process and yield messages as they arrive while True: @@ -184,7 +185,7 @@ async def run(self, messages: List[Dict[str, Any]]) -> AsyncGenerator[AgentRespo continue # Wait for loop to complete - await loop_task + await self.loop_task # Send completion message yield { @@ -200,6 +201,31 @@ async def run(self, messages: List[Dict[str, Any]]) -> AsyncGenerator[AgentRespo "content": f"Error: {str(e)}", "metadata": {"title": "❌ Error"}, } + + async def cancel(self) -> None: + """Cancel the currently running agent loop task. + + This method stops the ongoing processing in the agent loop + by cancelling the loop_task if it exists and is running. + """ + if self.loop_task and not self.loop_task.done(): + logger.info("Cancelling Anthropic loop task") + self.loop_task.cancel() + try: + # Wait for the task to be cancelled with a timeout + await asyncio.wait_for(self.loop_task, timeout=2.0) + except asyncio.TimeoutError: + logger.warning("Timeout while waiting for loop task to cancel") + except asyncio.CancelledError: + logger.info("Loop task cancelled successfully") + except Exception as e: + logger.error(f"Error while cancelling loop task: {str(e)}") + finally: + # Put None in the queue to signal any waiting consumers to stop + await self.queue.put(None) + logger.info("Anthropic loop task cancelled") + else: + logger.info("No active Anthropic loop task to cancel") ########################################### # AGENT LOOP IMPLEMENTATION @@ -257,8 +283,12 @@ async def _run_loop(self, queue: asyncio.Queue, messages: List[Dict[str, Any]]) # Create new turn directory for this API call self._create_turn_dir() + + # Apply image retention policy + self.message_manager.messages = messages.copy() + prepared_messages = self.message_manager.get_messages() # Convert standard messages to Anthropic format using utility function - anthropic_messages, system_content = to_anthropic_format(messages.copy()) + anthropic_messages, system_content = to_anthropic_format(prepared_messages) # Use API handler to make API call with Anthropic format response = await self.api_handler.make_api_call( diff --git a/libs/agent/agent/providers/anthropic/prompts.py b/libs/agent/agent/providers/anthropic/prompts.py index 90e35e7a..2267dab3 100644 --- a/libs/agent/agent/providers/anthropic/prompts.py +++ b/libs/agent/agent/providers/anthropic/prompts.py @@ -3,6 +3,9 @@ from datetime import datetime import platform +today = datetime.today() +today = f"{today.strftime('%A, %B')} {today.day}, {today.year}" + SYSTEM_PROMPT = f""" * You are utilising a macOS virtual machine using ARM architecture with internet access and Safari as default browser. * You can feel free to install macOS applications with your bash tool. Use curl instead of wget. @@ -10,7 +13,7 @@ * When using your bash tool with commands that are expected to output very large quantities of text, redirect into a tmp file and use str_replace_editor or `grep -n -B -A ` to confirm output. * When viewing a page it can be helpful to zoom out so that you can see everything on the page. Either that, or make sure you scroll down to see everything before deciding something isn't available. * When using your computer function calls, they take a while to run and send back to you. Where possible/feasible, try to chain multiple of these calls all into one function calls request. -* The current date is {datetime.today().strftime('%A, %B %-d, %Y')}. +* The current date is {today}. diff --git a/libs/agent/agent/providers/anthropic/tools/computer.py b/libs/agent/agent/providers/anthropic/tools/computer.py index ecf232bd..dd1dc281 100644 --- a/libs/agent/agent/providers/anthropic/tools/computer.py +++ b/libs/agent/agent/providers/anthropic/tools/computer.py @@ -205,26 +205,6 @@ async def __call__( self.logger.info(f" Coordinates: ({x}, {y})") try: - # Take pre-action screenshot to get current dimensions - pre_screenshot = await self.computer.interface.screenshot() - pre_img = Image.open(io.BytesIO(pre_screenshot)) - - # Scale image to match screen dimensions if needed - if pre_img.size != (self.width, self.height): - self.logger.info( - f"Scaling image from {pre_img.size} to {self.width}x{self.height} to match screen dimensions" - ) - if not isinstance(self.width, int) or not isinstance(self.height, int): - raise ToolError("Screen dimensions must be integers") - size = (int(self.width), int(self.height)) - pre_img = pre_img.resize(size, Image.Resampling.LANCZOS) - # Save the scaled image back to bytes - buffer = io.BytesIO() - pre_img.save(buffer, format="PNG") - pre_screenshot = buffer.getvalue() - - self.logger.info(f" Current dimensions: {pre_img.width}x{pre_img.height}") - # Perform the click action if action == "left_click": self.logger.info(f"Clicking at ({x}, {y})") @@ -242,45 +222,14 @@ async def __call__( # Wait briefly for any UI changes await asyncio.sleep(0.5) - # Take and save post-action screenshot - post_screenshot = await self.computer.interface.screenshot() - post_img = Image.open(io.BytesIO(post_screenshot)) - - # Scale post-action image if needed - if post_img.size != (self.width, self.height): - self.logger.info( - f"Scaling post-action image from {post_img.size} to {self.width}x{self.height}" - ) - post_img = post_img.resize( - (self.width, self.height), Image.Resampling.LANCZOS - ) - buffer = io.BytesIO() - post_img.save(buffer, format="PNG") - post_screenshot = buffer.getvalue() - return ToolResult( output=f"Performed {action} at ({x}, {y})", - base64_image=base64.b64encode(post_screenshot).decode(), ) except Exception as e: self.logger.error(f"Error during {action} action: {str(e)}") raise ToolError(f"Failed to perform {action}: {str(e)}") else: try: - # Take pre-action screenshot - pre_screenshot = await self.computer.interface.screenshot() - pre_img = Image.open(io.BytesIO(pre_screenshot)) - - # Scale image if needed - if pre_img.size != (self.width, self.height): - self.logger.info( - f"Scaling image from {pre_img.size} to {self.width}x{self.height}" - ) - if not isinstance(self.width, int) or not isinstance(self.height, int): - raise ToolError("Screen dimensions must be integers") - size = (int(self.width), int(self.height)) - pre_img = pre_img.resize(size, Image.Resampling.LANCZOS) - # Perform the click action if action == "left_click": self.logger.info("Performing left click at current position") @@ -295,25 +244,8 @@ async def __call__( # Wait briefly for any UI changes await asyncio.sleep(0.5) - # Take post-action screenshot - post_screenshot = await self.computer.interface.screenshot() - post_img = Image.open(io.BytesIO(post_screenshot)) - - # Scale post-action image if needed - if post_img.size != (self.width, self.height): - self.logger.info( - f"Scaling post-action image from {post_img.size} to {self.width}x{self.height}" - ) - post_img = post_img.resize( - (self.width, self.height), Image.Resampling.LANCZOS - ) - buffer = io.BytesIO() - post_img.save(buffer, format="PNG") - post_screenshot = buffer.getvalue() - return ToolResult( output=f"Performed {action} at current position", - base64_image=base64.b64encode(post_screenshot).decode(), ) except Exception as e: self.logger.error(f"Error during {action} action: {str(e)}") @@ -328,20 +260,6 @@ async def __call__( raise ToolError(f"{text} must be a string") try: - # Take pre-action screenshot - pre_screenshot = await self.computer.interface.screenshot() - pre_img = Image.open(io.BytesIO(pre_screenshot)) - - # Scale image if needed - if pre_img.size != (self.width, self.height): - self.logger.info( - f"Scaling image from {pre_img.size} to {self.width}x{self.height}" - ) - if not isinstance(self.width, int) or not isinstance(self.height, int): - raise ToolError("Screen dimensions must be integers") - size = (int(self.width), int(self.height)) - pre_img = pre_img.resize(size, Image.Resampling.LANCZOS) - if action == "key": # Special handling for page up/down on macOS if text.lower() in ["pagedown", "page_down", "page down"]: @@ -378,25 +296,8 @@ async def __call__( # Wait briefly for UI changes await asyncio.sleep(0.5) - # Take post-action screenshot - post_screenshot = await self.computer.interface.screenshot() - post_img = Image.open(io.BytesIO(post_screenshot)) - - # Scale post-action image if needed - if post_img.size != (self.width, self.height): - self.logger.info( - f"Scaling post-action image from {post_img.size} to {self.width}x{self.height}" - ) - post_img = post_img.resize( - (self.width, self.height), Image.Resampling.LANCZOS - ) - buffer = io.BytesIO() - post_img.save(buffer, format="PNG") - post_screenshot = buffer.getvalue() - return ToolResult( output=f"Pressed key: {output_text}", - base64_image=base64.b64encode(post_screenshot).decode(), ) elif action == "type": @@ -406,66 +307,13 @@ async def __call__( # Wait briefly for UI changes await asyncio.sleep(0.5) - # Take post-action screenshot - post_screenshot = await self.computer.interface.screenshot() - post_img = Image.open(io.BytesIO(post_screenshot)) - - # Scale post-action image if needed - if post_img.size != (self.width, self.height): - self.logger.info( - f"Scaling post-action image from {post_img.size} to {self.width}x{self.height}" - ) - post_img = post_img.resize( - (self.width, self.height), Image.Resampling.LANCZOS - ) - buffer = io.BytesIO() - post_img.save(buffer, format="PNG") - post_screenshot = buffer.getvalue() - return ToolResult( output=f"Typed text: {text}", - base64_image=base64.b64encode(post_screenshot).decode(), ) except Exception as e: self.logger.error(f"Error during {action} action: {str(e)}") raise ToolError(f"Failed to perform {action}: {str(e)}") - elif action in ("screenshot", "cursor_position"): - if text is not None: - raise ToolError(f"text is not accepted for {action}") - if coordinate is not None: - raise ToolError(f"coordinate is not accepted for {action}") - - try: - if action == "screenshot": - # Take screenshot - screenshot = await self.computer.interface.screenshot() - img = Image.open(io.BytesIO(screenshot)) - - # Scale image if needed - if img.size != (self.width, self.height): - self.logger.info( - f"Scaling image from {img.size} to {self.width}x{self.height}" - ) - if not isinstance(self.width, int) or not isinstance(self.height, int): - raise ToolError("Screen dimensions must be integers") - size = (int(self.width), int(self.height)) - img = img.resize(size, Image.Resampling.LANCZOS) - buffer = io.BytesIO() - img.save(buffer, format="PNG") - screenshot = buffer.getvalue() - - return ToolResult(base64_image=base64.b64encode(screenshot).decode()) - - elif action == "cursor_position": - pos = await self.computer.interface.get_cursor_position() - x, y = pos # Unpack the tuple - return ToolResult(output=f"X={int(x)},Y={int(y)}") - - except Exception as e: - self.logger.error(f"Error during {action} action: {str(e)}") - raise ToolError(f"Failed to perform {action}: {str(e)}") - elif action == "scroll": # Implement scroll action direction = kwargs.get("direction", "down") @@ -478,43 +326,29 @@ async def __call__( if direction == "down": # Scroll down (Page Down on macOS) self.logger.info(f"Scrolling down, amount: {amount}") - # Use fn+down for page down on macOS - for _ in range(amount): - await self.computer.interface.hotkey("fn", "down") - await asyncio.sleep(0.1) + await self.computer.interface.scroll_down(amount) else: # Scroll up (Page Up on macOS) self.logger.info(f"Scrolling up, amount: {amount}") - # Use fn+up for page up on macOS - for _ in range(amount): - await self.computer.interface.hotkey("fn", "up") - await asyncio.sleep(0.1) + await self.computer.interface.scroll_up(amount) # Wait briefly for UI changes await asyncio.sleep(0.5) - # Take post-action screenshot - post_screenshot = await self.computer.interface.screenshot() - post_img = Image.open(io.BytesIO(post_screenshot)) - - # Scale post-action image if needed - if post_img.size != (self.width, self.height): - self.logger.info( - f"Scaling post-action image from {post_img.size} to {self.width}x{self.height}" - ) - post_img = post_img.resize((self.width, self.height), Image.Resampling.LANCZOS) - buffer = io.BytesIO() - post_img.save(buffer, format="PNG") - post_screenshot = buffer.getvalue() - return ToolResult( output=f"Scrolled {direction} by {amount} steps", - base64_image=base64.b64encode(post_screenshot).decode(), ) except Exception as e: self.logger.error(f"Error during scroll action: {str(e)}") raise ToolError(f"Failed to perform scroll: {str(e)}") + elif action == "screenshot": + # Take screenshot + return await self.screenshot() + elif action == "cursor_position": + pos = await self.computer.interface.get_cursor_position() + x, y = pos # Unpack the tuple + return ToolResult(output=f"X={int(x)},Y={int(y)}") raise ToolError(f"Invalid action: {action}") async def screenshot(self): diff --git a/libs/agent/agent/providers/anthropic/utils.py b/libs/agent/agent/providers/anthropic/utils.py index c0afcd0f..f57ea829 100644 --- a/libs/agent/agent/providers/anthropic/utils.py +++ b/libs/agent/agent/providers/anthropic/utils.py @@ -4,7 +4,6 @@ import re from typing import Any, Dict, List, Optional, Tuple, cast from anthropic.types.beta import BetaMessage -from ..omni.parser import ParseResult from ...core.types import AgentResponse from datetime import datetime @@ -188,7 +187,7 @@ def from_anthropic_format(messages: List[Dict[str, Any]]) -> List[Dict[str, Any] async def to_agent_response_format( response: BetaMessage, messages: List[Dict[str, Any]], - parsed_screen: Optional[ParseResult] = None, + parsed_screen: Optional[dict] = None, parser: Optional[Any] = None, model: Optional[str] = None, ) -> AgentResponse: diff --git a/libs/agent/agent/providers/omni/loop.py b/libs/agent/agent/providers/omni/loop.py index 18e0375f..751d4fd3 100644 --- a/libs/agent/agent/providers/omni/loop.py +++ b/libs/agent/agent/providers/omni/loop.py @@ -105,6 +105,7 @@ def __init__( # Set API client attributes self.client = None self.retry_count = 0 + self.loop_task = None # Store the loop task for cancellation # Initialize handlers self.api_handler = OmniAPIHandler(loop=self) @@ -580,10 +581,55 @@ async def run(self, messages: List[Dict[str, Any]]) -> AsyncGenerator[AgentRespo Yields: Agent response format """ - # Initialize the message manager with the provided messages - self.message_manager.messages = messages.copy() - logger.info(f"Starting OmniLoop run with {len(self.message_manager.messages)} messages") + try: + logger.info(f"Starting OmniLoop run with {len(messages)} messages") + + # Initialize the message manager with the provided messages + self.message_manager.messages = messages.copy() + + # Create queue for response streaming + queue = asyncio.Queue() + + # Start loop in background task + self.loop_task = asyncio.create_task(self._run_loop(queue, messages)) + + # Process and yield messages as they arrive + while True: + try: + item = await queue.get() + if item is None: # Stop signal + break + yield item + queue.task_done() + except Exception as e: + logger.error(f"Error processing queue item: {str(e)}") + continue + + # Wait for loop to complete + await self.loop_task + # Send completion message + yield { + "role": "assistant", + "content": "Task completed successfully.", + "metadata": {"title": "✅ Complete"}, + } + + except Exception as e: + logger.error(f"Error in run method: {str(e)}") + yield { + "role": "assistant", + "content": f"Error: {str(e)}", + "metadata": {"title": "❌ Error"}, + } + + async def _run_loop(self, queue: asyncio.Queue, messages: List[Dict[str, Any]]) -> None: + """Internal method to run the agent loop with provided messages. + + Args: + queue: Queue to put responses into + messages: List of messages in standard OpenAI format + """ # Continue running until explicitly told to stop running = True turn_created = False @@ -673,8 +719,8 @@ async def run(self, messages: List[Dict[str, Any]]) -> AsyncGenerator[AgentRespo # Log standardized response for ease of parsing self._log_api_call("agent_response", request=None, response=openai_compatible_response) - # Yield the response to the caller - yield openai_compatible_response + # Put the response in the queue + await queue.put(openai_compatible_response) # Check if we should continue this conversation running = should_continue @@ -688,20 +734,47 @@ async def run(self, messages: List[Dict[str, Any]]) -> AsyncGenerator[AgentRespo except Exception as e: attempt += 1 - error_msg = f"Error in run method (attempt {attempt}/{max_attempts}): {str(e)}" + error_msg = f"Error in _run_loop method (attempt {attempt}/{max_attempts}): {str(e)}" logger.error(error_msg) # If this is our last attempt, provide more info about the error if attempt >= max_attempts: logger.error(f"Maximum retry attempts reached. Last error was: {str(e)}") - yield { - "error": str(e), + await queue.put({ + "role": "assistant", + "content": f"Error: {str(e)}", "metadata": {"title": "❌ Error"}, - } + }) # Create a brief delay before retrying await asyncio.sleep(1) + finally: + # Signal that we're done + await queue.put(None) + + async def cancel(self) -> None: + """Cancel the currently running agent loop task. + + This method stops the ongoing processing in the agent loop + by cancelling the loop_task if it exists and is running. + """ + if self.loop_task and not self.loop_task.done(): + logger.info("Cancelling Omni loop task") + self.loop_task.cancel() + try: + # Wait for the task to be cancelled with a timeout + await asyncio.wait_for(self.loop_task, timeout=2.0) + except asyncio.TimeoutError: + logger.warning("Timeout while waiting for loop task to cancel") + except asyncio.CancelledError: + logger.info("Loop task cancelled successfully") + except Exception as e: + logger.error(f"Error while cancelling loop task: {str(e)}") + finally: + logger.info("Omni loop task cancelled") + else: + logger.info("No active Omni loop task to cancel") async def process_model_response(self, response_text: str) -> Optional[Dict[str, Any]]: """Process model response to extract tool calls. diff --git a/libs/agent/agent/providers/openai/loop.py b/libs/agent/agent/providers/openai/loop.py index baff6ff0..4095cdc0 100644 --- a/libs/agent/agent/providers/openai/loop.py +++ b/libs/agent/agent/providers/openai/loop.py @@ -87,6 +87,7 @@ def __init__( self.acknowledge_safety_check_callback = acknowledge_safety_check_callback self.queue = asyncio.Queue() # Initialize queue self.last_response_id = None # Store the last response ID across runs + self.loop_task = None # Store the loop task for cancellation # Initialize handlers self.api_handler = OpenAIAPIHandler(self) @@ -132,28 +133,28 @@ async def run(self, messages: List[Dict[str, Any]]) -> AsyncGenerator[AgentRespo logger.info("Starting OpenAI loop run") # Create queue for response streaming - queue = asyncio.Queue() + self.queue = asyncio.Queue() # Ensure tool manager is initialized await self.tool_manager.initialize() # Start loop in background task - loop_task = asyncio.create_task(self._run_loop(queue, messages)) + self.loop_task = asyncio.create_task(self._run_loop(self.queue, messages)) # Process and yield messages as they arrive while True: try: - item = await queue.get() + item = await self.queue.get() if item is None: # Stop signal break yield item - queue.task_done() + self.queue.task_done() except Exception as e: logger.error(f"Error processing queue item: {str(e)}") continue # Wait for loop to complete - await loop_task + await self.loop_task # Send completion message yield { @@ -169,6 +170,31 @@ async def run(self, messages: List[Dict[str, Any]]) -> AsyncGenerator[AgentRespo "content": f"Error: {str(e)}", "metadata": {"title": "❌ Error"}, } + + async def cancel(self) -> None: + """Cancel the currently running agent loop task. + + This method stops the ongoing processing in the agent loop + by cancelling the loop_task if it exists and is running. + """ + if self.loop_task and not self.loop_task.done(): + logger.info("Cancelling OpenAI loop task") + self.loop_task.cancel() + try: + # Wait for the task to be cancelled with a timeout + await asyncio.wait_for(self.loop_task, timeout=2.0) + except asyncio.TimeoutError: + logger.warning("Timeout while waiting for loop task to cancel") + except asyncio.CancelledError: + logger.info("Loop task cancelled successfully") + except Exception as e: + logger.error(f"Error while cancelling loop task: {str(e)}") + finally: + # Put None in the queue to signal any waiting consumers to stop + await self.queue.put(None) + logger.info("OpenAI loop task cancelled") + else: + logger.info("No active OpenAI loop task to cancel") ########################################### # AGENT LOOP IMPLEMENTATION @@ -250,7 +276,7 @@ async def _run_loop(self, queue: asyncio.Queue, messages: List[Dict[str, Any]]) # Call API screen_size = await self.computer.interface.get_screen_size() response = await self.api_handler.send_initial_request( - messages=messages, + messages=self.message_manager.get_messages(), # Apply image retention policy display_width=str(screen_size["width"]), display_height=str(screen_size["height"]), previous_response_id=self.last_response_id, @@ -371,7 +397,7 @@ async def _run_loop(self, queue: asyncio.Queue, messages: List[Dict[str, Any]]) # The API handler will extract this from the message history if isinstance(self.last_response_id, str): response = await self.api_handler.send_computer_call_request( - messages=self.message_manager.messages, + messages=self.message_manager.get_messages(), # Apply image retention policy display_width=str(screen_size["width"]), display_height=str(screen_size["height"]), previous_response_id=self.last_response_id, # Use instance variable diff --git a/libs/agent/agent/providers/openai/tools/computer.py b/libs/agent/agent/providers/openai/tools/computer.py index c5602f4e..5575c792 100644 --- a/libs/agent/agent/providers/openai/tools/computer.py +++ b/libs/agent/agent/providers/openai/tools/computer.py @@ -61,9 +61,6 @@ class ComputerTool(BaseComputerTool, BaseOpenAITool): computer: Computer # The CUA Computer instance logger = logging.getLogger(__name__) - _screenshot_delay = 1.0 # macOS is generally faster than X11 - _scaling_enabled = True - def __init__(self, computer: Computer): """Initialize the computer tool. @@ -185,26 +182,23 @@ async def __call__( raise ToolError(f"Failed to execute {type}: {str(e)}") async def handle_click(self, button: str, x: int, y: int) -> ToolResult: - """Handle different click actions.""" + """Handle mouse clicks.""" try: - # Perform requested click action + # Perform the click based on button type if button == "left": await self.computer.interface.left_click(x, y) elif button == "right": await self.computer.interface.right_click(x, y) elif button == "double": await self.computer.interface.double_click(x, y) + else: + raise ToolError(f"Unsupported button type: {button}") - # Wait for UI to update - await asyncio.sleep(0.5) - - # Take screenshot after action - screenshot = await self.computer.interface.screenshot() - base64_screenshot = base64.b64encode(screenshot).decode("utf-8") + # Wait briefly for UI to update + await asyncio.sleep(0.3) return ToolResult( output=f"Performed {button} click at ({x}, {y})", - base64_image=base64_screenshot, ) except Exception as e: self.logger.error(f"Error in handle_click: {str(e)}") @@ -218,11 +212,7 @@ async def handle_typing(self, text: str) -> ToolResult: await asyncio.sleep(0.3) - # Take screenshot after typing - screenshot = await self.computer.interface.screenshot() - base64_screenshot = base64.b64encode(screenshot).decode("utf-8") - - return ToolResult(output=f"Typed: {text}", base64_image=base64_screenshot) + return ToolResult(output=f"Typed: {text}") except Exception as e: self.logger.error(f"Error in handle_typing: {str(e)}") raise ToolError(f"Failed to type '{text}': {str(e)}") @@ -254,11 +244,7 @@ async def handle_key(self, key: Union[str, List[str]]) -> ToolResult: # Wait briefly await asyncio.sleep(0.3) - # Take screenshot after action - screenshot = await self.computer.interface.screenshot() - base64_screenshot = base64.b64encode(screenshot).decode("utf-8") - - return ToolResult(output=f"Pressed key: {key}", base64_image=base64_screenshot) + return ToolResult(output=f"Pressed key: {key}") except Exception as e: self.logger.error(f"Error in handle_key: {str(e)}") raise ToolError(f"Failed to press key '{key}': {str(e)}") @@ -272,11 +258,7 @@ async def handle_mouse_move(self, x: int, y: int) -> ToolResult: # Wait briefly await asyncio.sleep(0.2) - # Take screenshot after action - screenshot = await self.computer.interface.screenshot() - base64_screenshot = base64.b64encode(screenshot).decode("utf-8") - - return ToolResult(output=f"Moved cursor to ({x}, {y})", base64_image=base64_screenshot) + return ToolResult(output=f"Moved cursor to ({x}, {y})") except Exception as e: self.logger.error(f"Error in handle_mouse_move: {str(e)}") raise ToolError(f"Failed to move cursor to ({x}, {y}): {str(e)}") @@ -296,14 +278,7 @@ async def handle_scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> T # Wait for UI to update await asyncio.sleep(0.5) - # Take screenshot after action - screenshot = await self.computer.interface.screenshot() - base64_screenshot = base64.b64encode(screenshot).decode("utf-8") - - return ToolResult( - output=f"Scrolled at ({x}, {y}) with delta ({scroll_x}, {scroll_y})", - base64_image=base64_screenshot, - ) + return ToolResult(output=f"Scrolled at ({x}, {y}) by ({scroll_x}, {scroll_y})") except Exception as e: self.logger.error(f"Error in handle_scroll: {str(e)}") raise ToolError(f"Failed to scroll at ({x}, {y}): {str(e)}") @@ -331,13 +306,8 @@ async def handle_drag(self, path: List[Dict[str, int]]) -> ToolResult: # Wait for UI to update await asyncio.sleep(0.5) - # Take screenshot after action - screenshot = await self.computer.interface.screenshot() - base64_screenshot = base64.b64encode(screenshot).decode("utf-8") - return ToolResult( output=f"Dragged from ({path[0]['x']}, {path[0]['y']}) to ({path[-1]['x']}, {path[-1]['y']})", - base64_image=base64_screenshot, ) except Exception as e: self.logger.error(f"Error in handle_drag: {str(e)}") diff --git a/libs/agent/agent/providers/uitars/clients/oaicompat.py b/libs/agent/agent/providers/uitars/clients/oaicompat.py index 423b1d3a..a3778be6 100644 --- a/libs/agent/agent/providers/uitars/clients/oaicompat.py +++ b/libs/agent/agent/providers/uitars/clients/oaicompat.py @@ -6,6 +6,7 @@ import aiohttp import re from .base import BaseUITarsClient +import asyncio logger = logging.getLogger(__name__) @@ -144,7 +145,7 @@ async def run_interleaved( else: message = {"role": "user", "content": [{"type": "text", "text": item}]} final_messages.append(message) - + payload = { "model": self.model, "messages": final_messages, @@ -192,7 +193,8 @@ async def run_interleaved( # if 503, then the endpoint is still warming up if response.status == 503: - logger.error(f"Endpoint is still warming up, please try again later") + logger.error(f"Endpoint is still warming up, trying again in 30 seconds...") + await asyncio.sleep(30) raise Exception(f"Endpoint is still warming up: {response_text}") # Try to parse as JSON if the content type is appropriate diff --git a/libs/agent/agent/providers/uitars/loop.py b/libs/agent/agent/providers/uitars/loop.py index 848e3504..133a3b83 100644 --- a/libs/agent/agent/providers/uitars/loop.py +++ b/libs/agent/agent/providers/uitars/loop.py @@ -93,6 +93,7 @@ def __init__( # Set API client attributes self.client = None self.retry_count = 0 + self.loop_task = None # Store the loop task for cancellation # Initialize visualization helper self.viz_helper = VisualizationHelper(agent=self) @@ -462,10 +463,55 @@ async def run(self, messages: List[Dict[str, Any]]) -> AsyncGenerator[AgentRespo Yields: Agent response format """ - # Initialize the message manager with the provided messages - self.message_manager.messages = messages.copy() - logger.info(f"Starting UITARSLoop run with {len(self.message_manager.messages)} messages") + try: + logger.info(f"Starting UITARSLoop run with {len(messages)} messages") + + # Initialize the message manager with the provided messages + self.message_manager.messages = messages.copy() + + # Create queue for response streaming + queue = asyncio.Queue() + + # Start loop in background task + self.loop_task = asyncio.create_task(self._run_loop(queue, messages)) + + # Process and yield messages as they arrive + while True: + try: + item = await queue.get() + if item is None: # Stop signal + break + yield item + queue.task_done() + except Exception as e: + logger.error(f"Error processing queue item: {str(e)}") + continue + + # Wait for loop to complete + await self.loop_task + # Send completion message + yield { + "role": "assistant", + "content": "Task completed successfully.", + "metadata": {"title": "✅ Complete"}, + } + + except Exception as e: + logger.error(f"Error in run method: {str(e)}") + yield { + "role": "assistant", + "content": f"Error: {str(e)}", + "metadata": {"title": "❌ Error"}, + } + + async def _run_loop(self, queue: asyncio.Queue, messages: List[Dict[str, Any]]) -> None: + """Internal method to run the agent loop with provided messages. + + Args: + queue: Queue to put responses into + messages: List of messages in standard OpenAI format + """ # Continue running until explicitly told to stop running = True turn_created = False @@ -475,88 +521,117 @@ async def run(self, messages: List[Dict[str, Any]]) -> AsyncGenerator[AgentRespo attempt = 0 max_attempts = 3 - while running and attempt < max_attempts: - try: - # Create a new turn directory if it's not already created - if not turn_created: - self._create_turn_dir() - turn_created = True + try: + while running and attempt < max_attempts: + try: + # Create a new turn directory if it's not already created + if not turn_created: + self._create_turn_dir() + turn_created = True - # Ensure client is initialized - if self.client is None: - logger.info("Initializing client...") - await self.initialize_client() + # Ensure client is initialized if self.client is None: - raise RuntimeError("Failed to initialize client") - logger.info("Client initialized successfully") - - # Get current screen - base64_screenshot = await self._get_current_screen() - - # Add screenshot to message history - self.message_manager.add_user_message( - [ - { - "type": "image_url", - "image_url": {"url": f"data:image/png;base64,{base64_screenshot}"}, - } - ] - ) - logger.info("Added screenshot to message history") + logger.info("Initializing client...") + await self.initialize_client() + if self.client is None: + raise RuntimeError("Failed to initialize client") + logger.info("Client initialized successfully") + + # Get current screen + base64_screenshot = await self._get_current_screen() + + # Add screenshot to message history + self.message_manager.add_user_message( + [ + { + "type": "image_url", + "image_url": {"url": f"data:image/png;base64,{base64_screenshot}"}, + } + ] + ) + logger.info("Added screenshot to message history") - # Get system prompt - system_prompt = self._get_system_prompt() + # Get system prompt + system_prompt = self._get_system_prompt() - # Make API call with retries - response = await self._make_api_call( - self.message_manager.messages, system_prompt - ) + # Make API call with retries + response = await self._make_api_call( + self.message_manager.messages, system_prompt + ) - # Handle the response (may execute actions) - # Returns: (should_continue, action_screenshot_saved) - should_continue, new_screenshot_saved = await self._handle_response( - response, self.message_manager.messages - ) + # Handle the response (may execute actions) + # Returns: (should_continue, action_screenshot_saved) + should_continue, new_screenshot_saved = await self._handle_response( + response, self.message_manager.messages + ) - # Update whether an action screenshot was saved this turn - action_screenshot_saved = action_screenshot_saved or new_screenshot_saved - - agent_response = await to_agent_response_format( - response, - messages, - model=self.model, - ) - # Log standardized response for ease of parsing - self._log_api_call("agent_response", request=None, response=agent_response) - yield agent_response - - # Check if we should continue this conversation - running = should_continue + # Update whether an action screenshot was saved this turn + action_screenshot_saved = action_screenshot_saved or new_screenshot_saved + + agent_response = await to_agent_response_format( + response, + messages, + model=self.model, + ) + # Log standardized response for ease of parsing + self._log_api_call("agent_response", request=None, response=agent_response) + + # Put the response in the queue + await queue.put(agent_response) + + # Check if we should continue this conversation + running = should_continue - # Create a new turn directory if we're continuing - if running: - turn_created = False + # Create a new turn directory if we're continuing + if running: + turn_created = False - # Reset attempt counter on success - attempt = 0 + # Reset attempt counter on success + attempt = 0 + except Exception as e: + attempt += 1 + error_msg = f"Error in run method (attempt {attempt}/{max_attempts}): {str(e)}" + logger.error(error_msg) + + # If this is our last attempt, provide more info about the error + if attempt >= max_attempts: + logger.error(f"Maximum retry attempts reached. Last error was: {str(e)}") + + await queue.put({ + "role": "assistant", + "content": f"Error: {str(e)}", + "metadata": {"title": "❌ Error"}, + }) + + # Create a brief delay before retrying + await asyncio.sleep(1) + finally: + # Signal that we're done + await queue.put(None) + + async def cancel(self) -> None: + """Cancel the currently running agent loop task. + + This method stops the ongoing processing in the agent loop + by cancelling the loop_task if it exists and is running. + """ + if self.loop_task and not self.loop_task.done(): + logger.info("Cancelling UITARS loop task") + self.loop_task.cancel() + try: + # Wait for the task to be cancelled with a timeout + await asyncio.wait_for(self.loop_task, timeout=2.0) + except asyncio.TimeoutError: + logger.warning("Timeout while waiting for loop task to cancel") + except asyncio.CancelledError: + logger.info("Loop task cancelled successfully") except Exception as e: - attempt += 1 - error_msg = f"Error in run method (attempt {attempt}/{max_attempts}): {str(e)}" - logger.error(error_msg) - - # If this is our last attempt, provide more info about the error - if attempt >= max_attempts: - logger.error(f"Maximum retry attempts reached. Last error was: {str(e)}") - - yield { - "role": "assistant", - "content": f"Error: {str(e)}", - "metadata": {"title": "❌ Error"}, - } - - # Create a brief delay before retrying - await asyncio.sleep(1) + logger.error(f"Error while cancelling loop task: {str(e)}") + finally: + logger.info("UITARS loop task cancelled") + else: + logger.info("No active UITARS loop task to cancel") ########################################### # UTILITY METHODS diff --git a/libs/agent/agent/ui/gradio/app.py b/libs/agent/agent/ui/gradio/app.py index a8202de9..8d173dd6 100644 --- a/libs/agent/agent/ui/gradio/app.py +++ b/libs/agent/agent/ui/gradio/app.py @@ -22,7 +22,7 @@ Requirements: - Mac with Apple Silicon (M1/M2/M3/M4) - macOS 14 (Sonoma) or newer - - Python 3.10+ + - Python 3.11+ - Lume CLI installed (https://github.com/trycua/cua) - OpenAI or Anthropic API key """ @@ -31,6 +31,7 @@ import asyncio import logging import json +import platform from pathlib import Path from typing import Dict, List, Optional, AsyncGenerator, Any, Tuple, Union import gradio as gr @@ -40,7 +41,6 @@ # Import from agent package from agent.core.types import AgentResponse from agent.core.callbacks import DefaultCallbackHandler -from agent.providers.omni.parser import ParseResult from computer import Computer from agent import ComputerAgent, AgentLoop, LLM, LLMProvider @@ -102,7 +102,7 @@ async def on_screenshot( self, screenshot_base64: str, action_type: str = "", - parsed_screen: Optional[ParseResult] = None, + parsed_screen: Optional[dict] = None, ) -> None: """Add screenshot to chatbot when a screenshot is taken and update the annotated image. @@ -129,6 +129,9 @@ async def on_screenshot( ) +# Detect if current device is MacOS +is_mac = platform.system().lower() == "darwin" + # Map model names to specific provider model names MODEL_MAPPINGS = { "openai": { @@ -165,7 +168,7 @@ async def on_screenshot( }, "uitars": { # UI-TARS models using MLXVLM provider - "default": "mlx-community/UI-TARS-1.5-7B-4bit", + "default": "mlx-community/UI-TARS-1.5-7B-4bit" if is_mac else "tgi", "mlx-community/UI-TARS-1.5-7B-4bit": "mlx-community/UI-TARS-1.5-7B-4bit", "mlx-community/UI-TARS-1.5-7B-6bit": "mlx-community/UI-TARS-1.5-7B-6bit" }, @@ -290,7 +293,7 @@ def get_provider_and_model(model_name: str, loop_provider: str) -> tuple: model_name_to_use = cleaned_model_name # agent_loop remains AgentLoop.OMNI elif agent_loop == AgentLoop.UITARS: - # For UITARS, use MLXVLM provider for the MLX models, OAICOMPAT for custom + # For UITARS, use MLXVLM for mlx-community models, OAICOMPAT for custom if model_name == "Custom model (OpenAI compatible API)": provider = LLMProvider.OAICOMPAT model_name_to_use = "tgi" @@ -333,12 +336,25 @@ def get_ollama_models() -> List[str]: logging.error(f"Error getting Ollama models: {e}") return [] -def create_computer_instance(verbosity: int = logging.INFO) -> Computer: + +def create_computer_instance( + verbosity: int = logging.INFO, + os_type: str = "macos", + provider_type: str = "lume", + name: Optional[str] = None, + api_key: Optional[str] = None +) -> Computer: """Create or get the global Computer instance.""" global global_computer if global_computer is None: - global_computer = Computer(verbosity=verbosity) + global_computer = Computer( + verbosity=verbosity, + os_type=os_type, + provider_type=provider_type, + name=name if name else "", + api_key=api_key + ) return global_computer @@ -353,12 +369,22 @@ def create_agent( verbosity: int = logging.INFO, use_oaicompat: bool = False, provider_base_url: Optional[str] = None, + computer_os: str = "macos", + computer_provider: str = "lume", + computer_name: Optional[str] = None, + computer_api_key: Optional[str] = None, ) -> ComputerAgent: """Create or update the global agent with the specified parameters.""" global global_agent # Create the computer if not already done - computer = create_computer_instance(verbosity=verbosity) + computer = create_computer_instance( + verbosity=verbosity, + os_type=computer_os, + provider_type=computer_provider, + name=computer_name, + api_key=computer_api_key + ) # Get API key from environment if not provided if api_key is None: @@ -401,6 +427,7 @@ def create_agent( return global_agent + def create_gradio_ui( provider_name: str = "openai", model_name: str = "gpt-4o", @@ -421,7 +448,8 @@ def create_gradio_ui( # Check for API keys openai_api_key = os.environ.get("OPENAI_API_KEY", "") anthropic_api_key = os.environ.get("ANTHROPIC_API_KEY", "") - + cua_api_key = os.environ.get("CUA_API_KEY", "") + # Always show models regardless of API key availability openai_models = ["OpenAI: Computer-Use Preview"] anthropic_models = [ @@ -439,22 +467,29 @@ def create_gradio_ui( # Check if API keys are available has_openai_key = bool(openai_api_key) has_anthropic_key = bool(anthropic_api_key) + has_cua_key = bool(cua_api_key) + + print("has_openai_key", has_openai_key) + print("has_anthropic_key", has_anthropic_key) + print("has_cua_key", has_cua_key) # Get Ollama models for OMNI ollama_models = get_ollama_models() if ollama_models: omni_models += ollama_models + # Detect if current device is MacOS + is_mac = platform.system().lower() == "darwin" + # Format model choices provider_to_models = { "OPENAI": openai_models, "ANTHROPIC": anthropic_models, "OMNI": omni_models + ["Custom model (OpenAI compatible API)", "Custom model (ollama)"], # Add custom model options - "UITARS": [ + "UITARS": ([ "mlx-community/UI-TARS-1.5-7B-4bit", "mlx-community/UI-TARS-1.5-7B-6bit", - "Custom model (OpenAI compatible API)" - ], # UI-TARS options with MLX models + ] if is_mac else []) + ["Custom model (OpenAI compatible API)"], # UI-TARS options with MLX models } # --- Apply Saved Settings (override defaults if available) --- @@ -473,7 +508,7 @@ def create_gradio_ui( elif initial_loop == "ANTHROPIC": initial_model = anthropic_models[0] if anthropic_models else "No models available" else: # OMNI - initial_model = omni_models[0] if omni_models else "No models available" + initial_model = omni_models[0] if omni_models else "Custom model (OpenAI compatible API)" if "Custom model (OpenAI compatible API)" in available_models_for_loop: initial_model = ( "Custom model (OpenAI compatible API)" # Default to custom if available and no other default fits @@ -494,7 +529,7 @@ def create_gradio_ui( ] # Function to generate Python code based on configuration and tasks - def generate_python_code(agent_loop_choice, provider, model_name, tasks, provider_url, recent_images=3, save_trajectory=True): + def generate_python_code(agent_loop_choice, provider, model_name, tasks, provider_url, recent_images=3, save_trajectory=True, computer_os="macos", computer_provider="lume", container_name="", cua_cloud_api_key=""): """Generate Python code for the current configuration and tasks. Args: @@ -505,6 +540,10 @@ def generate_python_code(agent_loop_choice, provider, model_name, tasks, provide provider_url: The provider base URL for OAICOMPAT providers recent_images: Number of recent images to keep in context save_trajectory: Whether to save the agent trajectory + computer_os: Operating system type for the computer + computer_provider: Provider type for the computer + container_name: Optional VM name + cua_cloud_api_key: Optional CUA Cloud API key Returns: Formatted Python code as a string @@ -515,13 +554,29 @@ def generate_python_code(agent_loop_choice, provider, model_name, tasks, provide if task and task.strip(): tasks_str += f' "{task}",\n' - # Create the Python code template + # Create the Python code template with computer configuration + computer_args = [] + if computer_os != "macos": + computer_args.append(f'os_type="{computer_os}"') + if computer_provider != "lume": + computer_args.append(f'provider_type="{computer_provider}"') + if container_name: + computer_args.append(f'name="{container_name}"') + if cua_cloud_api_key: + computer_args.append(f'api_key="{cua_cloud_api_key}"') + + computer_args_str = ", ".join(computer_args) + if computer_args_str: + computer_args_str = f"({computer_args_str})" + else: + computer_args_str = "()" + code = f'''import asyncio from computer import Computer from agent import ComputerAgent, LLM, AgentLoop, LLMProvider async def main(): - async with Computer() as macos_computer: + async with Computer{computer_args_str} as macos_computer: agent = ComputerAgent( computer=macos_computer, loop=AgentLoop.{agent_loop_choice}, @@ -660,12 +715,54 @@ async def main(): LLMProvider.OPENAI, "gpt-4o", [], - "https://openrouter.ai/api/v1" + "https://openrouter.ai/api/v1", + 3, # recent_images default + True, # save_trajectory default + "macos", + "lume", + "", + "" ), interactive=False, ) - with gr.Accordion("Configuration", open=True): + with gr.Accordion("Computer Configuration", open=True): + # Computer configuration options + computer_os = gr.Radio( + choices=["macos", "linux"], + label="Operating System", + value="macos", + info="Select the operating system for the computer", + ) + + # Detect if current device is MacOS + is_mac = platform.system().lower() == "darwin" + + computer_provider = gr.Radio( + choices=["cloud", "lume"], + label="Provider", + value="lume" if is_mac else "cloud", + visible=is_mac, + info="Select the computer provider", + ) + + container_name = gr.Textbox( + label="Container Name", + placeholder="Enter container name (optional)", + value="", + info="Optional name for the container", + ) + + cua_cloud_api_key = gr.Textbox( + label="CUA Cloud API Key", + placeholder="Enter your CUA Cloud API key", + value="", + type="password", + info="Required for cloud provider", + visible=(not has_cua_key) + ) + + with gr.Accordion("Agent Configuration", open=True): # Configuration options agent_loop = gr.Dropdown( choices=["OPENAI", "ANTHROPIC", "OMNI", "UITARS"], @@ -818,6 +915,7 @@ def update_ui(loop=None, openai_model=None, anthropic_model=None, omni_model=Non # Custom model fields visibility gr.update(visible=is_any_custom), # Custom model name always visible for any custom option gr.update(visible=is_custom_openai_api), # Provider base URL only for OpenAI compatible API + gr.update(visible=is_custom_openai_api), # Provider API key only for OpenAI compatible API # Update the hidden model_choice field gr.update(value=model_choice_value) ] @@ -985,6 +1083,10 @@ async def process_response( custom_api_key=None, openai_key_input=None, anthropic_key_input=None, + computer_os="macos", + computer_provider="lume", + container_name="", + cua_cloud_api_key="", ): if not history: yield history @@ -1017,12 +1119,18 @@ async def process_response( model_string_to_analyze = model_choice_value # Use the full UI string initially try: - # Special case for UITARS - use MLXVLM provider + # Special case for UITARS - use MLXVLM provider or OAICOMPAT for custom if agent_loop_choice == "UITARS": - provider = LLMProvider.MLXVLM - cleaned_model_name_from_func = model_string_to_analyze - agent_loop_type = AgentLoop.UITARS - print(f"Using MLXVLM provider for UITARS model: {model_string_to_analyze}") + if is_custom_openai_api: + provider = LLMProvider.OAICOMPAT + cleaned_model_name_from_func = custom_model_value + agent_loop_type = AgentLoop.UITARS + print(f"Using OAICOMPAT provider for custom UITARS model: {custom_model_value}") + else: + provider = LLMProvider.MLXVLM + cleaned_model_name_from_func = model_string_to_analyze + agent_loop_type = AgentLoop.UITARS + print(f"Using MLXVLM provider for UITARS model: {model_string_to_analyze}") # Special case for Ollama custom model elif is_custom_ollama and agent_loop_choice == "OMNI": provider = LLMProvider.OLLAMA @@ -1045,8 +1153,8 @@ async def process_response( else cleaned_model_name_from_func ) - # Determine if OAICOMPAT should be used (only for OpenAI compatible API custom model) - is_oaicompat = is_custom_openai_api and agent_loop_choice != "UITARS" + # Determine if OAICOMPAT should be used (for OpenAI compatible API custom model) + is_oaicompat = is_custom_openai_api # Get API key based on provider determined by get_provider_and_model if is_oaicompat and custom_api_key: @@ -1076,6 +1184,8 @@ async def process_response( else: # For Ollama or default OAICOMPAT (without custom key), no key needed/expected api_key = "" + + cua_cloud_api_key = cua_cloud_api_key or os.environ.get("CUA_API_KEY", "") # --- Save Settings Before Running Agent --- current_settings = { @@ -1085,6 +1195,10 @@ async def process_response( "provider_base_url": custom_url_value, "save_trajectory": save_traj, "recent_images": recent_imgs, + "computer_os": computer_os, + "computer_provider": computer_provider, + "container_name": container_name, + "cua_cloud_api_key": cua_cloud_api_key, } save_settings(current_settings) # --- End Save Settings --- @@ -1102,6 +1216,10 @@ async def process_response( use_oaicompat=is_oaicompat, # Set flag if custom model was selected # Pass custom URL only if custom model was selected provider_base_url=custom_url_value if is_oaicompat else None, + computer_os=computer_os, + computer_provider=computer_provider, + computer_name=container_name, + computer_api_key=cua_cloud_api_key, verbosity=logging.DEBUG, # Added verbosity here ) @@ -1228,6 +1346,10 @@ def generate_gradio_messages(): provider_api_key, openai_api_key_input, anthropic_api_key_input, + computer_os, + computer_provider, + container_name, + cua_cloud_api_key, ], outputs=[chatbot_history], queue=True, @@ -1246,82 +1368,20 @@ def generate_gradio_messages(): # Function to update the code display based on configuration and chat history - def update_code_display(agent_loop, model_choice_val, custom_model_val, chat_history, provider_base_url, recent_images_val, save_trajectory_val): + def update_code_display(agent_loop, model_choice_val, custom_model_val, chat_history, provider_base_url, recent_images_val, save_trajectory_val, computer_os, computer_provider, container_name, cua_cloud_api_key): # Extract messages from chat history messages = [] if chat_history: for msg in chat_history: - if msg.get("role") == "user": + if isinstance(msg, dict) and msg.get("role") == "user": messages.append(msg.get("content", "")) - # Determine if this is a custom model selection and which type - is_custom_openai_api = model_choice_val == "Custom model (OpenAI compatible API)" - is_custom_ollama = model_choice_val == "Custom model (ollama)" - is_custom_model_selected = is_custom_openai_api or is_custom_ollama - - # Determine provider and model name based on agent loop - if agent_loop == "OPENAI": - # For OPENAI loop, always use OPENAI provider with computer-use-preview - provider = LLMProvider.OPENAI - model_name = "computer-use-preview" - elif agent_loop == "ANTHROPIC": - # For ANTHROPIC loop, always use ANTHROPIC provider - provider = LLMProvider.ANTHROPIC - # Extract model name from the UI string - if model_choice_val.startswith("Anthropic: Claude "): - # Extract the model name based on the UI string - model_parts = model_choice_val.replace("Anthropic: Claude ", "").split(" (") - version = model_parts[0] # e.g., "3.7 Sonnet" - date = model_parts[1].replace(")", "") if len(model_parts) > 1 else "" # e.g., "20250219" - - # Format as claude-3-7-sonnet-20250219 or claude-3-5-sonnet-20240620 - version = version.replace(".", "-").replace(" ", "-").lower() - model_name = f"claude-{version}-{date}" - else: - # Use the model_choice_val directly if it doesn't match the expected format - model_name = model_choice_val - elif agent_loop == "UITARS": - # For UITARS, use MLXVLM for mlx-community models, OAICOMPAT for custom - if model_choice_val == "Custom model (OpenAI compatible API)": - provider = LLMProvider.OAICOMPAT - model_name = custom_model_val - else: - provider = LLMProvider.MLXVLM - model_name = model_choice_val - elif agent_loop == "OMNI": - # For OMNI, provider can be OPENAI, ANTHROPIC, OLLAMA, or OAICOMPAT - if is_custom_openai_api: - provider = LLMProvider.OAICOMPAT - model_name = custom_model_val - elif is_custom_ollama: - provider = LLMProvider.OLLAMA - model_name = custom_model_val - elif model_choice_val.startswith("OMNI: OpenAI "): - provider = LLMProvider.OPENAI - # Extract model name from UI string (e.g., "OMNI: OpenAI GPT-4o" -> "gpt-4o") - model_name = model_choice_val.replace("OMNI: OpenAI ", "").lower().replace(" ", "-") - elif model_choice_val.startswith("OMNI: Claude "): - provider = LLMProvider.ANTHROPIC - # Extract model name from UI string (similar to ANTHROPIC loop case) - model_parts = model_choice_val.replace("OMNI: Claude ", "").split(" (") - version = model_parts[0] # e.g., "3.7 Sonnet" - date = model_parts[1].replace(")", "") if len(model_parts) > 1 else "" # e.g., "20250219" - - # Format as claude-3-7-sonnet-20250219 or claude-3-5-sonnet-20240620 - version = version.replace(".", "-").replace(" ", "-").lower() - model_name = f"claude-{version}-{date}" - elif model_choice_val.startswith("OMNI: Ollama "): - provider = LLMProvider.OLLAMA - # Extract model name from UI string (e.g., "OMNI: Ollama llama3" -> "llama3") - model_name = model_choice_val.replace("OMNI: Ollama ", "") - else: - # Fallback to get_provider_and_model for any other cases - provider, model_name, _ = get_provider_and_model(model_choice_val, agent_loop) - else: - # Fallback for any other agent loop - provider, model_name, _ = get_provider_and_model(model_choice_val, agent_loop) + # Determine provider and model based on current selection + provider, model_name, _ = get_provider_and_model( + model_choice_val or custom_model_val or "gpt-4o", + agent_loop + ) - # Generate and return the code return generate_python_code( agent_loop, provider, @@ -1329,38 +1389,62 @@ def update_code_display(agent_loop, model_choice_val, custom_model_val, chat_his messages, provider_base_url, recent_images_val, - save_trajectory_val + save_trajectory_val, + computer_os, + computer_provider, + container_name, + cua_cloud_api_key ) # Update code display when configuration changes agent_loop.change( update_code_display, - inputs=[agent_loop, model_choice, custom_model, chatbot_history, provider_base_url, recent_images, save_trajectory], + inputs=[agent_loop, model_choice, custom_model, chatbot_history, provider_base_url, recent_images, save_trajectory, computer_os, computer_provider, container_name, cua_cloud_api_key], outputs=[code_display] ) model_choice.change( update_code_display, - inputs=[agent_loop, model_choice, custom_model, chatbot_history, provider_base_url, recent_images, save_trajectory], + inputs=[agent_loop, model_choice, custom_model, chatbot_history, provider_base_url, recent_images, save_trajectory, computer_os, computer_provider, container_name, cua_cloud_api_key], outputs=[code_display] ) custom_model.change( update_code_display, - inputs=[agent_loop, model_choice, custom_model, chatbot_history, provider_base_url, recent_images, save_trajectory], + inputs=[agent_loop, model_choice, custom_model, chatbot_history, provider_base_url, recent_images, save_trajectory, computer_os, computer_provider, container_name, cua_cloud_api_key], outputs=[code_display] ) chatbot_history.change( update_code_display, - inputs=[agent_loop, model_choice, custom_model, chatbot_history, provider_base_url, recent_images, save_trajectory], + inputs=[agent_loop, model_choice, custom_model, chatbot_history, provider_base_url, recent_images, save_trajectory, computer_os, computer_provider, container_name, cua_cloud_api_key], outputs=[code_display] ) recent_images.change( update_code_display, - inputs=[agent_loop, model_choice, custom_model, chatbot_history, provider_base_url, recent_images, save_trajectory], + inputs=[agent_loop, model_choice, custom_model, chatbot_history, provider_base_url, recent_images, save_trajectory, computer_os, computer_provider, container_name, cua_cloud_api_key], outputs=[code_display] ) save_trajectory.change( update_code_display, - inputs=[agent_loop, model_choice, custom_model, chatbot_history, provider_base_url, recent_images, save_trajectory], + inputs=[agent_loop, model_choice, custom_model, chatbot_history, provider_base_url, recent_images, save_trajectory, computer_os, computer_provider, container_name, cua_cloud_api_key], + outputs=[code_display] + ) + computer_os.change( + update_code_display, + inputs=[agent_loop, model_choice, custom_model, chatbot_history, provider_base_url, recent_images, save_trajectory, computer_os, computer_provider, container_name, cua_cloud_api_key], + outputs=[code_display] + ) + computer_provider.change( + update_code_display, + inputs=[agent_loop, model_choice, custom_model, chatbot_history, provider_base_url, recent_images, save_trajectory, computer_os, computer_provider, container_name, cua_cloud_api_key], + outputs=[code_display] + ) + container_name.change( + update_code_display, + inputs=[agent_loop, model_choice, custom_model, chatbot_history, provider_base_url, recent_images, save_trajectory, computer_os, computer_provider, container_name, cua_cloud_api_key], + outputs=[code_display] + ) + cua_cloud_api_key.change( + update_code_display, + inputs=[agent_loop, model_choice, custom_model, chatbot_history, provider_base_url, recent_images, save_trajectory, computer_os, computer_provider, container_name, cua_cloud_api_key], outputs=[code_display] ) @@ -1370,7 +1454,7 @@ def update_code_display(agent_loop, model_choice_val, custom_model_val, chat_his def test_cua(): """Standalone function to launch the Gradio app.""" demo = create_gradio_ui() - demo.launch(share=False) # Don't create a public link + demo.launch(share=False, inbrowser=True) # Don't create a public link if __name__ == "__main__": diff --git a/libs/agent/pyproject.toml b/libs/agent/pyproject.toml index 7b40614f..8ea6a3fc 100644 --- a/libs/agent/pyproject.toml +++ b/libs/agent/pyproject.toml @@ -19,11 +19,11 @@ dependencies = [ "pydantic>=2.6.4,<3.0.0", "rich>=13.7.1,<14.0.0", "python-dotenv>=1.0.1,<2.0.0", - "cua-computer>=0.1.0,<0.2.0", + "cua-computer>=0.2.0,<0.3.0", "cua-core>=0.1.0,<0.2.0", "certifi>=2024.2.2" ] -requires-python = ">=3.10" +requires-python = ">=3.11" [project.optional-dependencies] anthropic = [ @@ -102,11 +102,11 @@ source-includes = ["tests/", "README.md", "LICENSE"] [tool.black] line-length = 100 -target-version = ["py310"] +target-version = ["py311"] [tool.ruff] line-length = 100 -target-version = "py310" +target-version = "py311" select = ["E", "F", "B", "I"] fix = true @@ -115,7 +115,7 @@ docstring-code-format = true [tool.mypy] strict = true -python_version = "3.10" +python_version = "3.11" ignore_missing_imports = true disallow_untyped_defs = true check_untyped_defs = true diff --git a/libs/computer-server/computer_server/__main__.py b/libs/computer-server/computer_server/__main__.py new file mode 100644 index 00000000..89d33d0b --- /dev/null +++ b/libs/computer-server/computer_server/__main__.py @@ -0,0 +1,10 @@ +""" +Main entry point for running the Computer Server as a module. +This allows the server to be started with `python -m computer_server`. +""" + +import sys +from .cli import main + +if __name__ == "__main__": + sys.exit(main()) diff --git a/libs/computer-server/computer_server/cli.py b/libs/computer-server/computer_server/cli.py index 416e5e95..30f7e519 100644 --- a/libs/computer-server/computer_server/cli.py +++ b/libs/computer-server/computer_server/cli.py @@ -27,6 +27,16 @@ def parse_args(args: Optional[List[str]] = None) -> argparse.Namespace: default="info", help="Logging level (default: info)", ) + parser.add_argument( + "--ssl-keyfile", + type=str, + help="Path to SSL private key file (enables HTTPS)", + ) + parser.add_argument( + "--ssl-certfile", + type=str, + help="Path to SSL certificate file (enables HTTPS)", + ) return parser.parse_args(args) @@ -43,7 +53,21 @@ def main() -> None: # Create and start the server logger.info(f"Starting CUA Computer API server on {args.host}:{args.port}...") - server = Server(host=args.host, port=args.port, log_level=args.log_level) + + # Handle SSL configuration + ssl_args = {} + if args.ssl_keyfile and args.ssl_certfile: + ssl_args = { + "ssl_keyfile": args.ssl_keyfile, + "ssl_certfile": args.ssl_certfile, + } + logger.info("HTTPS mode enabled with SSL certificates") + elif args.ssl_keyfile or args.ssl_certfile: + logger.warning("Both --ssl-keyfile and --ssl-certfile are required for HTTPS. Running in HTTP mode.") + else: + logger.info("HTTP mode (no SSL certificates provided)") + + server = Server(host=args.host, port=args.port, log_level=args.log_level, **ssl_args) try: server.start() diff --git a/libs/computer-server/computer_server/diorama/__init__.py b/libs/computer-server/computer_server/diorama/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/computer-server/computer_server/diorama/base.py b/libs/computer-server/computer_server/diorama/base.py new file mode 100644 index 00000000..3ca01133 --- /dev/null +++ b/libs/computer-server/computer_server/diorama/base.py @@ -0,0 +1,4 @@ +class BaseDioramaHandler: + """Base Diorama handler for unsupported OSes.""" + async def diorama_cmd(self, action: str, arguments: dict = None) -> dict: + return {"success": False, "error": "Diorama is not supported on this OS yet."} diff --git a/libs/computer-server/computer_server/diorama/diorama.py b/libs/computer-server/computer_server/diorama/diorama.py new file mode 100644 index 00000000..fc426a7c --- /dev/null +++ b/libs/computer-server/computer_server/diorama/diorama.py @@ -0,0 +1,432 @@ +#!/usr/bin/env python3 +"""Diorama: A virtual desktop manager for macOS""" + +import os +import asyncio +import logging +import sys +import io +from typing import Union +from PIL import Image, ImageDraw + +from computer_server.diorama.draw import capture_all_apps, AppActivationContext, get_frontmost_and_active_app, get_all_windows, get_running_apps + +from computer_server.diorama.diorama_computer import DioramaComputer +from computer_server.handlers.macos import * + +# simple, nicely formatted logging +logging.basicConfig( + level=logging.INFO, + format='[%(asctime)s] [%(levelname)s] %(message)s', + datefmt='%H:%M:%S', + stream=sys.stdout +) +logger = logging.getLogger("diorama.virtual_desktop") + +automation_handler = MacOSAutomationHandler() + +class Diorama: + _scheduler_queue = None + _scheduler_task = None + _loop = None + _scheduler_started = False + + @classmethod + def create_from_apps(cls, *args) -> DioramaComputer: + cls._ensure_scheduler() + return cls(args).computer + + # Dictionary to store cursor positions for each unique app_list hash + _cursor_positions = {} + + def __init__(self, app_list): + self.app_list = app_list + self.interface = self.Interface(self) + self.computer = DioramaComputer(self) + self.focus_context = None + + # Create a hash for this app_list to use as a key + self.app_list_hash = hash(tuple(sorted(app_list))) + + # Initialize cursor position for this app_list if it doesn't exist + if self.app_list_hash not in Diorama._cursor_positions: + Diorama._cursor_positions[self.app_list_hash] = (0, 0) + + @classmethod + def _ensure_scheduler(cls): + if not cls._scheduler_started: + logger.info("Starting Diorama scheduler loop…") + cls._scheduler_queue = asyncio.Queue() + cls._loop = asyncio.get_event_loop() + cls._scheduler_task = cls._loop.create_task(cls._scheduler_loop()) + cls._scheduler_started = True + + @classmethod + async def _scheduler_loop(cls): + while True: + cmd = await cls._scheduler_queue.get() + action = cmd.get("action") + args = cmd.get("arguments", {}) + future = cmd.get("future") + logger.info(f"Processing command: {action} | args={args}") + + app_whitelist = args.get("app_list", []) + + all_windows = get_all_windows() + running_apps = get_running_apps() + frontmost_app, active_app_to_use, active_app_pid = get_frontmost_and_active_app(all_windows, running_apps, app_whitelist) + focus_context = AppActivationContext(active_app_pid, active_app_to_use, logger) + + with focus_context: + try: + if action == "screenshot": + logger.info(f"Taking screenshot for apps: {app_whitelist}") + result, img = capture_all_apps( + app_whitelist=app_whitelist, + save_to_disk=False, + take_focus=False + ) + logger.info("Screenshot complete.") + if future: + future.set_result((result, img)) + # Mouse actions + elif action in ["left_click", "right_click", "double_click", "move_cursor", "drag_to"]: + x = args.get("x") + y = args.get("y") + + duration = args.get("duration", 0.5) + if action == "left_click": + await automation_handler.left_click(x, y) + elif action == "right_click": + await automation_handler.right_click(x, y) + elif action == "double_click": + await automation_handler.double_click(x, y) + elif action == "move_cursor": + await automation_handler.move_cursor(x, y) + elif action == "drag_to": + await automation_handler.drag_to(x, y, duration=duration) + if future: + future.set_result(None) + elif action in ["scroll_up", "scroll_down"]: + x = args.get("x") + y = args.get("y") + if x is not None and y is not None: + await automation_handler.move_cursor(x, y) + + clicks = args.get("clicks", 1) + if action == "scroll_up": + await automation_handler.scroll_up(clicks) + else: + await automation_handler.scroll_down(clicks) + if future: + future.set_result(None) + # Keyboard actions + elif action == "type_text": + text = args.get("text") + await automation_handler.type_text(text) + if future: + future.set_result(None) + elif action == "press_key": + key = args.get("key") + await automation_handler.press_key(key) + if future: + future.set_result(None) + elif action == "hotkey": + keys = args.get("keys", []) + await automation_handler.hotkey(keys) + if future: + future.set_result(None) + elif action == "get_cursor_position": + pos = await automation_handler.get_cursor_position() + if future: + future.set_result(pos) + else: + logger.warning(f"Unknown action: {action}") + if future: + future.set_exception(ValueError(f"Unknown action: {action}")) + except Exception as e: + logger.error(f"Exception during {action}: {e}", exc_info=True) + if future: + future.set_exception(e) + + class Interface(): + def __init__(self, diorama): + self._diorama = diorama + + self._scene_hitboxes = [] + self._scene_size = None + + async def _send_cmd(self, action, arguments=None): + Diorama._ensure_scheduler() + loop = asyncio.get_event_loop() + future = loop.create_future() + logger.info(f"Enqueuing {action} command for apps: {self._diorama.app_list}") + await Diorama._scheduler_queue.put({ + "action": action, + "arguments": {"app_list": self._diorama.app_list, **(arguments or {})}, + "future": future + }) + try: + return await future + except asyncio.CancelledError: + logger.warning(f"Command was cancelled: {action}") + return None + + async def screenshot(self, as_bytes: bool = True) -> Union[str, Image.Image]: + import base64 + result, img = await self._send_cmd("screenshot") + self._scene_hitboxes = result.get("hitboxes", []) + self._scene_size = img.size + + if as_bytes: + # PIL Image to bytes, then base64 encode for JSON + import io + img_byte_arr = io.BytesIO() + img.save(img_byte_arr, format="PNG") + img_bytes = img_byte_arr.getvalue() + img_b64 = base64.b64encode(img_bytes).decode("ascii") + return img_b64 + else: + return img + + async def left_click(self, x, y): + # Get last cursor position for this app_list hash + app_list_hash = hash(tuple(sorted(self._diorama.app_list))) + last_pos = Diorama._cursor_positions.get(app_list_hash, (0, 0)) + x, y = x or last_pos[0], y or last_pos[1] + # Update cursor position for this app_list hash + Diorama._cursor_positions[app_list_hash] = (x, y) + + sx, sy = await self.to_screen_coordinates(x, y) + await self._send_cmd("left_click", {"x": sx, "y": sy}) + + async def right_click(self, x, y): + # Get last cursor position for this app_list hash + app_list_hash = hash(tuple(sorted(self._diorama.app_list))) + last_pos = Diorama._cursor_positions.get(app_list_hash, (0, 0)) + x, y = x or last_pos[0], y or last_pos[1] + # Update cursor position for this app_list hash + Diorama._cursor_positions[app_list_hash] = (x, y) + + sx, sy = await self.to_screen_coordinates(x, y) + await self._send_cmd("right_click", {"x": sx, "y": sy}) + + async def double_click(self, x, y): + # Get last cursor position for this app_list hash + app_list_hash = hash(tuple(sorted(self._diorama.app_list))) + last_pos = Diorama._cursor_positions.get(app_list_hash, (0, 0)) + x, y = x or last_pos[0], y or last_pos[1] + # Update cursor position for this app_list hash + Diorama._cursor_positions[app_list_hash] = (x, y) + + sx, sy = await self.to_screen_coordinates(x, y) + await self._send_cmd("double_click", {"x": sx, "y": sy}) + + async def move_cursor(self, x, y): + # Get last cursor position for this app_list hash + app_list_hash = hash(tuple(sorted(self._diorama.app_list))) + last_pos = Diorama._cursor_positions.get(app_list_hash, (0, 0)) + x, y = x or last_pos[0], y or last_pos[1] + # Update cursor position for this app_list hash + Diorama._cursor_positions[app_list_hash] = (x, y) + + sx, sy = await self.to_screen_coordinates(x, y) + await self._send_cmd("move_cursor", {"x": sx, "y": sy}) + + async def drag_to(self, x, y, duration=0.5): + # Get last cursor position for this app_list hash + app_list_hash = hash(tuple(sorted(self._diorama.app_list))) + last_pos = Diorama._cursor_positions.get(app_list_hash, (0, 0)) + x, y = x or last_pos[0], y or last_pos[1] + # Update cursor position for this app_list hash + Diorama._cursor_positions[app_list_hash] = (x, y) + + sx, sy = await self.to_screen_coordinates(x, y) + await self._send_cmd("drag_to", {"x": sx, "y": sy, "duration": duration}) + + async def get_cursor_position(self): + return await self._send_cmd("get_cursor_position") + + async def type_text(self, text): + await self._send_cmd("type_text", {"text": text}) + + async def press_key(self, key): + await self._send_cmd("press_key", {"key": key}) + + async def hotkey(self, keys): + await self._send_cmd("hotkey", {"keys": list(keys)}) + + async def scroll_up(self, clicks: int = 1): + # Get last cursor position for this app_list hash + app_list_hash = hash(tuple(sorted(self._diorama.app_list))) + last_pos = Diorama._cursor_positions.get(app_list_hash, (0, 0)) + x, y = last_pos[0], last_pos[1] + + await self._send_cmd("scroll_up", {"clicks": clicks, "x": x, "y": y}) + + async def scroll_down(self, clicks: int = 1): + # Get last cursor position for this app_list hash + app_list_hash = hash(tuple(sorted(self._diorama.app_list))) + last_pos = Diorama._cursor_positions.get(app_list_hash, (0, 0)) + x, y = last_pos[0], last_pos[1] + + await self._send_cmd("scroll_down", {"clicks": clicks, "x": x, "y": y}) + + async def get_screen_size(self) -> dict[str, int]: + if not self._scene_size: + await self.screenshot() + return { "width": self._scene_size[0], "height": self._scene_size[1] } + + async def to_screen_coordinates(self, x: float, y: float) -> tuple[float, float]: + """Convert screenshot coordinates to screen coordinates. + + Args: + x: X absolute coordinate in screenshot space + y: Y absolute coordinate in screenshot space + + Returns: + tuple[float, float]: (x, y) absolute coordinates in screen space + """ + if not self._scene_hitboxes: + await self.screenshot() # get hitboxes + # Try all hitboxes + for h in self._scene_hitboxes[::-1]: + rect_from = h.get("hitbox") + rect_to = h.get("target") + if not rect_from or len(rect_from) != 4: + continue + + # check if (x, y) is inside rect_from + x0, y0, x1, y1 = rect_from + if x0 <= x <= x1 and y0 <= y <= y1: + logger.info(f"Found hitbox: {h}") + # remap (x, y) to rect_to + tx0, ty0, tx1, ty1 = rect_to + + # calculate offset from x0, y0 + offset_x = x - x0 + offset_y = y - y0 + + # remap offset to rect_to + tx = tx0 + offset_x + ty = ty0 + offset_y + + return tx, ty + return x, y + + async def to_screenshot_coordinates(self, x: float, y: float) -> tuple[float, float]: + """Convert screen coordinates to screenshot coordinates. + + Args: + x: X absolute coordinate in screen space + y: Y absolute coordinate in screen space + + Returns: + tuple[float, float]: (x, y) absolute coordinates in screenshot space + """ + if not self._scene_hitboxes: + await self.screenshot() # get hitboxes + # Try all hitboxes + for h in self._scene_hitboxes[::-1]: + rect_from = h.get("target") + rect_to = h.get("hitbox") + if not rect_from or len(rect_from) != 4: + continue + + # check if (x, y) is inside rect_from + x0, y0, x1, y1 = rect_from + if x0 <= x <= x1 and y0 <= y <= y1: + # remap (x, y) to rect_to + tx0, ty0, tx1, ty1 = rect_to + + # calculate offset from x0, y0 + offset_x = x - x0 + offset_y = y - y0 + + # remap offset to rect_to + tx = tx0 + offset_x + ty = ty0 + offset_y + + return tx, ty + return x, y + +import pyautogui +import time + +async def main(): + desktop1 = Diorama.create_from_apps(["Discord", "Notes"]) + desktop2 = Diorama.create_from_apps(["Terminal"]) + + img1 = await desktop1.interface.screenshot(as_bytes=False) + img2 = await desktop2.interface.screenshot(as_bytes=False) + + img1.save("app_screenshots/desktop1.png") + img2.save("app_screenshots/desktop2.png") + # Initialize Diorama desktop + desktop3 = Diorama.create_from_apps("Safari") + screen_size = await desktop3.interface.get_screen_size() + print(screen_size) + + # Take initial screenshot + img = await desktop3.interface.screenshot(as_bytes=False) + img.save("app_screenshots/desktop3.png") + + # Prepare hitboxes and draw on the single screenshot + hitboxes = desktop3.interface._scene_hitboxes[::-1] + base_img = img.copy() + draw = ImageDraw.Draw(base_img) + for h in hitboxes: + rect = h.get("hitbox") + if not rect or len(rect) != 4: + continue + draw.rectangle(rect, outline="red", width=2) + + # Track and draw mouse position in real time (single screenshot size) + last_mouse_pos = None + print("Tracking mouse... Press Ctrl+C to stop.") + try: + while True: + mouse_x, mouse_y = pyautogui.position() + if last_mouse_pos != (mouse_x, mouse_y): + last_mouse_pos = (mouse_x, mouse_y) + # Map to screenshot coordinates + sx, sy = await desktop3.interface.to_screenshot_coordinates(mouse_x, mouse_y) + # Draw on a copy of the screenshot + frame = base_img.copy() + frame_draw = ImageDraw.Draw(frame) + frame_draw.ellipse((sx-5, sy-5, sx+5, sy+5), fill="blue", outline="blue") + # Save the frame + frame.save("app_screenshots/desktop3_mouse.png") + print(f"Mouse at screen ({mouse_x}, {mouse_y}) -> screenshot ({sx:.1f}, {sy:.1f})") + time.sleep(0.05) # Throttle updates to ~20 FPS + except KeyboardInterrupt: + print("Stopped tracking.") + + draw.text((rect[0], rect[1]), str(idx), fill="red") + + canvas.save("app_screenshots/desktop3_hitboxes.png") + + + + # move mouse in a square spiral around the screen + import math + import random + + step = 20 # pixels per move + dot_radius = 10 + width = screen_size["width"] + height = screen_size["height"] + x, y = 0, 10 + + while x < width and y < height: + await desktop3.interface.move_cursor(x, y) + img = await desktop3.interface.screenshot(as_bytes=False) + draw = ImageDraw.Draw(img) + draw.ellipse((x-dot_radius, y-dot_radius, x+dot_radius, y+dot_radius), fill="red") + img.save("current.png") + await asyncio.sleep(0.03) + x += step + y = math.sin(x / width * math.pi * 2) * 50 + 25 + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/libs/computer-server/computer_server/diorama/diorama_computer.py b/libs/computer-server/computer_server/diorama/diorama_computer.py new file mode 100644 index 00000000..4fc37b3f --- /dev/null +++ b/libs/computer-server/computer_server/diorama/diorama_computer.py @@ -0,0 +1,26 @@ +import asyncio + +class DioramaComputer: + """ + A minimal Computer-like interface for Diorama, compatible with ComputerAgent. + Implements _initialized, run(), and __aenter__ for agent compatibility. + """ + def __init__(self, diorama): + self.diorama = diorama + self.interface = self.diorama.interface + self._initialized = False + + async def __aenter__(self): + # Ensure the event loop is running (for compatibility) + try: + asyncio.get_running_loop() + except RuntimeError: + asyncio.set_event_loop(asyncio.new_event_loop()) + self._initialized = True + return self + + async def run(self): + # This is a stub for compatibility + if not self._initialized: + await self.__aenter__() + return self diff --git a/libs/computer-server/computer_server/diorama/draw.py b/libs/computer-server/computer_server/diorama/draw.py new file mode 100644 index 00000000..9fce809f --- /dev/null +++ b/libs/computer-server/computer_server/diorama/draw.py @@ -0,0 +1,1215 @@ +#!/usr/bin/env python3 +"""Diorama Renderer - A tool for rendering selective views of macOS desktops + +This script renders filtered views of the macOS desktop, preserving only selected applications +while maintaining system UI elements like menubar and dock. Each "diorama" shows a consistent +view of the system while isolating specific applications. + +The image is "smart resized" to remove any empty space around the menubar and dock. + +Key features: +- Captures shared window state, z-order and position information +- Filters windows by application based on whitelist +- Preserves system context (menubar, dock) in each view +- Preserves menu-owning / keyboard-focused window in each view +- Supports parallel views of the same desktop for multi-agent systems +""" + +import sys +import os +import time +import argparse +from typing import List, Dict, Any, Optional, Tuple +import json +from PIL import Image, ImageDraw +import io +import asyncio +import functools +import logging + +# simple, nicely formatted logging +logging.basicConfig( + level=logging.INFO, + format='[%(asctime)s] [%(levelname)s] %(message)s', + datefmt='%H:%M:%S', + stream=sys.stdout +) +logger = logging.getLogger("diorama.draw") + +from computer_server.diorama.safezone import ( + get_menubar_bounds, + get_dock_bounds, +) + +# Timing decorator for profiling +def timing_decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + start_time = time.time() + result = func(*args, **kwargs) + end_time = time.time() + elapsed_time = end_time - start_time + logger.debug(f"Function {func.__name__} took {elapsed_time:.4f} seconds to run") + return result + return wrapper + +# Import Objective-C bridge libraries +try: + import Quartz + import AppKit + from ApplicationServices import ( + AXUIElementCreateSystemWide, # type: ignore + AXUIElementCreateApplication, # type: ignore + AXUIElementCopyAttributeValue, # type: ignore + AXUIElementCopyAttributeValues, # type: ignore + kAXFocusedWindowAttribute, # type: ignore + kAXWindowsAttribute, # type: ignore + kAXMainWindowAttribute, # type: ignore + kAXChildrenAttribute, # type: ignore + kAXRoleAttribute, # type: ignore + kAXTitleAttribute, # type: ignore + kAXValueAttribute, # type: ignore + kAXDescriptionAttribute, # type: ignore + kAXEnabledAttribute, # type: ignore + kAXPositionAttribute, # type: ignore + kAXSizeAttribute, # type: ignore + kAXErrorSuccess, # type: ignore + AXValueGetType, # type: ignore + kAXValueCGSizeType, # type: ignore + kAXValueCGPointType, # type: ignore + kAXValueCFRangeType, # type: ignore + AXUIElementGetTypeID, # type: ignore + AXValueGetValue, # type: ignore + kAXVisibleChildrenAttribute, # type: ignore + kAXRoleDescriptionAttribute, # type: ignore + kAXFocusedApplicationAttribute, # type: ignore + kAXFocusedUIElementAttribute, # type: ignore + kAXSelectedTextAttribute, # type: ignore + kAXSelectedTextRangeAttribute, # type: ignore + ) + from AppKit import NSWorkspace, NSApplication, NSApp, NSRunningApplication + import Foundation + from Foundation import NSObject, NSMakeRect + import objc +except ImportError: + logger.error("Error: This script requires PyObjC to be installed.") + logger.error("Please install it with: pip install pyobjc") + sys.exit(1) + +# Constants for accessibility API +kAXErrorSuccess = 0 +kAXRoleAttribute = "AXRole" +kAXTitleAttribute = "AXTitle" +kAXValueAttribute = "AXValue" +kAXWindowsAttribute = "AXWindows" +kAXFocusedAttribute = "AXFocused" +kAXPositionAttribute = "AXPosition" +kAXSizeAttribute = "AXSize" +kAXChildrenAttribute = "AXChildren" +kAXMenuBarAttribute = "AXMenuBar" +kAXMenuBarItemAttribute = "AXMenuBarItem" + +# Constants for window properties +kCGWindowLayer = "kCGWindowLayer" # Z-order information (lower values are higher in the stack) +kCGWindowAlpha = "kCGWindowAlpha" # Window opacity + +# Constants for application activation options +NSApplicationActivationOptions = { + "regular": 0, # Default activation + "bringing_all_windows_forward": 1 << 0, # NSApplicationActivateAllWindows + "ignoring_other_apps": 1 << 1 # NSApplicationActivateIgnoringOtherApps +} + + +def CFAttributeToPyObject(attrValue): + def list_helper(list_value): + list_builder = [] + for item in list_value: + list_builder.append(CFAttributeToPyObject(item)) + return list_builder + + def number_helper(number_value): + success, int_value = Foundation.CFNumberGetValue( # type: ignore + number_value, Foundation.kCFNumberIntType, None # type: ignore + ) + if success: + return int(int_value) + + success, float_value = Foundation.CFNumberGetValue( # type: ignore + number_value, Foundation.kCFNumberDoubleType, None # type: ignore + ) + if success: + return float(float_value) + return None + + def axuielement_helper(element_value): + return element_value + + cf_attr_type = Foundation.CFGetTypeID(attrValue) # type: ignore + cf_type_mapping = { + Foundation.CFStringGetTypeID(): str, # type: ignore + Foundation.CFBooleanGetTypeID(): bool, # type: ignore + Foundation.CFArrayGetTypeID(): list_helper, # type: ignore + Foundation.CFNumberGetTypeID(): number_helper, # type: ignore + AXUIElementGetTypeID(): axuielement_helper, # type: ignore + } + try: + return cf_type_mapping[cf_attr_type](attrValue) + except KeyError: + # did not get a supported CF type. Move on to AX type + pass + + ax_attr_type = AXValueGetType(attrValue) + ax_type_map = { + kAXValueCGSizeType: Foundation.NSSizeFromString, # type: ignore + kAXValueCGPointType: Foundation.NSPointFromString, # type: ignore + kAXValueCFRangeType: Foundation.NSRangeFromString, # type: ignore + } + try: + search_result = re.search("{.*}", attrValue.description()) + if search_result: + extracted_str = search_result.group() + return tuple(ax_type_map[ax_attr_type](extracted_str)) + return None + except KeyError: + return None + +def element_attribute(element, attribute): + if attribute == kAXChildrenAttribute: + err, value = AXUIElementCopyAttributeValues(element, attribute, 0, 999, None) + if err == kAXErrorSuccess: + if isinstance(value, Foundation.NSArray): # type: ignore + return CFAttributeToPyObject(value) + else: + return value + err, value = AXUIElementCopyAttributeValue(element, attribute, None) + if err == kAXErrorSuccess: + if isinstance(value, Foundation.NSArray): # type: ignore + return CFAttributeToPyObject(value) + else: + return value + return None + +def element_value(element, type): + err, value = AXValueGetValue(element, type, None) + if err == True: + return value + return None + + +@timing_decorator +def get_running_apps() -> List[NSRunningApplication]: + """Get list of all running applications + + Returns: + List of NSRunningApplication objects + """ + return NSWorkspace.sharedWorkspace().runningApplications() + +# @timing_decorator +def get_app_info(app: NSRunningApplication) -> Dict[str, Any]: + """Get information about an application + + Args: + app: NSRunningApplication object + + Returns: + Dictionary with application information + """ + return { + "name": app.localizedName(), + "bundle_id": app.bundleIdentifier(), + "pid": app.processIdentifier(), + "active": app.isActive(), + "hidden": app.isHidden(), + "terminated": app.isTerminated(), + } + +@timing_decorator +def get_all_windows() -> List[Dict[str, Any]]: + """Get all windows from all applications with z-order information + + Returns: + List of window dictionaries with z-order information + """ + # Get all windows from Quartz + # The kCGWindowListOptionOnScreenOnly flag gets only visible windows with preserved z-order + window_list = Quartz.CGWindowListCopyWindowInfo( + Quartz.kCGWindowListOptionOnScreenOnly, + Quartz.kCGNullWindowID + ) + + # Create a dictionary of window z-order + z_order = {window['kCGWindowNumber']: z_index for z_index, window in enumerate(window_list[::-1])} + + # The kCGWindowListOptionAll flag gets all windows *without* z-order preserved + window_list_all = Quartz.CGWindowListCopyWindowInfo( + Quartz.kCGWindowListOptionAll, + Quartz.kCGNullWindowID + ) + + # Process all windows + windows = [] + for window in window_list_all: + # We track z_index which is the index in the window list (0 is the desktop / background) + + # Get window properties + window_id = window.get('kCGWindowNumber', 0) + window_name = window.get('kCGWindowName', '') + window_pid = window.get('kCGWindowOwnerPID', 0) + window_bounds = window.get('kCGWindowBounds', {}) + window_owner = window.get('kCGWindowOwnerName', '') + window_is_on_screen = window.get('kCGWindowIsOnscreen', False) + + # Get z-order information + # Note: kCGWindowLayer provides the system's layer value (lower values are higher in the stack) + layer = window.get(kCGWindowLayer, 0) + opacity = window.get(kCGWindowAlpha, 1.0) + z_index = z_order.get(window_id, -1) + + # Determine window role (desktop, dock, menubar, app) + if window_name == "Dock" and window_owner == "Dock": + role = "dock" + elif window_name == "Menubar" and window_owner == "Window Server": + role = "menubar" + elif window_owner in ["Window Server", "Dock"]: + role = "desktop" + else: + role = "app" + + # Only include windows with valid bounds + if window_bounds: + windows.append({ + "id": window_id, + "name": window_name or "Unnamed Window", + "pid": window_pid, + "owner": window_owner, + "role": role, + "is_on_screen": window_is_on_screen, + "bounds": { + "x": window_bounds.get('X', 0), + "y": window_bounds.get('Y', 0), + "width": window_bounds.get('Width', 0), + "height": window_bounds.get('Height', 0) + }, + "layer": layer, # System layer (lower values are higher in stack) + "z_index": z_index, # Our z-index (0 is the desktop) + "opacity": opacity + }) + + windows = sorted(windows, key=lambda x: x["z_index"]) + + return windows + +def get_app_windows(app_pid: int, all_windows: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """Get all windows for a specific application + + Args: + app_pid: Process ID of the application + all_windows: List of all windows with z-order information + + Returns: + List of window dictionaries for the app + """ + # Filter windows by PID + return [window for window in all_windows if window["pid"] == app_pid] + +@timing_decorator +def draw_desktop_screenshot(app_whitelist: List[str] = None, all_windows: List[Dict[str, Any]] = None, dock_bounds: Dict[str, float] = None, dock_items: List[Dict[str, Any]] = None, menubar_bounds: Dict[str, float] = None, menubar_items: List[Dict[str, Any]] = None) -> Tuple[Optional[Image.Image], List[Dict[str, Any]]]: + """Capture a screenshot of the entire desktop using Quartz compositing, including dock as a second pass. + Args: + app_whitelist: Optional list of app names to include in the screenshot + Returns: + PIL Image of the desktop or None if capture failed + """ + import ctypes + + if dock_bounds is None: + dock_bounds = get_dock_bounds() + if dock_items is None: + dock_items = get_dock_items() + if menubar_bounds is None: + menubar_bounds = get_menubar_bounds() + if menubar_items is None: + menubar_items = get_menubar_items() + if all_windows is None: + all_windows = get_all_windows() + all_windows = all_windows[::-1] + all_windows = [window for window in all_windows if window["is_on_screen"]] + + main_screen = AppKit.NSScreen.mainScreen() + if main_screen: + frame = main_screen.frame() + screen_rect = Quartz.CGRectMake(0, 0, frame.size.width, frame.size.height) + else: + screen_rect = Quartz.CGRectNull + + # Screenshot-to-screen hitboxes + hitboxes = [] + + if app_whitelist is None: + # Single pass: desktop, menubar, app, dock + window_list = Foundation.CFArrayCreateMutable(None, len(all_windows), None) + for window in all_windows: + Foundation.CFArrayAppendValue(window_list, window["id"]) + cg_image = Quartz.CGWindowListCreateImageFromArray( + screen_rect, window_list, Quartz.kCGWindowImageDefault + ) + if cg_image is None: + return None + + # Create CGContext for compositing + width = int(frame.size.width) + height = int(frame.size.height) + color_space = Quartz.CGColorSpaceCreateWithName(Quartz.kCGColorSpaceSRGB) + cg_context = Quartz.CGBitmapContextCreate( + None, width, height, 8, 0, color_space, Quartz.kCGImageAlphaPremultipliedLast + ) + Quartz.CGContextDrawImage(cg_context, screen_rect, cg_image) + hitboxes.append({ + "hitbox": [0, 0, width, height], + "target": [0, 0, width, height] + }) + else: + # Filter out windows that are not in the whitelist + all_windows = [window for window in all_windows if window["owner"] in app_whitelist or window["role"] != "app"] + app_windows = [window for window in all_windows if window["role"] == "app"] + + dock_orientation = "side" if dock_bounds["width"] < dock_bounds["height"] else "bottom" + + menubar_length = max(item["bounds"]["x"] + item["bounds"]["width"] for item in menubar_items) if menubar_items else 0 + + # Calculate bounds of app windows + app_bounds = { + "x": min(window["bounds"]["x"] for window in app_windows) if app_windows else 0, + "y": min(window["bounds"]["y"] for window in app_windows) if app_windows else 0, + } + app_bounds["width"] = max(window["bounds"]["x"] + window["bounds"]["width"] for window in app_windows) - app_bounds["x"] if app_windows else 0 + app_bounds["height"] = max(window["bounds"]["y"] + window["bounds"]["height"] for window in app_windows) - app_bounds["y"] if app_windows else 0 + + # Set minimum bounds of 256x256 + app_bounds["width"] = max(app_bounds["width"], 256) + app_bounds["height"] = max(app_bounds["height"], 256) + + # Add dock bounds to app bounds + if dock_orientation == "bottom": + app_bounds["height"] += dock_bounds["height"] + 4 + elif dock_orientation == "side": + if dock_bounds["x"] > frame.size.width / 2: + app_bounds["width"] += dock_bounds["width"] + 4 + else: + app_bounds["x"] -= dock_bounds["width"] + 4 + app_bounds["width"] += dock_bounds["width"] + 4 + + # Add menubar bounds to app bounds + app_bounds["height"] += menubar_bounds["height"] + + # Make sure app bounds contains menubar bounds + app_bounds["width"] = max(app_bounds["width"], menubar_length) + + # Clamp bounds to screen + app_bounds["x"] = max(app_bounds["x"], 0) + app_bounds["y"] = max(app_bounds["y"], 0) + app_bounds["width"] = min(app_bounds["width"], frame.size.width - app_bounds["x"]) + app_bounds["height"] = min(app_bounds["height"], frame.size.height - app_bounds["y"] + menubar_bounds["height"]) + + # Create CGContext for compositing + width = int(app_bounds["width"]) + height = int(app_bounds["height"]) + color_space = Quartz.CGColorSpaceCreateWithName(Quartz.kCGColorSpaceSRGB) + cg_context = Quartz.CGBitmapContextCreate( + None, width, height, 8, 0, color_space, Quartz.kCGImageAlphaPremultipliedLast + ) + + def _draw_layer(cg_context, all_windows, source_rect, target_rect): + """Draw a layer of windows from source_rect to target_rect on the given context.""" + window_list = Foundation.CFArrayCreateMutable(None, len(all_windows), None) + for window in all_windows: + Foundation.CFArrayAppendValue(window_list, window["id"]) + cg_image = Quartz.CGWindowListCreateImageFromArray( + source_rect, window_list, Quartz.kCGWindowImageDefault + ) + if cg_image is not None: + Quartz.CGContextDrawImage(cg_context, target_rect, cg_image) + + # --- FIRST PASS: desktop, apps --- + source_position = [app_bounds["x"], app_bounds["y"]] + source_size = [app_bounds["width"], app_bounds["height"]] + target_position = [ + 0, + min( + menubar_bounds["y"] + menubar_bounds["height"], + app_bounds["y"] + ) + ] + target_size = [app_bounds["width"], app_bounds["height"]] + + if dock_orientation == "bottom": + source_size[1] += dock_bounds["height"] + target_size[1] += dock_bounds["height"] + elif dock_orientation == "side": + if dock_bounds["x"] < frame.size.width / 2: + source_position[0] -= dock_bounds["width"] + target_position[0] -= dock_bounds["width"] + source_size[0] += dock_bounds["width"] + target_size[0] += dock_bounds["width"] + + app_source_rect = Quartz.CGRectMake( + source_position[0], source_position[1], source_size[0], source_size[1] + ) + app_target_rect = Quartz.CGRectMake( + target_position[0], app_bounds["height"] - target_position[1] - target_size[1], target_size[0], target_size[1] + ) + first_pass_windows = [w for w in all_windows if w["role"] == "app" or w["role"] == "desktop"] + _draw_layer(cg_context, first_pass_windows, app_source_rect, app_target_rect) + + hitboxes.append({ + "hitbox": [0, menubar_bounds["height"], app_bounds["width"], menubar_bounds["height"] + app_bounds["height"]], + "target": [ + app_source_rect.origin.x, + app_source_rect.origin.y, + app_source_rect.origin.x + app_bounds["width"], + app_source_rect.origin.y + app_bounds["height"] + ] + }) + + # --- SECOND PASS: menubar --- + allowed_roles = {"menubar"} + menubar_windows = [w for w in all_windows if w["role"] in allowed_roles] + menubar_source_rect = Quartz.CGRectMake( + 0, 0, app_bounds["width"], menubar_bounds["height"] + ) + menubar_target_rect = Quartz.CGRectMake( + 0, app_bounds["height"] - menubar_bounds["height"], app_bounds["width"], menubar_bounds["height"] + ) + _draw_layer(cg_context, menubar_windows, menubar_source_rect, menubar_target_rect) + + hitboxes.append({ + "hitbox": [0, 0, app_bounds["width"], menubar_bounds["height"]], + "target": [0, 0, app_bounds["width"], menubar_bounds["height"]] + }) + + # --- THIRD PASS: dock, filtered --- + # Step 1: Collect dock items to draw, with their computed target rects + dock_draw_items = [] + for index, item in enumerate(dock_items): + source_position = (item["bounds"]["x"], item["bounds"]["y"]) + source_size = (item["bounds"]["width"], item["bounds"]["height"]) + + # apply whitelist to middle items + if not (index == 0 or index == len(dock_items) - 1): + if item["subrole"] == "AXApplicationDockItem": + if item["title"] not in app_whitelist: + continue + elif item["subrole"] == "AXMinimizedWindowDockItem": + if not any(window["name"] == item["title"] and window["role"] == "app" and window["owner"] in app_whitelist for window in all_windows): + continue + elif item["subrole"] == "AXFolderDockItem": + continue + + # Preserve unscaled (original) source position and size before any modification + hitbox_position = source_position + hitbox_size = source_size + + screen_position = source_position + screen_size = source_size + + # stretch to screen size + padding = 32 + if dock_orientation == "bottom": + source_position = (source_position[0], 0) + source_size = (source_size[0], frame.size.height) + + hitbox_position = (source_position[0], app_bounds['height'] - hitbox_size[1]) + hitbox_size = (source_size[0], hitbox_size[1]) + + if index == 0: + source_size = (padding + source_size[0], source_size[1]) + source_position = (source_position[0] - padding, 0) + elif index == len(dock_items) - 1: + source_size = (source_size[0] + padding, source_size[1]) + source_position = (source_position[0], 0) + + elif dock_orientation == "side": + source_position = (0, source_position[1]) + source_size = (frame.size.width, source_size[1]) + + hitbox_position = ( + source_position[0] if dock_bounds['x'] < frame.size.width / 2 else app_bounds['width'] - hitbox_size[0], + source_position[1] + ) + hitbox_size = (hitbox_size[0], source_size[1]) + + if index == 0: + source_size = (source_size[0], padding + source_size[1]) + source_position = (0, source_position[1] - padding) + elif index == len(dock_items) - 1: + source_size = (source_size[0], source_size[1] + padding) + source_position = (0, source_position[1]) + + + # Compute the initial target position + target_position = source_position + target_size = source_size + + dock_draw_items.append({ + "item": item, + "index": index, + "source_position": source_position, + "source_size": source_size, + "target_size": target_size, + "target_position": target_position, # Will be updated after packing + "hitbox_position": hitbox_position, + "hitbox_size": hitbox_size, + "screen_position": screen_position, + "screen_size": screen_size, + }) + + # Step 2: Pack the target rects along the main axis, removing gaps + packed_positions = [] + if dock_orientation == "bottom": + # Pack left-to-right + x_cursor = 0 + for draw_item in dock_draw_items: + packed_positions.append((x_cursor, draw_item["target_position"][1])) + x_cursor += draw_item["target_size"][0] + packed_strip_length = x_cursor + # Center horizontally + x_offset = (app_bounds['width'] - packed_strip_length) / 2 + y_offset = (frame.size.height - app_bounds['height']) + for i, draw_item in enumerate(dock_draw_items): + px, py = packed_positions[i] + draw_item["target_position"] = (px + x_offset, py - y_offset) + + # Pack unscaled source rects + x_cursor = 0 + for draw_item in dock_draw_items: + draw_item["hitbox_position"] = (x_cursor, draw_item["hitbox_position"][1]) + x_cursor += draw_item["hitbox_size"][0] + packed_strip_length = x_cursor + # Center horizontally + x_offset = (app_bounds['width'] - packed_strip_length) / 2 + for i, draw_item in enumerate(dock_draw_items): + px, py = draw_item["hitbox_position"] + draw_item["hitbox_position"] = (px + x_offset, py) + elif dock_orientation == "side": + # Pack top-to-bottom + y_cursor = 0 + for draw_item in dock_draw_items: + packed_positions.append((draw_item["target_position"][0], y_cursor)) + y_cursor += draw_item["target_size"][1] + packed_strip_length = y_cursor + # Center vertically + y_offset = (app_bounds['height'] - packed_strip_length) / 2 + x_offset = 0 if dock_bounds['x'] < frame.size.width / 2 else frame.size.width - app_bounds['width'] + for i, draw_item in enumerate(dock_draw_items): + px, py = packed_positions[i] + draw_item["target_position"] = (px - x_offset, py + y_offset) + + # Pack unscaled source rects + y_cursor = 0 + for draw_item in dock_draw_items: + draw_item["hitbox_position"] = (draw_item["hitbox_position"][0], y_cursor) + y_cursor += draw_item["hitbox_size"][1] + packed_strip_length = y_cursor + # Center vertically + y_offset = (app_bounds['height'] - packed_strip_length) / 2 + for i, draw_item in enumerate(dock_draw_items): + px, py = draw_item["hitbox_position"] + draw_item["hitbox_position"] = (px, py + y_offset) + + dock_windows = [window for window in all_windows if window["role"] == "dock"] + # Step 3: Draw dock items using packed and recentered positions + for draw_item in dock_draw_items: + item = draw_item["item"] + source_position = draw_item["source_position"] + source_size = draw_item["source_size"] + target_position = draw_item["target_position"] + target_size = draw_item["target_size"] + + # flip target position y + target_position = (target_position[0], app_bounds['height'] - target_position[1] - target_size[1]) + + source_rect = Quartz.CGRectMake(*source_position, *source_size) + target_rect = Quartz.CGRectMake(*target_position, *target_size) + + _draw_layer(cg_context, dock_windows, source_rect, target_rect) + + hitbox_position = draw_item["hitbox_position"] + hitbox_size = draw_item["hitbox_size"] + + # Debug: Draw true hitbox rect (packed position, unscaled size) + # # Flip y like target_rect + # hitbox_position_flipped = ( + # hitbox_position[0], + # app_bounds['height'] - hitbox_position[1] - hitbox_size[1] + # ) + # hitbox_rect = Quartz.CGRectMake(*hitbox_position_flipped, *hitbox_size) + # Quartz.CGContextSetStrokeColorWithColor(cg_context, Quartz.CGColorCreateGenericRGB(0, 1, 0, 1)) + # Quartz.CGContextStrokeRect(cg_context, hitbox_rect) + + hitboxes.append({ + "hitbox": [*hitbox_position, hitbox_position[0] + hitbox_size[0], hitbox_position[1] + hitbox_size[1]], + "target": [*draw_item["screen_position"], draw_item["screen_position"][0] + draw_item["screen_size"][0], draw_item["screen_position"][1] + draw_item["screen_size"][1]] + }) + + + # Convert composited context to CGImage + final_cg_image = Quartz.CGBitmapContextCreateImage(cg_context) + ns_image = AppKit.NSImage.alloc().initWithCGImage_size_(final_cg_image, Foundation.NSZeroSize) + ns_data = ns_image.TIFFRepresentation() + bitmap_rep = AppKit.NSBitmapImageRep.imageRepWithData_(ns_data) + png_data = bitmap_rep.representationUsingType_properties_(AppKit.NSBitmapImageFileTypePNG, None) + image_data = io.BytesIO(png_data) + return Image.open(image_data), hitboxes + +@timing_decorator +def get_menubar_items(active_app_pid: int = None) -> List[Dict[str, Any]]: + """Get menubar items from the active application using Accessibility API + + Args: + active_app_pid: PID of the active application + + Returns: + List of dictionaries with menubar item information + """ + menubar_items = [] + + if active_app_pid is None: + # Get the frontmost application's PID if none provided + frontmost_app = NSWorkspace.sharedWorkspace().frontmostApplication() + if frontmost_app: + active_app_pid = frontmost_app.processIdentifier() + else: + logger.error("Error: Could not determine frontmost application") + return menubar_items + + # Create an accessibility element for the application + app_element = AXUIElementCreateApplication(active_app_pid) + if app_element is None: + logger.error(f"Error: Could not create accessibility element for PID {active_app_pid}") + return menubar_items + + # Get the menubar + menubar = element_attribute(app_element, kAXMenuBarAttribute) + if menubar is None: + logger.error(f"Error: Could not get menubar for application with PID {active_app_pid}") + return menubar_items + + # Get the menubar items + children = element_attribute(menubar, kAXChildrenAttribute) + if children is None: + logger.error("Error: Could not get menubar items") + return menubar_items + + # Process each menubar item + for i in range(len(children)): + item = children[i] + + # Get item title + title = element_attribute(item, kAXTitleAttribute) or "Untitled" + + # Create bounding box + bounds = { + "x": 0, + "y": 0, + "width": 0, + "height": 0 + } + + # Get item position + position_value = element_attribute(item, kAXPositionAttribute) + if position_value: + position_value = element_value(position_value, kAXValueCGPointType) + bounds["x"] = position_value.x + bounds["y"] = position_value.y + + # Get item size + size_value = element_attribute(item, kAXSizeAttribute) + if size_value: + size_value = element_value(size_value, kAXValueCGSizeType) + bounds["width"] = size_value.width + bounds["height"] = size_value.height + + + # Add to list + menubar_items.append({ + "title": title, + "bounds": bounds, + "index": i, + "app_pid": active_app_pid + }) + + return menubar_items + +@timing_decorator +def get_dock_items() -> List[Dict[str, Any]]: + """Get all items in the macOS Dock + + Returns: + List of dictionaries with Dock item information + """ + dock_items = [] + + # Find the Dock process + dock_pid = None + running_apps = get_running_apps() + for app in running_apps: + if app.localizedName() == "Dock" and app.bundleIdentifier() == "com.apple.dock": + dock_pid = app.processIdentifier() + break + + if dock_pid is None: + logger.error("Error: Could not find Dock process") + return dock_items + + # Create an accessibility element for the Dock + dock_element = AXUIElementCreateApplication(dock_pid) + if dock_element is None: + logger.error(f"Error: Could not create accessibility element for Dock (PID {dock_pid})") + return dock_items + + # Get the Dock's main element + dock_list = element_attribute(dock_element, kAXChildrenAttribute) + if dock_list is None or len(dock_list) == 0: + logger.error("Error: Could not get Dock children") + return dock_items + + # Find the Dock's application list (usually the first child) + dock_app_list = None + for child in dock_list: + role = element_attribute(child, kAXRoleAttribute) + if role == "AXList": + dock_app_list = child + break + + if dock_app_list is None: + logger.error("Error: Could not find Dock application list") + return dock_items + + # Get all items in the Dock + items = element_attribute(dock_app_list, kAXChildrenAttribute) + if items is None: + logger.error("Error: Could not get Dock items") + return dock_items + + # Process each Dock item + for i, item in enumerate(items): + # Get item attributes + title = element_attribute(item, kAXTitleAttribute) or "Untitled" + description = element_attribute(item, "AXDescription") or "" + role = element_attribute(item, kAXRoleAttribute) or "" + subrole = element_attribute(item, "AXSubrole") or "" + + # Create bounding box + bounds = { + "x": 0, + "y": 0, + "width": 0, + "height": 0 + } + + # Get item position + position_value = element_attribute(item, kAXPositionAttribute) + if position_value: + position_value = element_value(position_value, kAXValueCGPointType) + bounds["x"] = position_value.x + bounds["y"] = position_value.y + + # Get item size + size_value = element_attribute(item, kAXSizeAttribute) + if size_value: + size_value = element_value(size_value, kAXValueCGSizeType) + bounds["width"] = size_value.width + bounds["height"] = size_value.height + + # Determine if this is an application, file/folder, or separator + item_type = "unknown" + if subrole == "AXApplicationDockItem": + item_type = "application" + elif subrole == "AXFolderDockItem": + item_type = "folder" + elif subrole == "AXDocumentDockItem": + item_type = "document" + elif subrole == "AXSeparatorDockItem" or role == "AXSeparator": + item_type = "separator" + elif "trash" in title.lower(): + item_type = "trash" + + # Add to list + dock_items.append({ + "title": title, + "description": description, + "bounds": bounds, + "index": i, + "type": item_type, + "role": role, + "subrole": subrole + }) + + return dock_items + +class AppActivationContext: + def __init__(self, active_app_pid=None, active_app_to_use="", logger=None): + self.active_app_pid = active_app_pid + self.active_app_to_use = active_app_to_use + self.logger = logger + self.frontmost_app = None + + def __enter__(self): + from AppKit import NSWorkspace + if self.active_app_pid: + if self.logger and self.active_app_to_use: + self.logger.debug(f"Automatically activating app '{self.active_app_to_use}' for screenshot composition") + self.frontmost_app = NSWorkspace.sharedWorkspace().frontmostApplication() + running_apps_list = NSWorkspace.sharedWorkspace().runningApplications() + for app in running_apps_list: + if app.processIdentifier() == self.active_app_pid: + app.activateWithOptions_(0) + # sleep for 0.5 seconds + time.sleep(0.5) + break + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if self.frontmost_app: + # sleep for 0.5 seconds + time.sleep(0.5) + self.frontmost_app.activateWithOptions_(0) + + +def get_frontmost_and_active_app(all_windows, running_apps, app_whitelist): + from AppKit import NSWorkspace + frontmost_app = NSWorkspace.sharedWorkspace().frontmostApplication() + + active_app_to_use = None + active_app_pid = None + + # Find the topmost (highest z_index) non-filtered app + for window in reversed(all_windows): + owner = window.get("owner") + role = window.get("role") + is_on_screen = window.get("is_on_screen") + + # Skip non-app windows + if role != "app": + continue + + # Skip not-on-screen windows + if not is_on_screen: + continue + + # Skip filtered apps + if app_whitelist is not None and owner not in app_whitelist: + continue + + # Found a suitable app + active_app_to_use = owner + active_app_pid = window.get("pid") + break + + # If no suitable app found, use Finder + if active_app_to_use is None: + active_app_to_use = "Finder" + for app in running_apps: + if app.localizedName() == "Finder": + active_app_pid = app.processIdentifier() + break + + return frontmost_app, active_app_to_use, active_app_pid + +def capture_all_apps(save_to_disk: bool = False, app_whitelist: List[str] = None, output_dir: str = None, take_focus: bool = True) -> Tuple[Dict[str, Any], Optional[Image.Image]]: + """Capture screenshots of all running applications + + Args: + save_to_disk: Whether to save screenshots to disk + app_whitelist: Optional list of app names to include in the recomposited screenshot + (will always include 'Window Server' and 'Dock') + + Returns: + Dictionary with application information and screenshots + Optional PIL Image of the recomposited screenshot + """ + result = { + "timestamp": time.time(), + "applications": [], + "windows": [], # New array to store all windows, including those without apps + "menubar_items": [], # New array to store menubar items + "dock_items": [] # New array to store dock items + } + + # Get all windows with z-order information + all_windows = get_all_windows() + + # Get all running applications + running_apps = get_running_apps() + + frontmost_app, active_app_to_use, active_app_pid = get_frontmost_and_active_app(all_windows, running_apps, app_whitelist) if take_focus else (None, None, None) + + # Use AppActivationContext to activate the app and restore focus + with AppActivationContext(active_app_pid, active_app_to_use, logger): + + # Process applications + for app in running_apps: + # Skip system apps without a bundle ID + if app.bundleIdentifier() is None: + continue + + app_info = get_app_info(app) + app_windows = get_app_windows(app.processIdentifier(), all_windows) + + app_data = { + "info": app_info, + "windows": [ window["id"] for window in app_windows ] + } + + result["applications"].append(app_data) + + # Add all windows to the result + result["windows"] = all_windows + + # Get menubar items from the active application + menubar_items = get_menubar_items(active_app_pid) + result["menubar_items"] = menubar_items + + # Get dock items + dock_items = get_dock_items() + result["dock_items"] = dock_items + + # Get menubar bounds + menubar_bounds = get_menubar_bounds() + result["menubar_bounds"] = menubar_bounds + + # Get dock bounds + dock_bounds = get_dock_bounds() + result["dock_bounds"] = dock_bounds + + # Capture the entire desktop using Quartz compositing + desktop_screenshot, hitboxes = draw_desktop_screenshot(app_whitelist, all_windows, dock_bounds, dock_items, menubar_bounds, menubar_items) + + result["hitboxes"] = hitboxes + + from PIL import Image, ImageDraw, ImageChops + def _draw_hitboxes(img, hitboxes, key="target"): + """ + Overlay opaque colored rectangles for each hitbox (using hitbox[key]) + with color depending on index, then multiply overlay onto img. + Args: + img: PIL.Image (RGBA or RGB) + hitboxes: list of dicts with 'hitbox' and 'target' keys + key: 'hitbox' or 'target' + Returns: + PIL.Image with overlayed hitboxes (same mode/size as input) + """ + # Ensure RGBA mode for blending + base = img.convert("RGBA") + overlay = Image.new("RGBA", base.size, (0, 0, 0, 0)) + draw = ImageDraw.Draw(overlay) + + # Distinct colors for order + colors = [ + (255, 0, 0, 180), # Red + (0, 255, 0, 180), # Green + (0, 0, 255, 180), # Blue + (255, 255, 0, 180), # Yellow + (0, 255, 255, 180), # Cyan + (255, 0, 255, 180), # Magenta + (255, 128, 0, 180), # Orange + (128, 0, 255, 180), # Purple + (0, 128, 255, 180), # Sky blue + (128, 255, 0, 180), # Lime + ] + # Set minimum brightness for colors + min_brightness = 0 + colors = [ + (max(min_brightness, c[0]), max(min_brightness, c[1]), max(min_brightness, c[2]), c[3]) for c in colors + ] + + for i, h in enumerate(hitboxes): + rect = h.get(key) + color = colors[i % len(colors)] + if rect: + draw.rectangle(rect, fill=color) + + # Multiply blend overlay onto base + result = ImageChops.multiply(base, overlay) + return result + + # DEBUG: Save hitboxes to disk + if desktop_screenshot and save_to_disk and output_dir: + desktop_path = os.path.join(output_dir, "desktop.png") + desktop_screenshot.save(desktop_path) + result["desktop_screenshot"] = desktop_path + + logger.info(f"Saved desktop screenshot to {desktop_path}") + + if app_whitelist: + # Take screenshot without whitelist + desktop_screenshot_full, hitboxes_full = draw_desktop_screenshot( + None, all_windows, dock_bounds, dock_items, menubar_bounds, menubar_items) + + # Draw hitboxes on both images using overlay + img1 = _draw_hitboxes(desktop_screenshot.copy(), hitboxes, key="hitbox") + img2 = _draw_hitboxes(desktop_screenshot_full.copy(), hitboxes, key="target") if desktop_screenshot_full else None + + if img2 and hitboxes_full: + + # Compose side-by-side + from PIL import Image + width = img1.width + img2.width + height = max(img1.height, img2.height) + combined = Image.new('RGBA', (width, height), (0, 0, 0, 0)) + combined.paste(img1, (0, 0)) + combined.paste(img2, (img1.width, 0)) + side_by_side_path = os.path.join(output_dir, "side_by_side_hitboxes.png") + combined.save(side_by_side_path) + result["side_by_side_hitboxes"] = side_by_side_path + else: + # Overlay hitboxes using new function + hitbox_img = _draw_hitboxes(desktop_screenshot.copy(), hitboxes, key="hitbox") + hitbox_path = os.path.join(output_dir, "hitboxes.png") + hitbox_img.save(hitbox_path) + result["hitbox_screenshot"] = hitbox_path + + # Focus restoration is now handled by AppActivationContext + + return result, desktop_screenshot + +async def run_capture(): + """Run the screenshot capture asynchronously""" + # Parse command line arguments + parser = argparse.ArgumentParser(description="Capture screenshots of running macOS applications") + parser.add_argument("--output", "-o", help="Output directory for screenshots", default="app_screenshots") + parser.add_argument("--filter", "-f", nargs="+", help="Filter recomposited screenshot to only include specified apps") + parser.add_argument("--menubar", "-m", action="store_true", help="List menubar and status items with their bounding boxes") + parser.add_argument("--dock", "-d", action="store_true", help="List Dock items with their bounding boxes") + parser.add_argument("--demo", nargs="*", help="Demo mode: pass app names to capture individual and combinations, create mosaic PNG") + args = parser.parse_args() + + # Create output directory in the current directory if not absolute + if not os.path.isabs(args.output): + output_dir = os.path.join(os.getcwd(), args.output) + else: + output_dir = args.output + + # DEMO MODE: capture each app and all non-empty combinations, then mosaic + if args.demo: + from PIL import Image + demo_apps = args.demo + print(f"Running in DEMO mode for apps: {demo_apps}") + groups = [] + for item in demo_apps: + if "/" in item: + group = [x.strip() for x in item.split("/") if x.strip()] + else: + group = [item.strip()] + if group: + groups.append(group) + screenshots = [] + for group in groups: + print(f"Capturing for apps: {group}") + _, img = capture_all_apps(app_whitelist=group) + if img: + screenshots.append((group, img)) + if not screenshots: + print("No screenshots captured in demo mode.") + return + # Mosaic-pack: grid (rows of sqrt(N)) + def make_mosaic(images, pad=64, bg=(30,30,30)): + import rpack + sizes = [(img.width + pad, img.height + pad) for _, img in images] + positions = rpack.pack(sizes) + # Find the bounding box for the mosaic + max_x = max(x + w for (x, y), (w, h) in zip(positions, sizes)) + max_y = max(y + h for (x, y), (w, h) in zip(positions, sizes)) + mosaic = Image.new("RGBA", (max_x, max_y), bg) + for (group, img), (x, y) in zip(images, positions): + mosaic.paste(img, (x, y)) + return mosaic + mosaic_img = make_mosaic(screenshots) + mosaic_path = os.path.join(output_dir, "demo_mosaic.png") + os.makedirs(output_dir, exist_ok=True) + mosaic_img.save(mosaic_path) + print(f"Demo mosaic saved to: {mosaic_path}") + return + + # Capture all apps and save to disk, including a recomposited screenshot + print(f"Capturing screenshots of all running applications...") + print(f"Saving screenshots to: {output_dir}") + + # If filter is provided, show what we're filtering by + if args.filter: + print(f"Filtering recomposited screenshot to only include: {', '.join(args.filter)} (plus Window Server and Dock)") + + result, img = capture_all_apps( + save_to_disk=True, + app_whitelist=args.filter, + output_dir=output_dir, + take_focus=True + ) + + # Print summary + print(f"\nCapture complete!") + print(f"Captured {len(result['applications'])} applications") + + total_app_windows = sum(len(app["windows"]) for app in result["applications"]) + print(f"Total application windows captured: {total_app_windows}") + print(f"Total standalone windows captured: {len(result['windows'])}") + + # Print details of each application + print("\nApplication details:") + for app in result["applications"]: + app_info = app["info"] + windows = app["windows"] + print(f" - {app_info['name']} ({len(windows)} windows)") + + # Print recomposited screenshot path if available + if "desktop_screenshot" in result: + print(f"\nRecomposited screenshot saved to: {result['desktop_screenshot']}") + + # Print menubar items if requested + if args.menubar and "menubar_items" in result: + print("\nMenubar items:") + + # Find app name for the PID + app_name_by_pid = {} + for app in result["applications"]: + app_info = app["info"] + app_name_by_pid[app_info["pid"]] = app_info["name"] + + for item in result["menubar_items"]: + print(f" - {item['title']}") + print(f" Bounds: x={item['bounds']['x']}, y={item['bounds']['y']}, width={item['bounds']['width']}, height={item['bounds']['height']}") + + if "app_pid" in item: + app_name = app_name_by_pid.get(item["app_pid"], f"Unknown App (PID: {item['app_pid']})") + print(f" App: {app_name} (PID: {item['app_pid']})") + + if "window_id" in item: + print(f" Window ID: {item['window_id']}") + if "owner" in item: + print(f" Owner: {item['owner']}") + if "layer" in item and "z_index" in item: + print(f" Layer: {item['layer']}, Z-Index: {item['z_index']}") + print("") + + # Print dock items if requested + if args.dock and "dock_items" in result: + print("\nDock items:") + for item in result["dock_items"]: + print(f" - {item['title']} ({item['type']})") + print(f" Description: {item['description']}") + print(f" Bounds: x={item['bounds']['x']}, y={item['bounds']['y']}, width={item['bounds']['width']}, height={item['bounds']['height']}") + print(f" Role: {item['role']}, Subrole: {item['subrole']}") + print(f" Index: {item['index']}") + print("") + + # Save the metadata to a JSON file + metadata_path = os.path.join(output_dir, "metadata.json") + with open(metadata_path, "w") as f: + json.dump(result, f, indent=2) + + print(f"\nMetadata saved to: {metadata_path}") + +if __name__ == "__main__": + asyncio.run(run_capture()) \ No newline at end of file diff --git a/libs/computer-server/computer_server/diorama/macos.py b/libs/computer-server/computer_server/diorama/macos.py new file mode 100644 index 00000000..be266cae --- /dev/null +++ b/libs/computer-server/computer_server/diorama/macos.py @@ -0,0 +1,33 @@ +import platform +import sys +import platform +import inspect +from computer_server.diorama.diorama import Diorama +from computer_server.diorama.base import BaseDioramaHandler +from typing import Optional + +class MacOSDioramaHandler(BaseDioramaHandler): + """Handler for Diorama commands on macOS, using local diorama module.""" + async def diorama_cmd(self, action: str, arguments: Optional[dict] = None) -> dict: + if platform.system().lower() != "darwin": + return {"success": False, "error": "Diorama is only supported on macOS."} + try: + app_list = arguments.get("app_list") if arguments else None + if not app_list: + return {"success": False, "error": "Missing 'app_list' in arguments"} + diorama = Diorama(app_list) + interface = diorama.interface + if not hasattr(interface, action): + return {"success": False, "error": f"Unknown diorama action: {action}"} + method = getattr(interface, action) + # Remove app_list from arguments before calling the method + filtered_arguments = dict(arguments) + filtered_arguments.pop("app_list", None) + if inspect.iscoroutinefunction(method): + result = await method(**(filtered_arguments or {})) + else: + result = method(**(filtered_arguments or {})) + return {"success": True, "result": result} + except Exception as e: + import traceback + return {"success": False, "error": str(e), "trace": traceback.format_exc()} diff --git a/libs/computer-server/computer_server/diorama/safezone.py b/libs/computer-server/computer_server/diorama/safezone.py new file mode 100644 index 00000000..122b668f --- /dev/null +++ b/libs/computer-server/computer_server/diorama/safezone.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +""" +UI Safezone Helper - A utility to get accurate bounds for macOS UI elements + +This module provides helper functions to get accurate bounds for macOS UI elements +like the menubar and dock, which are needed for proper screenshot composition. +""" + +import sys +import time +from typing import Dict, Any, Optional, Tuple + +# Import Objective-C bridge libraries +try: + import AppKit + from ApplicationServices import ( + AXUIElementCreateSystemWide, + AXUIElementCreateApplication, + AXUIElementCopyAttributeValue, + AXUIElementCopyAttributeValues, + kAXChildrenAttribute, + kAXRoleAttribute, + kAXTitleAttribute, + kAXPositionAttribute, + kAXSizeAttribute, + kAXErrorSuccess, + AXValueGetType, + kAXValueCGSizeType, + kAXValueCGPointType, + AXUIElementGetTypeID, + AXValueGetValue, + kAXMenuBarAttribute, + ) + from AppKit import NSWorkspace, NSRunningApplication + import Foundation +except ImportError: + print("Error: This script requires PyObjC to be installed.") + print("Please install it with: pip install pyobjc") + sys.exit(1) + +# Constants for accessibility API +kAXErrorSuccess = 0 +kAXRoleAttribute = "AXRole" +kAXSubroleAttribute = "AXSubrole" +kAXTitleAttribute = "AXTitle" +kAXPositionAttribute = "AXPosition" +kAXSizeAttribute = "AXSize" +kAXChildrenAttribute = "AXChildren" +kAXMenuBarAttribute = "AXMenuBar" + + +def element_attribute(element, attribute): + """Get an attribute from an accessibility element""" + if attribute == kAXChildrenAttribute: + err, value = AXUIElementCopyAttributeValues(element, attribute, 0, 999, None) + if err == kAXErrorSuccess: + if isinstance(value, Foundation.NSArray): + return list(value) + else: + return value + err, value = AXUIElementCopyAttributeValue(element, attribute, None) + if err == kAXErrorSuccess: + return value + return None + + +def element_value(element, type): + """Get a value from an accessibility element""" + err, value = AXValueGetValue(element, type, None) + if err == True: + return value + return None + + +def get_element_bounds(element): + """Get the bounds of an accessibility element""" + bounds = { + "x": 0, + "y": 0, + "width": 0, + "height": 0 + } + + # Get position + position_value = element_attribute(element, kAXPositionAttribute) + if position_value: + position_value = element_value(position_value, kAXValueCGPointType) + if position_value: + bounds["x"] = position_value.x + bounds["y"] = position_value.y + + # Get size + size_value = element_attribute(element, kAXSizeAttribute) + if size_value: + size_value = element_value(size_value, kAXValueCGSizeType) + if size_value: + bounds["width"] = size_value.width + bounds["height"] = size_value.height + + return bounds + + +def find_dock_process(): + """Find the Dock process""" + running_apps = NSWorkspace.sharedWorkspace().runningApplications() + for app in running_apps: + if app.localizedName() == "Dock" and app.bundleIdentifier() == "com.apple.dock": + return app.processIdentifier() + return None + + +def get_menubar_bounds(): + """Get the bounds of the macOS menubar + + Returns: + Dictionary with x, y, width, height of the menubar + """ + # Get the system-wide accessibility element + system_element = AXUIElementCreateSystemWide() + + # Try to find the menubar + menubar = element_attribute(system_element, kAXMenuBarAttribute) + if menubar is None: + # If we can't get it directly, try through the frontmost app + frontmost_app = NSWorkspace.sharedWorkspace().frontmostApplication() + if frontmost_app: + app_pid = frontmost_app.processIdentifier() + app_element = AXUIElementCreateApplication(app_pid) + menubar = element_attribute(app_element, kAXMenuBarAttribute) + + if menubar is None: + print("Error: Could not get menubar") + # Return default menubar bounds as fallback + return {"x": 0, "y": 0, "width": 1800, "height": 24} + + # Get menubar bounds + return get_element_bounds(menubar) + + +def get_dock_bounds(): + """Get the bounds of the macOS Dock + + Returns: + Dictionary with x, y, width, height of the Dock + """ + dock_pid = find_dock_process() + if dock_pid is None: + print("Error: Could not find Dock process") + # Return empty bounds as fallback + return {"x": 0, "y": 0, "width": 0, "height": 0} + + # Create an accessibility element for the Dock + dock_element = AXUIElementCreateApplication(dock_pid) + if dock_element is None: + print(f"Error: Could not create accessibility element for Dock (PID {dock_pid})") + return {"x": 0, "y": 0, "width": 0, "height": 0} + + # Get the Dock's children + children = element_attribute(dock_element, kAXChildrenAttribute) + if not children or len(children) == 0: + print("Error: Could not get Dock children") + return {"x": 0, "y": 0, "width": 0, "height": 0} + + # Find the Dock's list (first child is usually the main dock list) + dock_list = None + for child in children: + role = element_attribute(child, kAXRoleAttribute) + if role == "AXList": + dock_list = child + break + + if dock_list is None: + print("Error: Could not find Dock list") + return {"x": 0, "y": 0, "width": 0, "height": 0} + + # Get the bounds of the dock list + return get_element_bounds(dock_list) + + +def get_ui_element_bounds(): + """Get the bounds of important UI elements like menubar and dock + + Returns: + Dictionary with menubar and dock bounds + """ + menubar_bounds = get_menubar_bounds() + dock_bounds = get_dock_bounds() + + return { + "menubar": menubar_bounds, + "dock": dock_bounds + } + + +if __name__ == "__main__": + # Example usage + bounds = get_ui_element_bounds() + print("Menubar bounds:", bounds["menubar"]) + print("Dock bounds:", bounds["dock"]) diff --git a/libs/computer-server/computer_server/handlers/base.py b/libs/computer-server/computer_server/handlers/base.py index 08d57ad5..82a8204e 100644 --- a/libs/computer-server/computer_server/handlers/base.py +++ b/libs/computer-server/computer_server/handlers/base.py @@ -16,6 +16,59 @@ async def find_element(self, role: Optional[str] = None, """Find an element in the accessibility tree by criteria.""" pass +class BaseFileHandler(ABC): + """Abstract base class for OS-specific file handlers.""" + + @abstractmethod + async def file_exists(self, path: str) -> Dict[str, Any]: + """Check if a file exists at the specified path.""" + pass + + @abstractmethod + async def directory_exists(self, path: str) -> Dict[str, Any]: + """Check if a directory exists at the specified path.""" + pass + + @abstractmethod + async def list_dir(self, path: str) -> Dict[str, Any]: + """List the contents of a directory.""" + pass + + @abstractmethod + async def read_text(self, path: str) -> Dict[str, Any]: + """Read the text contents of a file.""" + pass + + @abstractmethod + async def write_text(self, path: str, content: str) -> Dict[str, Any]: + """Write text content to a file.""" + pass + + @abstractmethod + async def read_bytes(self, path: str) -> Dict[str, Any]: + """Read the binary contents of a file. Sent over the websocket as a base64 string.""" + pass + + @abstractmethod + async def write_bytes(self, path: str, content_b64: str) -> Dict[str, Any]: + """Write binary content to a file. Sent over the websocket as a base64 string.""" + pass + + @abstractmethod + async def delete_file(self, path: str) -> Dict[str, Any]: + """Delete a file.""" + pass + + @abstractmethod + async def create_dir(self, path: str) -> Dict[str, Any]: + """Create a directory.""" + pass + + @abstractmethod + async def delete_dir(self, path: str) -> Dict[str, Any]: + """Delete a directory.""" + pass + class BaseAutomationHandler(ABC): """Abstract base class for OS-specific automation handlers. @@ -28,6 +81,16 @@ class BaseAutomationHandler(ABC): """ # Mouse Actions + @abstractmethod + async def mouse_down(self, x: Optional[int] = None, y: Optional[int] = None, button: str = "left") -> Dict[str, Any]: + """Perform a mouse down at the current or specified position.""" + pass + + @abstractmethod + async def mouse_up(self, x: Optional[int] = None, y: Optional[int] = None, button: str = "left") -> Dict[str, Any]: + """Perform a mouse up at the current or specified position.""" + pass + @abstractmethod async def left_click(self, x: Optional[int] = None, y: Optional[int] = None) -> Dict[str, Any]: """Perform a left click at the current or specified position.""" @@ -72,6 +135,16 @@ async def drag(self, path: List[Tuple[int, int]], button: str = "left", duration pass # Keyboard Actions + @abstractmethod + async def key_down(self, key: str) -> Dict[str, Any]: + """Press and hold the specified key.""" + pass + + @abstractmethod + async def key_up(self, key: str) -> Dict[str, Any]: + """Release the specified key.""" + pass + @abstractmethod async def type_text(self, text: str) -> Dict[str, Any]: """Type the specified text.""" @@ -88,6 +161,11 @@ async def hotkey(self, *keys: str) -> Dict[str, Any]: pass # Scrolling Actions + @abstractmethod + async def scroll(self, x: int, y: int) -> Dict[str, Any]: + """Scroll the specified amount.""" + pass + @abstractmethod async def scroll_down(self, clicks: int = 1) -> Dict[str, Any]: """Scroll down by the specified number of clicks.""" diff --git a/libs/computer-server/computer_server/handlers/factory.py b/libs/computer-server/computer_server/handlers/factory.py index 3e21af11..5a9dc414 100644 --- a/libs/computer-server/computer_server/handlers/factory.py +++ b/libs/computer-server/computer_server/handlers/factory.py @@ -1,9 +1,18 @@ import platform import subprocess from typing import Tuple, Type -from .base import BaseAccessibilityHandler, BaseAutomationHandler -from .macos import MacOSAccessibilityHandler, MacOSAutomationHandler -# from .linux import LinuxAccessibilityHandler, LinuxAutomationHandler +from .base import BaseAccessibilityHandler, BaseAutomationHandler, BaseFileHandler +from computer_server.diorama.base import BaseDioramaHandler + +# Conditionally import platform-specific handlers +system = platform.system().lower() +if system == 'darwin': + from .macos import MacOSAccessibilityHandler, MacOSAutomationHandler + from computer_server.diorama.macos import MacOSDioramaHandler +elif system == 'linux': + from .linux import LinuxAccessibilityHandler, LinuxAutomationHandler + +from .generic import GenericFileHandler class HandlerFactory: """Factory for creating OS-specific handlers.""" @@ -19,7 +28,12 @@ def _get_current_os() -> str: RuntimeError: If unable to determine the current OS """ try: - # Use uname -s to determine OS since this runs on the target machine + # Use platform.system() as primary method + system = platform.system().lower() + if system in ['darwin', 'linux', 'windows']: + return 'darwin' if system == 'darwin' else 'linux' if system == 'linux' else 'windows' + + # Fallback to uname if platform.system() doesn't return expected values result = subprocess.run(['uname', '-s'], capture_output=True, text=True) if result.returncode != 0: raise RuntimeError(f"uname command failed: {result.stderr}") @@ -28,13 +42,13 @@ def _get_current_os() -> str: raise RuntimeError(f"Failed to determine current OS: {str(e)}") @staticmethod - def create_handlers() -> Tuple[BaseAccessibilityHandler, BaseAutomationHandler]: + def create_handlers() -> Tuple[BaseAccessibilityHandler, BaseAutomationHandler, BaseDioramaHandler, BaseFileHandler]: """Create and return appropriate handlers for the current OS. Returns: - Tuple[BaseAccessibilityHandler, BaseAutomationHandler]: A tuple containing - the appropriate accessibility and automation handlers for the current OS. - + Tuple[BaseAccessibilityHandler, BaseAutomationHandler, BaseDioramaHandler, BaseFileHandler]: A tuple containing + the appropriate accessibility, automation, diorama, and file handlers for the current OS. + Raises: NotImplementedError: If the current OS is not supported RuntimeError: If unable to determine the current OS @@ -42,8 +56,8 @@ def create_handlers() -> Tuple[BaseAccessibilityHandler, BaseAutomationHandler]: os_type = HandlerFactory._get_current_os() if os_type == 'darwin': - return MacOSAccessibilityHandler(), MacOSAutomationHandler() - # elif os_type == 'linux': - # return LinuxAccessibilityHandler(), LinuxAutomationHandler() + return MacOSAccessibilityHandler(), MacOSAutomationHandler(), MacOSDioramaHandler(), GenericFileHandler() + elif os_type == 'linux': + return LinuxAccessibilityHandler(), LinuxAutomationHandler(), BaseDioramaHandler(), GenericFileHandler() else: raise NotImplementedError(f"OS '{os_type}' is not supported") \ No newline at end of file diff --git a/libs/computer-server/computer_server/handlers/generic.py b/libs/computer-server/computer_server/handlers/generic.py new file mode 100644 index 00000000..784900ef --- /dev/null +++ b/libs/computer-server/computer_server/handlers/generic.py @@ -0,0 +1,82 @@ +""" +Generic handlers for all OSes. + +Includes: +- FileHandler + +""" + +from pathlib import Path +from typing import Dict, Any +from .base import BaseFileHandler +import base64 + +def resolve_path(path: str) -> Path: + """Resolve a path to its absolute path. Expand ~ to the user's home directory.""" + return Path(path).expanduser().resolve() + +class GenericFileHandler(BaseFileHandler): + async def file_exists(self, path: str) -> Dict[str, Any]: + try: + return {"success": True, "exists": resolve_path(path).is_file()} + except Exception as e: + return {"success": False, "error": str(e)} + + async def directory_exists(self, path: str) -> Dict[str, Any]: + try: + return {"success": True, "exists": resolve_path(path).is_dir()} + except Exception as e: + return {"success": False, "error": str(e)} + + async def list_dir(self, path: str) -> Dict[str, Any]: + try: + return {"success": True, "files": [p.name for p in resolve_path(path).iterdir() if p.is_file() or p.is_dir()]} + except Exception as e: + return {"success": False, "error": str(e)} + + async def read_text(self, path: str) -> Dict[str, Any]: + try: + return {"success": True, "content": resolve_path(path).read_text()} + except Exception as e: + return {"success": False, "error": str(e)} + + async def write_text(self, path: str, content: str) -> Dict[str, Any]: + try: + resolve_path(path).write_text(content) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def write_bytes(self, path: str, content_b64: str) -> Dict[str, Any]: + try: + resolve_path(path).write_bytes(base64.b64decode(content_b64)) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def read_bytes(self, path: str) -> Dict[str, Any]: + try: + return {"success": True, "content_b64": base64.b64encode(resolve_path(path).read_bytes()).decode('utf-8')} + except Exception as e: + return {"success": False, "error": str(e)} + + async def delete_file(self, path: str) -> Dict[str, Any]: + try: + resolve_path(path).unlink() + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def create_dir(self, path: str) -> Dict[str, Any]: + try: + resolve_path(path).mkdir(parents=True, exist_ok=True) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def delete_dir(self, path: str) -> Dict[str, Any]: + try: + resolve_path(path).rmdir() + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} diff --git a/libs/computer-server/computer_server/handlers/linux.py b/libs/computer-server/computer_server/handlers/linux.py new file mode 100644 index 00000000..ac0bb91d --- /dev/null +++ b/libs/computer-server/computer_server/handlers/linux.py @@ -0,0 +1,284 @@ +""" +Linux implementation of automation and accessibility handlers. + +This implementation attempts to use pyautogui for GUI automation when available. +If running in a headless environment without X11, it will fall back to simulated responses. +To use GUI automation in a headless environment: +1. Install Xvfb: sudo apt-get install xvfb +2. Run with virtual display: xvfb-run python -m computer_server +""" +from typing import Dict, Any, List, Tuple, Optional +import logging +import subprocess +import base64 +import os +import json +from io import BytesIO + +# Configure logger +logger = logging.getLogger(__name__) + +# Try to import pyautogui, but don't fail if it's not available +# This allows the server to run in headless environments +try: + import pyautogui + + logger.info("pyautogui successfully imported, GUI automation available") +except Exception as e: + logger.warning(f"pyautogui import failed: {str(e)}. GUI operations will be simulated.") + +from .base import BaseAccessibilityHandler, BaseAutomationHandler + +class LinuxAccessibilityHandler(BaseAccessibilityHandler): + """Linux implementation of accessibility handler.""" + + async def get_accessibility_tree(self) -> Dict[str, Any]: + """Get the accessibility tree of the current window.""" + # Linux doesn't have equivalent accessibility API like macOS + # Return a minimal dummy tree + logger.info("Getting accessibility tree (simulated, no accessibility API available on Linux)") + return { + "success": True, + "tree": { + "role": "Window", + "title": "Linux Window", + "position": {"x": 0, "y": 0}, + "size": {"width": 1920, "height": 1080}, + "children": [] + } + } + + async def find_element(self, role: Optional[str] = None, + title: Optional[str] = None, + value: Optional[str] = None) -> Dict[str, Any]: + """Find an element in the accessibility tree by criteria.""" + logger.info(f"Finding element with role={role}, title={title}, value={value} (not supported on Linux)") + return { + "success": False, + "message": "Element search not supported on Linux" + } + + def get_cursor_position(self) -> Tuple[int, int]: + """Get the current cursor position.""" + try: + pos = pyautogui.position() + return pos.x, pos.y + except Exception as e: + logger.warning(f"Failed to get cursor position with pyautogui: {e}") + + logger.info("Getting cursor position (simulated)") + return 0, 0 + + def get_screen_size(self) -> Tuple[int, int]: + """Get the screen size.""" + try: + size = pyautogui.size() + return size.width, size.height + except Exception as e: + logger.warning(f"Failed to get screen size with pyautogui: {e}") + + logger.info("Getting screen size (simulated)") + return 1920, 1080 + +class LinuxAutomationHandler(BaseAutomationHandler): + """Linux implementation of automation handler using pyautogui.""" + + # Mouse Actions + async def mouse_down(self, x: Optional[int] = None, y: Optional[int] = None, button: str = "left") -> Dict[str, Any]: + try: + if x is not None and y is not None: + pyautogui.moveTo(x, y) + pyautogui.mouseDown(button=button) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def mouse_up(self, x: Optional[int] = None, y: Optional[int] = None, button: str = "left") -> Dict[str, Any]: + try: + if x is not None and y is not None: + pyautogui.moveTo(x, y) + pyautogui.mouseUp(button=button) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def move_cursor(self, x: int, y: int) -> Dict[str, Any]: + try: + pyautogui.moveTo(x, y) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def left_click(self, x: Optional[int] = None, y: Optional[int] = None) -> Dict[str, Any]: + try: + if x is not None and y is not None: + pyautogui.moveTo(x, y) + pyautogui.click() + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def right_click(self, x: Optional[int] = None, y: Optional[int] = None) -> Dict[str, Any]: + try: + if x is not None and y is not None: + pyautogui.moveTo(x, y) + pyautogui.rightClick() + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def double_click(self, x: Optional[int] = None, y: Optional[int] = None) -> Dict[str, Any]: + try: + if x is not None and y is not None: + pyautogui.moveTo(x, y) + pyautogui.doubleClick(interval=0.1) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def click(self, x: Optional[int] = None, y: Optional[int] = None, button: str = "left") -> Dict[str, Any]: + try: + if x is not None and y is not None: + pyautogui.moveTo(x, y) + pyautogui.click(button=button) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def drag_to(self, x: int, y: int, button: str = "left", duration: float = 0.5) -> Dict[str, Any]: + try: + pyautogui.dragTo(x, y, duration=duration, button=button) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def drag(self, start_x: int, start_y: int, end_x: int, end_y: int, button: str = "left") -> Dict[str, Any]: + try: + pyautogui.moveTo(start_x, start_y) + pyautogui.dragTo(end_x, end_y, duration=0.5, button=button) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def drag_path(self, path: List[Tuple[int, int]], button: str = "left", duration: float = 0.5) -> Dict[str, Any]: + try: + if not path: + return {"success": False, "error": "Path is empty"} + pyautogui.moveTo(*path[0]) + for x, y in path[1:]: + pyautogui.dragTo(x, y, duration=duration, button=button) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + # Keyboard Actions + async def key_down(self, key: str) -> Dict[str, Any]: + try: + pyautogui.keyDown(key) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def key_up(self, key: str) -> Dict[str, Any]: + try: + pyautogui.keyUp(key) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def type_text(self, text: str) -> Dict[str, Any]: + try: + pyautogui.write(text) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def press_key(self, key: str) -> Dict[str, Any]: + try: + pyautogui.press(key) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def hotkey(self, keys: List[str]) -> Dict[str, Any]: + try: + pyautogui.hotkey(*keys) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + # Scrolling Actions + async def scroll(self, x: int, y: int) -> Dict[str, Any]: + try: + pyautogui.scroll(x, y) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def scroll_down(self, clicks: int = 1) -> Dict[str, Any]: + try: + pyautogui.scroll(-clicks) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def scroll_up(self, clicks: int = 1) -> Dict[str, Any]: + try: + pyautogui.scroll(clicks) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + # Screen Actions + async def screenshot(self) -> Dict[str, Any]: + try: + from PIL import Image + screenshot = pyautogui.screenshot() + if not isinstance(screenshot, Image.Image): + return {"success": False, "error": "Failed to capture screenshot"} + buffered = BytesIO() + screenshot.save(buffered, format="PNG", optimize=True) + buffered.seek(0) + image_data = base64.b64encode(buffered.getvalue()).decode() + return {"success": True, "image_data": image_data} + except Exception as e: + return {"success": False, "error": f"Screenshot error: {str(e)}"} + + async def get_screen_size(self) -> Dict[str, Any]: + try: + size = pyautogui.size() + return {"success": True, "size": {"width": size.width, "height": size.height}} + except Exception as e: + return {"success": False, "error": str(e)} + + async def get_cursor_position(self) -> Dict[str, Any]: + try: + pos = pyautogui.position() + return {"success": True, "position": {"x": pos.x, "y": pos.y}} + except Exception as e: + return {"success": False, "error": str(e)} + + # Clipboard Actions + async def copy_to_clipboard(self) -> Dict[str, Any]: + try: + import pyperclip + content = pyperclip.paste() + return {"success": True, "content": content} + except Exception as e: + return {"success": False, "error": str(e)} + + async def set_clipboard(self, text: str) -> Dict[str, Any]: + try: + import pyperclip + pyperclip.copy(text) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + # Command Execution + async def run_command(self, command: str) -> Dict[str, Any]: + try: + process = subprocess.run(command, shell=True, capture_output=True, text=True) + return {"success": True, "stdout": process.stdout, "stderr": process.stderr, "return_code": process.returncode} + except Exception as e: + return {"success": False, "error": str(e)} diff --git a/libs/computer-server/computer_server/handlers/macos.py b/libs/computer-server/computer_server/handlers/macos.py index abdedc41..9d8e344a 100644 --- a/libs/computer-server/computer_server/handlers/macos.py +++ b/libs/computer-server/computer_server/handlers/macos.py @@ -1,4 +1,7 @@ import pyautogui +from pynput.mouse import Button, Controller as MouseController +from pynput.keyboard import Key, Controller as KeyboardController +import time import base64 from io import BytesIO from typing import Optional, Dict, Any, List, Tuple @@ -33,13 +36,43 @@ AXValueGetValue, # type: ignore kAXVisibleChildrenAttribute, # type: ignore kAXRoleDescriptionAttribute, # type: ignore + kAXFocusedApplicationAttribute, # type: ignore + kAXFocusedUIElementAttribute, # type: ignore + kAXSelectedTextAttribute, # type: ignore + kAXSelectedTextRangeAttribute, # type: ignore ) import objc import re import json import copy from .base import BaseAccessibilityHandler, BaseAutomationHandler - +import logging + +logger = logging.getLogger(__name__) + +# Constants for accessibility API +kAXErrorSuccess = 0 +kAXRoleAttribute = "AXRole" +kAXTitleAttribute = "AXTitle" +kAXValueAttribute = "AXValue" +kAXWindowsAttribute = "AXWindows" +kAXFocusedAttribute = "AXFocused" +kAXPositionAttribute = "AXPosition" +kAXSizeAttribute = "AXSize" +kAXChildrenAttribute = "AXChildren" +kAXMenuBarAttribute = "AXMenuBar" +kAXMenuBarItemAttribute = "AXMenuBarItem" + +# Constants for window properties +kCGWindowLayer = "kCGWindowLayer" # Z-order information (lower values are higher in the stack) +kCGWindowAlpha = "kCGWindowAlpha" # Window opacity + +# Constants for application activation options +NSApplicationActivationOptions = { + "regular": 0, # Default activation + "bringing_all_windows_forward": 1 << 0, # NSApplicationActivateAllWindows + "ignoring_other_apps": 1 << 1 # NSApplicationActivateIgnoringOtherApps +} def CFAttributeToPyObject(attrValue): def list_helper(list_value): @@ -200,15 +233,15 @@ def __init__(self, element, offset_x=0, offset_y=0, max_depth=None, parents_visi self.calculate_hashes() def _set_bboxes(self, parents_visible_bbox): - if not self.position or not self.size: + if not self.absolute_position or not self.size: self.bbox = None self.visible_bbox = None return self.bbox = [ - int(self.position.x), - int(self.position.y), - int(self.position.x + self.size.width), - int(self.position.y + self.size.height), + int(self.absolute_position.x), + int(self.absolute_position.y), + int(self.absolute_position.x + self.size.width), + int(self.absolute_position.y + self.size.height), ] if parents_visible_bbox: # check if not intersected @@ -317,7 +350,7 @@ def children_to_dict(children): size = f"{self.size.width:.0f};{self.size.height:.0f}" else: size = "" - + return { "id": self.identifier, "name": self.name, @@ -335,7 +368,221 @@ def children_to_dict(children): } +import Quartz +from AppKit import NSWorkspace, NSRunningApplication +from pathlib import Path + +def get_all_windows_zorder(): + window_list = Quartz.CGWindowListCopyWindowInfo( + Quartz.kCGWindowListOptionOnScreenOnly, + Quartz.kCGNullWindowID + ) + z_order = {window['kCGWindowNumber']: z_index for z_index, window in enumerate(window_list[::-1])} + window_list_all = Quartz.CGWindowListCopyWindowInfo( + Quartz.kCGWindowListOptionAll, + Quartz.kCGNullWindowID + ) + windows = [] + for window in window_list_all: + window_id = window.get('kCGWindowNumber', 0) + window_name = window.get('kCGWindowName', '') + window_pid = window.get('kCGWindowOwnerPID', 0) + window_bounds = window.get('kCGWindowBounds', {}) + window_owner = window.get('kCGWindowOwnerName', '') + window_is_on_screen = window.get('kCGWindowIsOnscreen', False) + layer = window.get('kCGWindowLayer', 0) + opacity = window.get('kCGWindowAlpha', 1.0) + z_index = z_order.get(window_id, -1) + if window_name == "Dock" and window_owner == "Dock": + role = "dock" + elif window_name == "Menubar" and window_owner == "Window Server": + role = "menubar" + elif window_owner in ["Window Server", "Dock"]: + role = "desktop" + else: + role = "app" + if window_bounds: + windows.append({ + "id": window_id, + "name": window_name or "Unnamed Window", + "pid": window_pid, + "owner": window_owner, + "role": role, + "is_on_screen": window_is_on_screen, + "bounds": { + "x": window_bounds.get('X', 0), + "y": window_bounds.get('Y', 0), + "width": window_bounds.get('Width', 0), + "height": window_bounds.get('Height', 0) + }, + "layer": layer, + "z_index": z_index, + "opacity": opacity + }) + windows = sorted(windows, key=lambda x: x["z_index"]) + return windows + +def get_app_info(app): + return { + "name": app.localizedName(), + "bundle_id": app.bundleIdentifier(), + "pid": app.processIdentifier(), + "active": app.isActive(), + "hidden": app.isHidden(), + "terminated": app.isTerminated(), + } + +def get_menubar_items(active_app_pid=None): + menubar_items = [] + if active_app_pid is None: + frontmost_app = NSWorkspace.sharedWorkspace().frontmostApplication() + if frontmost_app: + active_app_pid = frontmost_app.processIdentifier() + else: + return menubar_items + app_element = AXUIElementCreateApplication(active_app_pid) + if app_element is None: + return menubar_items + menubar = element_attribute(app_element, kAXMenuBarAttribute) + if menubar is None: + return menubar_items + children = element_attribute(menubar, kAXChildrenAttribute) + if children is None: + return menubar_items + for i, item in enumerate(children): + title = element_attribute(item, kAXTitleAttribute) or "Untitled" + bounds = {"x": 0, "y": 0, "width": 0, "height": 0} + position_value = element_attribute(item, kAXPositionAttribute) + if position_value: + position_value = element_value(position_value, kAXValueCGPointType) + bounds["x"] = getattr(position_value, 'x', 0) + bounds["y"] = getattr(position_value, 'y', 0) + size_value = element_attribute(item, kAXSizeAttribute) + if size_value: + size_value = element_value(size_value, kAXValueCGSizeType) + bounds["width"] = getattr(size_value, 'width', 0) + bounds["height"] = getattr(size_value, 'height', 0) + menubar_items.append({ + "title": title, + "bounds": bounds, + "index": i, + "app_pid": active_app_pid + }) + return menubar_items + +def get_dock_items(): + dock_items = [] + dock_pid = None + running_apps = NSWorkspace.sharedWorkspace().runningApplications() + for app in running_apps: + if app.localizedName() == "Dock" and app.bundleIdentifier() == "com.apple.dock": + dock_pid = app.processIdentifier() + break + if dock_pid is None: + return dock_items + dock_element = AXUIElementCreateApplication(dock_pid) + if dock_element is None: + return dock_items + dock_list = element_attribute(dock_element, kAXChildrenAttribute) + if dock_list is None or len(dock_list) == 0: + return dock_items + dock_app_list = None + for child in dock_list: + role = element_attribute(child, kAXRoleAttribute) + if role == "AXList": + dock_app_list = child + break + if dock_app_list is None: + return dock_items + items = element_attribute(dock_app_list, kAXChildrenAttribute) + if items is None: + return dock_items + for i, item in enumerate(items): + title = element_attribute(item, kAXTitleAttribute) or "Untitled" + description = element_attribute(item, kAXDescriptionAttribute) or "" + role = element_attribute(item, kAXRoleAttribute) or "" + subrole = element_attribute(item, "AXSubrole") or "" + bounds = {"x": 0, "y": 0, "width": 0, "height": 0} + position_value = element_attribute(item, kAXPositionAttribute) + if position_value: + position_value = element_value(position_value, kAXValueCGPointType) + bounds["x"] = getattr(position_value, 'x', 0) + bounds["y"] = getattr(position_value, 'y', 0) + size_value = element_attribute(item, kAXSizeAttribute) + if size_value: + size_value = element_value(size_value, kAXValueCGSizeType) + bounds["width"] = getattr(size_value, 'width', 0) + bounds["height"] = getattr(size_value, 'height', 0) + item_type = "unknown" + if subrole == "AXApplicationDockItem": + item_type = "application" + elif subrole == "AXFolderDockItem": + item_type = "folder" + elif subrole == "AXDocumentDockItem": + item_type = "document" + elif subrole == "AXSeparatorDockItem" or role == "AXSeparator": + item_type = "separator" + elif "trash" in title.lower(): + item_type = "trash" + dock_items.append({ + "title": title, + "description": description, + "bounds": bounds, + "index": i, + "type": item_type, + "role": role, + "subrole": subrole + }) + return dock_items + class MacOSAccessibilityHandler(BaseAccessibilityHandler): + def get_desktop_state(self): + windows = [w for w in get_all_windows_zorder() if w.get("is_on_screen")] + running_apps = self.get_running_apps() + applications = [] + pid_to_window_ids = {} + # Build a mapping: pid -> list of AX window trees + pid_to_ax_trees = {} + for app in running_apps: + pid = app.processIdentifier() + try: + app_elem = AXUIElementCreateApplication(pid) + err, app_windows = AXUIElementCopyAttributeValue(app_elem, kAXWindowsAttribute, None) + trees = [] + if err == kAXErrorSuccess and app_windows: + for ax_win in app_windows: + try: + trees.append(UIElement(ax_win).to_dict()) + except Exception as e: + trees.append({"error": str(e)}) + pid_to_ax_trees[pid] = trees + except Exception as e: + pid_to_ax_trees[pid] = [{"error": str(e)}] + # Attach children by pid and index (order) + pid_to_idx = {} + for win in windows: + pid = win["pid"] + idx = pid_to_idx.get(pid, 0) + ax_trees = pid_to_ax_trees.get(pid, []) + win["children"] = ax_trees[idx]["children"] if idx < len(ax_trees) and "children" in ax_trees[idx] else [] + pid_to_idx[pid] = idx + 1 + pid_to_window_ids.setdefault(pid, []).append(win["id"]) + for app in running_apps: + info = get_app_info(app) + app_pid = info["pid"] + applications.append({ + "info": info, + "windows": pid_to_window_ids.get(app_pid, []) + }) + menubar_items = get_menubar_items() + dock_items = get_dock_items() + return { + "applications": applications, + "windows": windows, + "menubar_items": menubar_items, + "dock_items": dock_items + } + def get_application_windows(self, pid: int): """Get all windows for a specific application.""" try: @@ -420,64 +667,13 @@ def serialize_node(self, element): return result - async def get_accessibility_tree(self) -> Dict[str, Any]: + async def get_accessibility_tree(self) -> Dict[str, Any]: try: - # Get all visible windows first - windows = self.get_all_windows() - if not windows: - return {"success": False, "error": "No visible windows found in the system"} - - # Get the frontmost window - frontmost_app = next((w for w in windows if w["frontmost"]), None) - if not frontmost_app: - frontmost_app = windows[0] - - app_name = frontmost_app["app_name"] - - # Process all applications and their windows - processed_windows = [] - for app in windows: - app_windows = app.get("windows", []) - if app_windows: - window_trees = [] - for window in app_windows: - try: - window_element = UIElement(window) - window_trees.append(window_element.to_dict()) - except: - continue - - processed_windows.append( - { - "app_name": app["app_name"], - "pid": app["pid"], - "frontmost": app["frontmost"], - "has_windows": app["has_windows"], - "windows": window_trees, - } - ) - - if not any(app["windows"] for app in processed_windows): - return { - "success": False, - "error": f"No accessible windows found. Available applications:\n" - + "\n".join( - [ - f"- {w['app_name']} (PID: {w['pid']}, Active: {w['frontmost']}, Has Windows: {w['has_windows']})" - for w in windows - ] - ) - + "\nPlease ensure:\n" - + "1. The terminal has accessibility permissions\n" - + "2. The applications have visible windows\n" - + "3. Try clicking on a window you want to inspect", - } - + desktop_state = self.get_desktop_state() return { "success": True, - "frontmost_application": app_name, - "windows": processed_windows, - } + **desktop_state + } except Exception as e: return {"success": False, "error": str(e)} @@ -515,14 +711,34 @@ def search_tree(element): except Exception as e: return {"success": False, "error": str(e)} - class MacOSAutomationHandler(BaseAutomationHandler): # Mouse Actions + mouse = MouseController() + keyboard = KeyboardController() + + async def mouse_down(self, x: Optional[int] = None, y: Optional[int] = None, button: str = "left") -> Dict[str, Any]: + try: + if x is not None and y is not None: + self.mouse.position = (x, y) + self.mouse.press(Button.left if button == "left" else Button.right if button == "right" else Button.middle) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def mouse_up(self, x: Optional[int] = None, y: Optional[int] = None, button: str = "left") -> Dict[str, Any]: + try: + if x is not None and y is not None: + self.mouse.position = (x, y) + self.mouse.release(Button.left if button == "left" else Button.right if button == "right" else Button.middle) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + async def left_click(self, x: Optional[int] = None, y: Optional[int] = None) -> Dict[str, Any]: try: if x is not None and y is not None: - pyautogui.moveTo(x, y) - pyautogui.click() + self.mouse.position = (x, y) + self.mouse.click(Button.left, 1) return {"success": True} except Exception as e: return {"success": False, "error": str(e)} @@ -530,8 +746,8 @@ async def left_click(self, x: Optional[int] = None, y: Optional[int] = None) -> async def right_click(self, x: Optional[int] = None, y: Optional[int] = None) -> Dict[str, Any]: try: if x is not None and y is not None: - pyautogui.moveTo(x, y) - pyautogui.rightClick() + self.mouse.position = (x, y) + self.mouse.click(Button.right, 1) return {"success": True} except Exception as e: return {"success": False, "error": str(e)} @@ -541,15 +757,15 @@ async def double_click( ) -> Dict[str, Any]: try: if x is not None and y is not None: - pyautogui.moveTo(x, y) - pyautogui.doubleClick(interval=0.1) + self.mouse.position = (x, y) + self.mouse.click(Button.left, 2) return {"success": True} except Exception as e: return {"success": False, "error": str(e)} async def move_cursor(self, x: int, y: int) -> Dict[str, Any]: try: - pyautogui.moveTo(x, y) + self.mouse.position = (x, y) return {"success": True} except Exception as e: return {"success": False, "error": str(e)} @@ -558,9 +774,26 @@ async def drag_to( self, x: int, y: int, button: str = "left", duration: float = 0.5 ) -> Dict[str, Any]: try: - pyautogui.dragTo(x, y, button=button, duration=duration) + btn = Button.left if button == "left" else Button.right if button == "right" else Button.middle + # Press + self.mouse.press(btn) + # Move with sleep to simulate drag duration + start = self.mouse.position + steps = 20 + start_x, start_y = start + dx = (x - start_x) / steps + dy = (y - start_y) / steps + for i in range(steps): + self.mouse.position = (int(start_x + dx * (i + 1)), int(start_y + dy * (i + 1))) + time.sleep(duration / steps) + # Release + self.mouse.release(btn) return {"success": True} except Exception as e: + try: + self.mouse.release(btn) + except: + pass return {"success": False, "error": str(e)} async def drag( @@ -569,43 +802,51 @@ async def drag( try: if not path or len(path) < 2: return {"success": False, "error": "Path must contain at least 2 points"} - + btn = Button.left if button == "left" else Button.right if button == "right" else Button.middle # Move to the first point - start_x, start_y = path[0] - pyautogui.moveTo(start_x, start_y) - - # Press the mouse button - pyautogui.mouseDown(button=button) - - # Calculate time between points to distribute duration evenly + self.mouse.position = path[0] + self.mouse.press(btn) step_duration = duration / (len(path) - 1) if len(path) > 1 else duration - - # Move through each subsequent point for x, y in path[1:]: - pyautogui.moveTo(x, y, duration=step_duration) - - # Release the mouse button - pyautogui.mouseUp(button=button) - + self.mouse.position = (x, y) + time.sleep(step_duration) + self.mouse.release(btn) return {"success": True} except Exception as e: - # Make sure to release the mouse button if an error occurs try: - pyautogui.mouseUp(button=button) + self.mouse.release(btn) except: pass return {"success": False, "error": str(e)} # Keyboard Actions + async def key_down(self, key: str) -> Dict[str, Any]: + try: + # use pyautogui for their key names + pyautogui.keyDown(key) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def key_up(self, key: str) -> Dict[str, Any]: + try: + # use pyautogui for their key names + pyautogui.keyUp(key) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + async def type_text(self, text: str) -> Dict[str, Any]: try: - pyautogui.write(text) + # use pynput for Unicode support + self.keyboard.type(text) return {"success": True} except Exception as e: return {"success": False, "error": str(e)} async def press_key(self, key: str) -> Dict[str, Any]: try: + # use pyautogui for their key names pyautogui.press(key) return {"success": True} except Exception as e: @@ -613,22 +854,30 @@ async def press_key(self, key: str) -> Dict[str, Any]: async def hotkey(self, keys: List[str]) -> Dict[str, Any]: try: + # use pyautogui for their key names pyautogui.hotkey(*keys) return {"success": True} except Exception as e: return {"success": False, "error": str(e)} # Scrolling Actions + async def scroll(self, x: int, y: int) -> Dict[str, Any]: + try: + self.mouse.scroll(x, y) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + async def scroll_down(self, clicks: int = 1) -> Dict[str, Any]: try: - pyautogui.scroll(-clicks) + self.mouse.scroll(0, -clicks) return {"success": True} except Exception as e: return {"success": False, "error": str(e)} async def scroll_up(self, clicks: int = 1) -> Dict[str, Any]: try: - pyautogui.scroll(clicks) + self.mouse.scroll(0, clicks) return {"success": True} except Exception as e: return {"success": False, "error": str(e)} @@ -659,8 +908,8 @@ async def get_screen_size(self) -> Dict[str, Any]: async def get_cursor_position(self) -> Dict[str, Any]: try: - pos = pyautogui.position() - return {"success": True, "position": {"x": pos.x, "y": pos.y}} + x, y = self.mouse.position + return {"success": True, "position": {"x": x, "y": y}} except Exception as e: return {"success": False, "error": str(e)} diff --git a/libs/computer-server/computer_server/main.py b/libs/computer-server/computer_server/main.py index d7f66f89..bdca3693 100644 --- a/libs/computer-server/computer_server/main.py +++ b/libs/computer-server/computer_server/main.py @@ -8,11 +8,11 @@ from contextlib import redirect_stdout, redirect_stderr from io import StringIO from .handlers.factory import HandlerFactory +import os +import aiohttp # Set up logging with more detail -logging.basicConfig( - level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" -) +logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Configure WebSocket with larger message size @@ -31,7 +31,7 @@ class ConnectionManager: def __init__(self): self.active_connections: List[WebSocket] = [] # Create OS-specific handlers - self.accessibility_handler, self.automation_handler = HandlerFactory.create_handlers() + self.accessibility_handler, self.automation_handler, self.diorama_handler, self.file_handler = HandlerFactory.create_handlers() async def connect(self, websocket: WebSocket): await websocket.accept() @@ -48,30 +48,159 @@ def disconnect(self, websocket: WebSocket): async def websocket_endpoint(websocket: WebSocket): # WebSocket message size is configured at the app or endpoint level, not on the instance await manager.connect(websocket) + + # Check if CONTAINER_NAME is set (indicating cloud provider) + container_name = os.environ.get("CONTAINER_NAME") + + # If cloud provider, perform authentication handshake + if container_name: + try: + logger.info(f"Cloud provider detected. CONTAINER_NAME: {container_name}. Waiting for authentication...") + + # Wait for authentication message + auth_data = await websocket.receive_json() + + # Validate auth message format + if auth_data.get("command") != "authenticate": + await websocket.send_json({ + "success": False, + "error": "First message must be authentication" + }) + await websocket.close() + manager.disconnect(websocket) + return + + # Extract credentials + client_api_key = auth_data.get("params", {}).get("api_key") + client_container_name = auth_data.get("params", {}).get("container_name") + + # Layer 1: VM Identity Verification + if client_container_name != container_name: + logger.warning(f"VM name mismatch. Expected: {container_name}, Got: {client_container_name}") + await websocket.send_json({ + "success": False, + "error": "VM name mismatch" + }) + await websocket.close() + manager.disconnect(websocket) + return + + # Layer 2: API Key Validation with TryCUA API + if not client_api_key: + await websocket.send_json({ + "success": False, + "error": "API key required" + }) + await websocket.close() + manager.disconnect(websocket) + return + + # Validate with TryCUA API + try: + async with aiohttp.ClientSession() as session: + headers = { + "Authorization": f"Bearer {client_api_key}" + } + + async with session.get( + f"https://www.trycua.com/api/vm/auth?container_name={container_name}", + headers=headers, + ) as resp: + if resp.status != 200: + error_msg = await resp.text() + logger.warning(f"API validation failed: {error_msg}") + await websocket.send_json({ + "success": False, + "error": "Authentication failed" + }) + await websocket.close() + manager.disconnect(websocket) + return + + # If we get a 200 response with VNC URL, the VM exists and user has access + vnc_url = (await resp.text()).strip() + if not vnc_url: + logger.warning(f"No VNC URL returned for VM: {container_name}") + await websocket.send_json({ + "success": False, + "error": "VM not found" + }) + await websocket.close() + manager.disconnect(websocket) + return + + logger.info(f"Authentication successful for VM: {container_name}") + await websocket.send_json({ + "success": True, + "message": "Authenticated" + }) + + except Exception as e: + logger.error(f"Error validating with TryCUA API: {e}") + await websocket.send_json({ + "success": False, + "error": "Authentication service unavailable" + }) + await websocket.close() + manager.disconnect(websocket) + return + + except Exception as e: + logger.error(f"Authentication error: {e}") + await websocket.send_json({ + "success": False, + "error": "Authentication failed" + }) + await websocket.close() + manager.disconnect(websocket) + return # Map commands to appropriate handler methods handlers = { + # App-Use commands + "diorama_cmd": manager.diorama_handler.diorama_cmd, # Accessibility commands "get_accessibility_tree": manager.accessibility_handler.get_accessibility_tree, "find_element": manager.accessibility_handler.find_element, - # Automation commands - "screenshot": manager.automation_handler.screenshot, + # Shell commands + "run_command": manager.automation_handler.run_command, + # File system commands + "file_exists": manager.file_handler.file_exists, + "directory_exists": manager.file_handler.directory_exists, + "list_dir": manager.file_handler.list_dir, + "read_text": manager.file_handler.read_text, + "write_text": manager.file_handler.write_text, + "read_bytes": manager.file_handler.read_bytes, + "write_bytes": manager.file_handler.write_bytes, + "delete_file": manager.file_handler.delete_file, + "create_dir": manager.file_handler.create_dir, + "delete_dir": manager.file_handler.delete_dir, + # Mouse commands + "mouse_down": manager.automation_handler.mouse_down, + "mouse_up": manager.automation_handler.mouse_up, "left_click": manager.automation_handler.left_click, "right_click": manager.automation_handler.right_click, "double_click": manager.automation_handler.double_click, - "scroll_down": manager.automation_handler.scroll_down, - "scroll_up": manager.automation_handler.scroll_up, "move_cursor": manager.automation_handler.move_cursor, - "type_text": manager.automation_handler.type_text, - "press_key": manager.automation_handler.press_key, "drag_to": manager.automation_handler.drag_to, "drag": manager.automation_handler.drag, + # Keyboard commands + "key_down": manager.automation_handler.key_down, + "key_up": manager.automation_handler.key_up, + "type_text": manager.automation_handler.type_text, + "press_key": manager.automation_handler.press_key, "hotkey": manager.automation_handler.hotkey, + # Scrolling actions + "scroll": manager.automation_handler.scroll, + "scroll_down": manager.automation_handler.scroll_down, + "scroll_up": manager.automation_handler.scroll_up, + # Screen actions + "screenshot": manager.automation_handler.screenshot, "get_cursor_position": manager.automation_handler.get_cursor_position, "get_screen_size": manager.automation_handler.get_screen_size, + # Clipboard actions "copy_to_clipboard": manager.automation_handler.copy_to_clipboard, "set_clipboard": manager.automation_handler.set_clipboard, - "run_command": manager.automation_handler.run_command, } try: diff --git a/libs/computer-server/computer_server/server.py b/libs/computer-server/computer_server/server.py index 2a3d4340..aed874d4 100644 --- a/libs/computer-server/computer_server/server.py +++ b/libs/computer-server/computer_server/server.py @@ -32,7 +32,8 @@ class Server: await server.stop() # Stop the server """ - def __init__(self, host: str = "0.0.0.0", port: int = 8000, log_level: str = "info"): + def __init__(self, host: str = "0.0.0.0", port: int = 8000, log_level: str = "info", + ssl_keyfile: Optional[str] = None, ssl_certfile: Optional[str] = None): """ Initialize the server. @@ -40,10 +41,14 @@ def __init__(self, host: str = "0.0.0.0", port: int = 8000, log_level: str = "in host: Host to bind the server to port: Port to bind the server to log_level: Logging level (debug, info, warning, error, critical) + ssl_keyfile: Path to SSL private key file (for HTTPS) + ssl_certfile: Path to SSL certificate file (for HTTPS) """ self.host = host self.port = port self.log_level = log_level + self.ssl_keyfile = ssl_keyfile + self.ssl_certfile = ssl_certfile self.app = fastapi_app self._server_task: Optional[asyncio.Task] = None self._should_exit = asyncio.Event() @@ -52,7 +57,14 @@ def start(self) -> None: """ Start the server synchronously. This will block until the server is stopped. """ - uvicorn.run(self.app, host=self.host, port=self.port, log_level=self.log_level) + uvicorn.run( + self.app, + host=self.host, + port=self.port, + log_level=self.log_level, + ssl_keyfile=self.ssl_keyfile, + ssl_certfile=self.ssl_certfile + ) async def start_async(self) -> None: """ @@ -60,7 +72,12 @@ async def start_async(self) -> None: will run in the background. """ server_config = uvicorn.Config( - self.app, host=self.host, port=self.port, log_level=self.log_level + self.app, + host=self.host, + port=self.port, + log_level=self.log_level, + ssl_keyfile=self.ssl_keyfile, + ssl_certfile=self.ssl_certfile ) self._should_exit.clear() @@ -72,7 +89,8 @@ async def start_async(self) -> None: # Wait a short time to ensure the server starts await asyncio.sleep(0.5) - logger.info(f"Server started at http://{self.host}:{self.port}") + protocol = "https" if self.ssl_certfile else "http" + logger.info(f"Server started at {protocol}://{self.host}:{self.port}") async def stop(self) -> None: """ diff --git a/libs/computer-server/pyproject.toml b/libs/computer-server/pyproject.toml index cc8e76cd..cbf9821a 100644 --- a/libs/computer-server/pyproject.toml +++ b/libs/computer-server/pyproject.toml @@ -9,20 +9,28 @@ description = "Server component for the Computer-Use Interface (CUI) framework p authors = [ { name = "TryCua", email = "gh@trycua.com" } ] +readme = "README.md" +license = { text = "MIT" } +requires-python = ">=3.9" dependencies = [ "fastapi>=0.111.0", "uvicorn[standard]>=0.27.0", "pydantic>=2.0.0", "pyautogui>=0.9.54", - "pyobjc-framework-Cocoa>=10.1; sys_platform == 'darwin'", - "pyobjc-framework-Quartz>=10.1; sys_platform == 'darwin'", - "pyobjc-framework-ApplicationServices>=10.1; sys_platform == 'darwin'", - "python-xlib>=0.33; sys_platform == 'linux'", - "pillow>=10.2.0" + "pynput>=1.8.1", + "pillow>=10.2.0", + "aiohttp>=3.9.1" +] + +[project.optional-dependencies] +macos = [ + "pyobjc-framework-Cocoa>=10.1", + "pyobjc-framework-Quartz>=10.1", + "pyobjc-framework-ApplicationServices>=10.1" +] +linux = [ + "python-xlib>=0.33" ] -requires-python = ">=3.10" -readme = "README.md" -license = { text = "MIT" } [project.urls] homepage = "https://github.com/trycua/cua" @@ -47,14 +55,14 @@ format = [ "black>=23.0.0", "isort>=5.12.0" ] +dev = [ + "ruff>=0.0.241", + "mypy>=0.971" +] [tool.pdm.scripts] api = "python -m computer_server" -[tool.black] -line-length = 100 -target-version = ["py310"] - [tool.ruff] line-length = 100 target-version = "py310" diff --git a/libs/computer/README.md b/libs/computer/README.md index c817c8cd..33d4c0e3 100644 --- a/libs/computer/README.md +++ b/libs/computer/README.md @@ -54,7 +54,7 @@ finally: To install the Computer-Use Interface (CUI): ```bash -pip install cua-computer +pip install "cua-computer[all]" ``` The `cua-computer` PyPi package pulls automatically the latest executable version of Lume through [pylume](https://github.com/trycua/pylume). diff --git a/libs/computer/computer/__init__.py b/libs/computer/computer/__init__.py index e50f5983..90d20454 100644 --- a/libs/computer/computer/__init__.py +++ b/libs/computer/computer/__init__.py @@ -42,6 +42,10 @@ # Other issues with telemetry logger.warning(f"Error initializing telemetry: {e}") +# Core components from .computer import Computer -__all__ = ["Computer"] +# Provider components +from .providers.base import VMProviderType + +__all__ = ["Computer", "VMProviderType"] diff --git a/libs/computer/computer/computer.py b/libs/computer/computer/computer.py index ddb68f9e..4249f3f4 100644 --- a/libs/computer/computer/computer.py +++ b/libs/computer/computer/computer.py @@ -1,6 +1,4 @@ from typing import Optional, List, Literal, Dict, Any, Union, TYPE_CHECKING, cast -from pylume import PyLume -from pylume.models import VMRunOpts, VMUpdateOpts, ImageRef, SharedDirectory, VMStatus import asyncio from .models import Computer as ComputerConfig, Display from .interface.factory import InterfaceFactory @@ -13,17 +11,31 @@ import logging from .telemetry import record_computer_initialization import os +from . import helpers -OSType = Literal["macos", "linux"] - -# Import BaseComputerInterface for type annotations -if TYPE_CHECKING: - from .interface.base import BaseComputerInterface +# Import provider related modules +from .providers.base import VMProviderType +from .providers.factory import VMProviderFactory +OSType = Literal["macos", "linux", "windows"] class Computer: """Computer is the main class for interacting with the computer.""" + def create_desktop_from_apps(self, apps): + """ + Create a virtual desktop from a list of app names, returning a DioramaComputer + that proxies Diorama.Interface but uses diorama_cmds via the computer interface. + + Args: + apps (list[str]): List of application names to include in the desktop. + Returns: + DioramaComputer: A proxy object with the Diorama interface, but using diorama_cmds. + """ + assert "app-use" in self.experiments, "App Usage is an experimental feature. Enable it by passing experiments=['app-use'] to Computer()" + from .diorama_computer import DioramaComputer + return DioramaComputer(self, apps) + def __init__( self, display: Union[Display, Dict[str, int], str] = "1024x768", @@ -36,8 +48,14 @@ def __init__( use_host_computer_server: bool = False, verbosity: Union[int, LogLevel] = logging.INFO, telemetry_enabled: bool = True, - port: Optional[int] = 3000, + provider_type: Union[str, VMProviderType] = VMProviderType.LUME, + port: Optional[int] = 7777, + noVNC_port: Optional[int] = 8006, host: str = os.environ.get("PYLUME_HOST", "localhost"), + storage: Optional[str] = None, + ephemeral: bool = False, + api_key: Optional[str] = None, + experiments: Optional[List[str]] = None ): """Initialize a new Computer instance. @@ -49,7 +67,7 @@ def __init__( Defaults to "1024x768" memory: The VM memory allocation. Defaults to "8GB" cpu: The VM CPU allocation. Defaults to "4" - os: The operating system type ('macos' or 'linux') + os_type: The operating system type ('macos' or 'linux') name: The VM name image: The VM image name shared_directories: Optional list of directory paths to share with the VM @@ -57,8 +75,14 @@ def __init__( verbosity: Logging level (standard Python logging levels: logging.DEBUG, logging.INFO, etc.) LogLevel enum values are still accepted for backward compatibility telemetry_enabled: Whether to enable telemetry tracking. Defaults to True. - port: Optional port to use for the PyLume server - host: Host to use for PyLume connections (e.g. "localhost", "host.docker.internal") + provider_type: The VM provider type to use (lume, qemu, cloud) + port: Optional port to use for the VM provider server + noVNC_port: Optional port for the noVNC web interface (Lumier provider) + host: Host to use for VM provider connections (e.g. "localhost", "host.docker.internal") + storage: Optional path for persistent VM storage (Lumier provider) + ephemeral: Whether to use ephemeral storage + api_key: Optional API key for cloud providers + experiments: Optional list of experimental features to enable (e.g. ["app-use"]) """ self.logger = Logger("cua.computer", verbosity) @@ -67,8 +91,29 @@ def __init__( # Store original parameters self.image = image self.port = port + self.noVNC_port = noVNC_port self.host = host self.os_type = os_type + self.provider_type = provider_type + self.ephemeral = ephemeral + + self.api_key = api_key + self.experiments = experiments or [] + + if "app-use" in self.experiments: + assert self.os_type == "macos", "App use experiment is only supported on macOS" + + # The default is currently to use non-ephemeral storage + if storage and ephemeral and storage != "ephemeral": + raise ValueError("Storage path and ephemeral flag cannot be used together") + self.storage = "ephemeral" if ephemeral else storage + + # For Lumier provider, store the first shared directory path to use + # for VM file sharing + self.shared_path = None + if shared_directories and len(shared_directories) > 0: + self.shared_path = shared_directories[0] + self.logger.info(f"Using first shared directory for VM file sharing: {self.shared_path}") # Store telemetry preference self._telemetry_enabled = telemetry_enabled @@ -116,25 +161,17 @@ def __init__( memory=memory, cpu=cpu, ) - # Initialize PyLume but don't start the server yet - we'll do that in run() - self.config.pylume = PyLume( - debug=(self.verbosity == LogLevel.DEBUG), - port=3000, - use_existing_server=False, - server_start_timeout=120, # Increase timeout to 2 minutes - ) + # Initialize VM provider but don't start it yet - we'll do that in run() + self.config.vm_provider = None # Will be initialized in run() + + # Store shared directories config + self.shared_directories = shared_directories or [] + + # Placeholder for VM provider context manager + self._provider_context = None # Initialize with proper typing - None at first, will be set in run() self._interface = None - self.os = os - self.shared_paths = [] - if shared_directories: - for path in shared_directories: - abs_path = os.path.abspath(os.path.expanduser(path)) - if not os.path.exists(abs_path): - raise ValueError(f"Shared directory does not exist: {path}") - self.shared_paths.append(abs_path) - self._pylume_context = None self.use_host_computer_server = use_host_computer_server # Record initialization in telemetry (if enabled) @@ -144,27 +181,27 @@ def __init__( self.logger.debug("Telemetry disabled - skipping initialization tracking") async def __aenter__(self): - """Enter async context manager.""" + """Start the computer.""" await self.run() return self async def __aexit__(self, exc_type, exc_val, exc_tb): - """Exit async context manager.""" - pass + """Stop the computer.""" + await self.disconnect() def __enter__(self): - """Enter synchronous context manager.""" - # Run the event loop to call the async run method + """Start the computer.""" + # Run the event loop to call the async enter method loop = asyncio.get_event_loop() - loop.run_until_complete(self.run()) + loop.run_until_complete(self.__aenter__()) return self def __exit__(self, exc_type, exc_val, exc_tb): - """Exit synchronous context manager.""" - # We could add cleanup here if needed in the future - pass + """Stop the computer.""" + loop = asyncio.get_event_loop() + loop.run_until_complete(self.__aexit__(exc_type, exc_val, exc_tb)) - async def run(self) -> None: + async def run(self) -> Optional[str]: """Initialize the VM and computer interface.""" if TYPE_CHECKING: from .interface.base import BaseComputerInterface @@ -199,87 +236,170 @@ async def run(self) -> None: else: # Start or connect to VM self.logger.info(f"Starting VM: {self.image}") - if not self._pylume_context: + if not self._provider_context: try: - self.logger.verbose("Initializing PyLume context...") - - # Configure PyLume based on initialization parameters - pylume_kwargs = { - "debug": self.verbosity <= LogLevel.DEBUG, - "server_start_timeout": 120, # Increase timeout to 2 minutes - } - - # Add port if specified - if hasattr(self, "port") and self.port is not None: - pylume_kwargs["port"] = self.port - self.logger.verbose(f"Using specified port for PyLume: {self.port}") - - # Add host if specified - if hasattr(self, "host") and self.host != "localhost": - pylume_kwargs["host"] = self.host - self.logger.verbose(f"Using specified host for PyLume: {self.host}") - - # Create PyLume instance with configured parameters - self.config.pylume = PyLume(**pylume_kwargs) - - self._pylume_context = await self.config.pylume.__aenter__() # type: ignore[attr-defined] - self.logger.verbose("PyLume context initialized successfully") + provider_type_name = self.provider_type.name if isinstance(self.provider_type, VMProviderType) else self.provider_type + self.logger.verbose(f"Initializing {provider_type_name} provider context...") + + # Explicitly set provider parameters + storage = "ephemeral" if self.ephemeral else self.storage + verbose = self.verbosity >= LogLevel.DEBUG + ephemeral = self.ephemeral + port = self.port if self.port is not None else 7777 + host = self.host if self.host else "localhost" + image = self.image + shared_path = self.shared_path + noVNC_port = self.noVNC_port + + # Create VM provider instance with explicit parameters + try: + if self.provider_type == VMProviderType.LUMIER: + self.logger.info(f"Using VM image for Lumier provider: {image}") + if shared_path: + self.logger.info(f"Using shared path for Lumier provider: {shared_path}") + if noVNC_port: + self.logger.info(f"Using noVNC port for Lumier provider: {noVNC_port}") + self.config.vm_provider = VMProviderFactory.create_provider( + self.provider_type, + port=port, + host=host, + storage=storage, + shared_path=shared_path, + image=image, + verbose=verbose, + ephemeral=ephemeral, + noVNC_port=noVNC_port, + ) + elif self.provider_type == VMProviderType.LUME: + self.config.vm_provider = VMProviderFactory.create_provider( + self.provider_type, + port=port, + host=host, + storage=storage, + verbose=verbose, + ephemeral=ephemeral, + ) + elif self.provider_type == VMProviderType.CLOUD: + self.config.vm_provider = VMProviderFactory.create_provider( + self.provider_type, + api_key=self.api_key, + verbose=verbose, + ) + else: + raise ValueError(f"Unsupported provider type: {self.provider_type}") + self._provider_context = await self.config.vm_provider.__aenter__() + self.logger.verbose("VM provider context initialized successfully") + except ImportError as ie: + self.logger.error(f"Failed to import provider dependencies: {ie}") + if str(ie).find("lume") >= 0 and str(ie).find("lumier") < 0: + self.logger.error("Please install with: pip install cua-computer[lume]") + elif str(ie).find("lumier") >= 0 or str(ie).find("docker") >= 0: + self.logger.error("Please install with: pip install cua-computer[lumier] and make sure Docker is installed") + elif str(ie).find("cloud") >= 0: + self.logger.error("Please install with: pip install cua-computer[cloud]") + raise except Exception as e: - self.logger.error(f"Failed to initialize PyLume context: {e}") - raise RuntimeError(f"Failed to initialize PyLume: {e}") + self.logger.error(f"Failed to initialize provider context: {e}") + raise RuntimeError(f"Failed to initialize VM provider: {e}") - # Try to get the VM, if it doesn't exist, return an error + # Check if VM exists or create it + is_running = False try: - vm = await self.config.pylume.get_vm(self.config.name) # type: ignore[attr-defined] + if self.config.vm_provider is None: + raise RuntimeError(f"VM provider not initialized for {self.config.name}") + + vm = await self.config.vm_provider.get_vm(self.config.name) self.logger.verbose(f"Found existing VM: {self.config.name}") + is_running = vm.get("status") == "running" except Exception as e: self.logger.error(f"VM not found: {self.config.name}") - self.logger.error( - f"Please pull the VM first with lume pull macos-sequoia-cua-sparse:latest: {e}" - ) + self.logger.error(f"Error: {e}") raise RuntimeError( - f"VM not found: {self.config.name}. Please pull the VM first." - ) - - # Convert paths to SharedDirectory objects - shared_directories = [] - for path in self.shared_paths: - self.logger.verbose(f"Adding shared directory: {path}") - shared_directories.append( - SharedDirectory(host_path=path) # type: ignore[arg-type] + f"VM {self.config.name} could not be found or created." ) - # Run with shared directories - self.logger.info(f"Starting VM {self.config.name}...") - run_opts = VMRunOpts( - no_display=False, # type: ignore[arg-type] - shared_directories=shared_directories, # type: ignore[arg-type] - ) - - # Log the run options for debugging - self.logger.info(f"VM run options: {vars(run_opts)}") - - # Log the equivalent curl command for debugging - payload = json.dumps({"noDisplay": False, "sharedDirectories": []}) - curl_cmd = f"curl -X POST 'http://localhost:3000/lume/vms/{self.config.name}/run' -H 'Content-Type: application/json' -d '{payload}'" - self.logger.info(f"Equivalent curl command:") - self.logger.info(f"{curl_cmd}") + # Start the VM if it's not running + if not is_running: + self.logger.info(f"VM {self.config.name} is not running, starting it...") + + # Convert paths to dictionary format for shared directories + shared_dirs = [] + for path in self.shared_directories: + self.logger.verbose(f"Adding shared directory: {path}") + path = os.path.abspath(os.path.expanduser(path)) + if os.path.exists(path): + # Add path in format expected by Lume API + shared_dirs.append({ + "hostPath": path, + "readOnly": False + }) + else: + self.logger.warning(f"Shared directory does not exist: {path}") + + # Prepare run options to pass to the provider + run_opts = {} + + # Add display information if available + if self.config.display is not None: + display_info = { + "width": self.config.display.width, + "height": self.config.display.height, + } + + # Check if scale_factor exists before adding it + if hasattr(self.config.display, "scale_factor"): + display_info["scale_factor"] = self.config.display.scale_factor + + run_opts["display"] = display_info + + # Add shared directories if available + if self.shared_directories: + run_opts["shared_directories"] = shared_dirs.copy() + + # Run the VM with the provider + try: + if self.config.vm_provider is None: + raise RuntimeError(f"VM provider not initialized for {self.config.name}") + + # Use the complete run_opts we prepared earlier + # Handle ephemeral storage for run_vm method too + storage_param = "ephemeral" if self.ephemeral else self.storage + + # Log the image being used + self.logger.info(f"Running VM using image: {self.image}") + + # Call provider.run_vm with explicit image parameter + response = await self.config.vm_provider.run_vm( + image=self.image, + name=self.config.name, + run_opts=run_opts, + storage=storage_param + ) + self.logger.info(f"VM run response: {response if response else 'None'}") + except Exception as run_error: + self.logger.error(f"Failed to run VM: {run_error}") + raise RuntimeError(f"Failed to start VM: {run_error}") + # Wait for VM to be ready with a valid IP address + self.logger.info("Waiting for VM to be ready with a valid IP address...") try: - response = await self.config.pylume.run_vm(self.config.name, run_opts) # type: ignore[attr-defined] - self.logger.info(f"VM run response: {response if response else 'None'}") - except Exception as run_error: - self.logger.error(f"Failed to run VM: {run_error}") - raise RuntimeError(f"Failed to start VM: {run_error}") - - # Wait for VM to be ready with required properties - self.logger.info("Waiting for VM to be ready...") - try: - vm = await self.wait_vm_ready() - if not vm or not vm.ip_address: # type: ignore[attr-defined] - raise RuntimeError(f"VM {self.config.name} failed to get IP address") - ip_address = vm.ip_address # type: ignore[attr-defined] - self.logger.info(f"VM is ready with IP: {ip_address}") + # Increased values for Lumier provider which needs more time for initial setup + if self.provider_type == VMProviderType.LUMIER: + max_retries = 60 # Increased for Lumier VM startup which takes longer + retry_delay = 3 # 3 seconds between retries for Lumier + else: + max_retries = 30 # Default for other providers + retry_delay = 2 # 2 seconds between retries + + self.logger.info(f"Waiting up to {max_retries * retry_delay} seconds for VM to be ready...") + ip = await self.get_ip(max_retries=max_retries, retry_delay=retry_delay) + + # If we get here, we have a valid IP + self.logger.info(f"VM is ready with IP: {ip}") + ip_address = ip + except TimeoutError as timeout_error: + self.logger.error(str(timeout_error)) + raise RuntimeError(f"VM startup timed out: {timeout_error}") except Exception as wait_error: self.logger.error(f"Error waiting for VM: {wait_error}") raise RuntimeError(f"VM failed to become ready: {wait_error}") @@ -288,29 +408,50 @@ async def run(self) -> None: raise RuntimeError(f"Failed to initialize computer: {e}") try: + # Verify we have a valid IP before initializing the interface + if not ip_address or ip_address == "unknown" or ip_address == "0.0.0.0": + raise RuntimeError(f"Cannot initialize interface - invalid IP address: {ip_address}") + # Initialize the interface using the factory with the specified OS self.logger.info(f"Initializing interface for {self.os_type} at {ip_address}") from .interface.base import BaseComputerInterface - self._interface = cast( - BaseComputerInterface, - InterfaceFactory.create_interface_for_os( - os=self.os_type, ip_address=ip_address # type: ignore[arg-type] - ), - ) + # Pass authentication credentials if using cloud provider + if self.provider_type == VMProviderType.CLOUD and self.api_key and self.config.name: + self._interface = cast( + BaseComputerInterface, + InterfaceFactory.create_interface_for_os( + os=self.os_type, + ip_address=ip_address, + api_key=self.api_key, + vm_name=self.config.name + ), + ) + else: + self._interface = cast( + BaseComputerInterface, + InterfaceFactory.create_interface_for_os( + os=self.os_type, + ip_address=ip_address + ), + ) # Wait for the WebSocket interface to be ready self.logger.info("Connecting to WebSocket interface...") try: # Use a single timeout for the entire connection process - await self._interface.wait_for_ready(timeout=60) + # The VM should already be ready at this point, so we're just establishing the connection + await self._interface.wait_for_ready(timeout=30) self.logger.info("WebSocket interface connected successfully") except TimeoutError as e: - self.logger.error("Failed to connect to WebSocket interface") + self.logger.error(f"Failed to connect to WebSocket interface at {ip_address}") raise TimeoutError( f"Could not connect to WebSocket interface at {ip_address}:8000/ws: {str(e)}" ) + # self.logger.warning( + # f"Could not connect to WebSocket interface at {ip_address}:8000/ws: {str(e)}, expect missing functionality" + # ) # Create an event to keep the VM running in background if needed if not self.use_host_computer_server: @@ -321,6 +462,10 @@ async def run(self) -> None: # Set the initialization flag and clear the initializing flag self._initialized = True + + # Set this instance as the default computer for remote decorators + helpers.set_default_computer(self) + self.logger.info("Computer successfully initialized") except Exception as e: raise @@ -329,47 +474,38 @@ async def run(self) -> None: duration_ms = (time.time() - start_time) * 1000 self.logger.debug(f"Computer initialization took {duration_ms:.2f}ms") return + + async def disconnect(self) -> None: + """Disconnect from the computer's WebSocket interface.""" + if self._interface: + self._interface.close() async def stop(self) -> None: - """Stop computer control.""" + """Disconnect from the computer's WebSocket interface and stop the computer.""" start_time = time.time() try: - if self._running: - self._running = False - self.logger.info("Stopping Computer...") - - if hasattr(self, "_stop_event"): - self._stop_event.set() - if hasattr(self, "_keep_alive_task"): - await self._keep_alive_task - - if self._interface: # Only try to close interface if it exists - self.logger.verbose("Closing interface...") - # For host computer server, just use normal close to keep the server running - if self.use_host_computer_server: - self._interface.close() - else: - # For VM mode, force close the connection - if hasattr(self._interface, "force_close"): - self._interface.force_close() - else: - self._interface.close() + self.logger.info("Stopping Computer...") - if not self.use_host_computer_server and self._pylume_context: + # In VM mode, first explicitly stop the VM, then exit the provider context + if not self.use_host_computer_server and self._provider_context and self.config.vm_provider is not None: try: self.logger.info(f"Stopping VM {self.config.name}...") - await self.config.pylume.stop_vm(self.config.name) # type: ignore[attr-defined] + await self.config.vm_provider.stop_vm( + name=self.config.name, + storage=self.storage # Pass storage explicitly for clarity + ) except Exception as e: - self.logger.verbose(f"Error stopping VM: {e}") # VM might already be stopped - self.logger.verbose("Closing PyLume context...") - await self.config.pylume.__aexit__(None, None, None) # type: ignore[attr-defined] - self._pylume_context = None + self.logger.error(f"Error stopping VM: {e}") + + self.logger.verbose("Closing VM provider context...") + await self.config.vm_provider.__aexit__(None, None, None) + self._provider_context = None + + await self.disconnect() self.logger.info("Computer stopped") except Exception as e: - self.logger.debug( - f"Error during cleanup: {e}" - ) # Log as debug since this might be expected + self.logger.debug(f"Error during cleanup: {e}") # Log as debug since this might be expected finally: # Log stop time for performance monitoring duration_ms = (time.time() - start_time) * 1000 @@ -377,14 +513,49 @@ async def stop(self) -> None: return # @property - async def get_ip(self) -> str: - """Get the IP address of the VM or localhost if using host computer server.""" + async def get_ip(self, max_retries: int = 15, retry_delay: int = 2) -> str: + """Get the IP address of the VM or localhost if using host computer server. + + This method delegates to the provider's get_ip method, which waits indefinitely + until the VM has a valid IP address. + + Args: + max_retries: Unused parameter, kept for backward compatibility + retry_delay: Delay between retries in seconds (default: 2) + + Returns: + IP address of the VM or localhost if using host computer server + """ + # For host computer server, always return localhost immediately if self.use_host_computer_server: return "127.0.0.1" - ip = await self.config.get_ip() - return ip or "unknown" # Return "unknown" if ip is None + + # Get IP from the provider - each provider implements its own waiting logic + if self.config.vm_provider is None: + raise RuntimeError("VM provider is not initialized") + + # Log that we're waiting for the IP + self.logger.info(f"Waiting for VM {self.config.name} to get an IP address...") + + # Call the provider's get_ip method which will wait indefinitely + storage_param = "ephemeral" if self.ephemeral else self.storage + + # Log the image being used + self.logger.info(f"Running VM using image: {self.image}") + + # Call provider.get_ip with explicit image parameter + ip = await self.config.vm_provider.get_ip( + name=self.config.name, + storage=storage_param, + retry_delay=retry_delay + ) + + # Log success + self.logger.info(f"VM {self.config.name} has IP address: {ip}") + return ip + - async def wait_vm_ready(self) -> Optional[Union[Dict[str, Any], "VMStatus"]]: + async def wait_vm_ready(self) -> Optional[Dict[str, Any]]: """Wait for VM to be ready with an IP address. Returns: @@ -407,7 +578,11 @@ async def wait_vm_ready(self) -> Optional[Union[Dict[str, Any], "VMStatus"]]: try: # Keep polling for VM info - vm = await self.config.pylume.get_vm(self.config.name) # type: ignore[attr-defined] + if self.config.vm_provider is None: + self.logger.error("VM provider is not initialized") + vm = None + else: + vm = await self.config.vm_provider.get_vm(self.config.name) # Log full VM properties for debugging (every 30 attempts) if attempts % 30 == 0: @@ -447,10 +622,11 @@ async def wait_vm_ready(self) -> Optional[Union[Dict[str, Any], "VMStatus"]]: self.logger.error(f"Persistent error getting VM status: {str(e)}") self.logger.info("Trying to get VM list for debugging...") try: - vms = await self.config.pylume.list_vms() # type: ignore[attr-defined] - self.logger.info( - f"Available VMs: {[vm.name for vm in vms if hasattr(vm, 'name')]}" - ) + if self.config.vm_provider is not None: + vms = await self.config.vm_provider.list_vms() + self.logger.info( + f"Available VMs: {[getattr(vm, 'name', None) for vm in vms if hasattr(vm, 'name')]}" + ) except Exception as list_error: self.logger.error(f"Failed to list VMs: {str(list_error)}") @@ -462,9 +638,14 @@ async def wait_vm_ready(self) -> Optional[Union[Dict[str, Any], "VMStatus"]]: # Try to get final VM status for debugging try: - vm = await self.config.pylume.get_vm(self.config.name) # type: ignore[attr-defined] - status = getattr(vm, "status", "unknown") if vm else "unknown" - ip = getattr(vm, "ip_address", None) if vm else None + if self.config.vm_provider is not None: + vm = await self.config.vm_provider.get_vm(self.config.name) + # VM data is returned as a dictionary from the Lumier provider + status = vm.get('status', 'unknown') if vm else "unknown" + ip = vm.get('ip_address') if vm else None + else: + status = "unknown" + ip = None self.logger.error(f"Final VM status: {status}, IP: {ip}") except Exception as e: self.logger.error(f"Failed to get final VM status: {str(e)}") @@ -478,10 +659,18 @@ async def update(self, cpu: Optional[int] = None, memory: Optional[str] = None): self.logger.info( f"Updating VM settings: CPU={cpu or self.config.cpu}, Memory={memory or self.config.memory}" ) - update_opts = VMUpdateOpts( - cpu=cpu or int(self.config.cpu), memory=memory or self.config.memory - ) - await self.config.pylume.update_vm(self.config.image, update_opts) # type: ignore[attr-defined] + update_opts = { + "cpu": cpu or int(self.config.cpu), + "memory": memory or self.config.memory + } + if self.config.vm_provider is not None: + await self.config.vm_provider.update_vm( + name=self.config.name, + update_opts=update_opts, + storage=self.storage # Pass storage explicitly for clarity + ) + else: + raise RuntimeError("VM provider not initialized") def get_screenshot_size(self, screenshot: bytes) -> Dict[str, int]: """Get the dimensions of a screenshot. @@ -545,3 +734,177 @@ async def to_screenshot_coordinates(self, x: float, y: float) -> tuple[float, fl tuple[float, float]: (x, y) coordinates in screenshot space """ return await self.interface.to_screenshot_coordinates(x, y) + + + # Add virtual environment management functions to computer interface + async def venv_install(self, venv_name: str, requirements: list[str]) -> tuple[str, str]: + """Install packages in a virtual environment. + + Args: + venv_name: Name of the virtual environment + requirements: List of package requirements to install + + Returns: + Tuple of (stdout, stderr) from the installation command + """ + requirements = requirements or [] + + # Create virtual environment if it doesn't exist + venv_path = f"~/.venvs/{venv_name}" + create_cmd = f"mkdir -p ~/.venvs && python3 -m venv {venv_path}" + + # Check if venv exists, if not create it + check_cmd = f"test -d {venv_path} || ({create_cmd})" + _, _ = await self.interface.run_command(check_cmd) + + # Install packages + requirements_str = " ".join(requirements) + install_cmd = f". {venv_path}/bin/activate && pip install {requirements_str}" + return await self.interface.run_command(install_cmd) + + async def venv_cmd(self, venv_name: str, command: str) -> tuple[str, str]: + """Execute a shell command in a virtual environment. + + Args: + venv_name: Name of the virtual environment + command: Shell command to execute in the virtual environment + + Returns: + Tuple of (stdout, stderr) from the command execution + """ + venv_path = f"~/.venvs/{venv_name}" + + # Check if virtual environment exists + check_cmd = f"test -d {venv_path}" + stdout, stderr = await self.interface.run_command(check_cmd) + + if stderr or "test:" in stdout: # venv doesn't exist + return "", f"Virtual environment '{venv_name}' does not exist. Create it first using venv_install." + + # Activate virtual environment and run command + full_command = f". {venv_path}/bin/activate && {command}" + return await self.interface.run_command(full_command) + + async def venv_exec(self, venv_name: str, python_func, *args, **kwargs): + """Execute Python function in a virtual environment using source code extraction. + + Args: + venv_name: Name of the virtual environment + python_func: A callable function to execute + *args: Positional arguments to pass to the function + **kwargs: Keyword arguments to pass to the function + + Returns: + The result of the function execution, or raises any exception that occurred + """ + import base64 + import inspect + import json + import textwrap + + try: + # Get function source code using inspect.getsource + source = inspect.getsource(python_func) + # Remove common leading whitespace (dedent) + func_source = textwrap.dedent(source).strip() + + # Remove decorators + while func_source.lstrip().startswith("@"): + func_source = func_source.split("\n", 1)[1].strip() + + # Get function name for execution + func_name = python_func.__name__ + + # Serialize args and kwargs as JSON (safer than dill for cross-version compatibility) + args_json = json.dumps(args, default=str) + kwargs_json = json.dumps(kwargs, default=str) + + except OSError as e: + raise Exception(f"Cannot retrieve source code for function {python_func.__name__}: {e}") + except Exception as e: + raise Exception(f"Failed to reconstruct function source: {e}") + + # Create Python code that will define and execute the function + python_code = f''' +import json +import traceback + +try: + # Define the function from source +{textwrap.indent(func_source, " ")} + + # Deserialize args and kwargs from JSON + args_json = """{args_json}""" + kwargs_json = """{kwargs_json}""" + args = json.loads(args_json) + kwargs = json.loads(kwargs_json) + + # Execute the function + result = {func_name}(*args, **kwargs) + + # Create success output payload + output_payload = {{ + "success": True, + "result": result, + "error": None + }} + +except Exception as e: + # Create error output payload + output_payload = {{ + "success": False, + "result": None, + "error": {{ + "type": type(e).__name__, + "message": str(e), + "traceback": traceback.format_exc() + }} + }} + +# Serialize the output payload as JSON +import json +output_json = json.dumps(output_payload, default=str) + +# Print the JSON output with markers +print(f"<<>>{{output_json}}<<>>") +''' + + # Encode the Python code in base64 to avoid shell escaping issues + encoded_code = base64.b64encode(python_code.encode('utf-8')).decode('ascii') + + # Execute the Python code in the virtual environment + python_command = f"python -c \"import base64; exec(base64.b64decode('{encoded_code}').decode('utf-8'))\"" + stdout, stderr = await self.venv_cmd(venv_name, python_command) + + # Parse the output to extract the payload + start_marker = "<<>>" + end_marker = "<<>>" + + # Print original stdout + print(stdout[:stdout.find(start_marker)]) + + if start_marker in stdout and end_marker in stdout: + start_idx = stdout.find(start_marker) + len(start_marker) + end_idx = stdout.find(end_marker) + + if start_idx < end_idx: + output_json = stdout[start_idx:end_idx] + + try: + # Decode and deserialize the output payload from JSON + output_payload = json.loads(output_json) + except Exception as e: + raise Exception(f"Failed to decode output payload: {e}") + + if output_payload["success"]: + return output_payload["result"] + else: + # Recreate and raise the original exception + error_info = output_payload["error"] + error_class = eval(error_info["type"]) + raise error_class(error_info["message"]) + else: + raise Exception("Invalid output format: markers found but no content between them") + else: + # Fallback: return stdout/stderr if no payload markers found + raise Exception(f"No output payload found. stdout: {stdout}, stderr: {stderr}") diff --git a/libs/computer/computer/diorama_computer.py b/libs/computer/computer/diorama_computer.py new file mode 100644 index 00000000..2eee77f0 --- /dev/null +++ b/libs/computer/computer/diorama_computer.py @@ -0,0 +1,104 @@ +import asyncio +from .interface.models import KeyType, Key + +class DioramaComputer: + """ + A Computer-compatible proxy for Diorama that sends commands over the ComputerInterface. + """ + def __init__(self, computer, apps): + self.computer = computer + self.apps = apps + self.interface = DioramaComputerInterface(computer, apps) + self._initialized = False + + async def __aenter__(self): + self._initialized = True + return self + + async def run(self): + if not self._initialized: + await self.__aenter__() + return self + +class DioramaComputerInterface: + """ + Diorama Interface proxy that sends diorama_cmds via the Computer's interface. + """ + def __init__(self, computer, apps): + self.computer = computer + self.apps = apps + self._scene_size = None + + async def _send_cmd(self, action, arguments=None): + arguments = arguments or {} + arguments = {"app_list": self.apps, **arguments} + # Use the computer's interface (must be initialized) + iface = getattr(self.computer, "_interface", None) + if iface is None: + raise RuntimeError("Computer interface not initialized. Call run() first.") + result = await iface.diorama_cmd(action, arguments) + if not result.get("success"): + raise RuntimeError(f"Diorama command failed: {result.get('error')}\n{result.get('trace')}") + return result.get("result") + + async def screenshot(self, as_bytes=True): + from PIL import Image + import base64 + result = await self._send_cmd("screenshot") + # assume result is a b64 string of an image + img_bytes = base64.b64decode(result) + import io + img = Image.open(io.BytesIO(img_bytes)) + self._scene_size = img.size + return img_bytes if as_bytes else img + + async def get_screen_size(self): + if not self._scene_size: + await self.screenshot(as_bytes=False) + return {"width": self._scene_size[0], "height": self._scene_size[1]} + + async def move_cursor(self, x, y): + await self._send_cmd("move_cursor", {"x": x, "y": y}) + + async def left_click(self, x=None, y=None): + await self._send_cmd("left_click", {"x": x, "y": y}) + + async def right_click(self, x=None, y=None): + await self._send_cmd("right_click", {"x": x, "y": y}) + + async def double_click(self, x=None, y=None): + await self._send_cmd("double_click", {"x": x, "y": y}) + + async def scroll_up(self, clicks=1): + await self._send_cmd("scroll_up", {"clicks": clicks}) + + async def scroll_down(self, clicks=1): + await self._send_cmd("scroll_down", {"clicks": clicks}) + + async def drag_to(self, x, y, duration=0.5): + await self._send_cmd("drag_to", {"x": x, "y": y, "duration": duration}) + + async def get_cursor_position(self): + return await self._send_cmd("get_cursor_position") + + async def type_text(self, text): + await self._send_cmd("type_text", {"text": text}) + + async def press_key(self, key): + await self._send_cmd("press_key", {"key": key}) + + async def hotkey(self, *keys): + actual_keys = [] + for key in keys: + if isinstance(key, Key): + actual_keys.append(key.value) + elif isinstance(key, str): + # Try to convert to enum if it matches a known key + key_or_enum = Key.from_string(key) + actual_keys.append(key_or_enum.value if isinstance(key_or_enum, Key) else key_or_enum) + else: + raise ValueError(f"Invalid key type: {type(key)}. Must be Key enum or string.") + await self._send_cmd("hotkey", {"keys": actual_keys}) + + async def to_screen_coordinates(self, x, y): + return await self._send_cmd("to_screen_coordinates", {"x": x, "y": y}) diff --git a/libs/computer/computer/helpers.py b/libs/computer/computer/helpers.py new file mode 100644 index 00000000..b472c047 --- /dev/null +++ b/libs/computer/computer/helpers.py @@ -0,0 +1,49 @@ +""" +Helper functions and decorators for the Computer module. +""" +import asyncio +from functools import wraps +from typing import Any, Callable, Optional, TypeVar, cast + +# Global reference to the default computer instance +_default_computer = None + +def set_default_computer(computer): + """ + Set the default computer instance to be used by the remote decorator. + + Args: + computer: The computer instance to use as default + """ + global _default_computer + _default_computer = computer + + +def sandboxed(venv_name: str = "default", computer: str = "default", max_retries: int = 3): + """ + Decorator that wraps a function to be executed remotely via computer.venv_exec + + Args: + venv_name: Name of the virtual environment to execute in + computer: The computer instance to use, or "default" to use the globally set default + max_retries: Maximum number of retries for the remote execution + """ + def decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + # Determine which computer instance to use + comp = computer if computer != "default" else _default_computer + + if comp is None: + raise RuntimeError("No computer instance available. Either specify a computer instance or call set_default_computer() first.") + + for i in range(max_retries): + try: + return await comp.venv_exec(venv_name, func, *args, **kwargs) + except Exception as e: + print(f"Attempt {i+1} failed: {e}") + await asyncio.sleep(1) + if i == max_retries - 1: + raise e + return wrapper + return decorator diff --git a/libs/computer/computer/interface/base.py b/libs/computer/computer/interface/base.py index 8fcbd21c..09cc46f2 100644 --- a/libs/computer/computer/interface/base.py +++ b/libs/computer/computer/interface/base.py @@ -3,22 +3,27 @@ from abc import ABC, abstractmethod from typing import Optional, Dict, Any, Tuple, List from ..logger import Logger, LogLevel +from .models import MouseButton class BaseComputerInterface(ABC): """Base class for computer control interfaces.""" - def __init__(self, ip_address: str, username: str = "lume", password: str = "lume"): + def __init__(self, ip_address: str, username: str = "lume", password: str = "lume", api_key: Optional[str] = None, vm_name: Optional[str] = None): """Initialize interface. Args: ip_address: IP address of the computer to control username: Username for authentication password: Password for authentication + api_key: Optional API key for cloud authentication + vm_name: Optional VM name for cloud authentication """ self.ip_address = ip_address self.username = username self.password = password + self.api_key = api_key + self.vm_name = vm_name self.logger = Logger("cua.interface", LogLevel.NORMAL) @abstractmethod @@ -47,6 +52,16 @@ def force_close(self) -> None: self.close() # Mouse Actions + @abstractmethod + async def mouse_down(self, x: Optional[int] = None, y: Optional[int] = None, button: "MouseButton" = "left") -> None: + """Press and hold a mouse button.""" + pass + + @abstractmethod + async def mouse_up(self, x: Optional[int] = None, y: Optional[int] = None, button: "MouseButton" = "left") -> None: + """Release a mouse button.""" + pass + @abstractmethod async def left_click(self, x: Optional[int] = None, y: Optional[int] = None) -> None: """Perform a left click.""" @@ -91,6 +106,16 @@ async def drag(self, path: List[Tuple[int, int]], button: str = "left", duration pass # Keyboard Actions + @abstractmethod + async def key_down(self, key: str) -> None: + """Press and hold a key.""" + pass + + @abstractmethod + async def key_up(self, key: str) -> None: + """Release a key.""" + pass + @abstractmethod async def type_text(self, text: str) -> None: """Type the specified text.""" @@ -107,6 +132,11 @@ async def hotkey(self, *keys: str) -> None: pass # Scrolling Actions + @abstractmethod + async def scroll(self, x: int, y: int) -> None: + """Scroll the mouse wheel.""" + pass + @abstractmethod async def scroll_down(self, clicks: int = 1) -> None: """Scroll down.""" @@ -162,7 +192,47 @@ async def file_exists(self, path: str) -> bool: async def directory_exists(self, path: str) -> bool: """Check if directory exists.""" pass - + + @abstractmethod + async def list_dir(self, path: str) -> List[str]: + """List directory contents.""" + pass + + @abstractmethod + async def read_text(self, path: str) -> str: + """Read file text contents.""" + pass + + @abstractmethod + async def write_text(self, path: str, content: str) -> None: + """Write file text contents.""" + pass + + @abstractmethod + async def read_bytes(self, path: str) -> bytes: + """Read file binary contents.""" + pass + + @abstractmethod + async def write_bytes(self, path: str, content: bytes) -> None: + """Write file binary contents.""" + pass + + @abstractmethod + async def delete_file(self, path: str) -> None: + """Delete file.""" + pass + + @abstractmethod + async def create_dir(self, path: str) -> None: + """Create directory.""" + pass + + @abstractmethod + async def delete_dir(self, path: str) -> None: + """Delete directory.""" + pass + @abstractmethod async def run_command(self, command: str) -> Tuple[str, str]: """Run shell command.""" @@ -173,7 +243,7 @@ async def run_command(self, command: str) -> Tuple[str, str]: async def get_accessibility_tree(self) -> Dict: """Get the accessibility tree of the current screen.""" pass - + @abstractmethod async def to_screen_coordinates(self, x: float, y: float) -> tuple[float, float]: """Convert screenshot coordinates to screen coordinates. diff --git a/libs/computer/computer/interface/factory.py b/libs/computer/computer/interface/factory.py index cb9a8d93..949972c4 100644 --- a/libs/computer/computer/interface/factory.py +++ b/libs/computer/computer/interface/factory.py @@ -1,6 +1,6 @@ """Factory for creating computer interfaces.""" -from typing import Literal +from typing import Literal, Optional from .base import BaseComputerInterface class InterfaceFactory: @@ -9,13 +9,17 @@ class InterfaceFactory: @staticmethod def create_interface_for_os( os: Literal['macos', 'linux'], - ip_address: str + ip_address: str, + api_key: Optional[str] = None, + vm_name: Optional[str] = None ) -> BaseComputerInterface: """Create an interface for the specified OS. Args: os: Operating system type ('macos' or 'linux') ip_address: IP address of the computer to control + api_key: Optional API key for cloud authentication + vm_name: Optional VM name for cloud authentication Returns: BaseComputerInterface: The appropriate interface for the OS @@ -25,8 +29,11 @@ def create_interface_for_os( """ # Import implementations here to avoid circular imports from .macos import MacOSComputerInterface + from .linux import LinuxComputerInterface if os == 'macos': - return MacOSComputerInterface(ip_address) + return MacOSComputerInterface(ip_address, api_key=api_key, vm_name=vm_name) + elif os == 'linux': + return LinuxComputerInterface(ip_address, api_key=api_key, vm_name=vm_name) else: - raise ValueError(f"Unsupported OS type: {os}") \ No newline at end of file + raise ValueError(f"Unsupported OS type: {os}") \ No newline at end of file diff --git a/libs/computer/computer/interface/linux.py b/libs/computer/computer/interface/linux.py index 52e173b4..e96cde50 100644 --- a/libs/computer/computer/interface/linux.py +++ b/libs/computer/computer/interface/linux.py @@ -1,27 +1,688 @@ -"""Linux computer interface implementation.""" +import asyncio +import json +import time +from typing import Any, Dict, List, Optional, Tuple +from PIL import Image -from typing import Dict +import websockets + +from ..logger import Logger, LogLevel from .base import BaseComputerInterface +from ..utils import decode_base64_image, encode_base64_image, bytes_to_image, draw_box, resize_image +from .models import Key, KeyType, MouseButton + + +class LinuxComputerInterface(BaseComputerInterface): + """Interface for Linux.""" + + def __init__(self, ip_address: str, username: str = "lume", password: str = "lume", api_key: Optional[str] = None, vm_name: Optional[str] = None): + super().__init__(ip_address, username, password, api_key, vm_name) + self._ws = None + self._reconnect_task = None + self._closed = False + self._last_ping = 0 + self._ping_interval = 5 # Send ping every 5 seconds + self._ping_timeout = 120 # Wait 120 seconds for pong response + self._reconnect_delay = 1 # Start with 1 second delay + self._max_reconnect_delay = 30 # Maximum delay between reconnection attempts + self._log_connection_attempts = True # Flag to control connection attempt logging + self._authenticated = False # Track authentication status + self._command_lock = asyncio.Lock() # Lock to ensure only one command at a time + + # Set logger name for Linux interface + self.logger = Logger("cua.interface.linux", LogLevel.NORMAL) + + @property + def ws_uri(self) -> str: + """Get the WebSocket URI using the current IP address. + + Returns: + WebSocket URI for the Computer API Server + """ + protocol = "wss" if self.api_key else "ws" + port = "8443" if self.api_key else "8000" + return f"{protocol}://{self.ip_address}:{port}/ws" + + async def _keep_alive(self): + """Keep the WebSocket connection alive with automatic reconnection.""" + retry_count = 0 + max_log_attempts = 1 # Only log the first attempt at INFO level + log_interval = 500 # Then log every 500th attempt (significantly increased from 30) + last_warning_time = 0 + min_warning_interval = 30 # Minimum seconds between connection lost warnings + min_retry_delay = 0.5 # Minimum delay between connection attempts (500ms) + + while not self._closed: + try: + if self._ws is None or ( + self._ws and self._ws.state == websockets.protocol.State.CLOSED + ): + try: + retry_count += 1 + + # Add a minimum delay between connection attempts to avoid flooding + if retry_count > 1: + await asyncio.sleep(min_retry_delay) + + # Only log the first attempt at INFO level, then every Nth attempt + if retry_count == 1: + self.logger.info(f"Attempting WebSocket connection to {self.ws_uri}") + elif retry_count % log_interval == 0: + self.logger.info( + f"Still attempting WebSocket connection (attempt {retry_count})..." + ) + else: + # All other attempts are logged at DEBUG level + self.logger.debug( + f"Attempting WebSocket connection to {self.ws_uri} (attempt {retry_count})" + ) + + self._ws = await asyncio.wait_for( + websockets.connect( + self.ws_uri, + max_size=1024 * 1024 * 10, # 10MB limit + max_queue=32, + ping_interval=self._ping_interval, + ping_timeout=self._ping_timeout, + close_timeout=5, + compression=None, # Disable compression to reduce overhead + ), + timeout=120, + ) + self.logger.info("WebSocket connection established") + + # Authentication will be handled by the first command that needs it + # Don't do authentication here to avoid recv conflicts + + self._reconnect_delay = 1 # Reset reconnect delay on successful connection + self._last_ping = time.time() + retry_count = 0 # Reset retry count on successful connection + self._authenticated = False # Reset auth status on new connection + + except (asyncio.TimeoutError, websockets.exceptions.WebSocketException) as e: + next_retry = self._reconnect_delay + + # Only log the first error at WARNING level, then every Nth attempt + if retry_count == 1: + self.logger.warning( + f"Computer API Server not ready yet. Will retry automatically." + ) + elif retry_count % log_interval == 0: + self.logger.warning( + f"Still waiting for Computer API Server (attempt {retry_count})..." + ) + else: + # All other errors are logged at DEBUG level + self.logger.debug(f"Connection attempt {retry_count} failed: {e}") + + if self._ws: + try: + await self._ws.close() + except: + pass + self._ws = None + + # Regular ping to check connection + if self._ws and self._ws.state == websockets.protocol.State.OPEN: + try: + if time.time() - self._last_ping >= self._ping_interval: + pong_waiter = await self._ws.ping() + await asyncio.wait_for(pong_waiter, timeout=self._ping_timeout) + self._last_ping = time.time() + except Exception as e: + self.logger.debug(f"Ping failed: {e}") + if self._ws: + try: + await self._ws.close() + except: + pass + self._ws = None + continue + + await asyncio.sleep(1) + + except Exception as e: + current_time = time.time() + # Only log connection lost warnings at most once every min_warning_interval seconds + if current_time - last_warning_time >= min_warning_interval: + self.logger.warning( + f"Computer API Server connection lost. Will retry automatically." + ) + last_warning_time = current_time + else: + # Log at debug level instead + self.logger.debug(f"Connection lost: {e}") + + if self._ws: + try: + await self._ws.close() + except: + pass + self._ws = None + + async def _ensure_connection(self): + """Ensure WebSocket connection is established.""" + if self._reconnect_task is None or self._reconnect_task.done(): + self._reconnect_task = asyncio.create_task(self._keep_alive()) + + retry_count = 0 + max_retries = 5 + + while retry_count < max_retries: + try: + if self._ws and self._ws.state == websockets.protocol.State.OPEN: + return + retry_count += 1 + await asyncio.sleep(1) + except Exception as e: + # Only log at ERROR level for the last retry attempt + if retry_count == max_retries - 1: + self.logger.error( + f"Persistent connection check error after {retry_count} attempts: {e}" + ) + else: + self.logger.debug(f"Connection check error (attempt {retry_count}): {e}") + retry_count += 1 + await asyncio.sleep(1) + continue + + raise ConnectionError("Failed to establish WebSocket connection after multiple retries") + + async def _send_command(self, command: str, params: Optional[Dict] = None) -> Dict[str, Any]: + """Send command through WebSocket.""" + max_retries = 3 + retry_count = 0 + last_error = None + + # Acquire lock to ensure only one command is processed at a time + async with self._command_lock: + self.logger.debug(f"Acquired lock for command: {command}") + while retry_count < max_retries: + try: + await self._ensure_connection() + if not self._ws: + raise ConnectionError("WebSocket connection is not established") + + # Handle authentication if needed + if self.api_key and self.vm_name and not self._authenticated: + self.logger.info("Performing authentication handshake...") + auth_message = { + "command": "authenticate", + "params": { + "api_key": self.api_key, + "container_name": self.vm_name + } + } + await self._ws.send(json.dumps(auth_message)) + + # Wait for authentication response + auth_response = await asyncio.wait_for(self._ws.recv(), timeout=10) + auth_result = json.loads(auth_response) + + if not auth_result.get("success"): + error_msg = auth_result.get("error", "Authentication failed") + self.logger.error(f"Authentication failed: {error_msg}") + self._authenticated = False + raise ConnectionError(f"Authentication failed: {error_msg}") + + self.logger.info("Authentication successful") + self._authenticated = True + + message = {"command": command, "params": params or {}} + await self._ws.send(json.dumps(message)) + response = await asyncio.wait_for(self._ws.recv(), timeout=30) + self.logger.debug(f"Completed command: {command}") + return json.loads(response) + except Exception as e: + last_error = e + retry_count += 1 + if retry_count < max_retries: + # Only log at debug level for intermediate retries + self.logger.debug( + f"Command '{command}' failed (attempt {retry_count}/{max_retries}): {e}" + ) + await asyncio.sleep(1) + continue + else: + # Only log at error level for the final failure + self.logger.error( + f"Failed to send command '{command}' after {max_retries} retries" + ) + self.logger.debug(f"Command failure details: {e}") + raise last_error if last_error else RuntimeError("Failed to send command") -class LinuxInterface(BaseComputerInterface): - """Linux-specific computer interface.""" + async def wait_for_ready(self, timeout: int = 60, interval: float = 1.0): + """Wait for WebSocket connection to become available.""" + start_time = time.time() + last_error = None + attempt_count = 0 + progress_interval = 10 # Log progress every 10 seconds + last_progress_time = start_time + + # Disable detailed logging for connection attempts + self._log_connection_attempts = False + + try: + self.logger.info( + f"Waiting for Computer API Server to be ready (timeout: {timeout}s)..." + ) + + # Start the keep-alive task if it's not already running + if self._reconnect_task is None or self._reconnect_task.done(): + self._reconnect_task = asyncio.create_task(self._keep_alive()) + + # Wait for the connection to be established + while time.time() - start_time < timeout: + try: + attempt_count += 1 + current_time = time.time() + + # Log progress periodically without flooding logs + if current_time - last_progress_time >= progress_interval: + elapsed = current_time - start_time + self.logger.info( + f"Still waiting for Computer API Server... (elapsed: {elapsed:.1f}s, attempts: {attempt_count})" + ) + last_progress_time = current_time + + # Check if we have a connection + if self._ws and self._ws.state == websockets.protocol.State.OPEN: + # Test the connection with a simple command + try: + await self._send_command("get_screen_size") + elapsed = time.time() - start_time + self.logger.info( + f"Computer API Server is ready (after {elapsed:.1f}s, {attempt_count} attempts)" + ) + return # Connection is fully working + except Exception as e: + last_error = e + self.logger.debug(f"Connection test failed: {e}") + + # Wait before trying again + await asyncio.sleep(interval) + + except Exception as e: + last_error = e + self.logger.debug(f"Connection attempt {attempt_count} failed: {e}") + await asyncio.sleep(interval) + + # If we get here, we've timed out + error_msg = f"Could not connect to {self.ip_address} after {timeout} seconds" + if last_error: + error_msg += f": {str(last_error)}" + self.logger.error(error_msg) + raise TimeoutError(error_msg) + finally: + # Reset to default logging behavior + self._log_connection_attempts = False + + def close(self): + """Close WebSocket connection. + + Note: In host computer server mode, we leave the connection open + to allow other clients to connect to the same server. The server + will handle cleaning up idle connections. + """ + # Only cancel the reconnect task + if self._reconnect_task: + self._reconnect_task.cancel() + + # Don't set closed flag or close websocket by default + # This allows the server to stay connected for other clients + # self._closed = True + # if self._ws: + # asyncio.create_task(self._ws.close()) + # self._ws = None + + def force_close(self): + """Force close the WebSocket connection. + + This method should be called when you want to completely + shut down the connection, not just for regular cleanup. + """ + self._closed = True + if self._reconnect_task: + self._reconnect_task.cancel() + if self._ws: + asyncio.create_task(self._ws.close()) + self._ws = None + + # Mouse Actions + async def mouse_down(self, x: Optional[int] = None, y: Optional[int] = None, button: str = "left") -> None: + await self._send_command("mouse_down", {"x": x, "y": y, "button": button}) + + async def mouse_up(self, x: Optional[int] = None, y: Optional[int] = None, button: str = "left") -> None: + await self._send_command("mouse_up", {"x": x, "y": y, "button": button}) - async def wait_for_ready(self, timeout: int = 60) -> None: - """Wait for interface to be ready.""" - # Placeholder implementation - pass + async def left_click(self, x: Optional[int] = None, y: Optional[int] = None) -> None: + await self._send_command("left_click", {"x": x, "y": y}) + + async def right_click(self, x: Optional[int] = None, y: Optional[int] = None) -> None: + await self._send_command("right_click", {"x": x, "y": y}) + + async def double_click(self, x: Optional[int] = None, y: Optional[int] = None) -> None: + await self._send_command("double_click", {"x": x, "y": y}) + + async def move_cursor(self, x: int, y: int) -> None: + await self._send_command("move_cursor", {"x": x, "y": y}) + + async def drag_to(self, x: int, y: int, button: "MouseButton" = "left", duration: float = 0.5) -> None: + await self._send_command( + "drag_to", {"x": x, "y": y, "button": button, "duration": duration} + ) + + async def drag(self, path: List[Tuple[int, int]], button: "MouseButton" = "left", duration: float = 0.5) -> None: + await self._send_command( + "drag", {"path": path, "button": button, "duration": duration} + ) + + # Keyboard Actions + async def key_down(self, key: "KeyType") -> None: + await self._send_command("key_down", {"key": key}) - def close(self) -> None: - """Close the interface connection.""" - # Placeholder implementation - pass + async def key_up(self, key: "KeyType") -> None: + await self._send_command("key_up", {"key": key}) - async def get_screen_size(self) -> Dict[str, int]: - """Get the screen dimensions.""" - # Placeholder implementation - return {"width": 1920, "height": 1080} + async def type_text(self, text: str) -> None: + # Temporary fix for https://github.com/trycua/cua/issues/165 + # Check if text contains Unicode characters + if any(ord(char) > 127 for char in text): + # For Unicode text, use clipboard and paste + await self.set_clipboard(text) + await self.hotkey(Key.COMMAND, 'v') + else: + # For ASCII text, use the regular typing method + await self._send_command("type_text", {"text": text}) + + async def press(self, key: "KeyType") -> None: + """Press a single key. + + Args: + key: The key to press. Can be any of: + - A Key enum value (recommended), e.g. Key.PAGE_DOWN + - A direct key value string, e.g. 'pagedown' + - A single character string, e.g. 'a' + + Examples: + ```python + # Using enum (recommended) + await interface.press(Key.PAGE_DOWN) + await interface.press(Key.ENTER) + + # Using direct values + await interface.press('pagedown') + await interface.press('enter') + + # Using single characters + await interface.press('a') + ``` + + Raises: + ValueError: If the key type is invalid or the key is not recognized + """ + if isinstance(key, Key): + actual_key = key.value + elif isinstance(key, str): + # Try to convert to enum if it matches a known key + key_or_enum = Key.from_string(key) + actual_key = key_or_enum.value if isinstance(key_or_enum, Key) else key_or_enum + else: + raise ValueError(f"Invalid key type: {type(key)}. Must be Key enum or string.") + + await self._send_command("press_key", {"key": actual_key}) + + async def press_key(self, key: "KeyType") -> None: + """DEPRECATED: Use press() instead. + + This method is kept for backward compatibility but will be removed in a future version. + Please use the press() method instead. + """ + await self.press(key) + + async def hotkey(self, *keys: "KeyType") -> None: + """Press multiple keys simultaneously. + + Args: + *keys: Multiple keys to press simultaneously. Each key can be any of: + - A Key enum value (recommended), e.g. Key.COMMAND + - A direct key value string, e.g. 'command' + - A single character string, e.g. 'a' + + Examples: + ```python + # Using enums (recommended) + await interface.hotkey(Key.COMMAND, Key.C) # Copy + await interface.hotkey(Key.COMMAND, Key.V) # Paste + + # Using mixed formats + await interface.hotkey(Key.COMMAND, 'a') # Select all + ``` + + Raises: + ValueError: If any key type is invalid or not recognized + """ + actual_keys = [] + for key in keys: + if isinstance(key, Key): + actual_keys.append(key.value) + elif isinstance(key, str): + # Try to convert to enum if it matches a known key + key_or_enum = Key.from_string(key) + actual_keys.append(key_or_enum.value if isinstance(key_or_enum, Key) else key_or_enum) + else: + raise ValueError(f"Invalid key type: {type(key)}. Must be Key enum or string.") + + await self._send_command("hotkey", {"keys": actual_keys}) + + # Scrolling Actions + async def scroll(self, x: int, y: int) -> None: + await self._send_command("scroll", {"x": x, "y": y}) - async def screenshot(self) -> bytes: - """Take a screenshot.""" - # Placeholder implementation - return b"" \ No newline at end of file + async def scroll_down(self, clicks: int = 1) -> None: + await self._send_command("scroll_down", {"clicks": clicks}) + + async def scroll_up(self, clicks: int = 1) -> None: + await self._send_command("scroll_up", {"clicks": clicks}) + + # Screen Actions + async def screenshot( + self, + boxes: Optional[List[Tuple[int, int, int, int]]] = None, + box_color: str = "#FF0000", + box_thickness: int = 2, + scale_factor: float = 1.0, + ) -> bytes: + """Take a screenshot with optional box drawing and scaling. + + Args: + boxes: Optional list of (x, y, width, height) tuples defining boxes to draw in screen coordinates + box_color: Color of the boxes in hex format (default: "#FF0000" red) + box_thickness: Thickness of the box borders in pixels (default: 2) + scale_factor: Factor to scale the final image by (default: 1.0) + Use > 1.0 to enlarge, < 1.0 to shrink (e.g., 0.5 for half size, 2.0 for double) + + Returns: + bytes: The screenshot image data, optionally with boxes drawn on it and scaled + """ + result = await self._send_command("screenshot") + if not result.get("image_data"): + raise RuntimeError("Failed to take screenshot") + + screenshot = decode_base64_image(result["image_data"]) + + if boxes: + # Get the natural scaling between screen and screenshot + screen_size = await self.get_screen_size() + screenshot_width, screenshot_height = bytes_to_image(screenshot).size + width_scale = screenshot_width / screen_size["width"] + height_scale = screenshot_height / screen_size["height"] + + # Scale box coordinates from screen space to screenshot space + for box in boxes: + scaled_box = ( + int(box[0] * width_scale), # x + int(box[1] * height_scale), # y + int(box[2] * width_scale), # width + int(box[3] * height_scale), # height + ) + screenshot = draw_box( + screenshot, + x=scaled_box[0], + y=scaled_box[1], + width=scaled_box[2], + height=scaled_box[3], + color=box_color, + thickness=box_thickness, + ) + + if scale_factor != 1.0: + screenshot = resize_image(screenshot, scale_factor) + + return screenshot + + async def get_screen_size(self) -> Dict[str, int]: + result = await self._send_command("get_screen_size") + if result["success"] and result["size"]: + return result["size"] + raise RuntimeError("Failed to get screen size") + + async def get_cursor_position(self) -> Dict[str, int]: + result = await self._send_command("get_cursor_position") + if result["success"] and result["position"]: + return result["position"] + raise RuntimeError("Failed to get cursor position") + + # Clipboard Actions + async def copy_to_clipboard(self) -> str: + result = await self._send_command("copy_to_clipboard") + if result["success"] and result["content"]: + return result["content"] + raise RuntimeError("Failed to get clipboard content") + + async def set_clipboard(self, text: str) -> None: + await self._send_command("set_clipboard", {"text": text}) + + # File System Actions + async def file_exists(self, path: str) -> bool: + result = await self._send_command("file_exists", {"path": path}) + return result.get("exists", False) + + async def directory_exists(self, path: str) -> bool: + result = await self._send_command("directory_exists", {"path": path}) + return result.get("exists", False) + + async def list_dir(self, path: str) -> list[str]: + result = await self._send_command("list_dir", {"path": path}) + if not result.get("success", False): + raise RuntimeError(result.get("error", "Failed to list directory")) + return result.get("files", []) + + async def read_text(self, path: str) -> str: + result = await self._send_command("read_text", {"path": path}) + if not result.get("success", False): + raise RuntimeError(result.get("error", "Failed to read file")) + return result.get("content", "") + + async def write_text(self, path: str, content: str) -> None: + result = await self._send_command("write_text", {"path": path, "content": content}) + if not result.get("success", False): + raise RuntimeError(result.get("error", "Failed to write file")) + + async def read_bytes(self, path: str) -> bytes: + result = await self._send_command("read_bytes", {"path": path}) + if not result.get("success", False): + raise RuntimeError(result.get("error", "Failed to read file")) + content_b64 = result.get("content_b64", "") + return decode_base64_image(content_b64) + + async def write_bytes(self, path: str, content: bytes) -> None: + result = await self._send_command("write_bytes", {"path": path, "content_b64": encode_base64_image(content)}) + if not result.get("success", False): + raise RuntimeError(result.get("error", "Failed to write file")) + + async def delete_file(self, path: str) -> None: + result = await self._send_command("delete_file", {"path": path}) + if not result.get("success", False): + raise RuntimeError(result.get("error", "Failed to delete file")) + + async def create_dir(self, path: str) -> None: + result = await self._send_command("create_dir", {"path": path}) + if not result.get("success", False): + raise RuntimeError(result.get("error", "Failed to create directory")) + + async def delete_dir(self, path: str) -> None: + result = await self._send_command("delete_dir", {"path": path}) + if not result.get("success", False): + raise RuntimeError(result.get("error", "Failed to delete directory")) + + async def run_command(self, command: str) -> Tuple[str, str]: + result = await self._send_command("run_command", {"command": command}) + if not result.get("success", False): + raise RuntimeError(result.get("error", "Failed to run command")) + return result.get("stdout", ""), result.get("stderr", "") + + # Accessibility Actions + async def get_accessibility_tree(self) -> Dict[str, Any]: + """Get the accessibility tree of the current screen.""" + result = await self._send_command("get_accessibility_tree") + if not result.get("success", False): + raise RuntimeError(result.get("error", "Failed to get accessibility tree")) + return result + + async def get_active_window_bounds(self) -> Dict[str, int]: + """Get the bounds of the currently active window.""" + result = await self._send_command("get_active_window_bounds") + if result["success"] and result["bounds"]: + return result["bounds"] + raise RuntimeError("Failed to get active window bounds") + + async def to_screen_coordinates(self, x: float, y: float) -> tuple[float, float]: + """Convert screenshot coordinates to screen coordinates. + + Args: + x: X coordinate in screenshot space + y: Y coordinate in screenshot space + + Returns: + tuple[float, float]: (x, y) coordinates in screen space + """ + screen_size = await self.get_screen_size() + screenshot = await self.screenshot() + screenshot_img = bytes_to_image(screenshot) + screenshot_width, screenshot_height = screenshot_img.size + + # Calculate scaling factors + width_scale = screen_size["width"] / screenshot_width + height_scale = screen_size["height"] / screenshot_height + + # Convert coordinates + screen_x = x * width_scale + screen_y = y * height_scale + + return screen_x, screen_y + + async def to_screenshot_coordinates(self, x: float, y: float) -> tuple[float, float]: + """Convert screen coordinates to screenshot coordinates. + + Args: + x: X coordinate in screen space + y: Y coordinate in screen space + + Returns: + tuple[float, float]: (x, y) coordinates in screenshot space + """ + screen_size = await self.get_screen_size() + screenshot = await self.screenshot() + screenshot_img = bytes_to_image(screenshot) + screenshot_width, screenshot_height = screenshot_img.size + + # Calculate scaling factors + width_scale = screenshot_width / screen_size["width"] + height_scale = screenshot_height / screen_size["height"] + + # Convert coordinates + screenshot_x = x * width_scale + screenshot_y = y * height_scale + + return screenshot_x, screenshot_y diff --git a/libs/computer/computer/interface/macos.py b/libs/computer/computer/interface/macos.py index dcac6342..539303e4 100644 --- a/libs/computer/computer/interface/macos.py +++ b/libs/computer/computer/interface/macos.py @@ -8,29 +8,40 @@ from ..logger import Logger, LogLevel from .base import BaseComputerInterface -from ..utils import decode_base64_image, bytes_to_image, draw_box, resize_image -from .models import Key, KeyType +from ..utils import decode_base64_image, encode_base64_image, bytes_to_image, draw_box, resize_image +from .models import Key, KeyType, MouseButton class MacOSComputerInterface(BaseComputerInterface): - """Interface for MacOS.""" + """Interface for macOS.""" - def __init__(self, ip_address: str, username: str = "lume", password: str = "lume"): - super().__init__(ip_address, username, password) - self.ws_uri = f"ws://{ip_address}:8000/ws" + def __init__(self, ip_address: str, username: str = "lume", password: str = "lume", api_key: Optional[str] = None, vm_name: Optional[str] = None): + super().__init__(ip_address, username, password, api_key, vm_name) self._ws = None self._reconnect_task = None self._closed = False self._last_ping = 0 self._ping_interval = 5 # Send ping every 5 seconds - self._ping_timeout = 10 # Wait 10 seconds for pong response + self._ping_timeout = 120 # Wait 120 seconds for pong response self._reconnect_delay = 1 # Start with 1 second delay self._max_reconnect_delay = 30 # Maximum delay between reconnection attempts self._log_connection_attempts = True # Flag to control connection attempt logging + self._command_lock = asyncio.Lock() # Lock to ensure only one command at a time - # Set logger name for MacOS interface + # Set logger name for macOS interface self.logger = Logger("cua.interface.macos", LogLevel.NORMAL) + @property + def ws_uri(self) -> str: + """Get the WebSocket URI using the current IP address. + + Returns: + WebSocket URI for the Computer API Server + """ + protocol = "wss" if self.api_key else "ws" + port = "8443" if self.api_key else "8000" + return f"{protocol}://{self.ip_address}:{port}/ws" + async def _keep_alive(self): """Keep the WebSocket connection alive with automatic reconnection.""" retry_count = 0 @@ -75,9 +86,35 @@ async def _keep_alive(self): close_timeout=5, compression=None, # Disable compression to reduce overhead ), - timeout=30, + timeout=120, ) self.logger.info("WebSocket connection established") + + # If api_key and vm_name are provided, perform authentication handshake + if self.api_key and self.vm_name: + self.logger.info("Performing authentication handshake...") + auth_message = { + "command": "authenticate", + "params": { + "api_key": self.api_key, + "container_name": self.vm_name + } + } + await self._ws.send(json.dumps(auth_message)) + + # Wait for authentication response + auth_response = await asyncio.wait_for(self._ws.recv(), timeout=10) + auth_result = json.loads(auth_response) + + if not auth_result.get("success"): + error_msg = auth_result.get("error", "Authentication failed") + self.logger.error(f"Authentication failed: {error_msg}") + await self._ws.close() + self._ws = None + raise ConnectionError(f"Authentication failed: {error_msg}") + + self.logger.info("Authentication successful") + self._reconnect_delay = 1 # Reset reconnect delay on successful connection self._last_ping = time.time() retry_count = 0 # Reset retry count on successful connection @@ -183,35 +220,39 @@ async def _send_command(self, command: str, params: Optional[Dict] = None) -> Di retry_count = 0 last_error = None - while retry_count < max_retries: - try: - await self._ensure_connection() - if not self._ws: - raise ConnectionError("WebSocket connection is not established") - - message = {"command": command, "params": params or {}} - await self._ws.send(json.dumps(message)) - response = await asyncio.wait_for(self._ws.recv(), timeout=30) - return json.loads(response) - except Exception as e: - last_error = e - retry_count += 1 - if retry_count < max_retries: - # Only log at debug level for intermediate retries - self.logger.debug( - f"Command '{command}' failed (attempt {retry_count}/{max_retries}): {e}" - ) - await asyncio.sleep(1) - continue - else: - # Only log at error level for the final failure - self.logger.error( - f"Failed to send command '{command}' after {max_retries} retries" - ) - self.logger.debug(f"Command failure details: {e}") - raise + # Acquire lock to ensure only one command is processed at a time + async with self._command_lock: + self.logger.debug(f"Acquired lock for command: {command}") + while retry_count < max_retries: + try: + await self._ensure_connection() + if not self._ws: + raise ConnectionError("WebSocket connection is not established") + + message = {"command": command, "params": params or {}} + await self._ws.send(json.dumps(message)) + response = await asyncio.wait_for(self._ws.recv(), timeout=120) + self.logger.debug(f"Completed command: {command}") + return json.loads(response) + except Exception as e: + last_error = e + retry_count += 1 + if retry_count < max_retries: + # Only log at debug level for intermediate retries + self.logger.debug( + f"Command '{command}' failed (attempt {retry_count}/{max_retries}): {e}" + ) + await asyncio.sleep(1) + continue + else: + # Only log at error level for the final failure + self.logger.error( + f"Failed to send command '{command}' after {max_retries} retries" + ) + self.logger.debug(f"Command failure details: {e}") + raise - raise last_error if last_error else RuntimeError("Failed to send command") + raise last_error if last_error else RuntimeError("Failed to send command") async def wait_for_ready(self, timeout: int = 60, interval: float = 1.0): """Wait for WebSocket connection to become available.""" @@ -310,7 +351,17 @@ def force_close(self): asyncio.create_task(self._ws.close()) self._ws = None + async def diorama_cmd(self, action: str, arguments: Optional[dict] = None) -> dict: + """Send a diorama command to the server (macOS only).""" + return await self._send_command("diorama_cmd", {"action": action, "arguments": arguments or {}}) + # Mouse Actions + async def mouse_down(self, x: Optional[int] = None, y: Optional[int] = None, button: "MouseButton" = "left") -> None: + await self._send_command("mouse_down", {"x": x, "y": y, "button": button}) + + async def mouse_up(self, x: Optional[int] = None, y: Optional[int] = None, button: "MouseButton" = "left") -> None: + await self._send_command("mouse_up", {"x": x, "y": y, "button": button}) + async def left_click(self, x: Optional[int] = None, y: Optional[int] = None) -> None: await self._send_command("left_click", {"x": x, "y": y}) @@ -334,6 +385,12 @@ async def drag(self, path: List[Tuple[int, int]], button: str = "left", duration ) # Keyboard Actions + async def key_down(self, key: "KeyType") -> None: + await self._send_command("key_down", {"key": key}) + + async def key_up(self, key: "KeyType") -> None: + await self._send_command("key_up", {"key": key}) + async def type_text(self, text: str) -> None: # Temporary fix for https://github.com/trycua/cua/issues/165 # Check if text contains Unicode characters @@ -426,6 +483,9 @@ async def hotkey(self, *keys: "KeyType") -> None: await self._send_command("hotkey", {"keys": actual_keys}) # Scrolling Actions + async def scroll(self, x: int, y: int) -> None: + await self._send_command("scroll", {"x": x, "y": y}) + async def scroll_down(self, clicks: int = 1) -> None: await self._send_command("scroll_down", {"clicks": clicks}) @@ -519,6 +579,50 @@ async def directory_exists(self, path: str) -> bool: result = await self._send_command("directory_exists", {"path": path}) return result.get("exists", False) + async def list_dir(self, path: str) -> list[str]: + result = await self._send_command("list_dir", {"path": path}) + if not result.get("success", False): + raise RuntimeError(result.get("error", "Failed to list directory")) + return result.get("files", []) + + async def read_text(self, path: str) -> str: + result = await self._send_command("read_text", {"path": path}) + if not result.get("success", False): + raise RuntimeError(result.get("error", "Failed to read file")) + return result.get("content", "") + + async def write_text(self, path: str, content: str) -> None: + result = await self._send_command("write_text", {"path": path, "content": content}) + if not result.get("success", False): + raise RuntimeError(result.get("error", "Failed to write file")) + + async def read_bytes(self, path: str) -> bytes: + result = await self._send_command("read_bytes", {"path": path}) + if not result.get("success", False): + raise RuntimeError(result.get("error", "Failed to read file")) + content_b64 = result.get("content_b64", "") + return decode_base64_image(content_b64) + + async def write_bytes(self, path: str, content: bytes) -> None: + result = await self._send_command("write_bytes", {"path": path, "content_b64": encode_base64_image(content)}) + if not result.get("success", False): + raise RuntimeError(result.get("error", "Failed to write file")) + + async def delete_file(self, path: str) -> None: + result = await self._send_command("delete_file", {"path": path}) + if not result.get("success", False): + raise RuntimeError(result.get("error", "Failed to delete file")) + + async def create_dir(self, path: str) -> None: + result = await self._send_command("create_dir", {"path": path}) + if not result.get("success", False): + raise RuntimeError(result.get("error", "Failed to create directory")) + + async def delete_dir(self, path: str) -> None: + result = await self._send_command("delete_dir", {"path": path}) + if not result.get("success", False): + raise RuntimeError(result.get("error", "Failed to delete directory")) + async def run_command(self, command: str) -> Tuple[str, str]: result = await self._send_command("run_command", {"command": command}) if not result.get("success", False): @@ -532,7 +636,7 @@ async def get_accessibility_tree(self) -> Dict[str, Any]: if not result.get("success", False): raise RuntimeError(result.get("error", "Failed to get accessibility tree")) return result - + async def get_active_window_bounds(self) -> Dict[str, int]: """Get the bounds of the currently active window.""" result = await self._send_command("get_active_window_bounds") diff --git a/libs/computer/computer/interface/models.py b/libs/computer/computer/interface/models.py index e8ec1b47..515b5f2b 100644 --- a/libs/computer/computer/interface/models.py +++ b/libs/computer/computer/interface/models.py @@ -106,6 +106,9 @@ def from_string(cls, key: str) -> 'Key | str': # Combined key type KeyType = Union[Key, NavigationKey, SpecialKey, ModifierKey, FunctionKey, str] +# Key type for mouse actions +MouseButton = Literal['left', 'right', 'middle'] + class AccessibilityWindow(TypedDict): """Information about a window in the accessibility tree.""" app_name: str diff --git a/libs/computer/computer/models.py b/libs/computer/computer/models.py index 13ff36b2..5ead143f 100644 --- a/libs/computer/computer/models.py +++ b/libs/computer/computer/models.py @@ -1,8 +1,10 @@ """Models for computer configuration.""" from dataclasses import dataclass -from typing import Optional -from pylume import PyLume +from typing import Optional, Any, Dict + +# Import base provider interface +from .providers.base import BaseVMProvider @dataclass class Display: @@ -26,10 +28,20 @@ class Computer: display: Display memory: str cpu: str - pylume: Optional[PyLume] = None + vm_provider: Optional[BaseVMProvider] = None # @property # Remove the property decorator async def get_ip(self) -> Optional[str]: """Get the IP address of the VM.""" - vm = await self.pylume.get_vm(self.name) # type: ignore[attr-defined] - return vm.ip_address if vm else None \ No newline at end of file + if not self.vm_provider: + return None + + vm = await self.vm_provider.get_vm(self.name) + # Handle both object attribute and dictionary access for ip_address + if vm: + if isinstance(vm, dict): + return vm.get("ip_address") + else: + # Access as attribute for object-based return values + return getattr(vm, "ip_address", None) + return None \ No newline at end of file diff --git a/libs/computer/computer/providers/__init__.py b/libs/computer/computer/providers/__init__.py new file mode 100644 index 00000000..4ac67dd6 --- /dev/null +++ b/libs/computer/computer/providers/__init__.py @@ -0,0 +1,4 @@ +"""Provider implementations for different VM backends.""" + +# Import specific providers only when needed to avoid circular imports +__all__ = [] # Let each provider module handle its own exports diff --git a/libs/computer/computer/providers/base.py b/libs/computer/computer/providers/base.py new file mode 100644 index 00000000..4a8f8fdf --- /dev/null +++ b/libs/computer/computer/providers/base.py @@ -0,0 +1,105 @@ +"""Base provider interface for VM backends.""" + +import abc +from enum import StrEnum +from typing import Dict, List, Optional, Any, AsyncContextManager + + +class VMProviderType(StrEnum): + """Enum of supported VM provider types.""" + LUME = "lume" + LUMIER = "lumier" + CLOUD = "cloud" + UNKNOWN = "unknown" + + +class BaseVMProvider(AsyncContextManager): + """Base interface for VM providers. + + All VM provider implementations must implement this interface. + """ + + @property + @abc.abstractmethod + def provider_type(self) -> VMProviderType: + """Get the provider type.""" + pass + + @abc.abstractmethod + async def get_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]: + """Get VM information by name. + + Args: + name: Name of the VM to get information for + storage: Optional storage path override. If provided, this will be used + instead of the provider's default storage path. + + Returns: + Dictionary with VM information including status, IP address, etc. + """ + pass + + @abc.abstractmethod + async def list_vms(self) -> List[Dict[str, Any]]: + """List all available VMs.""" + pass + + @abc.abstractmethod + async def run_vm(self, image: str, name: str, run_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]: + """Run a VM by name with the given options. + + Args: + image: Name/tag of the image to use + name: Name of the VM to run + run_opts: Dictionary of run options (memory, cpu, etc.) + storage: Optional storage path override. If provided, this will be used + instead of the provider's default storage path. + + Returns: + Dictionary with VM run status and information + """ + pass + + @abc.abstractmethod + async def stop_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]: + """Stop a VM by name. + + Args: + name: Name of the VM to stop + storage: Optional storage path override. If provided, this will be used + instead of the provider's default storage path. + + Returns: + Dictionary with VM stop status and information + """ + pass + + @abc.abstractmethod + async def update_vm(self, name: str, update_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]: + """Update VM configuration. + + Args: + name: Name of the VM to update + update_opts: Dictionary of update options (memory, cpu, etc.) + storage: Optional storage path override. If provided, this will be used + instead of the provider's default storage path. + + Returns: + Dictionary with VM update status and information + """ + pass + + @abc.abstractmethod + async def get_ip(self, name: str, storage: Optional[str] = None, retry_delay: int = 2) -> str: + """Get the IP address of a VM, waiting indefinitely until it's available. + + Args: + name: Name of the VM to get the IP for + storage: Optional storage path override. If provided, this will be used + instead of the provider's default storage path. + retry_delay: Delay between retries in seconds (default: 2) + + Returns: + IP address of the VM when it becomes available + """ + pass diff --git a/libs/computer/computer/providers/cloud/__init__.py b/libs/computer/computer/providers/cloud/__init__.py new file mode 100644 index 00000000..421e72c1 --- /dev/null +++ b/libs/computer/computer/providers/cloud/__init__.py @@ -0,0 +1,5 @@ +"""CloudProvider module for interacting with cloud-based virtual machines.""" + +from .provider import CloudProvider + +__all__ = ["CloudProvider"] diff --git a/libs/computer/computer/providers/cloud/provider.py b/libs/computer/computer/providers/cloud/provider.py new file mode 100644 index 00000000..1cfba161 --- /dev/null +++ b/libs/computer/computer/providers/cloud/provider.py @@ -0,0 +1,75 @@ +"""Cloud VM provider implementation. + +This module contains a stub implementation for a future cloud VM provider. +""" + +import logging +from typing import Dict, List, Optional, Any + +from ..base import BaseVMProvider, VMProviderType + +# Setup logging +logger = logging.getLogger(__name__) + +import asyncio +import aiohttp +from urllib.parse import urlparse + +class CloudProvider(BaseVMProvider): + """Cloud VM Provider implementation.""" + def __init__( + self, + api_key: str, + verbose: bool = False, + **kwargs, + ): + """ + Args: + api_key: API key for authentication + name: Name of the VM + verbose: Enable verbose logging + """ + assert api_key, "api_key required for CloudProvider" + self.api_key = api_key + self.verbose = verbose + + @property + def provider_type(self) -> VMProviderType: + return VMProviderType.CLOUD + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + pass + + async def get_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]: + """Get VM VNC URL by name using the cloud API.""" + return {"name": name, "hostname": f"{name}.containers.cloud.trycua.com"} + + async def list_vms(self) -> List[Dict[str, Any]]: + logger.warning("CloudProvider.list_vms is not implemented") + return [] + + async def run_vm(self, image: str, name: str, run_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]: + # logger.warning("CloudProvider.run_vm is not implemented") + return {"name": name, "status": "unavailable", "message": "CloudProvider.run_vm is not implemented"} + + async def stop_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]: + logger.warning("CloudProvider.stop_vm is not implemented. To clean up resources, please use Computer.disconnect()") + return {"name": name, "status": "stopped", "message": "CloudProvider is not implemented"} + + async def update_vm(self, name: str, update_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]: + logger.warning("CloudProvider.update_vm is not implemented") + return {"name": name, "status": "unchanged", "message": "CloudProvider is not implemented"} + + async def get_ip(self, name: Optional[str] = None, storage: Optional[str] = None, retry_delay: int = 2) -> str: + """ + Return the VM's IP address as '{container_name}.containers.cloud.trycua.com'. + Uses the provided 'name' argument (the VM name requested by the caller), + falling back to self.name only if 'name' is None. + Retries up to 3 times with retry_delay seconds if hostname is not available. + """ + if name is None: + raise ValueError("VM name is required for CloudProvider.get_ip") + return f"{name}.containers.cloud.trycua.com" diff --git a/libs/computer/computer/providers/factory.py b/libs/computer/computer/providers/factory.py new file mode 100644 index 00000000..6491b754 --- /dev/null +++ b/libs/computer/computer/providers/factory.py @@ -0,0 +1,116 @@ +"""Factory for creating VM providers.""" + +import logging +from typing import Dict, Optional, Any, Type, Union + +from .base import BaseVMProvider, VMProviderType + +logger = logging.getLogger(__name__) + + +class VMProviderFactory: + """Factory for creating VM providers based on provider type.""" + + @staticmethod + def create_provider( + provider_type: Union[str, VMProviderType], + port: int = 7777, + host: str = "localhost", + bin_path: Optional[str] = None, + storage: Optional[str] = None, + shared_path: Optional[str] = None, + image: Optional[str] = None, + verbose: bool = False, + ephemeral: bool = False, + noVNC_port: Optional[int] = None, + **kwargs, + ) -> BaseVMProvider: + """Create a VM provider of the specified type. + + Args: + provider_type: Type of VM provider to create + port: Port for the API server + host: Hostname for the API server + bin_path: Path to provider binary if needed + storage: Path for persistent VM storage + shared_path: Path for shared folder between host and VM + image: VM image to use (for Lumier provider) + verbose: Enable verbose logging + ephemeral: Use ephemeral (temporary) storage + noVNC_port: Specific port for noVNC interface (for Lumier provider) + + Returns: + An instance of the requested VM provider + + Raises: + ImportError: If the required dependencies for the provider are not installed + ValueError: If the provider type is not supported + """ + # Convert string to enum if needed + if isinstance(provider_type, str): + try: + provider_type = VMProviderType(provider_type.lower()) + except ValueError: + provider_type = VMProviderType.UNKNOWN + + if provider_type == VMProviderType.LUME: + try: + from .lume import LumeProvider, HAS_LUME + if not HAS_LUME: + raise ImportError( + "The pylume package is required for LumeProvider. " + "Please install it with 'pip install cua-computer[lume]'" + ) + return LumeProvider( + port=port, + host=host, + storage=storage, + verbose=verbose, + ephemeral=ephemeral + ) + except ImportError as e: + logger.error(f"Failed to import LumeProvider: {e}") + raise ImportError( + "The pylume package is required for LumeProvider. " + "Please install it with 'pip install cua-computer[lume]'" + ) from e + elif provider_type == VMProviderType.LUMIER: + try: + from .lumier import LumierProvider, HAS_LUMIER + if not HAS_LUMIER: + raise ImportError( + "Docker is required for LumierProvider. " + "Please install Docker for Apple Silicon and Lume CLI before using this provider." + ) + return LumierProvider( + port=port, + host=host, + storage=storage, + shared_path=shared_path, + image=image or "macos-sequoia-cua:latest", + verbose=verbose, + ephemeral=ephemeral, + noVNC_port=noVNC_port + ) + except ImportError as e: + logger.error(f"Failed to import LumierProvider: {e}") + raise ImportError( + "Docker and Lume CLI are required for LumierProvider. " + "Please install Docker for Apple Silicon and run the Lume installer script." + ) from e + + elif provider_type == VMProviderType.CLOUD: + try: + from .cloud import CloudProvider + return CloudProvider( + verbose=verbose, + **kwargs, + ) + except ImportError as e: + logger.error(f"Failed to import CloudProvider: {e}") + raise ImportError( + "The CloudProvider is not fully implemented yet. " + "Please use LUME or LUMIER provider instead." + ) from e + else: + raise ValueError(f"Unsupported provider type: {provider_type}") diff --git a/libs/computer/computer/providers/lume/__init__.py b/libs/computer/computer/providers/lume/__init__.py new file mode 100644 index 00000000..8196c49b --- /dev/null +++ b/libs/computer/computer/providers/lume/__init__.py @@ -0,0 +1,9 @@ +"""Lume VM provider implementation.""" + +try: + from .provider import LumeProvider + HAS_LUME = True + __all__ = ["LumeProvider"] +except ImportError: + HAS_LUME = False + __all__ = [] diff --git a/libs/computer/computer/providers/lume/provider.py b/libs/computer/computer/providers/lume/provider.py new file mode 100644 index 00000000..5816e53e --- /dev/null +++ b/libs/computer/computer/providers/lume/provider.py @@ -0,0 +1,541 @@ +"""Lume VM provider implementation using curl commands. + +This provider uses direct curl commands to interact with the Lume API, +removing the dependency on the pylume Python package. +""" + +import os +import re +import asyncio +import json +import logging +import subprocess +import urllib.parse +from typing import Dict, Any, Optional, List, Tuple + +from ..base import BaseVMProvider, VMProviderType +from ...logger import Logger, LogLevel +from ..lume_api import ( + lume_api_get, + lume_api_run, + lume_api_stop, + lume_api_update, + lume_api_pull, + HAS_CURL, + parse_memory +) + +# Setup logging +logger = logging.getLogger(__name__) + +class LumeProvider(BaseVMProvider): + """Lume VM provider implementation using direct curl commands. + + This provider uses curl to interact with the Lume API server, + removing the dependency on the pylume Python package. + """ + + def __init__( + self, + port: int = 7777, + host: str = "localhost", + storage: Optional[str] = None, + verbose: bool = False, + ephemeral: bool = False, + ): + """Initialize the Lume provider. + + Args: + port: Port for the Lume API server (default: 7777) + host: Host to use for API connections (default: localhost) + storage: Path to store VM data + verbose: Enable verbose logging + """ + if not HAS_CURL: + raise ImportError( + "curl is required for LumeProvider. " + "Please ensure it is installed and in your PATH." + ) + + self.host = host + self.port = port # Default port for Lume API + self.storage = storage + self.verbose = verbose + self.ephemeral = ephemeral # If True, VMs will be deleted after stopping + + # Base API URL for Lume API calls + self.api_base_url = f"http://{self.host}:{self.port}" + + self.logger = logging.getLogger(__name__) + + @property + def provider_type(self) -> VMProviderType: + """Get the provider type.""" + return VMProviderType.LUME + + async def __aenter__(self): + """Enter async context manager.""" + # No initialization needed, just return self + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Exit async context manager.""" + # No cleanup needed + pass + + def _lume_api_get(self, vm_name: str = "", storage: Optional[str] = None, debug: bool = False) -> Dict[str, Any]: + """Get VM information using shared lume_api function. + + Args: + vm_name: Optional name of the VM to get info for. + If empty, lists all VMs. + storage: Optional storage path override. If provided, this will be used instead of self.storage + debug: Whether to show debug output + + Returns: + Dictionary with VM status information parsed from JSON response + """ + # Use the shared implementation from lume_api module + return lume_api_get( + vm_name=vm_name, + host=self.host, + port=self.port, + storage=storage if storage is not None else self.storage, + debug=debug, + verbose=self.verbose + ) + + def _lume_api_run(self, vm_name: str, run_opts: Dict[str, Any], debug: bool = False) -> Dict[str, Any]: + """Run a VM using shared lume_api function. + + Args: + vm_name: Name of the VM to run + run_opts: Dictionary of run options + debug: Whether to show debug output + + Returns: + Dictionary with API response or error information + """ + # Use the shared implementation from lume_api module + return lume_api_run( + vm_name=vm_name, + host=self.host, + port=self.port, + run_opts=run_opts, + storage=self.storage, + debug=debug, + verbose=self.verbose + ) + + def _lume_api_stop(self, vm_name: str, debug: bool = False) -> Dict[str, Any]: + """Stop a VM using shared lume_api function. + + Args: + vm_name: Name of the VM to stop + debug: Whether to show debug output + + Returns: + Dictionary with API response or error information + """ + # Use the shared implementation from lume_api module + return lume_api_stop( + vm_name=vm_name, + host=self.host, + port=self.port, + storage=self.storage, + debug=debug, + verbose=self.verbose + ) + + def _lume_api_update(self, vm_name: str, update_opts: Dict[str, Any], debug: bool = False) -> Dict[str, Any]: + """Update VM configuration using shared lume_api function. + + Args: + vm_name: Name of the VM to update + update_opts: Dictionary of update options + debug: Whether to show debug output + + Returns: + Dictionary with API response or error information + """ + # Use the shared implementation from lume_api module + return lume_api_update( + vm_name=vm_name, + host=self.host, + port=self.port, + update_opts=update_opts, + storage=self.storage, + debug=debug, + verbose=self.verbose + ) + + async def get_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]: + """Get VM information by name. + + Args: + name: Name of the VM to get information for + storage: Optional storage path override. If provided, this will be used + instead of the provider's default storage path. + + Returns: + Dictionary with VM information including status, IP address, etc. + + Note: + If storage is not provided, the provider's default storage path will be used. + The storage parameter allows overriding the storage location for this specific call. + """ + if not HAS_CURL: + logger.error("curl is not available. Cannot get VM status.") + return { + "name": name, + "status": "unavailable", + "error": "curl is not available" + } + + # First try to get detailed VM info from the API + try: + # Query the Lume API for VM status using the provider's storage_path + vm_info = self._lume_api_get( + vm_name=name, + storage=storage if storage is not None else self.storage, + debug=self.verbose + ) + + # Check for API errors + if "error" in vm_info: + logger.debug(f"API request error: {vm_info['error']}") + # If we got an error from the API, report the VM as not ready yet + return { + "name": name, + "status": "starting", # VM is still starting - do not attempt to connect yet + "api_status": "error", + "error": vm_info["error"] + } + + # Process the VM status information + vm_status = vm_info.get("status", "unknown") + + # Check if VM is stopped or not running - don't wait for IP in this case + if vm_status == "stopped": + logger.info(f"VM {name} is in '{vm_status}' state - not waiting for IP address") + # Return the status as-is without waiting for an IP + result = { + "name": name, + "status": vm_status, + **vm_info # Include all original fields from the API response + } + return result + + # Handle field name differences between APIs + # Some APIs use camelCase, others use snake_case + if "vncUrl" in vm_info: + vnc_url = vm_info["vncUrl"] + elif "vnc_url" in vm_info: + vnc_url = vm_info["vnc_url"] + else: + vnc_url = "" + + if "ipAddress" in vm_info: + ip_address = vm_info["ipAddress"] + elif "ip_address" in vm_info: + ip_address = vm_info["ip_address"] + else: + # If no IP address is provided and VM is supposed to be running, + # report it as still starting + ip_address = None + logger.info(f"VM {name} is in '{vm_status}' state but no IP address found - reporting as still starting") + + logger.info(f"VM {name} status: {vm_status}") + + # Return the complete status information + result = { + "name": name, + "status": vm_status if vm_status else "running", + "ip_address": ip_address, + "vnc_url": vnc_url, + "api_status": "ok" + } + + # Include all original fields from the API response + if isinstance(vm_info, dict): + for key, value in vm_info.items(): + if key not in result: # Don't override our carefully processed fields + result[key] = value + + return result + + except Exception as e: + logger.error(f"Failed to get VM status: {e}") + # Return a fallback status that indicates the VM is not ready yet + return { + "name": name, + "status": "initializing", # VM is still initializing + "error": f"Failed to get VM status: {str(e)}" + } + + async def list_vms(self) -> List[Dict[str, Any]]: + """List all available VMs.""" + result = self._lume_api_get(debug=self.verbose) + + # Extract the VMs list from the response + if "vms" in result and isinstance(result["vms"], list): + return result["vms"] + elif "error" in result: + logger.error(f"Error listing VMs: {result['error']}") + return [] + else: + return [] + + async def run_vm(self, image: str, name: str, run_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]: + """Run a VM with the given options. + + If the VM does not exist in the storage location, this will attempt to pull it + from the Lume registry first. + + Args: + image: Image name to use when pulling the VM if it doesn't exist + name: Name of the VM to run + run_opts: Dictionary of run options (memory, cpu, etc.) + storage: Optional storage path override. If provided, this will be used + instead of the provider's default storage path. + + Returns: + Dictionary with VM run status and information + """ + # First check if VM exists by trying to get its info + vm_info = await self.get_vm(name, storage=storage) + + if "error" in vm_info: + # VM doesn't exist, try to pull it + self.logger.info(f"VM {name} not found, attempting to pull image {image} from registry...") + + # Call pull_vm with the image parameter + pull_result = await self.pull_vm( + name=name, + image=image, + storage=storage + ) + + # Check if pull was successful + if "error" in pull_result: + self.logger.error(f"Failed to pull VM image: {pull_result['error']}") + return pull_result # Return the error from pull + + self.logger.info(f"Successfully pulled VM image {image} as {name}") + + # Now run the VM with the given options + self.logger.info(f"Running VM {name} with options: {run_opts}") + + from ..lume_api import lume_api_run + return lume_api_run( + vm_name=name, + host=self.host, + port=self.port, + run_opts=run_opts, + storage=storage if storage is not None else self.storage, + debug=self.verbose, + verbose=self.verbose + ) + + async def stop_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]: + """Stop a running VM. + + If this provider was initialized with ephemeral=True, the VM will also + be deleted after it is stopped. + + Args: + name: Name of the VM to stop + storage: Optional storage path override + + Returns: + Dictionary with stop status and information + """ + # Stop the VM first + stop_result = self._lume_api_stop(name, debug=self.verbose) + + # Log ephemeral status for debugging + self.logger.info(f"Ephemeral mode status: {self.ephemeral}") + + # If ephemeral mode is enabled, delete the VM after stopping + if self.ephemeral and (stop_result.get("success", False) or "error" not in stop_result): + self.logger.info(f"Ephemeral mode enabled - deleting VM {name} after stopping") + try: + delete_result = await self.delete_vm(name, storage=storage) + + # Return combined result + return { + **stop_result, # Include all stop result info + "deleted": True, + "delete_result": delete_result + } + except Exception as e: + self.logger.error(f"Failed to delete ephemeral VM {name}: {e}") + # Include the error but still return stop result + return { + **stop_result, + "deleted": False, + "delete_error": str(e) + } + + # Just return the stop result if not ephemeral + return stop_result + + async def pull_vm( + self, + name: str, + image: str, + storage: Optional[str] = None, + registry: str = "ghcr.io", + organization: str = "trycua", + pull_opts: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: + """Pull a VM image from the registry. + + Args: + name: Name for the VM after pulling + image: The image name to pull (e.g. 'macos-sequoia-cua:latest') + storage: Optional storage path to use + registry: Registry to pull from (default: ghcr.io) + organization: Organization in registry (default: trycua) + pull_opts: Additional options for pulling the VM (optional) + + Returns: + Dictionary with information about the pulled VM + + Raises: + RuntimeError: If pull operation fails or image is not provided + """ + # Validate image parameter + if not image: + raise ValueError("Image parameter is required for pull_vm") + + self.logger.info(f"Pulling VM image '{image}' as '{name}'") + self.logger.info("You can check the pull progress using: lume logs -f") + + # Set default pull_opts if not provided + if pull_opts is None: + pull_opts = {} + + # Log information about the operation + self.logger.debug(f"Pull storage location: {storage or 'default'}") + + try: + # Call the lume_api_pull function from lume_api.py + from ..lume_api import lume_api_pull + + result = lume_api_pull( + image=image, + name=name, + host=self.host, + port=self.port, + storage=storage if storage is not None else self.storage, + registry=registry, + organization=organization, + debug=self.verbose, + verbose=self.verbose + ) + + # Check for errors in the result + if "error" in result: + self.logger.error(f"Failed to pull VM image: {result['error']}") + return result + + self.logger.info(f"Successfully pulled VM image '{image}' as '{name}'") + return result + except Exception as e: + self.logger.error(f"Failed to pull VM image '{image}': {e}") + return {"error": f"Failed to pull VM: {str(e)}"} + + async def delete_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]: + """Delete a VM permanently. + + Args: + name: Name of the VM to delete + storage: Optional storage path override + + Returns: + Dictionary with delete status and information + """ + self.logger.info(f"Deleting VM {name}...") + + try: + # Call the lume_api_delete function we created + from ..lume_api import lume_api_delete + + result = lume_api_delete( + vm_name=name, + host=self.host, + port=self.port, + storage=storage if storage is not None else self.storage, + debug=self.verbose, + verbose=self.verbose + ) + + # Check for errors in the result + if "error" in result: + self.logger.error(f"Failed to delete VM: {result['error']}") + return result + + self.logger.info(f"Successfully deleted VM '{name}'") + return result + except Exception as e: + self.logger.error(f"Failed to delete VM '{name}': {e}") + return {"error": f"Failed to delete VM: {str(e)}"} + + async def update_vm(self, name: str, update_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]: + """Update VM configuration.""" + return self._lume_api_update(name, update_opts, debug=self.verbose) + + async def get_ip(self, name: str, storage: Optional[str] = None, retry_delay: int = 2) -> str: + """Get the IP address of a VM, waiting indefinitely until it's available. + + Args: + name: Name of the VM to get the IP for + storage: Optional storage path override + retry_delay: Delay between retries in seconds (default: 2) + + Returns: + IP address of the VM when it becomes available + """ + # Track total attempts for logging purposes + total_attempts = 0 + + # Loop indefinitely until we get a valid IP + while True: + total_attempts += 1 + + # Log retry message but not on first attempt + if total_attempts > 1: + self.logger.info(f"Waiting for VM {name} IP address (attempt {total_attempts})...") + + try: + # Get VM information + vm_info = await self.get_vm(name, storage=storage) + + # Check if we got a valid IP + ip = vm_info.get("ip_address", None) + if ip and ip != "unknown" and not ip.startswith("0.0.0.0"): + self.logger.info(f"Got valid VM IP address: {ip}") + return ip + + # Check the VM status + status = vm_info.get("status", "unknown") + + # If VM is not running yet, log and wait + if status != "running": + self.logger.info(f"VM is not running yet (status: {status}). Waiting...") + # If VM is running but no IP yet, wait and retry + else: + self.logger.info("VM is running but no valid IP address yet. Waiting...") + + except Exception as e: + self.logger.warning(f"Error getting VM {name} IP: {e}, continuing to wait...") + + # Wait before next retry + await asyncio.sleep(retry_delay) + + # Add progress log every 10 attempts + if total_attempts % 10 == 0: + self.logger.info(f"Still waiting for VM {name} IP after {total_attempts} attempts...") + + diff --git a/libs/computer/computer/providers/lume_api.py b/libs/computer/computer/providers/lume_api.py new file mode 100644 index 00000000..fbfaca4b --- /dev/null +++ b/libs/computer/computer/providers/lume_api.py @@ -0,0 +1,559 @@ +"""Shared API utilities for Lume and Lumier providers. + +This module contains shared functions for interacting with the Lume API, +used by both the LumeProvider and LumierProvider classes. +""" + +import logging +import json +import subprocess +import urllib.parse +from typing import Dict, List, Optional, Any + +# Setup logging +logger = logging.getLogger(__name__) + +# Check if curl is available +try: + subprocess.run(["curl", "--version"], capture_output=True, check=True) + HAS_CURL = True +except (subprocess.SubprocessError, FileNotFoundError): + HAS_CURL = False + + +def lume_api_get( + vm_name: str, + host: str, + port: int, + storage: Optional[str] = None, + debug: bool = False, + verbose: bool = False +) -> Dict[str, Any]: + """Use curl to get VM information from Lume API. + + Args: + vm_name: Name of the VM to get info for + host: API host + port: API port + storage: Storage path for the VM + debug: Whether to show debug output + verbose: Enable verbose logging + + Returns: + Dictionary with VM status information parsed from JSON response + """ + # URL encode the storage parameter for the query + encoded_storage = "" + storage_param = "" + + if storage: + # First encode the storage path properly + encoded_storage = urllib.parse.quote(storage, safe='') + storage_param = f"?storage={encoded_storage}" + + # Construct API URL with encoded storage parameter if needed + api_url = f"http://{host}:{port}/lume/vms/{vm_name}{storage_param}" + + # Construct the curl command with increased timeouts for more reliability + # --connect-timeout: Time to establish connection (15 seconds) + # --max-time: Maximum time for the whole operation (20 seconds) + # -f: Fail silently (no output at all) on server errors + # Add single quotes around URL to ensure special characters are handled correctly + cmd = ["curl", "--connect-timeout", "15", "--max-time", "20", "-s", "-f", f"'{api_url}'"] + + # For logging and display, show the properly escaped URL + display_cmd = ["curl", "--connect-timeout", "15", "--max-time", "20", "-s", "-f", api_url] + + # Only print the curl command when debug is enabled + display_curl_string = ' '.join(display_cmd) + if debug or verbose: + print(f"DEBUG: Executing curl API call: {display_curl_string}") + logger.debug(f"Executing API request: {display_curl_string}") + + # Execute the command - for execution we need to use shell=True to handle URLs with special characters + try: + # Use a single string with shell=True for proper URL handling + shell_cmd = ' '.join(cmd) + result = subprocess.run(shell_cmd, shell=True, capture_output=True, text=True) + + # Handle curl exit codes + if result.returncode != 0: + curl_error = "Unknown error" + + # Map common curl error codes to helpful messages + if result.returncode == 7: + curl_error = "Failed to connect to the API server - it might still be starting up" + elif result.returncode == 22: + curl_error = "HTTP error returned from API server" + elif result.returncode == 28: + curl_error = "Operation timeout - the API server is taking too long to respond" + elif result.returncode == 52: + curl_error = "Empty reply from server - the API server is starting but not fully ready yet" + elif result.returncode == 56: + curl_error = "Network problem during data transfer - check container networking" + + # Only log at debug level to reduce noise during retries + logger.debug(f"API request failed with code {result.returncode}: {curl_error}") + + # Return a more useful error message + return { + "error": f"API request failed: {curl_error}", + "curl_code": result.returncode, + "vm_name": vm_name, + "status": "unknown" # We don't know the actual status due to API error + } + + # Try to parse the response as JSON + if result.stdout and result.stdout.strip(): + try: + vm_status = json.loads(result.stdout) + if debug or verbose: + logger.info(f"Successfully parsed VM status: {vm_status.get('status', 'unknown')}") + return vm_status + except json.JSONDecodeError as e: + # Return the raw response if it's not valid JSON + logger.warning(f"Invalid JSON response: {e}") + if "Virtual machine not found" in result.stdout: + return {"status": "not_found", "message": "VM not found in Lume API"} + + return {"error": f"Invalid JSON response: {result.stdout[:100]}...", "status": "unknown"} + else: + return {"error": "Empty response from API", "status": "unknown"} + except subprocess.SubprocessError as e: + logger.error(f"Failed to execute API request: {e}") + return {"error": f"Failed to execute API request: {str(e)}", "status": "unknown"} + + +def lume_api_run( + vm_name: str, + host: str, + port: int, + run_opts: Dict[str, Any], + storage: Optional[str] = None, + debug: bool = False, + verbose: bool = False +) -> Dict[str, Any]: + """Run a VM using curl. + + Args: + vm_name: Name of the VM to run + host: API host + port: API port + run_opts: Dictionary of run options + storage: Storage path for the VM + debug: Whether to show debug output + verbose: Enable verbose logging + + Returns: + Dictionary with API response or error information + """ + # Construct API URL + api_url = f"http://{host}:{port}/lume/vms/{vm_name}/run" + + # Prepare JSON payload with required parameters + payload = {} + + # Add CPU cores if specified + if "cpu" in run_opts: + payload["cpu"] = run_opts["cpu"] + + # Add memory if specified + if "memory" in run_opts: + payload["memory"] = run_opts["memory"] + + # Add storage parameter if specified + if storage: + payload["storage"] = storage + elif "storage" in run_opts: + payload["storage"] = run_opts["storage"] + + # Add shared directories if specified + if "shared_directories" in run_opts and run_opts["shared_directories"]: + payload["sharedDirectories"] = run_opts["shared_directories"] + + # Log the payload for debugging + if debug or verbose: + print(f"DEBUG: Payload for {vm_name} run request: {json.dumps(payload, indent=2)}") + logger.debug(f"API payload: {json.dumps(payload, indent=2)}") + + # Construct the curl command + cmd = [ + "curl", "--connect-timeout", "30", "--max-time", "30", + "-s", "-X", "POST", "-H", "Content-Type: application/json", + "-d", json.dumps(payload), + api_url + ] + + # Always print the command for debugging + if debug or verbose: + print(f"DEBUG: Executing curl run API call: {' '.join(cmd)}") + print(f"Run payload: {json.dumps(payload, indent=2)}") + + # Execute the command + try: + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode != 0: + logger.warning(f"API request failed with code {result.returncode}: {result.stderr}") + return {"error": f"API request failed: {result.stderr}"} + + # Try to parse the response as JSON + if result.stdout and result.stdout.strip(): + try: + response = json.loads(result.stdout) + return response + except json.JSONDecodeError: + # Return the raw response if it's not valid JSON + return {"success": True, "message": "VM started successfully", "raw_response": result.stdout} + else: + return {"success": True, "message": "VM started successfully"} + except subprocess.SubprocessError as e: + logger.error(f"Failed to execute run request: {e}") + return {"error": f"Failed to execute run request: {str(e)}"} + + +def lume_api_stop( + vm_name: str, + host: str, + port: int, + storage: Optional[str] = None, + debug: bool = False, + verbose: bool = False +) -> Dict[str, Any]: + """Stop a VM using curl. + + Args: + vm_name: Name of the VM to stop + host: API host + port: API port + storage: Storage path for the VM + debug: Whether to show debug output + verbose: Enable verbose logging + + Returns: + Dictionary with API response or error information + """ + # Construct API URL + api_url = f"http://{host}:{port}/lume/vms/{vm_name}/stop" + + # Prepare JSON payload with required parameters + payload = {} + + # Add storage path if specified + if storage: + payload["storage"] = storage + + # Construct the curl command + cmd = [ + "curl", "--connect-timeout", "15", "--max-time", "20", + "-s", "-X", "POST", "-H", "Content-Type: application/json", + "-d", json.dumps(payload), + api_url + ] + + # Execute the command + try: + if debug or verbose: + logger.info(f"Executing: {' '.join(cmd)}") + + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode != 0: + logger.warning(f"API request failed with code {result.returncode}: {result.stderr}") + return {"error": f"API request failed: {result.stderr}"} + + # Try to parse the response as JSON + if result.stdout and result.stdout.strip(): + try: + response = json.loads(result.stdout) + return response + except json.JSONDecodeError: + # Return the raw response if it's not valid JSON + return {"success": True, "message": "VM stopped successfully", "raw_response": result.stdout} + else: + return {"success": True, "message": "VM stopped successfully"} + except subprocess.SubprocessError as e: + logger.error(f"Failed to execute stop request: {e}") + return {"error": f"Failed to execute stop request: {str(e)}"} + + +def lume_api_update( + vm_name: str, + host: str, + port: int, + update_opts: Dict[str, Any], + storage: Optional[str] = None, + debug: bool = False, + verbose: bool = False +) -> Dict[str, Any]: + """Update VM settings using curl. + + Args: + vm_name: Name of the VM to update + host: API host + port: API port + update_opts: Dictionary of update options + storage: Storage path for the VM + debug: Whether to show debug output + verbose: Enable verbose logging + + Returns: + Dictionary with API response or error information + """ + # Construct API URL + api_url = f"http://{host}:{port}/lume/vms/{vm_name}/update" + + # Prepare JSON payload with required parameters + payload = {} + + # Add CPU cores if specified + if "cpu" in update_opts: + payload["cpu"] = update_opts["cpu"] + + # Add memory if specified + if "memory" in update_opts: + payload["memory"] = update_opts["memory"] + + # Add storage path if specified + if storage: + payload["storage"] = storage + + # Construct the curl command + cmd = [ + "curl", "--connect-timeout", "15", "--max-time", "20", + "-s", "-X", "POST", "-H", "Content-Type: application/json", + "-d", json.dumps(payload), + api_url + ] + + # Execute the command + try: + if debug: + logger.info(f"Executing: {' '.join(cmd)}") + + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode != 0: + logger.warning(f"API request failed with code {result.returncode}: {result.stderr}") + return {"error": f"API request failed: {result.stderr}"} + + # Try to parse the response as JSON + if result.stdout and result.stdout.strip(): + try: + response = json.loads(result.stdout) + return response + except json.JSONDecodeError: + # Return the raw response if it's not valid JSON + return {"success": True, "message": "VM updated successfully", "raw_response": result.stdout} + else: + return {"success": True, "message": "VM updated successfully"} + except subprocess.SubprocessError as e: + logger.error(f"Failed to execute update request: {e}") + return {"error": f"Failed to execute update request: {str(e)}"} + + +def lume_api_pull( + image: str, + name: str, + host: str, + port: int, + storage: Optional[str] = None, + registry: str = "ghcr.io", + organization: str = "trycua", + debug: bool = False, + verbose: bool = False +) -> Dict[str, Any]: + """Pull a VM image from a registry using curl. + + Args: + image: Name/tag of the image to pull + name: Name to give the VM after pulling + host: API host + port: API port + storage: Storage path for the VM + registry: Registry to pull from (default: ghcr.io) + organization: Organization in registry (default: trycua) + debug: Whether to show debug output + verbose: Enable verbose logging + + Returns: + Dictionary with pull status and information + """ + # Prepare pull request payload + pull_payload = { + "image": image, # Use provided image name + "name": name, # Always use name as the target VM name + "registry": registry, + "organization": organization + } + + if storage: + pull_payload["storage"] = storage + + # Construct pull command with proper JSON payload + pull_cmd = [ + "curl" + ] + + if not verbose: + pull_cmd.append("-s") + + pull_cmd.extend([ + "-X", "POST", + "-H", "Content-Type: application/json", + "-d", json.dumps(pull_payload), + f"http://{host}:{port}/lume/pull" + ]) + + if debug or verbose: + print(f"DEBUG: Executing curl API call: {' '.join(pull_cmd)}") + logger.debug(f"Executing API request: {' '.join(pull_cmd)}") + + try: + # Execute pull command + result = subprocess.run(pull_cmd, capture_output=True, text=True) + + if result.returncode != 0: + error_msg = f"Failed to pull VM {name}: {result.stderr}" + logger.error(error_msg) + return {"error": error_msg} + + try: + response = json.loads(result.stdout) + logger.info(f"Successfully initiated pull for VM {name}") + return response + except json.JSONDecodeError: + if result.stdout: + logger.info(f"Pull response: {result.stdout}") + return {"success": True, "message": f"Successfully initiated pull for VM {name}"} + + except subprocess.SubprocessError as e: + error_msg = f"Failed to execute pull command: {str(e)}" + logger.error(error_msg) + return {"error": error_msg} + + +def lume_api_delete( + vm_name: str, + host: str, + port: int, + storage: Optional[str] = None, + debug: bool = False, + verbose: bool = False +) -> Dict[str, Any]: + """Delete a VM using curl. + + Args: + vm_name: Name of the VM to delete + host: API host + port: API port + storage: Storage path for the VM + debug: Whether to show debug output + verbose: Enable verbose logging + + Returns: + Dictionary with API response or error information + """ + # URL encode the storage parameter for the query + encoded_storage = "" + storage_param = "" + + if storage: + # First encode the storage path properly + encoded_storage = urllib.parse.quote(storage, safe='') + storage_param = f"?storage={encoded_storage}" + + # Construct API URL with encoded storage parameter if needed + api_url = f"http://{host}:{port}/lume/vms/{vm_name}{storage_param}" + + # Construct the curl command for DELETE operation - using much longer timeouts matching shell implementation + cmd = ["curl", "--connect-timeout", "6000", "--max-time", "5000", "-s", "-X", "DELETE", f"'{api_url}'"] + + # For logging and display, show the properly escaped URL + display_cmd = ["curl", "--connect-timeout", "6000", "--max-time", "5000", "-s", "-X", "DELETE", api_url] + + # Only print the curl command when debug is enabled + display_curl_string = ' '.join(display_cmd) + if debug or verbose: + print(f"DEBUG: Executing curl API call: {display_curl_string}") + logger.debug(f"Executing API request: {display_curl_string}") + + # Execute the command - for execution we need to use shell=True to handle URLs with special characters + try: + # Use a single string with shell=True for proper URL handling + shell_cmd = ' '.join(cmd) + result = subprocess.run(shell_cmd, shell=True, capture_output=True, text=True) + + # Handle curl exit codes + if result.returncode != 0: + curl_error = "Unknown error" + + # Map common curl error codes to helpful messages + if result.returncode == 7: + curl_error = "Failed to connect to the API server - it might still be starting up" + elif result.returncode == 22: + curl_error = "HTTP error returned from API server" + elif result.returncode == 28: + curl_error = "Operation timeout - the API server is taking too long to respond" + elif result.returncode == 52: + curl_error = "Empty reply from server - the API server is starting but not fully ready yet" + elif result.returncode == 56: + curl_error = "Network problem during data transfer - check container networking" + + # Only log at debug level to reduce noise during retries + logger.debug(f"API request failed with code {result.returncode}: {curl_error}") + + # Return a more useful error message + return { + "error": f"API request failed: {curl_error}", + "curl_code": result.returncode, + "vm_name": vm_name, + "storage": storage + } + + # Try to parse the response as JSON + if result.stdout and result.stdout.strip(): + try: + response = json.loads(result.stdout) + return response + except json.JSONDecodeError: + # Return the raw response if it's not valid JSON + return {"success": True, "message": "VM deleted successfully", "raw_response": result.stdout} + else: + return {"success": True, "message": "VM deleted successfully"} + except subprocess.SubprocessError as e: + logger.error(f"Failed to execute delete request: {e}") + return {"error": f"Failed to execute delete request: {str(e)}"} + + +def parse_memory(memory_str: str) -> int: + """Parse memory string to MB integer. + + Examples: + "8GB" -> 8192 + "1024MB" -> 1024 + "512" -> 512 + + Returns: + Memory value in MB + """ + if isinstance(memory_str, int): + return memory_str + + if isinstance(memory_str, str): + # Extract number and unit + import re + match = re.match(r"(\d+)([A-Za-z]*)", memory_str) + if match: + value, unit = match.groups() + value = int(value) + unit = unit.upper() + + if unit == "GB" or unit == "G": + return value * 1024 + elif unit == "MB" or unit == "M" or unit == "": + return value + + # Default fallback + logger.warning(f"Could not parse memory string '{memory_str}', using 8GB default") + return 8192 # Default to 8GB diff --git a/libs/computer/computer/providers/lumier/__init__.py b/libs/computer/computer/providers/lumier/__init__.py new file mode 100644 index 00000000..32a3954b --- /dev/null +++ b/libs/computer/computer/providers/lumier/__init__.py @@ -0,0 +1,8 @@ +"""Lumier VM provider implementation.""" + +try: + # Use the same import approach as in the Lume provider + from .provider import LumierProvider + HAS_LUMIER = True +except ImportError: + HAS_LUMIER = False diff --git a/libs/computer/computer/providers/lumier/provider.py b/libs/computer/computer/providers/lumier/provider.py new file mode 100644 index 00000000..14c5620d --- /dev/null +++ b/libs/computer/computer/providers/lumier/provider.py @@ -0,0 +1,949 @@ +""" +Lumier VM provider implementation. + +This provider uses Docker containers running the Lumier image to create +macOS and Linux VMs. It handles VM lifecycle operations through Docker +commands and container management. +""" + +import logging +import os +import json +import asyncio +from typing import Dict, List, Optional, Any +import subprocess +import time +import re + +from ..base import BaseVMProvider, VMProviderType +from ..lume_api import ( + lume_api_get, + lume_api_run, + lume_api_stop, + lume_api_update +) + +# Setup logging +logger = logging.getLogger(__name__) + +# Check if Docker is available +try: + subprocess.run(["docker", "--version"], capture_output=True, check=True) + HAS_LUMIER = True +except (subprocess.SubprocessError, FileNotFoundError): + HAS_LUMIER = False + + +class LumierProvider(BaseVMProvider): + """ + Lumier VM Provider implementation using Docker containers. + + This provider uses Docker to run Lumier containers that can create + macOS and Linux VMs through containerization. + """ + + def __init__( + self, + port: Optional[int] = 7777, + host: str = "localhost", + storage: Optional[str] = None, # Can be a path or 'ephemeral' + shared_path: Optional[str] = None, + image: str = "macos-sequoia-cua:latest", # VM image to use + verbose: bool = False, + ephemeral: bool = False, + noVNC_port: Optional[int] = 8006, + ): + """Initialize the Lumier VM Provider. + + Args: + port: Port for the API server (default: 7777) + host: Hostname for the API server (default: localhost) + storage: Path for persistent VM storage + shared_path: Path for shared folder between host and VM + image: VM image to use (e.g. "macos-sequoia-cua:latest") + verbose: Enable verbose logging + ephemeral: Use ephemeral (temporary) storage + noVNC_port: Specific port for noVNC interface (default: 8006) + """ + self.host = host + # Always ensure api_port has a valid value (7777 is the default) + self.api_port = 7777 if port is None else port + self.vnc_port = noVNC_port # User-specified noVNC port, will be set in run_vm if provided + self.ephemeral = ephemeral + + # Handle ephemeral storage (temporary directory) + if ephemeral: + self.storage = "ephemeral" + else: + self.storage = storage + + self.shared_path = shared_path + self.image = image # Store the VM image name to use + # The container_name will be set in run_vm using the VM name + self.verbose = verbose + self._container_id = None + self._api_url = None # Will be set after container starts + + @property + def provider_type(self) -> VMProviderType: + """Return the provider type.""" + return VMProviderType.LUMIER + + def _parse_memory(self, memory_str: str) -> int: + """Parse memory string to MB integer. + + Examples: + "8GB" -> 8192 + "1024MB" -> 1024 + "512" -> 512 + """ + if isinstance(memory_str, int): + return memory_str + + if isinstance(memory_str, str): + # Extract number and unit + match = re.match(r"(\d+)([A-Za-z]*)", memory_str) + if match: + value, unit = match.groups() + value = int(value) + unit = unit.upper() + + if unit == "GB" or unit == "G": + return value * 1024 + elif unit == "MB" or unit == "M" or unit == "": + return value + + # Default fallback + logger.warning(f"Could not parse memory string '{memory_str}', using 8GB default") + return 8192 # Default to 8GB + + # Helper methods for interacting with the Lumier API through curl + # These methods handle the various VM operations via API calls + + def _get_curl_error_message(self, return_code: int) -> str: + """Get a descriptive error message for curl return codes. + + Args: + return_code: The curl return code + + Returns: + A descriptive error message + """ + # Map common curl error codes to helpful messages + if return_code == 7: + return "Failed to connect - API server is starting up" + elif return_code == 22: + return "HTTP error returned from API server" + elif return_code == 28: + return "Operation timeout - API server is slow to respond" + elif return_code == 52: + return "Empty reply from server - API is starting but not ready" + elif return_code == 56: + return "Network problem during data transfer" + else: + return f"Unknown curl error code: {return_code}" + + + async def get_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]: + """Get VM information by name. + + Args: + name: Name of the VM to get information for + storage: Optional storage path override. If provided, this will be used + instead of the provider's default storage path. + + Returns: + Dictionary with VM information including status, IP address, etc. + """ + if not HAS_LUMIER: + logger.error("Docker is not available. Cannot get VM status.") + return { + "name": name, + "status": "unavailable", + "error": "Docker is not available" + } + + # Store the current name for API requests + self.container_name = name + + try: + # Check if the container exists and is running + check_cmd = ["docker", "ps", "-a", "--filter", f"name={name}", "--format", "{{.Status}}"] + check_result = subprocess.run(check_cmd, capture_output=True, text=True) + container_status = check_result.stdout.strip() + + if not container_status: + logger.info(f"Container {name} does not exist. Will create when run_vm is called.") + return { + "name": name, + "status": "not_found", + "message": "Container doesn't exist yet" + } + + # Container exists, check if it's running + is_running = container_status.startswith("Up") + + if not is_running: + logger.info(f"Container {name} exists but is not running. Status: {container_status}") + return { + "name": name, + "status": "stopped", + "container_status": container_status, + } + + # Container is running, get the IP address and API status from Lumier API + logger.info(f"Container {name} is running. Getting VM status from API.") + + # Use the shared lume_api_get function directly + vm_info = lume_api_get( + vm_name=name, + host=self.host, + port=self.api_port, + storage=storage if storage is not None else self.storage, + debug=self.verbose, + verbose=self.verbose + ) + + # Check for API errors + if "error" in vm_info: + # Use debug level instead of warning to reduce log noise during polling + logger.debug(f"API request error: {vm_info['error']}") + return { + "name": name, + "status": "running", # Container is running even if API is not responsive + "api_status": "error", + "error": vm_info["error"], + "container_status": container_status + } + + # Process the VM status information + vm_status = vm_info.get("status", "unknown") + vnc_url = vm_info.get("vncUrl", "") + ip_address = vm_info.get("ipAddress", "") + + # IMPORTANT: Always ensure we have a valid IP address for connectivity + # If the API doesn't return an IP address, default to localhost (127.0.0.1) + # This makes the behavior consistent with LumeProvider + if not ip_address and vm_status == "running": + ip_address = "127.0.0.1" + logger.info(f"No IP address returned from API, defaulting to {ip_address}") + vm_info["ipAddress"] = ip_address + + logger.info(f"VM {name} status: {vm_status}") + + if ip_address and vnc_url: + logger.info(f"VM {name} has IP: {ip_address} and VNC URL: {vnc_url}") + elif not ip_address and not vnc_url and vm_status != "running": + # Not running is expected in this case + logger.info(f"VM {name} is not running yet. Status: {vm_status}") + else: + # Missing IP or VNC but status is running - this is unusual but handled with default IP + logger.warning(f"VM {name} is running but missing expected fields. API response: {vm_info}") + + # Return the full status information + return { + "name": name, + "status": vm_status, + "ip_address": ip_address, + "vnc_url": vnc_url, + "api_status": "ok", + "container_status": container_status, + **vm_info # Include all fields from the API response + } + except subprocess.SubprocessError as e: + logger.error(f"Failed to check container status: {e}") + return { + "name": name, + "status": "error", + "error": f"Failed to check container status: {str(e)}" + } + + async def list_vms(self) -> List[Dict[str, Any]]: + """List all VMs managed by this provider. + + For Lumier provider, there is only one VM per container. + """ + try: + status = await self.get_vm("default") + return [status] if status.get("status") != "unknown" else [] + except Exception as e: + logger.error(f"Failed to list VMs: {e}") + return [] + + async def run_vm(self, image: str, name: str, run_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]: + """Run a VM with the given options. + + Args: + image: Name/tag of the image to use + name: Name of the VM to run (used for the container name and Docker image tag) + run_opts: Options for running the VM, including: + - cpu: Number of CPU cores + - memory: Amount of memory (e.g. "8GB") + - noVNC_port: Specific port for noVNC interface + + Returns: + Dictionary with VM status information + """ + # Set the container name using the VM name for consistency + self.container_name = name + try: + # First, check if container already exists and remove it + try: + check_cmd = ["docker", "ps", "-a", "--filter", f"name={self.container_name}", "--format", "{{.ID}}"] + check_result = subprocess.run(check_cmd, capture_output=True, text=True) + existing_container = check_result.stdout.strip() + + if existing_container: + logger.info(f"Removing existing container: {self.container_name}") + remove_cmd = ["docker", "rm", "-f", self.container_name] + subprocess.run(remove_cmd, check=True) + except subprocess.CalledProcessError as e: + logger.warning(f"Error removing existing container: {e}") + # Continue anyway, next steps will fail if there's a real problem + + # Prepare the Docker run command + cmd = ["docker", "run", "-d", "--name", self.container_name] + + cmd.extend(["-p", f"{self.vnc_port}:8006"]) + print(f"Using specified noVNC_port: {self.vnc_port}") + + # Set API URL using the API port + self._api_url = f"http://{self.host}:{self.api_port}" + + # Parse memory setting + memory_mb = self._parse_memory(run_opts.get("memory", "8GB")) + + # Add storage volume mount if storage is specified (for persistent VM storage) + if self.storage and self.storage != "ephemeral": + # Create storage directory if it doesn't exist + storage_dir = os.path.abspath(os.path.expanduser(self.storage or "")) + os.makedirs(storage_dir, exist_ok=True) + + # Add volume mount for storage + cmd.extend([ + "-v", f"{storage_dir}:/storage", + "-e", f"HOST_STORAGE_PATH={storage_dir}" + ]) + print(f"Using persistent storage at: {storage_dir}") + + # Add shared folder volume mount if shared_path is specified + if self.shared_path: + # Create shared directory if it doesn't exist + shared_dir = os.path.abspath(os.path.expanduser(self.shared_path or "")) + os.makedirs(shared_dir, exist_ok=True) + + # Add volume mount for shared folder + cmd.extend([ + "-v", f"{shared_dir}:/shared", + "-e", f"HOST_SHARED_PATH={shared_dir}" + ]) + print(f"Using shared folder at: {shared_dir}") + + # Add environment variables + # Always use the container_name as the VM_NAME for consistency + # Use the VM image passed from the Computer class + print(f"Using VM image: {self.image}") + + # If ghcr.io is in the image, use the full image name + if "ghcr.io" in self.image: + vm_image = self.image + else: + vm_image = f"ghcr.io/trycua/{self.image}" + + cmd.extend([ + "-e", f"VM_NAME={self.container_name}", + "-e", f"VERSION={vm_image}", + "-e", f"CPU_CORES={run_opts.get('cpu', '4')}", + "-e", f"RAM_SIZE={memory_mb}", + ]) + + # Specify the Lumier image with the full image name + lumier_image = "trycua/lumier:latest" + + # First check if the image exists locally + try: + print(f"Checking if Docker image {lumier_image} exists locally...") + check_image_cmd = ["docker", "image", "inspect", lumier_image] + subprocess.run(check_image_cmd, capture_output=True, check=True) + print(f"Docker image {lumier_image} found locally.") + except subprocess.CalledProcessError: + # Image doesn't exist locally + print(f"\nWARNING: Docker image {lumier_image} not found locally.") + print("The system will attempt to pull it from Docker Hub, which may fail if you have network connectivity issues.") + print("If the Docker pull fails, you may need to manually pull the image first with:") + print(f" docker pull {lumier_image}\n") + + # Add the image to the command + cmd.append(lumier_image) + + # Print the Docker command for debugging + print(f"DOCKER COMMAND: {' '.join(cmd)}") + + # Run the container with improved error handling + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + except subprocess.CalledProcessError as e: + if "no route to host" in str(e.stderr).lower() or "failed to resolve reference" in str(e.stderr).lower(): + error_msg = (f"Network error while trying to pull Docker image '{lumier_image}'\n" + f"Error: {e.stderr}\n\n" + f"SOLUTION: Please try one of the following:\n" + f"1. Check your internet connection\n" + f"2. Pull the image manually with: docker pull {lumier_image}\n" + f"3. Check if Docker is running properly\n") + logger.error(error_msg) + raise RuntimeError(error_msg) + raise + + # Container started, now check VM status with polling + print("Container started, checking VM status...") + print("NOTE: This may take some time while the VM image is being pulled and initialized") + + # Start a background thread to show container logs in real-time + import threading + + def show_container_logs(): + # Give the container a moment to start generating logs + time.sleep(1) + print(f"\n---- CONTAINER LOGS FOR '{name}' (LIVE) ----") + print("Showing logs as they are generated. Press Ctrl+C to stop viewing logs...\n") + + try: + # Use docker logs with follow option + log_cmd = ["docker", "logs", "--tail", "30", "--follow", name] + process = subprocess.Popen(log_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, bufsize=1, universal_newlines=True) + + # Read and print logs line by line + for line in process.stdout: + print(line, end='') + + # Break if process has exited + if process.poll() is not None: + break + except Exception as e: + print(f"\nError showing container logs: {e}") + if self.verbose: + logger.error(f"Error in log streaming thread: {e}") + finally: + print("\n---- LOG STREAMING ENDED ----") + # Make sure process is terminated + if 'process' in locals() and process.poll() is None: + process.terminate() + + # Start log streaming in a background thread if verbose mode is enabled + log_thread = threading.Thread(target=show_container_logs) + log_thread.daemon = True # Thread will exit when main program exits + log_thread.start() + + # Skip waiting for container readiness and just poll get_vm directly + # Poll the get_vm method indefinitely until the VM is ready with an IP address + attempt = 0 + consecutive_errors = 0 + vm_running = False + + while True: # Wait indefinitely + try: + # Use longer delays to give the system time to initialize + if attempt > 0: + # Start with 5s delay, then increase gradually up to 30s for later attempts + # But use shorter delays while we're getting API errors + if consecutive_errors > 0 and consecutive_errors < 5: + wait_time = 3 # Use shorter delays when we're getting API errors + else: + wait_time = min(30, 5 + (attempt * 2)) + + print(f"Waiting {wait_time}s before retry #{attempt+1}...") + await asyncio.sleep(wait_time) + + # Try to get VM status + print(f"Checking VM status (attempt {attempt+1})...") + vm_status = await self.get_vm(name) + + # Check for API errors + if 'error' in vm_status: + consecutive_errors += 1 + error_msg = vm_status.get('error', 'Unknown error') + + # Only print a user-friendly status message, not the raw error + # since _lume_api_get already logged the technical details + if consecutive_errors == 1 or attempt % 5 == 0: + if 'Empty reply from server' in error_msg: + print("API server is starting up - container is running, but API isn't fully initialized yet.") + print("This is expected during the initial VM setup - will continue polling...") + else: + # Don't repeat the exact same error message each time + logger.debug(f"API request error (attempt {attempt+1}): {error_msg}") + # Just log that we're still working on it + if attempt > 3: + print("Still waiting for the API server to become available...") + + # If we're getting errors but container is running, that's normal during startup + if vm_status.get('status') == 'running': + if not vm_running: + print("Container is running, waiting for the VM within it to become fully ready...") + print("This might take a minute while the VM initializes...") + vm_running = True + + # Increase counter and continue + attempt += 1 + continue + + # Reset consecutive error counter when we get a successful response + consecutive_errors = 0 + + # If the VM is running, check if it has an IP address (which means it's fully ready) + if vm_status.get('status') == 'running': + vm_running = True + + # Check if we have an IP address, which means the VM is fully ready + if 'ip_address' in vm_status and vm_status['ip_address']: + print(f"VM is now fully running with IP: {vm_status.get('ip_address')}") + if 'vnc_url' in vm_status and vm_status['vnc_url']: + print(f"VNC URL: {vm_status.get('vnc_url')}") + return vm_status + else: + print("VM is running but still initializing network interfaces...") + print("Waiting for IP address to be assigned...") + else: + # VM exists but might still be starting up + status = vm_status.get('status', 'unknown') + print(f"VM found but status is: {status}. Continuing to poll...") + + # Increase counter for next iteration's delay calculation + attempt += 1 + + # If we reach a very large number of attempts, give a reassuring message but continue + if attempt % 10 == 0: + print(f"Still waiting after {attempt} attempts. This might take several minutes for first-time setup.") + if not vm_running and attempt >= 20: + print("\nNOTE: First-time VM initialization can be slow as images are downloaded.") + print("If this continues for more than 10 minutes, you may want to check:") + print(" 1. Docker logs with: docker logs " + name) + print(" 2. If your network can access container registries") + print("Press Ctrl+C to abort if needed.\n") + + # After 150 attempts (likely over 30-40 minutes), return current status + if attempt >= 150: + print(f"Reached 150 polling attempts. VM status is: {vm_status.get('status', 'unknown')}") + print("Returning current VM status, but please check Docker logs if there are issues.") + return vm_status + + except Exception as e: + # Always continue retrying, but with increasing delays + logger.warning(f"Error checking VM status (attempt {attempt+1}): {e}. Will retry.") + consecutive_errors += 1 + + # If we've had too many consecutive errors, might be a deeper problem + if consecutive_errors >= 10: + print(f"\nWARNING: Encountered {consecutive_errors} consecutive errors while checking VM status.") + print("You may need to check the Docker container logs or restart the process.") + print(f"Error details: {str(e)}\n") + + # Increase attempt counter for next iteration + attempt += 1 + + # After many consecutive errors, add a delay to avoid hammering the system + if attempt > 5: + error_delay = min(30, 10 + attempt) + print(f"Multiple connection errors, waiting {error_delay}s before next attempt...") + await asyncio.sleep(error_delay) + + except subprocess.CalledProcessError as e: + error_msg = f"Failed to start Lumier container: {e.stderr if hasattr(e, 'stderr') else str(e)}" + logger.error(error_msg) + raise RuntimeError(error_msg) + + async def _wait_for_container_ready(self, container_name: str, timeout: int = 90) -> bool: + """Wait for the Lumier container to be fully ready with a valid API response. + + Args: + container_name: Name of the Docker container to check + timeout: Maximum time to wait in seconds (default: 90 seconds) + + Returns: + True if the container is running, even if API is not fully ready. + This allows operations to continue with appropriate fallbacks. + """ + start_time = time.time() + api_ready = False + container_running = False + + print(f"Waiting for container {container_name} to be ready (timeout: {timeout}s)...") + + while time.time() - start_time < timeout: + # Check if container is running + try: + check_cmd = ["docker", "ps", "--filter", f"name={container_name}", "--format", "{{.Status}}"] + result = subprocess.run(check_cmd, capture_output=True, text=True, check=True) + container_status = result.stdout.strip() + + if container_status and container_status.startswith("Up"): + container_running = True + print(f"Container {container_name} is running") + logger.info(f"Container {container_name} is running with status: {container_status}") + else: + logger.warning(f"Container {container_name} not yet running, status: {container_status}") + # container is not running yet, wait and try again + await asyncio.sleep(2) # Longer sleep to give Docker time + continue + except subprocess.CalledProcessError as e: + logger.warning(f"Error checking container status: {e}") + await asyncio.sleep(2) + continue + + # Container is running, check if API is responsive + try: + # First check the health endpoint + api_url = f"http://{self.host}:{self.api_port}/health" + logger.info(f"Checking API health at: {api_url}") + + # Use longer timeout for API health check since it may still be initializing + curl_cmd = ["curl", "-s", "--connect-timeout", "5", "--max-time", "10", api_url] + result = subprocess.run(curl_cmd, capture_output=True, text=True) + + if result.returncode == 0 and "ok" in result.stdout.lower(): + api_ready = True + print(f"API is ready at {api_url}") + logger.info(f"API is ready at {api_url}") + break + else: + # API health check failed, now let's check if the VM status endpoint is responsive + # This covers cases where the health endpoint isn't implemented but the VM API is working + vm_api_url = f"http://{self.host}:{self.api_port}/lume/vms/{container_name}" + if self.storage: + import urllib.parse + encoded_storage = urllib.parse.quote_plus(self.storage) + vm_api_url += f"?storage={encoded_storage}" + + curl_vm_cmd = ["curl", "-s", "--connect-timeout", "5", "--max-time", "10", vm_api_url] + vm_result = subprocess.run(curl_vm_cmd, capture_output=True, text=True) + + if vm_result.returncode == 0 and vm_result.stdout.strip(): + # VM API responded with something - consider the API ready + api_ready = True + print(f"VM API is ready at {vm_api_url}") + logger.info(f"VM API is ready at {vm_api_url}") + break + else: + curl_code = result.returncode + if curl_code == 0: + curl_code = vm_result.returncode + + # Map common curl error codes to helpful messages + if curl_code == 7: + curl_error = "Failed to connect - API server is starting up" + elif curl_code == 22: + curl_error = "HTTP error returned from API server" + elif curl_code == 28: + curl_error = "Operation timeout - API server is slow to respond" + elif curl_code == 52: + curl_error = "Empty reply from server - API is starting but not ready" + elif curl_code == 56: + curl_error = "Network problem during data transfer" + else: + curl_error = f"Unknown curl error code: {curl_code}" + + print(f"API not ready yet: {curl_error}") + logger.info(f"API not ready yet: {curl_error}") + except subprocess.SubprocessError as e: + logger.warning(f"Error checking API status: {e}") + + # If the container is running but API is not ready, that's OK - we'll just wait + # a bit longer before checking again, as the container may still be initializing + elapsed_seconds = time.time() - start_time + if int(elapsed_seconds) % 5 == 0: # Only print status every 5 seconds to reduce verbosity + print(f"Waiting for API to initialize... ({elapsed_seconds:.1f}s / {timeout}s)") + + await asyncio.sleep(3) # Longer sleep between API checks + + # Handle timeout - if the container is running but API is not ready, that's not + # necessarily an error - the API might just need more time to start up + if not container_running: + print(f"Timed out waiting for container {container_name} to start") + logger.warning(f"Timed out waiting for container {container_name} to start") + return False + + if not api_ready: + print(f"Container {container_name} is running, but API is not fully ready yet.") + print("Proceeding with operations. API will become available shortly.") + print("NOTE: You may see some 'API request failed' messages while the API initializes.") + logger.warning(f"Container {container_name} is running, but API is not fully ready yet.") + + # Return True if container is running, even if API isn't ready yet + # This allows VM operations to proceed, with appropriate retries for API calls + return container_running + + async def stop_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]: + """Stop a running VM by stopping the Lumier container.""" + try: + # Use Docker commands to stop the container directly + if hasattr(self, '_container_id') and self._container_id: + logger.info(f"Stopping Lumier container: {self.container_name}") + cmd = ["docker", "stop", self.container_name] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + logger.info(f"Container stopped: {result.stdout.strip()}") + + # Return minimal status info + return { + "name": name, + "status": "stopped", + "container_id": self._container_id, + } + else: + # Try to find the container by name + check_cmd = ["docker", "ps", "-a", "--filter", f"name={self.container_name}", "--format", "{{.ID}}"] + check_result = subprocess.run(check_cmd, capture_output=True, text=True) + container_id = check_result.stdout.strip() + + if container_id: + logger.info(f"Found container ID: {container_id}") + cmd = ["docker", "stop", self.container_name] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + logger.info(f"Container stopped: {result.stdout.strip()}") + + return { + "name": name, + "status": "stopped", + "container_id": container_id, + } + else: + logger.warning(f"No container found with name {self.container_name}") + return { + "name": name, + "status": "unknown", + } + except subprocess.CalledProcessError as e: + error_msg = f"Failed to stop container: {e.stderr if hasattr(e, 'stderr') else str(e)}" + logger.error(error_msg) + raise RuntimeError(f"Failed to stop Lumier container: {error_msg}") + + # update_vm is not implemented as it's not needed for Lumier + # The BaseVMProvider requires it, so we provide a minimal implementation + async def update_vm(self, name: str, update_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]: + """Not implemented for Lumier provider.""" + logger.warning("update_vm is not implemented for Lumier provider") + return {"name": name, "status": "unchanged"} + + async def get_logs(self, name: str, num_lines: int = 100, follow: bool = False, timeout: Optional[int] = None) -> str: + """Get the logs from the Lumier container. + + Args: + name: Name of the VM/container to get logs for + num_lines: Number of recent log lines to return (default: 100) + follow: If True, follow the logs (stream new logs as they are generated) + timeout: Optional timeout in seconds for follow mode (None means no timeout) + + Returns: + Container logs as a string + + Note: + If follow=True, this function will continuously stream logs until timeout + or until interrupted. The output will be printed to console in real-time. + """ + if not HAS_LUMIER: + error_msg = "Docker is not available. Cannot get container logs." + logger.error(error_msg) + return error_msg + + # Make sure we have a container name + container_name = name + + # Check if the container exists and is running + try: + # Check if the container exists + inspect_cmd = ["docker", "container", "inspect", container_name] + result = subprocess.run(inspect_cmd, capture_output=True, text=True) + + if result.returncode != 0: + error_msg = f"Container '{container_name}' does not exist or is not accessible" + logger.error(error_msg) + return error_msg + except Exception as e: + error_msg = f"Error checking container status: {str(e)}" + logger.error(error_msg) + return error_msg + + # Base docker logs command + log_cmd = ["docker", "logs"] + + # Add tail parameter to limit the number of lines + log_cmd.extend(["--tail", str(num_lines)]) + + # Handle follow mode with or without timeout + if follow: + log_cmd.append("--follow") + + if timeout is not None: + # For follow mode with timeout, we'll run the command and handle the timeout + log_cmd.append(container_name) + logger.info(f"Following logs for container '{container_name}' with timeout {timeout}s") + print(f"\n---- CONTAINER LOGS FOR '{container_name}' (LIVE) ----") + print(f"Press Ctrl+C to stop following logs\n") + + try: + # Run with timeout + process = subprocess.Popen(log_cmd, text=True) + + # Wait for the specified timeout + if timeout: + try: + process.wait(timeout=timeout) + except subprocess.TimeoutExpired: + process.terminate() # Stop after timeout + print(f"\n---- LOG FOLLOWING STOPPED (timeout {timeout}s reached) ----") + else: + # Without timeout, wait for user interruption + process.wait() + + return "Logs were displayed to console in follow mode" + except KeyboardInterrupt: + process.terminate() + print("\n---- LOG FOLLOWING STOPPED (user interrupted) ----") + return "Logs were displayed to console in follow mode (interrupted)" + else: + # For follow mode without timeout, we'll print a helpful message + log_cmd.append(container_name) + logger.info(f"Following logs for container '{container_name}' indefinitely") + print(f"\n---- CONTAINER LOGS FOR '{container_name}' (LIVE) ----") + print(f"Press Ctrl+C to stop following logs\n") + + try: + # Run the command and let it run until interrupted + process = subprocess.Popen(log_cmd, text=True) + process.wait() # Wait indefinitely (until user interrupts) + return "Logs were displayed to console in follow mode" + except KeyboardInterrupt: + process.terminate() + print("\n---- LOG FOLLOWING STOPPED (user interrupted) ----") + return "Logs were displayed to console in follow mode (interrupted)" + else: + # For non-follow mode, capture and return the logs as a string + log_cmd.append(container_name) + logger.info(f"Getting {num_lines} log lines for container '{container_name}'") + + try: + result = subprocess.run(log_cmd, capture_output=True, text=True, check=True) + logs = result.stdout + + # Only print header and logs if there's content + if logs.strip(): + print(f"\n---- CONTAINER LOGS FOR '{container_name}' (LAST {num_lines} LINES) ----\n") + print(logs) + print(f"\n---- END OF LOGS ----") + else: + print(f"\nNo logs available for container '{container_name}'") + + return logs + except subprocess.CalledProcessError as e: + error_msg = f"Error getting logs: {e.stderr}" + logger.error(error_msg) + return error_msg + except Exception as e: + error_msg = f"Unexpected error getting logs: {str(e)}" + logger.error(error_msg) + return error_msg + + async def get_ip(self, name: str, storage: Optional[str] = None, retry_delay: int = 2) -> str: + """Get the IP address of a VM, waiting indefinitely until it's available. + + Args: + name: Name of the VM to get the IP for + storage: Optional storage path override + retry_delay: Delay between retries in seconds (default: 2) + + Returns: + IP address of the VM when it becomes available + """ + # Use container_name = name for consistency + self.container_name = name + + # Track total attempts for logging purposes + total_attempts = 0 + + # Loop indefinitely until we get a valid IP + while True: + total_attempts += 1 + + # Log retry message but not on first attempt + if total_attempts > 1: + logger.info(f"Waiting for VM {name} IP address (attempt {total_attempts})...") + + try: + # Get VM information + vm_info = await self.get_vm(name, storage=storage) + + # Check if we got a valid IP + ip = vm_info.get("ip_address", None) + if ip and ip != "unknown" and not ip.startswith("0.0.0.0"): + logger.info(f"Got valid VM IP address: {ip}") + return ip + + # Check the VM status + status = vm_info.get("status", "unknown") + + # Special handling for Lumier: it may report "stopped" even when the VM is starting + # If the VM information contains an IP but status is stopped, it might be a race condition + if status == "stopped" and "ip_address" in vm_info: + ip = vm_info.get("ip_address") + if ip and ip != "unknown" and not ip.startswith("0.0.0.0"): + logger.info(f"Found valid IP {ip} despite VM status being {status}") + return ip + logger.info(f"VM status is {status}, but still waiting for IP to be assigned") + # If VM is not running yet, log and wait + elif status != "running": + logger.info(f"VM is not running yet (status: {status}). Waiting...") + # If VM is running but no IP yet, wait and retry + else: + logger.info("VM is running but no valid IP address yet. Waiting...") + + except Exception as e: + logger.warning(f"Error getting VM {name} IP: {e}, continuing to wait...") + + # Wait before next retry + await asyncio.sleep(retry_delay) + + # Add progress log every 10 attempts + if total_attempts % 10 == 0: + logger.info(f"Still waiting for VM {name} IP after {total_attempts} attempts...") + + async def __aenter__(self): + """Async context manager entry. + + This method is called when entering an async context manager block. + Returns self to be used in the context. + """ + logger.debug("Entering LumierProvider context") + + # Initialize the API URL with the default value if not already set + # This ensures get_vm can work before run_vm is called + if not hasattr(self, '_api_url') or not self._api_url: + self._api_url = f"http://{self.host}:{self.api_port}" + logger.info(f"Initialized default Lumier API URL: {self._api_url}") + + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit. + + This method is called when exiting an async context manager block. + It handles proper cleanup of resources, including stopping any running containers. + """ + logger.debug(f"Exiting LumierProvider context, handling exceptions: {exc_type}") + try: + # If we have a container ID, we should stop it to clean up resources + if hasattr(self, '_container_id') and self._container_id: + logger.info(f"Stopping Lumier container on context exit: {self.container_name}") + try: + cmd = ["docker", "stop", self.container_name] + subprocess.run(cmd, capture_output=True, text=True, check=True) + logger.info(f"Container stopped during context exit: {self.container_name}") + except subprocess.CalledProcessError as e: + logger.warning(f"Failed to stop container during cleanup: {e.stderr}") + # Don't raise an exception here, we want to continue with cleanup + except Exception as e: + logger.error(f"Error during LumierProvider cleanup: {e}") + # We don't want to suppress the original exception if there was one + if exc_type is None: + raise + # Return False to indicate that any exception should propagate + return False diff --git a/libs/computer/computer/ui/gradio/app.py b/libs/computer/computer/ui/gradio/app.py index e68ea26a..a9ae2154 100644 --- a/libs/computer/computer/ui/gradio/app.py +++ b/libs/computer/computer/ui/gradio/app.py @@ -17,7 +17,7 @@ from datetime import datetime from PIL import Image from huggingface_hub import DatasetCard, DatasetCardData -from computer import Computer +from computer import Computer, VMProviderType from gradio.components import ChatMessage import pandas as pd from datasets import Dataset, Features, Sequence, concatenate_datasets @@ -463,7 +463,7 @@ async def execute(name, action, arguments): elif action == "left_click": if "x" in arguments and "y" in arguments: await computer.interface.move_cursor(arguments["x"], arguments["y"]) - await computer.interface.left_click() + await computer.interface.left_click(arguments["x"], arguments["y"]) await asyncio.sleep(0.5) elif action == "right_click": if "x" in arguments and "y" in arguments: @@ -528,21 +528,92 @@ async def execute(name, action, arguments): return results -async def handle_init_computer(): - """Initialize the computer instance and tools""" +async def handle_init_computer(os_choice: str, app_list=None, provider="lume", container_name=None, api_key=None): + """Initialize the computer instance and tools for macOS or Ubuntu + + Args: + os_choice: The OS to use ("macOS" or "Ubuntu") + app_list: Optional list of apps to focus on using the app-use experiment + provider: The provider to use ("lume" or "self" or "cloud") + container_name: The container name to use for cloud provider + api_key: The API key to use for cloud provider + """ global computer, tool_call_logs, tools - computer = Computer(os_type="macos", display="1024x768", memory="8GB", cpu="4") + # Check if we should enable app-use experiment + use_app_experiment = app_list and len(app_list) > 0 + experiments = ["app-use"] if use_app_experiment else None + + # Determine if we should use host computer server + use_host_computer_server = provider == "self" + + if os_choice == "Ubuntu": + os_type_str = "linux" + image_str = "ubuntu-noble-vanilla:latest" + else: + os_type_str = "macos" + image_str = "macos-sequoia-cua:latest" + + # Create computer instance with appropriate configuration + if use_host_computer_server: + computer = Computer( + os_type=os_type_str, + use_host_computer_server=True, + experiments=experiments + ) + elif provider == "cloud": + # Use API key from environment variable or field input + cloud_api_key = os.environ.get("CUA_API_KEY") or api_key + computer = Computer( + os_type=os_type_str, + provider_type=VMProviderType.CLOUD, + name=container_name, + api_key=cloud_api_key, + experiments=experiments + ) + else: + computer = Computer( + image=image_str, + os_type=os_type_str, + provider_type=VMProviderType.LUME, + display="1024x768", + memory="8GB", + cpu="4", + experiments=experiments + ) + await computer.run() + # If app list is provided, create desktop from apps + if use_app_experiment: + computer = computer.create_desktop_from_apps(app_list) + # Log computer initialization as a tool call - result = await execute("computer", "initialize", { - "os": "macos", - "display": "1024x768", - "memory": "8GB", - "cpu": "4" - }) + init_params = { + "os": os_type_str, + "provider": provider + } + # Add VM-specific parameters if not using host computer server + if not use_host_computer_server: + init_params.update({ + "image": image_str, + "display": "1024x768", + "memory": "8GB", + "cpu": "4" + }) + + # Add app list to the log if provided + if use_app_experiment: + init_params["apps"] = app_list + init_params["experiments"] = ["app-use"] + + # Add container name to the log if using cloud provider + if provider == "cloud": + init_params["container_name"] = container_name + + result = await execute("computer", "initialize", init_params) + return result["screenshot"], json.dumps(tool_call_logs, indent=2) async def handle_screenshot(): @@ -1004,8 +1075,69 @@ def update_random_name(): run_setup_btn = gr.Button("⚙️ Run Task Setup") # Setup status textbox setup_status = gr.Textbox(label="Setup Status", value="") + + with gr.Group(): + with gr.Accordion("Computer Configuration", open=False): + with gr.Row(): + os_choice = gr.Radio( + label="OS", + choices=["macOS", "Ubuntu"], + value="macOS", + ) + + # Provider selection radio + provider_choice = gr.Radio( + label="Provider", + choices=["lume", "self", "cloud"], + value="lume", + info="'lume' uses a VM, 'self' uses the host computer server, 'cloud' uses a cloud container" + ) + + # Container name field for cloud provider (initially hidden) + container_name = gr.Textbox( + label="Container Name", + placeholder="Enter your container name", + visible=False, + info="Get your container from [trycua.com](https://trycua.com/)" + ) + + # Check if CUA_API_KEY is set in environment + has_cua_key = os.environ.get("CUA_API_KEY") is not None + + # API key field for cloud provider (visible only if no env key and cloud selected) + api_key_field = gr.Textbox( + label="CUA API Key", + placeholder="Enter your CUA API key", + type="password", + visible=False, + info="Required for cloud provider. Set CUA_API_KEY environment variable to hide this field." + ) + + # App filtering dropdown for app-use experiment + app_filter = gr.Dropdown( + label="Filter by apps (App-Use)", + multiselect=True, + allow_custom_value=True, + info="When apps are selected, the computer will focus on those apps using the app-use experiment" + ) + + # Function to show/hide container name and API key fields based on provider selection + def update_cloud_fields_visibility(provider): + show_container = provider == "cloud" + show_api_key = provider == "cloud" and not has_cua_key + return ( + gr.update(visible=show_container), + gr.update(visible=show_api_key) + ) + + # Connect provider choice to field visibility + provider_choice.change( + update_cloud_fields_visibility, + inputs=provider_choice, + outputs=[container_name, api_key_field] + ) - start_btn = gr.Button("Initialize Computer") + start_btn = gr.Button("Initialize Computer") with gr.Group(): input_text = gr.Textbox(label="Type Text") @@ -1068,7 +1200,7 @@ def update_random_name(): value=False ) message_submit_btn = gr.Button("Submit Message") - message_status = gr.Textbox(label="Status", value="") + message_status = gr.Textbox(label="Status") with gr.Accordion("Clipboard Operations", open=False): clipboard_content = gr.Textbox(label="Clipboard Content") @@ -1169,7 +1301,7 @@ async def run_task_setup(task_text): ) img.select(handle_click, inputs=[img, click_type], outputs=[img, action_log]) - start_btn.click(handle_init_computer, outputs=[img, action_log]) + start_btn.click(handle_init_computer, inputs=[os_choice, app_filter, provider_choice, container_name, api_key_field], outputs=[img, action_log]) wait_btn.click(handle_wait, outputs=[img, action_log]) # DONE and FAIL buttons just do a placeholder action diff --git a/libs/computer/pyproject.toml b/libs/computer/pyproject.toml index ed438947..c9aa46da 100644 --- a/libs/computer/pyproject.toml +++ b/libs/computer/pyproject.toml @@ -11,7 +11,6 @@ authors = [ { name = "TryCua", email = "gh@trycua.com" } ] dependencies = [ - "pylume>=0.1.8", "pillow>=10.0.0", "websocket-client>=1.8.0", "websockets>=12.0", @@ -19,12 +18,23 @@ dependencies = [ "cua-core>=0.1.0,<0.2.0", "pydantic>=2.11.1" ] -requires-python = ">=3.10" +requires-python = ">=3.11" [project.optional-dependencies] +lume = [ +] +lumier = [ +] ui = [ "gradio>=5.23.3,<6.0.0", "python-dotenv>=1.0.1,<2.0.0", + "datasets>=3.6.0,<4.0.0", +] +all = [ + # Include all optional dependencies + "gradio>=5.23.3,<6.0.0", + "python-dotenv>=1.0.1,<2.0.0", + "datasets>=3.6.0,<4.0.0", ] [tool.pdm] @@ -36,11 +46,11 @@ source-includes = ["tests/", "README.md", "LICENSE"] [tool.black] line-length = 100 -target-version = ["py310"] +target-version = ["py311"] [tool.ruff] line-length = 100 -target-version = "py310" +target-version = "py311" select = ["E", "F", "B", "I"] fix = true @@ -49,7 +59,7 @@ docstring-code-format = true [tool.mypy] strict = true -python_version = "3.10" +python_version = "3.11" ignore_missing_imports = true disallow_untyped_defs = true check_untyped_defs = true diff --git a/libs/core/pyproject.toml b/libs/core/pyproject.toml index bf0f1cf5..82ecc775 100644 --- a/libs/core/pyproject.toml +++ b/libs/core/pyproject.toml @@ -15,7 +15,7 @@ dependencies = [ "httpx>=0.24.0", "posthog>=3.20.0" ] -requires-python = ">=3.10" +requires-python = ">=3.11" [tool.pdm] distribution = true @@ -26,11 +26,11 @@ source-includes = ["tests/", "README.md", "LICENSE"] [tool.black] line-length = 100 -target-version = ["py310"] +target-version = ["py311"] [tool.ruff] line-length = 100 -target-version = "py310" +target-version = "py311" select = ["E", "F", "B", "I"] fix = true @@ -39,7 +39,7 @@ docstring-code-format = true [tool.mypy] strict = true -python_version = "3.10" +python_version = "3.11" ignore_missing_imports = true disallow_untyped_defs = true check_untyped_defs = true diff --git a/libs/lume/README.md b/libs/lume/README.md index b7112b07..ee0b966c 100644 --- a/libs/lume/README.md +++ b/libs/lume/README.md @@ -136,7 +136,7 @@ Command Options: set Enable or disable image caching serve: - --port Port to listen on (default: 3000) + --port Port to listen on (default: 7777) ``` ## Install @@ -178,7 +178,7 @@ For additional disk space, resize the VM disk after pulling the image using the ## Local API Server -`lume` exposes a local HTTP API server that listens on `http://localhost:3000/lume`, enabling automated management of VMs. +`lume` exposes a local HTTP API server that listens on `http://localhost:7777/lume`, enabling automated management of VMs. ```bash lume serve diff --git a/libs/lume/docs/API-Reference.md b/libs/lume/docs/API-Reference.md index 7ab9459b..5af09cdf 100644 --- a/libs/lume/docs/API-Reference.md +++ b/libs/lume/docs/API-Reference.md @@ -18,7 +18,7 @@ curl --connect-timeout 6000 \ "ipsw": "latest", "storage": "ssd" }' \ - http://localhost:3000/lume/vms + http://localhost:7777/lume/vms ``` @@ -30,7 +30,7 @@ curl --connect-timeout 6000 \ curl --connect-timeout 6000 \ --max-time 5000 \ -X POST \ - http://localhost:3000/lume/vms/my-vm-name/run + http://localhost:7777/lume/vms/my-vm-name/run # Run with VNC client started and shared directory curl --connect-timeout 6000 \ @@ -48,7 +48,7 @@ curl --connect-timeout 6000 \ "recoveryMode": false, "storage": "ssd" }' \ - http://localhost:3000/lume/vms/lume_vm/run + http://localhost:7777/lume/vms/lume_vm/run ``` @@ -58,7 +58,7 @@ curl --connect-timeout 6000 \ ```bash curl --connect-timeout 6000 \ --max-time 5000 \ - http://localhost:3000/lume/vms + http://localhost:7777/lume/vms ``` ``` [ @@ -89,12 +89,12 @@ curl --connect-timeout 6000 \ # Basic get curl --connect-timeout 6000 \ --max-time 5000 \ - http://localhost:3000/lume/vms/lume_vm + http://localhost:7777/lume/vms/lume_vm # Get with storage location specified curl --connect-timeout 6000 \ --max-time 5000 \ - http://localhost:3000/lume/vms/lume_vm?storage=ssd + http://localhost:7777/lume/vms/lume_vm?storage=ssd ``` ``` { @@ -122,7 +122,7 @@ curl --connect-timeout 6000 \ "diskSize": "128GB", "storage": "ssd" }' \ - http://localhost:3000/lume/vms/my-vm-name + http://localhost:7777/lume/vms/my-vm-name ``` @@ -134,13 +134,13 @@ curl --connect-timeout 6000 \ curl --connect-timeout 6000 \ --max-time 5000 \ -X POST \ - http://localhost:3000/lume/vms/my-vm-name/stop + http://localhost:7777/lume/vms/my-vm-name/stop # Stop with storage location specified curl --connect-timeout 6000 \ --max-time 5000 \ -X POST \ - http://localhost:3000/lume/vms/my-vm-name/stop?storage=ssd + http://localhost:7777/lume/vms/my-vm-name/stop?storage=ssd ``` @@ -152,13 +152,13 @@ curl --connect-timeout 6000 \ curl --connect-timeout 6000 \ --max-time 5000 \ -X DELETE \ - http://localhost:3000/lume/vms/my-vm-name + http://localhost:7777/lume/vms/my-vm-name # Delete with storage location specified curl --connect-timeout 6000 \ --max-time 5000 \ -X DELETE \ - http://localhost:3000/lume/vms/my-vm-name?storage=ssd + http://localhost:7777/lume/vms/my-vm-name?storage=ssd ``` @@ -177,7 +177,7 @@ curl --connect-timeout 6000 \ "organization": "trycua", "storage": "ssd" }' \ - http://localhost:3000/lume/pull + http://localhost:7777/lume/pull ``` ```bash @@ -189,7 +189,7 @@ curl --connect-timeout 6000 \ "image": "macos-sequoia-vanilla:15.2", "name": "macos-sequoia-vanilla" }' \ - http://localhost:3000/lume/pull + http://localhost:7777/lume/pull ``` @@ -211,7 +211,7 @@ curl --connect-timeout 6000 \ "chunkSizeMb": 512, "storage": null }' \ - http://localhost:3000/lume/vms/push + http://localhost:7777/lume/vms/push ``` **Response (202 Accepted):** @@ -243,7 +243,7 @@ curl --connect-timeout 6000 \ "sourceLocation": "default", "destLocation": "ssd" }' \ - http://localhost:3000/lume/vms/clone + http://localhost:7777/lume/vms/clone ``` @@ -253,7 +253,7 @@ curl --connect-timeout 6000 \ ```bash curl --connect-timeout 6000 \ --max-time 5000 \ - http://localhost:3000/lume/ipsw + http://localhost:7777/lume/ipsw ``` @@ -264,7 +264,7 @@ curl --connect-timeout 6000 \ # List images with default organization (trycua) curl --connect-timeout 6000 \ --max-time 5000 \ - http://localhost:3000/lume/images + http://localhost:7777/lume/images ``` ```json @@ -284,7 +284,7 @@ curl --connect-timeout 6000 \ curl --connect-timeout 6000 \ --max-time 5000 \ -X POST \ - http://localhost:3000/lume/prune + http://localhost:7777/lume/prune ``` @@ -294,7 +294,7 @@ curl --connect-timeout 6000 \ ```bash curl --connect-timeout 6000 \ --max-time 5000 \ - http://localhost:3000/lume/config + http://localhost:7777/lume/config ``` ```json @@ -319,7 +319,7 @@ curl --connect-timeout 6000 \ "cacheDirectory": "~/custom/lume/cache", "cachingEnabled": true }' \ - http://localhost:3000/lume/config + http://localhost:7777/lume/config ``` @@ -329,7 +329,7 @@ curl --connect-timeout 6000 \ ```bash curl --connect-timeout 6000 \ --max-time 5000 \ - http://localhost:3000/lume/config/locations + http://localhost:7777/lume/config/locations ``` ```json @@ -360,7 +360,7 @@ curl --connect-timeout 6000 \ "name": "ssd", "path": "/Volumes/SSD/lume/vms" }' \ - http://localhost:3000/lume/config/locations + http://localhost:7777/lume/config/locations ``` @@ -371,7 +371,7 @@ curl --connect-timeout 6000 \ curl --connect-timeout 6000 \ --max-time 5000 \ -X DELETE \ - http://localhost:3000/lume/config/locations/ssd + http://localhost:7777/lume/config/locations/ssd ``` @@ -382,6 +382,6 @@ curl --connect-timeout 6000 \ curl --connect-timeout 6000 \ --max-time 5000 \ -X POST \ - http://localhost:3000/lume/config/locations/default/ssd + http://localhost:7777/lume/config/locations/default/ssd ``` diff --git a/libs/lume/scripts/build/build-release-notarized.sh b/libs/lume/scripts/build/build-release-notarized.sh index 19fb2e88..603446b7 100755 --- a/libs/lume/scripts/build/build-release-notarized.sh +++ b/libs/lume/scripts/build/build-release-notarized.sh @@ -72,12 +72,23 @@ cp -f .build/release/lume "$TEMP_ROOT/usr/local/bin/" # Build the installer package log "essential" "Building installer package..." -pkgbuild --root "$TEMP_ROOT" \ +if ! pkgbuild --root "$TEMP_ROOT" \ --identifier "com.trycua.lume" \ --version "1.0" \ --install-location "/" \ --sign "$CERT_INSTALLER_NAME" \ - ./.release/lume.pkg 2> /dev/null + ./.release/lume.pkg; then + log "error" "Failed to build installer package" + exit 1 +fi + +# Verify the package was created +if [ ! -f "./.release/lume.pkg" ]; then + log "error" "Package file ./.release/lume.pkg was not created" + exit 1 +fi + +log "essential" "Package created successfully" # Submit for notarization using stored credentials log "essential" "Submitting for notarization..." @@ -89,24 +100,33 @@ if [ "$LOG_LEVEL" = "minimal" ] || [ "$LOG_LEVEL" = "none" ]; then --password "${APP_SPECIFIC_PASSWORD}" \ --wait 2>&1) - # Just show success or failure + # Check if notarization was successful if echo "$NOTARY_OUTPUT" | grep -q "status: Accepted"; then log "essential" "Notarization successful!" else log "error" "Notarization failed. Please check logs." + log "error" "Notarization output:" + echo "$NOTARY_OUTPUT" + exit 1 fi else # Normal verbose output - xcrun notarytool submit ./.release/lume.pkg \ + if ! xcrun notarytool submit ./.release/lume.pkg \ --apple-id "${APPLE_ID}" \ --team-id "${TEAM_ID}" \ --password "${APP_SPECIFIC_PASSWORD}" \ - --wait + --wait; then + log "error" "Notarization failed" + exit 1 + fi fi # Staple the notarization ticket log "essential" "Stapling notarization ticket..." -xcrun stapler staple ./.release/lume.pkg > /dev/null 2>&1 +if ! xcrun stapler staple ./.release/lume.pkg > /dev/null 2>&1; then + log "error" "Failed to staple notarization ticket" + exit 1 +fi # Create temporary directory for package extraction EXTRACT_ROOT=$(mktemp -d) diff --git a/libs/lume/scripts/install.sh b/libs/lume/scripts/install.sh index 11629f49..96512866 100755 --- a/libs/lume/scripts/install.sh +++ b/libs/lume/scripts/install.sh @@ -33,8 +33,8 @@ LATEST_RELEASE_URL="https://api.github.com/repos/$GITHUB_REPO/releases/latest" # Option to skip background service setup (default: install it) INSTALL_BACKGROUND_SERVICE=true -# Default port for lume serve (default: 3000) -LUME_PORT=3000 +# Default port for lume serve (default: 7777) +LUME_PORT=7777 # Parse command line arguments while [ "$#" -gt 0 ]; do @@ -56,14 +56,14 @@ while [ "$#" -gt 0 ]; do echo "" echo "Options:" echo " --install-dir DIR Install to the specified directory (default: $DEFAULT_INSTALL_DIR)" - echo " --port PORT Specify the port for lume serve (default: 3000)" + echo " --port PORT Specify the port for lume serve (default: 7777)" echo " --no-background-service Do not setup the Lume background service (LaunchAgent)" echo " --help Display this help message" echo "" echo "Examples:" echo " $0 # Install to $DEFAULT_INSTALL_DIR and setup background service" echo " $0 --install-dir=/usr/local/bin # Install to system directory (may require root privileges)" - echo " $0 --port 3001 # Use port 3001 instead of the default 3000" + echo " $0 --port 7778 # Use port 7778 instead of the default 7777" echo " $0 --no-background-service # Install without setting up the background service" echo " INSTALL_DIR=/opt/lume $0 # Install to /opt/lume (legacy env var support)" exit 0 diff --git a/libs/lume/src/Commands/Serve.swift b/libs/lume/src/Commands/Serve.swift index 556800c6..f22e8107 100644 --- a/libs/lume/src/Commands/Serve.swift +++ b/libs/lume/src/Commands/Serve.swift @@ -7,7 +7,7 @@ struct Serve: AsyncParsableCommand { ) @Option(help: "Port to listen on") - var port: UInt16 = 3000 + var port: UInt16 = 7777 func run() async throws { let server = await Server(port: port) diff --git a/libs/lume/src/ContainerRegistry/ImageContainerRegistry.swift b/libs/lume/src/ContainerRegistry/ImageContainerRegistry.swift index a7a68212..8646370d 100644 --- a/libs/lume/src/ContainerRegistry/ImageContainerRegistry.swift +++ b/libs/lume/src/ContainerRegistry/ImageContainerRegistry.swift @@ -512,6 +512,24 @@ class ImageContainerRegistry: @unchecked Sendable { return false } + // Check if we have a reassembled image + let reassembledCachePath = getImageCacheDirectory(manifestId: manifestId) + .appendingPathComponent("disk.img.reassembled") + if FileManager.default.fileExists(atPath: reassembledCachePath.path) { + Logger.info("Found reassembled disk image in cache validation") + + // If we have a reassembled image, we only need to make sure the manifest matches + guard let cachedManifest = loadCachedManifest(manifestId: manifestId), + cachedManifest.layers == manifest.layers + else { + return false + } + + // We have a reassembled image and the manifest matches + return true + } + + // If no reassembled image, check layer files // First check if manifest exists and matches guard let cachedManifest = loadCachedManifest(manifestId: manifestId), cachedManifest.layers == manifest.layers @@ -612,6 +630,52 @@ class ImageContainerRegistry: @unchecked Sendable { let metadata = try? JSONDecoder().decode(ImageMetadata.self, from: metadataData) { if metadata.image == image { + // Before removing, check if there's a reassembled image we should preserve + let reassembledPath = itemPath.appendingPathComponent("disk.img.reassembled") + let nvramPath = itemPath.appendingPathComponent("nvram.bin") + let configPath = itemPath.appendingPathComponent("config.json") + + // Preserve reassembled image if it exists + if FileManager.default.fileExists(atPath: reassembledPath.path) { + Logger.info( + "Preserving reassembled disk image during cleanup", + metadata: ["manifest_id": item]) + + // Ensure the current cache directory exists + let currentCacheDir = getImageCacheDirectory(manifestId: currentManifestId) + try FileManager.default.createDirectory( + at: currentCacheDir, withIntermediateDirectories: true) + + // Move reassembled image to current cache directory + let currentReassembledPath = currentCacheDir.appendingPathComponent( + "disk.img.reassembled") + if !FileManager.default.fileExists(atPath: currentReassembledPath.path) { + try FileManager.default.copyItem( + at: reassembledPath, to: currentReassembledPath) + } + + // Also preserve nvram if it exists + if FileManager.default.fileExists(atPath: nvramPath.path) { + let currentNvramPath = currentCacheDir.appendingPathComponent( + "nvram.bin") + if !FileManager.default.fileExists(atPath: currentNvramPath.path) { + try FileManager.default.copyItem( + at: nvramPath, to: currentNvramPath) + } + } + + // Also preserve config if it exists + if FileManager.default.fileExists(atPath: configPath.path) { + let currentConfigPath = currentCacheDir.appendingPathComponent( + "config.json") + if !FileManager.default.fileExists(atPath: currentConfigPath.path) { + try FileManager.default.copyItem( + at: configPath, to: currentConfigPath) + } + } + } + + // Now remove the old directory try FileManager.default.removeItem(at: itemPath) Logger.info( "Removed old version of image", @@ -652,10 +716,12 @@ class ImageContainerRegistry: @unchecked Sendable { // Use provided name or derive from image let vmName = name ?? image.split(separator: ":").first.map(String.init) ?? "" - + // Determine if locationName is a direct path or a named storage location let vmDir: VMDirectory - if let locationName = locationName, locationName.contains("/") || locationName.contains("\\") { + if let locationName = locationName, + locationName.contains("/") || locationName.contains("\\") + { // Direct path vmDir = try home.getVMDirectoryFromPath(vmName, storagePath: locationName) } else { @@ -1417,9 +1483,85 @@ class ImageContainerRegistry: @unchecked Sendable { let outputURL = destination.appendingPathComponent("disk.img") var expectedTotalSize: UInt64? = nil // Use optional to handle missing config + // Define the path for the reassembled cache image + let cacheDir = getImageCacheDirectory(manifestId: manifestId) + let reassembledCachePath = cacheDir.appendingPathComponent("disk.img.reassembled") + let nvramCachePath = cacheDir.appendingPathComponent("nvram.bin") + + // First check if we already have a reassembled image in the cache + if FileManager.default.fileExists(atPath: reassembledCachePath.path) { + Logger.info("Found reassembled disk image in cache, using it directly") + + // Copy reassembled disk image + try FileManager.default.copyItem(at: reassembledCachePath, to: outputURL) + + // Copy nvram if it exists + if FileManager.default.fileExists(atPath: nvramCachePath.path) { + try FileManager.default.copyItem( + at: nvramCachePath, + to: destination.appendingPathComponent("nvram.bin") + ) + Logger.info("Using cached nvram.bin file") + } else { + // Look for nvram in layer cache if needed + let nvramLayers = manifest.layers.filter { + $0.mediaType == "application/octet-stream" + } + if let nvramLayer = nvramLayers.first { + let cachedNvram = getCachedLayerPath( + manifestId: manifestId, digest: nvramLayer.digest) + if FileManager.default.fileExists(atPath: cachedNvram.path) { + try FileManager.default.copyItem( + at: cachedNvram, + to: destination.appendingPathComponent("nvram.bin") + ) + // Also save it to the dedicated nvram location for future use + try FileManager.default.copyItem(at: cachedNvram, to: nvramCachePath) + Logger.info("Recovered nvram.bin from layer cache") + } + } + } + + // Copy config if it exists + let configCachePath = cacheDir.appendingPathComponent("config.json") + if FileManager.default.fileExists(atPath: configCachePath.path) { + try FileManager.default.copyItem( + at: configCachePath, + to: destination.appendingPathComponent("config.json") + ) + Logger.info("Using cached config.json file") + } else { + // Look for config in layer cache if needed + let configLayers = manifest.layers.filter { + $0.mediaType == "application/vnd.oci.image.config.v1+json" + } + if let configLayer = configLayers.first { + let cachedConfig = getCachedLayerPath( + manifestId: manifestId, digest: configLayer.digest) + if FileManager.default.fileExists(atPath: cachedConfig.path) { + try FileManager.default.copyItem( + at: cachedConfig, + to: destination.appendingPathComponent("config.json") + ) + // Also save it to the dedicated config location for future use + try FileManager.default.copyItem(at: cachedConfig, to: configCachePath) + Logger.info("Recovered config.json from layer cache") + } + } + } + + Logger.info("Cache copy complete using reassembled image") + return + } + + // If we don't have a reassembled image, proceed with legacy part handling + Logger.info("No reassembled image found, using part-based reassembly") + // Instantiate collector let diskPartsCollector = DiskPartsCollector() var lz4LayerCount = 0 // Count lz4 layers found + var hasNvram = false + var configPath: URL? = nil // First identify disk parts and non-disk files for layer in manifest.layers { @@ -1447,9 +1589,13 @@ class ImageContainerRegistry: @unchecked Sendable { switch layer.mediaType { case "application/vnd.oci.image.config.v1+json": fileName = "config.json" + configPath = cachedLayer case "application/octet-stream": // Assume nvram if config layer exists, otherwise assume single disk image fileName = manifest.config != nil ? "nvram.bin" : "disk.img" + if fileName == "nvram.bin" { + hasNvram = true + } case "application/vnd.oci.image.layer.v1.tar", "application/octet-stream+gzip": // Assume disk image for these types as well if encountered in cache scenario @@ -1700,6 +1846,89 @@ class ImageContainerRegistry: @unchecked Sendable { try chmodProcess.run() chmodProcess.waitUntilExit() } + + // After successful reassembly, store the reassembled image in the cache + if cachingEnabled { + Logger.info("Saving reassembled disk image to cache for future use") + + // Copy the reassembled disk image to the cache + try FileManager.default.copyItem(at: outputURL, to: reassembledCachePath) + + // Clean up disk parts after successful reassembly + Logger.info("Cleaning up disk part files from cache") + + // Use an array to track unique file paths to avoid trying to delete the same file multiple times + var processedPaths: [String] = [] + + for (_, partURL) in diskPartSources { + let path = partURL.path + + // Skip if we've already processed this exact path + if processedPaths.contains(path) { + Logger.info("Skipping duplicate part file: \(partURL.lastPathComponent)") + continue + } + + // Add to processed array + processedPaths.append(path) + + // Check if file exists before attempting to delete + if FileManager.default.fileExists(atPath: path) { + do { + try FileManager.default.removeItem(at: partURL) + Logger.info("Removed disk part: \(partURL.lastPathComponent)") + } catch { + Logger.info( + "Failed to remove disk part: \(partURL.lastPathComponent) - \(error.localizedDescription)" + ) + } + } else { + Logger.info("Disk part already removed: \(partURL.lastPathComponent)") + } + } + + // Also save nvram if we have it + if hasNvram { + let srcNvram = destination.appendingPathComponent("nvram.bin") + if FileManager.default.fileExists(atPath: srcNvram.path) { + try? FileManager.default.copyItem(at: srcNvram, to: nvramCachePath) + } + } + + // Save config.json in the cache for future use if it exists + if let configPath = configPath { + let cacheConfigPath = cacheDir.appendingPathComponent("config.json") + try? FileManager.default.copyItem(at: configPath, to: cacheConfigPath) + } + + // Perform a final cleanup to catch any leftover part files + Logger.info("Performing final cleanup of any remaining part files") + do { + let cacheContents = try FileManager.default.contentsOfDirectory( + at: cacheDir, includingPropertiesForKeys: nil) + + for item in cacheContents { + let fileName = item.lastPathComponent + // Only remove sha256_ files that aren't the reassembled image, nvram or config + if fileName.starts(with: "sha256_") && fileName != "disk.img.reassembled" + && fileName != "nvram.bin" && fileName != "config.json" + && fileName != "manifest.json" && fileName != "metadata.json" + { + do { + try FileManager.default.removeItem(at: item) + Logger.info( + "Removed leftover file during final cleanup: \(fileName)") + } catch { + Logger.info( + "Failed to remove leftover file: \(fileName) - \(error.localizedDescription)" + ) + } + } + } + } catch { + Logger.info("Error during final cleanup: \(error.localizedDescription)") + } + } } Logger.info("Cache copy complete") diff --git a/libs/lume/src/FileSystem/Home.swift b/libs/lume/src/FileSystem/Home.swift index 634e3bef..5a91b490 100644 --- a/libs/lume/src/FileSystem/Home.swift +++ b/libs/lume/src/FileSystem/Home.swift @@ -87,7 +87,14 @@ final class Home { let baseDir = Path(cleanPath) return VMDirectory(baseDir.directory(name)) } - + + // Check if storage is a direct path + if let storage = storage, (storage.contains("/") || storage.contains("\\")) { + let cleanPath = storage.hasSuffix("/") ? String(storage.dropLast()) : storage + let baseDir = Path(cleanPath) + return VMDirectory(baseDir.directory(name)) + } + let location: VMLocation if let storage = storage { diff --git a/libs/lume/src/Server/Handlers.swift b/libs/lume/src/Server/Handlers.swift index 9ac20732..0fe61e3b 100644 --- a/libs/lume/src/Server/Handlers.swift +++ b/libs/lume/src/Server/Handlers.swift @@ -12,16 +12,55 @@ extension Server { let vms = try vmController.list(storage: storage) return try .json(vms) } catch { + print( + "ERROR: Failed to list VMs: \(error.localizedDescription), storage=\(String(describing: storage))" + ) return .badRequest(message: error.localizedDescription) } } func handleGetVM(name: String, storage: String? = nil) async throws -> HTTPResponse { + print("Getting VM details: name=\(name), storage=\(String(describing: storage))") + do { let vmController = LumeController() + print("Created VM controller, attempting to get VM") let vm = try vmController.get(name: name, storage: storage) - return try .json(vm.details) + print("Successfully retrieved VM") + + // Check for nil values that might cause crashes + if vm.vmDirContext.config.macAddress == nil { + print("ERROR: VM has nil macAddress") + return .badRequest(message: "VM configuration is invalid (nil macAddress)") + } + print("MacAddress check passed") + + // Log that we're about to access details + print("Preparing VM details response") + + // Print the full details object for debugging + let details = vm.details + print("VM DETAILS: \(details)") + print(" name: \(details.name)") + print(" os: \(details.os)") + print(" cpuCount: \(details.cpuCount)") + print(" memorySize: \(details.memorySize)") + print(" diskSize: \(details.diskSize)") + print(" display: \(details.display)") + print(" status: \(details.status)") + print(" vncUrl: \(String(describing: details.vncUrl))") + print(" ipAddress: \(String(describing: details.ipAddress))") + print(" locationName: \(details.locationName)") + + // Serialize the VM details + print("About to serialize VM details") + let response = try HTTPResponse.json(vm.details) + print("Successfully serialized VM details") + return response + } catch { + // This will catch errors from both vmController.get and the json serialization + print("ERROR: Failed to get VM details: \(error.localizedDescription)") return .badRequest(message: error.localizedDescription) } } @@ -158,15 +197,51 @@ extension Server { } func handleStopVM(name: String, storage: String? = nil) async throws -> HTTPResponse { + Logger.info( + "Stopping VM", metadata: ["name": name, "storage": String(describing: storage)]) + do { + Logger.info("Creating VM controller", metadata: ["name": name]) let vmController = LumeController() + + Logger.info("Calling stopVM on controller", metadata: ["name": name]) try await vmController.stopVM(name: name, storage: storage) + + Logger.info( + "VM stopped, waiting 5 seconds for locks to clear", metadata: ["name": name]) + + // Add a delay to ensure locks are fully released before returning + for i in 1...5 { + try? await Task.sleep(nanoseconds: 1_000_000_000) + Logger.info("Lock clearing delay", metadata: ["name": name, "seconds": "\(i)/5"]) + } + + // Verify the VM is really in a stopped state + Logger.info("Verifying VM is stopped", metadata: ["name": name]) + let vm = try? vmController.get(name: name, storage: storage) + if let vm = vm, vm.details.status == "running" { + Logger.info( + "VM still reports as running despite stop operation", + metadata: ["name": name, "severity": "warning"]) + } else { + Logger.info( + "Verification complete: VM is in stopped state", metadata: ["name": name]) + } + + Logger.info("Returning successful response", metadata: ["name": name]) return HTTPResponse( statusCode: .ok, headers: ["Content-Type": "application/json"], body: try JSONEncoder().encode(["message": "VM stopped successfully"]) ) } catch { + Logger.error( + "Failed to stop VM", + metadata: [ + "name": name, + "error": error.localizedDescription, + "storage": String(describing: storage), + ]) return HTTPResponse( statusCode: .badRequest, headers: ["Content-Type": "application/json"], @@ -176,14 +251,39 @@ extension Server { } func handleRunVM(name: String, body: Data?) async throws -> HTTPResponse { - let request = - body.flatMap { try? JSONDecoder().decode(RunVMRequest.self, from: $0) } - ?? RunVMRequest(noDisplay: nil, sharedDirectories: nil, recoveryMode: nil, storage: nil) + Logger.info("Running VM", metadata: ["name": name]) + + // Log the raw body data if available + if let body = body, let bodyString = String(data: body, encoding: .utf8) { + Logger.info("Run VM raw request body", metadata: ["name": name, "body": bodyString]) + } else { + Logger.info("No request body or could not decode as string", metadata: ["name": name]) + } do { + Logger.info("Creating VM controller and parsing request", metadata: ["name": name]) + let request = + body.flatMap { try? JSONDecoder().decode(RunVMRequest.self, from: $0) } + ?? RunVMRequest( + noDisplay: nil, sharedDirectories: nil, recoveryMode: nil, storage: nil) + + Logger.info( + "Parsed request", + metadata: [ + "name": name, + "noDisplay": String(describing: request.noDisplay), + "sharedDirectories": "\(request.sharedDirectories?.count ?? 0)", + "storage": String(describing: request.storage), + ]) + + Logger.info("Parsing shared directories", metadata: ["name": name]) let dirs = try request.parse() + Logger.info( + "Successfully parsed shared directories", + metadata: ["name": name, "count": "\(dirs.count)"]) // Start VM in background + Logger.info("Starting VM in background", metadata: ["name": name]) startVM( name: name, noDisplay: request.noDisplay ?? false, @@ -191,6 +291,7 @@ extension Server { recoveryMode: request.recoveryMode ?? false, storage: request.storage ) + Logger.info("VM start initiated in background", metadata: ["name": name]) // Return response immediately return HTTPResponse( @@ -203,6 +304,12 @@ extension Server { ]) ) } catch { + Logger.error( + "Failed to run VM", + metadata: [ + "name": name, + "error": error.localizedDescription, + ]) return HTTPResponse( statusCode: .badRequest, headers: ["Content-Type": "application/json"], @@ -290,7 +397,7 @@ extension Server { func handlePush(_ body: Data?) async throws -> HTTPResponse { guard let body = body, - let request = try? JSONDecoder().decode(PushRequest.self, from: body) + let request = try? JSONDecoder().decode(PushRequest.self, from: body) else { return HTTPResponse( statusCode: .badRequest, @@ -311,15 +418,16 @@ extension Server { organization: request.organization, storage: request.storage, chunkSizeMb: request.chunkSizeMb, - verbose: false, // Verbose typically handled by server logs - dryRun: false, // Default API behavior is likely non-dry-run - reassemble: false // Default API behavior is likely non-reassemble + verbose: false, // Verbose typically handled by server logs + dryRun: false, // Default API behavior is likely non-dry-run + reassemble: false // Default API behavior is likely non-reassemble + ) + print( + "Background push completed successfully for image: \(request.imageName):\(request.tags.joined(separator: ","))" ) - Logger.info("Background push completed successfully for image: \(request.imageName):\(request.tags.joined(separator: ","))") } catch { - Logger.error( - "Background push failed for image: \(request.imageName):\(request.tags.joined(separator: ","))", - metadata: ["error": error.localizedDescription] + print( + "Background push failed for image: \(request.imageName):\(request.tags.joined(separator: ",")) - Error: \(error.localizedDescription)" ) } } @@ -520,25 +628,25 @@ extension Server { } // MARK: - Log Handlers - + func handleGetLogs(type: String?, lines: Int?) async throws -> HTTPResponse { do { let logType = type?.lowercased() ?? "all" let infoPath = "/tmp/lume_daemon.log" let errorPath = "/tmp/lume_daemon.error.log" - + let fileManager = FileManager.default var response: [String: String] = [:] - + // Function to read log files func readLogFile(path: String) -> String? { guard fileManager.fileExists(atPath: path) else { return nil } - + do { let content = try String(contentsOfFile: path, encoding: .utf8) - + // If lines parameter is provided, return only the specified number of lines from the end if let lineCount = lines { let allLines = content.components(separatedBy: .newlines) @@ -546,28 +654,28 @@ extension Server { let lastLines = Array(allLines[startIndex...]) return lastLines.joined(separator: "\n") } - + return content } catch { return "Error reading log file: \(error.localizedDescription)" } } - + // Get logs based on requested type if logType == "info" || logType == "all" { response["info"] = readLogFile(path: infoPath) ?? "Info log file not found" } - + if logType == "error" || logType == "all" { response["error"] = readLogFile(path: errorPath) ?? "Error log file not found" } - + return try .json(response) } catch { return .badRequest(message: error.localizedDescription) } } - + // MARK: - Private Helper Methods nonisolated private func startVM( @@ -577,10 +685,27 @@ extension Server { recoveryMode: Bool = false, storage: String? = nil ) { + Logger.info( + "Starting VM in detached task", + metadata: [ + "name": name, + "noDisplay": "\(noDisplay)", + "recoveryMode": "\(recoveryMode)", + "storage": String(describing: storage), + ]) + Task.detached { @MainActor @Sendable in - Logger.info("Starting VM in background", metadata: ["name": name]) + Logger.info("Background task started for VM", metadata: ["name": name]) do { + Logger.info("Creating VM controller in background task", metadata: ["name": name]) let vmController = LumeController() + + Logger.info( + "Calling runVM on controller", + metadata: [ + "name": name, + "noDisplay": "\(noDisplay)", + ]) try await vmController.runVM( name: name, noDisplay: noDisplay, @@ -588,15 +713,16 @@ extension Server { recoveryMode: recoveryMode, storage: storage ) - Logger.info("VM started successfully in background", metadata: ["name": name]) + Logger.info("VM started successfully in background task", metadata: ["name": name]) } catch { Logger.error( - "Failed to start VM in background", + "Failed to start VM in background task", metadata: [ "name": name, "error": error.localizedDescription, ]) } } + Logger.info("Background task dispatched for VM", metadata: ["name": name]) } } diff --git a/libs/lume/src/Server/Server.swift b/libs/lume/src/Server/Server.swift index fda4d92e..6f279a42 100644 --- a/libs/lume/src/Server/Server.swift +++ b/libs/lume/src/Server/Server.swift @@ -68,7 +68,7 @@ final class Server { private var routes: [Route] // MARK: - Initialization - init(port: UInt16 = 3000) { + init(port: UInt16 = 7777) { self.port = NWEndpoint.Port(rawValue: port)! self.controller = LumeController() self.routes = [] diff --git a/libs/lume/src/VM/VM.swift b/libs/lume/src/VM/VM.swift index b9e22b98..fef55bf9 100644 --- a/libs/lume/src/VM/VM.swift +++ b/libs/lume/src/VM/VM.swift @@ -65,9 +65,16 @@ class VM { // MARK: - VM State Management private var isRunning: Bool { - // First check if we have an IP address - guard let ipAddress = DHCPLeaseParser.getIPAddress(forMAC: vmDirContext.config.macAddress!) - else { + // First check if we have a MAC address + guard let macAddress = vmDirContext.config.macAddress else { + Logger.info( + "Cannot check if VM is running: macAddress is nil", + metadata: ["name": vmDirContext.name]) + return false + } + + // Then check if we have an IP address + guard let ipAddress = DHCPLeaseParser.getIPAddress(forMAC: macAddress) else { return false } @@ -78,37 +85,35 @@ class VM { var details: VMDetails { let isRunning: Bool = self.isRunning let vncUrl = isRunning ? getVNCUrl() : nil - - // Try to load shared directories from the session file - var sharedDirs: [SharedDirectory]? = nil - - // Check if sessions file exists and load shared directories - let sessionsPath = vmDirContext.dir.sessionsPath.path - let fileExists = FileManager.default.fileExists(atPath: sessionsPath) - + + // Safely get disk size with fallback + let diskSizeValue: DiskSize do { - if fileExists { - let session = try vmDirContext.dir.loadSession() - sharedDirs = session.sharedDirectories - } + diskSizeValue = try getDiskSize() } catch { - // It's okay if we don't have a saved session - Logger.error("Failed to load session data", metadata: ["name": vmDirContext.name, "error": "\(error)"]) + Logger.error( + "Failed to get disk size", + metadata: ["name": vmDirContext.name, "error": "\(error)"]) + // Provide a fallback value to avoid crashing + diskSizeValue = DiskSize(allocated: 0, total: vmDirContext.config.diskSize ?? 0) } + // Safely access MAC address + let macAddress = vmDirContext.config.macAddress + let ipAddress: String? = + isRunning && macAddress != nil ? DHCPLeaseParser.getIPAddress(forMAC: macAddress!) : nil + return VMDetails( name: vmDirContext.name, os: getOSType(), cpuCount: vmDirContext.config.cpuCount ?? 0, memorySize: vmDirContext.config.memorySize ?? 0, - diskSize: try! getDiskSize(), + diskSize: diskSizeValue, display: vmDirContext.config.display.string, status: isRunning ? "running" : "stopped", vncUrl: vncUrl, - ipAddress: isRunning - ? DHCPLeaseParser.getIPAddress(forMAC: vmDirContext.config.macAddress!) : nil, - locationName: vmDirContext.storage ?? "default", - sharedDirectories: sharedDirs + ipAddress: ipAddress, + locationName: vmDirContext.storage ?? "default" ) } @@ -118,57 +123,84 @@ class VM { noDisplay: Bool, sharedDirectories: [SharedDirectory], mount: Path?, vncPort: Int = 0, recoveryMode: Bool = false, usbMassStoragePaths: [Path]? = nil ) async throws { + Logger.info( + "VM.run method called", + metadata: [ + "name": vmDirContext.name, + "noDisplay": "\(noDisplay)", + "recoveryMode": "\(recoveryMode)", + ]) + guard vmDirContext.initialized else { + Logger.error("VM not initialized", metadata: ["name": vmDirContext.name]) throw VMError.notInitialized(vmDirContext.name) } guard let cpuCount = vmDirContext.config.cpuCount, let memorySize = vmDirContext.config.memorySize else { + Logger.error("VM missing cpuCount or memorySize", metadata: ["name": vmDirContext.name]) throw VMError.notInitialized(vmDirContext.name) } // Try to acquire lock on config file - let fileHandle = try FileHandle(forWritingTo: vmDirContext.dir.configPath.url) - guard flock(fileHandle.fileDescriptor, LOCK_EX | LOCK_NB) == 0 else { + Logger.info( + "Attempting to acquire lock on config file", + metadata: [ + "path": vmDirContext.dir.configPath.path, + "name": vmDirContext.name, + ]) + var fileHandle = try FileHandle(forWritingTo: vmDirContext.dir.configPath.url) + + if flock(fileHandle.fileDescriptor, LOCK_EX | LOCK_NB) != 0 { try? fileHandle.close() - throw VMError.alreadyRunning(vmDirContext.name) - } + Logger.error( + "VM already running (failed to acquire lock)", metadata: ["name": vmDirContext.name] + ) - // Keep track of shared directories for logging + // Try to forcibly clear the lock before giving up + Logger.info("Attempting emergency lock cleanup", metadata: ["name": vmDirContext.name]) + unlockConfigFile() + + // Try one more time to acquire the lock + if let retryHandle = try? FileHandle(forWritingTo: vmDirContext.dir.configPath.url), + flock(retryHandle.fileDescriptor, LOCK_EX | LOCK_NB) == 0 + { + Logger.info("Emergency lock cleanup worked", metadata: ["name": vmDirContext.name]) + // Continue with a fresh file handle + try? retryHandle.close() + // Get a completely new file handle to be safe + guard let newHandle = try? FileHandle(forWritingTo: vmDirContext.dir.configPath.url) + else { + throw VMError.internalError("Failed to open file handle after lock cleanup") + } + // Update our main file handle + fileHandle = newHandle + } else { + // If we still can't get the lock, give up + Logger.error( + "Could not acquire lock even after emergency cleanup", + metadata: ["name": vmDirContext.name]) + throw VMError.alreadyRunning(vmDirContext.name) + } + } + Logger.info("Successfully acquired lock", metadata: ["name": vmDirContext.name]) Logger.info( "Running VM with configuration", metadata: [ + "name": vmDirContext.name, "cpuCount": "\(cpuCount)", "memorySize": "\(memorySize)", "diskSize": "\(vmDirContext.config.diskSize ?? 0)", - "macAddress": vmDirContext.config.macAddress ?? "none", - "sharedDirectoryCount": "\(sharedDirectories.count)", - "mount": mount?.path ?? "none", - "vncPort": "\(vncPort)", + "sharedDirectories": sharedDirectories.map { $0.string }.joined(separator: ", "), "recoveryMode": "\(recoveryMode)", - "usbMassStorageDeviceCount": "\(usbMassStoragePaths?.count ?? 0)", - ]) - - // Log disk paths and existence for debugging - Logger.info( - "VM disk paths", - metadata: [ - "diskPath": vmDirContext.diskPath.path, - "diskExists": - "\(FileManager.default.fileExists(atPath: vmDirContext.diskPath.path))", - "nvramPath": vmDirContext.nvramPath.path, - "nvramExists": - "\(FileManager.default.fileExists(atPath: vmDirContext.nvramPath.path))", - "configPath": vmDirContext.dir.configPath.path, - "configExists": - "\(FileManager.default.fileExists(atPath: vmDirContext.dir.configPath.path))", - "locationName": vmDirContext.storage ?? "default", ]) // Create and configure the VM do { + Logger.info( + "Creating virtualization service context", metadata: ["name": vmDirContext.name]) let config = try createVMVirtualizationServiceContext( cpuCount: cpuCount, memorySize: memorySize, @@ -178,32 +210,64 @@ class VM { recoveryMode: recoveryMode, usbMassStoragePaths: usbMassStoragePaths ) + Logger.info( + "Successfully created virtualization service context", + metadata: ["name": vmDirContext.name]) + + Logger.info( + "Initializing virtualization service", metadata: ["name": vmDirContext.name]) virtualizationService = try virtualizationServiceFactory(config) + Logger.info( + "Successfully initialized virtualization service", + metadata: ["name": vmDirContext.name]) - let vncInfo = try await setupSession(noDisplay: noDisplay, port: vncPort, sharedDirectories: sharedDirectories) - Logger.info("VNC info", metadata: ["vncInfo": vncInfo]) + Logger.info( + "Setting up VNC", + metadata: [ + "name": vmDirContext.name, + "noDisplay": "\(noDisplay)", + "port": "\(vncPort)", + ]) + let vncInfo = try await setupSession( + noDisplay: noDisplay, port: vncPort, sharedDirectories: sharedDirectories) + Logger.info( + "VNC setup successful", metadata: ["name": vmDirContext.name, "vncInfo": vncInfo]) // Start the VM guard let service = virtualizationService else { + Logger.error("Virtualization service is nil", metadata: ["name": vmDirContext.name]) throw VMError.internalError("Virtualization service not initialized") } + Logger.info( + "Starting VM via virtualization service", metadata: ["name": vmDirContext.name]) try await service.start() + Logger.info("VM started successfully", metadata: ["name": vmDirContext.name]) while true { try await Task.sleep(nanoseconds: UInt64(1e9)) } } catch { Logger.error( - "Failed to create/start VM", + "Failed in VM.run", metadata: [ - "error": "\(error)", + "name": vmDirContext.name, + "error": error.localizedDescription, "errorType": "\(type(of: error))", ]) virtualizationService = nil vncService.stop() + // Release lock + Logger.info("Releasing file lock after error", metadata: ["name": vmDirContext.name]) flock(fileHandle.fileDescriptor, LOCK_UN) try? fileHandle.close() + + // Additionally, perform our aggressive unlock to ensure no locks remain + Logger.info( + "Performing additional lock cleanup after error", + metadata: ["name": vmDirContext.name]) + unlockConfigFile() + throw error } } @@ -219,34 +283,55 @@ class VM { // If we have a virtualization service, try to stop it cleanly first if let service = virtualizationService { do { + Logger.info( + "Stopping VM via virtualization service", metadata: ["name": vmDirContext.name]) try await service.stop() virtualizationService = nil vncService.stop() Logger.info( "VM stopped successfully via virtualization service", metadata: ["name": vmDirContext.name]) + + // Try to ensure any existing locks are released + Logger.info( + "Attempting to clear any locks on config file", + metadata: ["name": vmDirContext.name]) + unlockConfigFile() + return } catch let error { Logger.error( - "Failed to stop VM via virtualization service, falling back to process termination", + "Failed to stop VM via virtualization service", metadata: [ "name": vmDirContext.name, - "error": "\(error)", + "error": error.localizedDescription, ]) // Fall through to process termination } } - // Try to open config file to get file descriptor - note that this matches with the serve process - so this is only for the command line + // Try to open config file to get file descriptor + Logger.info( + "Attempting to access config file lock", + metadata: [ + "path": vmDirContext.dir.configPath.path, + "name": vmDirContext.name, + ]) let fileHandle = try? FileHandle(forReadingFrom: vmDirContext.dir.configPath.url) guard let fileHandle = fileHandle else { - Logger.error( - "Failed to open config file - VM not running", metadata: ["name": vmDirContext.name] - ) + Logger.info( + "Failed to open config file - VM may not be running", + metadata: ["name": vmDirContext.name]) + + // Even though we couldn't open the file, try to force unlock anyway + unlockConfigFile() + throw VMError.notRunning(vmDirContext.name) } // Get the PID of the process holding the lock using lsof command + Logger.info( + "Finding process holding lock on config file", metadata: ["name": vmDirContext.name]) let task = Process() task.executableURL = URL(fileURLWithPath: "/usr/sbin/lsof") task.arguments = ["-F", "p", vmDirContext.dir.configPath.path] @@ -263,29 +348,44 @@ class VM { let pid = pid_t(pidString) else { try? fileHandle.close() - Logger.error( - "Failed to find VM process - VM not running", metadata: ["name": vmDirContext.name]) + Logger.info( + "Failed to find process holding lock - VM may not be running", + metadata: ["name": vmDirContext.name]) + + // Even though we couldn't find the process, try to force unlock + unlockConfigFile() + throw VMError.notRunning(vmDirContext.name) } + Logger.info( + "Found process \(pid) holding lock on config file", + metadata: ["name": vmDirContext.name]) + // First try graceful shutdown with SIGINT if kill(pid, SIGINT) == 0 { - Logger.info( - "Sent SIGINT to VM process", metadata: ["name": vmDirContext.name, "pid": "\(pid)"]) + Logger.info("Sent SIGINT to VM process \(pid)", metadata: ["name": vmDirContext.name]) } // Wait for process to stop with timeout var attempts = 0 while attempts < 10 { + Logger.info( + "Waiting for process \(pid) to terminate (attempt \(attempts + 1)/10)", + metadata: ["name": vmDirContext.name]) try await Task.sleep(nanoseconds: 1_000_000_000) // Check if process still exists if kill(pid, 0) != 0 { // Process is gone, do final cleanup + Logger.info("Process \(pid) has terminated", metadata: ["name": vmDirContext.name]) virtualizationService = nil vncService.stop() try? fileHandle.close() + // Force unlock the config file + unlockConfigFile() + Logger.info( "VM stopped successfully via process termination", metadata: ["name": vmDirContext.name]) @@ -296,8 +396,11 @@ class VM { // If graceful shutdown failed, force kill the process Logger.info( - "Graceful shutdown failed, forcing termination", metadata: ["name": vmDirContext.name]) + "Graceful shutdown failed, forcing termination of process \(pid)", + metadata: ["name": vmDirContext.name]) if kill(pid, SIGKILL) == 0 { + Logger.info("Sent SIGKILL to process \(pid)", metadata: ["name": vmDirContext.name]) + // Wait a moment for the process to be fully killed try await Task.sleep(nanoseconds: 2_000_000_000) @@ -306,16 +409,124 @@ class VM { vncService.stop() try? fileHandle.close() + // Force unlock the config file + unlockConfigFile() + Logger.info("VM forcefully stopped", metadata: ["name": vmDirContext.name]) return } // If we get here, something went very wrong try? fileHandle.close() - Logger.error("Failed to stop VM", metadata: ["name": vmDirContext.name, "pid": "\(pid)"]) + Logger.error( + "Failed to stop VM - could not terminate process \(pid)", + metadata: ["name": vmDirContext.name]) + + // As a last resort, try to force unlock + unlockConfigFile() + throw VMError.internalError("Failed to stop VM process") } + // Helper method to forcibly clear any locks on the config file + private func unlockConfigFile() { + Logger.info( + "Forcibly clearing locks on config file", + metadata: [ + "path": vmDirContext.dir.configPath.path, + "name": vmDirContext.name, + ]) + + // First attempt: standard unlock methods + if let fileHandle = try? FileHandle(forWritingTo: vmDirContext.dir.configPath.url) { + // Use F_GETLK and F_SETLK to check and clear locks + var lockInfo = flock() + lockInfo.l_type = Int16(F_UNLCK) + lockInfo.l_whence = Int16(SEEK_SET) + lockInfo.l_start = 0 + lockInfo.l_len = 0 + + // Try to unlock the file using fcntl + _ = fcntl(fileHandle.fileDescriptor, F_SETLK, &lockInfo) + + // Also try the regular flock method + flock(fileHandle.fileDescriptor, LOCK_UN) + + try? fileHandle.close() + Logger.info("Standard unlock attempts performed", metadata: ["name": vmDirContext.name]) + } + + // Second attempt: try to acquire and immediately release a fresh lock + if let tempHandle = try? FileHandle(forWritingTo: vmDirContext.dir.configPath.url) { + if flock(tempHandle.fileDescriptor, LOCK_EX | LOCK_NB) == 0 { + Logger.info( + "Successfully acquired and released lock to reset state", + metadata: ["name": vmDirContext.name]) + flock(tempHandle.fileDescriptor, LOCK_UN) + } else { + Logger.info( + "Could not acquire lock for resetting - may still be locked", + metadata: ["name": vmDirContext.name]) + } + try? tempHandle.close() + } + + // Third attempt (most aggressive): copy the config file, remove the original, and restore + Logger.info( + "Trying aggressive method: backup and restore config file", + metadata: ["name": vmDirContext.name]) + // Only proceed if the config file exists + let fileManager = FileManager.default + let configPath = vmDirContext.dir.configPath.path + let backupPath = configPath + ".backup" + + if fileManager.fileExists(atPath: configPath) { + // Create a backup of the config file + if let configData = try? Data(contentsOf: URL(fileURLWithPath: configPath)) { + // Make backup + try? configData.write(to: URL(fileURLWithPath: backupPath)) + + // Remove the original file to clear all locks + try? fileManager.removeItem(atPath: configPath) + Logger.info( + "Removed original config file to clear locks", + metadata: ["name": vmDirContext.name]) + + // Wait a moment for OS to fully release resources + Thread.sleep(forTimeInterval: 0.1) + + // Restore from backup + try? configData.write(to: URL(fileURLWithPath: configPath)) + Logger.info( + "Restored config file from backup", metadata: ["name": vmDirContext.name]) + } else { + Logger.error( + "Could not read config file content for backup", + metadata: ["name": vmDirContext.name]) + } + } else { + Logger.info( + "Config file does not exist, cannot perform aggressive unlock", + metadata: ["name": vmDirContext.name]) + } + + // Final check + if let finalHandle = try? FileHandle(forWritingTo: vmDirContext.dir.configPath.url) { + let lockResult = flock(finalHandle.fileDescriptor, LOCK_EX | LOCK_NB) + if lockResult == 0 { + Logger.info( + "Lock successfully cleared - verified by acquiring test lock", + metadata: ["name": vmDirContext.name]) + flock(finalHandle.fileDescriptor, LOCK_UN) + } else { + Logger.info( + "Lock still present after all clearing attempts", + metadata: ["name": vmDirContext.name, "severity": "warning"]) + } + try? finalHandle.close() + } + } + // MARK: - Resource Management func updateVMConfig(vmConfig: VMConfig) throws { @@ -422,40 +633,44 @@ class VM { guard let url = vncService.url else { throw VMError.vncNotConfigured } - + return url } - + /// Saves the session information including shared directories to disk private func saveSessionData(url: String, sharedDirectories: [SharedDirectory]) { do { - let session = VNCSession(url: url, sharedDirectories: sharedDirectories.isEmpty ? nil : sharedDirectories) + let session = VNCSession( + url: url, sharedDirectories: sharedDirectories.isEmpty ? nil : sharedDirectories) try vmDirContext.dir.saveSession(session) - Logger.info("Saved VNC session with shared directories", - metadata: [ - "count": "\(sharedDirectories.count)", - "dirs": "\(sharedDirectories.map { $0.hostPath }.joined(separator: ", "))", - "sessionsPath": "\(vmDirContext.dir.sessionsPath.path)" - ]) + Logger.info( + "Saved VNC session with shared directories", + metadata: [ + "count": "\(sharedDirectories.count)", + "dirs": "\(sharedDirectories.map { $0.hostPath }.joined(separator: ", "))", + "sessionsPath": "\(vmDirContext.dir.sessionsPath.path)", + ]) } catch { Logger.error("Failed to save VNC session", metadata: ["error": "\(error)"]) } } - + /// Main session setup method that handles VNC and persists session data - private func setupSession(noDisplay: Bool, port: Int = 0, sharedDirectories: [SharedDirectory] = []) async throws -> String { + private func setupSession( + noDisplay: Bool, port: Int = 0, sharedDirectories: [SharedDirectory] = [] + ) async throws -> String { // Start the VNC service and get the URL let url = try await startVNCService(port: port) - + // Save the session data saveSessionData(url: url, sharedDirectories: sharedDirectories) - + // Open the VNC client if needed if !noDisplay { - Logger.info("Starting VNC session") + Logger.info("Starting VNC session", metadata: ["name": vmDirContext.name]) try await vncService.openClient(url: url) } - + return url } @@ -599,7 +814,8 @@ class VM { ) virtualizationService = try virtualizationServiceFactory(config) - let vncInfo = try await setupSession(noDisplay: noDisplay, port: vncPort, sharedDirectories: sharedDirectories) + let vncInfo = try await setupSession( + noDisplay: noDisplay, port: vncPort, sharedDirectories: sharedDirectories) Logger.info("VNC info", metadata: ["vncInfo": vncInfo]) // Start the VM diff --git a/libs/lume/src/Virtualization/VMVirtualizationService.swift b/libs/lume/src/Virtualization/VMVirtualizationService.swift index 93cb4db0..b358659b 100644 --- a/libs/lume/src/Virtualization/VMVirtualizationService.swift +++ b/libs/lume/src/Virtualization/VMVirtualizationService.swift @@ -246,6 +246,27 @@ final class DarwinVirtualizationService: BaseVirtualizationService { ] vzConfig.memoryBalloonDevices = [VZVirtioTraditionalMemoryBalloonDeviceConfiguration()] vzConfig.entropyDevices = [VZVirtioEntropyDeviceConfiguration()] + + // Audio configuration + let soundDeviceConfiguration = VZVirtioSoundDeviceConfiguration() + let inputAudioStreamConfiguration = VZVirtioSoundDeviceInputStreamConfiguration() + let outputAudioStreamConfiguration = VZVirtioSoundDeviceOutputStreamConfiguration() + + inputAudioStreamConfiguration.source = VZHostAudioInputStreamSource() + outputAudioStreamConfiguration.sink = VZHostAudioOutputStreamSink() + + soundDeviceConfiguration.streams = [inputAudioStreamConfiguration, outputAudioStreamConfiguration] + vzConfig.audioDevices = [soundDeviceConfiguration] + + // Clipboard sharing via Spice agent + let spiceAgentConsoleDevice = VZVirtioConsoleDeviceConfiguration() + let spiceAgentPort = VZVirtioConsolePortConfiguration() + spiceAgentPort.name = VZSpiceAgentPortAttachment.spiceAgentPortName + let spiceAgentPortAttachment = VZSpiceAgentPortAttachment() + spiceAgentPortAttachment.sharesClipboard = true + spiceAgentPort.attachment = spiceAgentPortAttachment + spiceAgentConsoleDevice.ports[0] = spiceAgentPort + vzConfig.consoleDevices.append(spiceAgentConsoleDevice) // Directory sharing let directorySharingDevices = createDirectorySharingDevices( @@ -376,6 +397,27 @@ final class LinuxVirtualizationService: BaseVirtualizationService { ] vzConfig.memoryBalloonDevices = [VZVirtioTraditionalMemoryBalloonDeviceConfiguration()] vzConfig.entropyDevices = [VZVirtioEntropyDeviceConfiguration()] + + // Audio configuration + let soundDeviceConfiguration = VZVirtioSoundDeviceConfiguration() + let inputAudioStreamConfiguration = VZVirtioSoundDeviceInputStreamConfiguration() + let outputAudioStreamConfiguration = VZVirtioSoundDeviceOutputStreamConfiguration() + + inputAudioStreamConfiguration.source = VZHostAudioInputStreamSource() + outputAudioStreamConfiguration.sink = VZHostAudioOutputStreamSink() + + soundDeviceConfiguration.streams = [inputAudioStreamConfiguration, outputAudioStreamConfiguration] + vzConfig.audioDevices = [soundDeviceConfiguration] + + // Clipboard sharing via Spice agent + let spiceAgentConsoleDevice = VZVirtioConsoleDeviceConfiguration() + let spiceAgentPort = VZVirtioConsolePortConfiguration() + spiceAgentPort.name = VZSpiceAgentPortAttachment.spiceAgentPortName + let spiceAgentPortAttachment = VZSpiceAgentPortAttachment() + spiceAgentPortAttachment.sharesClipboard = true + spiceAgentPort.attachment = spiceAgentPortAttachment + spiceAgentConsoleDevice.ports[0] = spiceAgentPort + vzConfig.consoleDevices.append(spiceAgentConsoleDevice) // Directory sharing var directorySharingDevices = createDirectorySharingDevices( diff --git a/libs/lumier/README.md b/libs/lumier/README.md index 826d5097..287a6e96 100644 --- a/libs/lumier/README.md +++ b/libs/lumier/README.md @@ -10,13 +10,16 @@ [![Swift 6](https://img.shields.io/badge/Swift_6-F54A2A?logo=swift&logoColor=white&labelColor=F54A2A)](#) [![macOS](https://img.shields.io/badge/macOS-000000?logo=apple&logoColor=F0F0F0)](#) - [![Homebrew](https://img.shields.io/badge/Homebrew-FBB040?logo=homebrew&logoColor=fff)](#install) [![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white)](https://discord.com/invite/mVnXXpdE85) macOS and Linux virtual machines in a Docker container. +
+ +
+ ## What is Lumier? **Lumier** is an interface for running macOS virtual machines with minimal setup. It uses Docker as a packaging system to deliver a pre-configured environment that connects to the `lume` virtualization service running on your host machine. With Lumier, you get: @@ -36,7 +39,7 @@ Before using Lumier, make sure you have: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/lume/scripts/install.sh)" ``` -After installation, Lume runs as a background service and listens on port 3000. This service allows Lumier to create and manage virtual machines. If port 3000 is already in use on your system, you can specify a different port with the `--port` option when running the `install.sh` script. +After installation, Lume runs as a background service and listens on port 7777. This service allows Lumier to create and manage virtual machines. If port 7777 is already in use on your system, you can specify a different port with the `--port` option when running the `install.sh` script. ## How It Works @@ -52,14 +55,11 @@ Here's what's happening behind the scenes: ## Getting Started ```bash -# 1. Navigate to the Lumier directory -cd libs/lumier - -# 2. Run the container with temporary storage (using pre-built image from Docker Hub) +# Run the container with temporary storage (using pre-built image from Docker Hub) docker run -it --rm \ - --name lumier-vm \ + --name macos-vm \ -p 8006:8006 \ - -e VM_NAME=lumier-vm \ + -e VM_NAME=macos-vm \ -e VERSION=ghcr.io/trycua/macos-sequoia-cua:latest \ -e CPU_CORES=4 \ -e RAM_SIZE=8192 \ @@ -262,4 +262,4 @@ Main differences with dockur/macos: - Lumier is specifically designed for macOS virtualization - Lumier supports Apple Silicon (M1/M2/M3/M4) while dockur/macos only supports Intel - Lumier uses the Apple Virtualization Framework (Vz) through the `lume` CLI to create true virtual machines, while dockur relies on KVM. -- Image specification is different, with Lumier and Lume relying on Apple Vz spec (disk.img and nvram.bin) \ No newline at end of file +- Image specification is different, with Lumier and Lume relying on Apple Vz spec (disk.img and nvram.bin) diff --git a/libs/lumier/lifecycle/setup.sh b/libs/lumier/lifecycle/setup.sh deleted file mode 100755 index 8897896e..00000000 --- a/libs/lumier/lifecycle/setup.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -echo "Creating helloworld.txt on the Desktop..." -if [ ! -f ~/Desktop/helloworld.txt ]; then - echo "Hello, World!" > ~/Desktop/helloworld.txt - echo "helloworld.txt created successfully." -else - echo "helloworld.txt already exists." -fi \ No newline at end of file diff --git a/libs/lumier/src/bin/entry.sh b/libs/lumier/src/bin/entry.sh index c9cd2f0f..d6f96f52 100755 --- a/libs/lumier/src/bin/entry.sh +++ b/libs/lumier/src/bin/entry.sh @@ -64,34 +64,58 @@ fi echo "Lumier VM is starting..." # Cleanup function to ensure VM and noVNC proxy shutdown on container stop +# Counter for signal handling +SIGNAL_COUNT=0 + cleanup() { + local signal_name=$1 set +e # Don't exit on error in cleanup - echo "[cleanup] Caught signal, shutting down..." - # Check if we're in the middle of an image pull - if [[ "$PULL_IN_PROGRESS" == "1" ]]; then - echo "[cleanup] Interrupted during image pull, skipping VM stop." - else - echo "[cleanup] Stopping VM..." - stop_vm true - fi + # Increment signal counter + SIGNAL_COUNT=$((SIGNAL_COUNT + 1)) - # Attempt to clean up ephemeral storage if it's in the /private/tmp directory - if [[ "$HOST_STORAGE_PATH" == "ephemeral" ]]; then - # First check if VM actually exists - VM_INFO=$(lume_get "$VM_NAME" "$HOST_STORAGE_PATH" "json" "false") + # If this is the first signal, try graceful shutdown + if [ $SIGNAL_COUNT -eq 1 ]; then + echo "[cleanup] Caught $signal_name signal, shutting down..." - # Only try VM deletion if VM exists and not in the middle of a pull - if [[ "$PULL_IN_PROGRESS" != "1" && $VM_INFO != *"Virtual machine not found"* ]]; then - echo "[cleanup] Cleaning up VM..." - lume_delete "$VM_NAME" "$HOST_STORAGE_PATH" > /dev/null 2>&1 + # Check if we're in the middle of an image pull + if [[ "$PULL_IN_PROGRESS" == "1" ]]; then + echo "[cleanup] Interrupted during image pull, skipping VM stop." + else + echo "[cleanup] Stopping VM..." + stop_vm true fi + + # Attempt to clean up ephemeral storage if it's in the /private/tmp directory + if [[ "$HOST_STORAGE_PATH" == "ephemeral" ]]; then + # First check if VM actually exists + VM_INFO=$(lume_get "$VM_NAME" "$HOST_STORAGE_PATH" "json" "false") + + # Only try VM deletion if VM exists and not in the middle of a pull + if [[ "$PULL_IN_PROGRESS" != "1" && $VM_INFO != *"Virtual machine not found"* ]]; then + echo "[cleanup] Cleaning up VM..." + lume_delete "$VM_NAME" "$HOST_STORAGE_PATH" > /dev/null 2>&1 + fi + fi + else + # For multiple signals, force an immediate exit + echo "got $SIGNAL_COUNT SIGTERM/SIGINTs, forcefully exiting" fi - exit 0 + # If we've received multiple signals, just exit immediately + if [ $SIGNAL_COUNT -ge 3 ]; then + exit 1 + fi + + # Exit with success for the first signal + if [ $SIGNAL_COUNT -eq 1 ]; then + exit 0 + fi } # Ensure we catch all typical container termination signals -trap cleanup SIGTERM SIGINT SIGHUP +trap 'cleanup SIGTERM' SIGTERM +trap 'cleanup SIGINT' SIGINT +trap 'cleanup SIGHUP' SIGHUP # Now enable strict error handling after initialization set -euo pipefail @@ -116,4 +140,14 @@ if [ -n "${VNC_PORT:-}" ] && [ -n "${VNC_PASSWORD:-}" ]; then fi echo "Lumier is running. Press Ctrl+C to stop." -tail -f /dev/null \ No newline at end of file + +# Instead of tail -f /dev/null, use a wait loop that can be interrupted by signals +while true; do + # Sleep in small increments to make signal handling more responsive + sleep 1 & + wait $! + # Break the loop if we've received a signal + if [ $SIGNAL_COUNT -gt 0 ]; then + break + fi +done \ No newline at end of file diff --git a/libs/lumier/src/lib/vm.sh b/libs/lumier/src/lib/vm.sh index b2bd10d7..540d64b7 100755 --- a/libs/lumier/src/lib/vm.sh +++ b/libs/lumier/src/lib/vm.sh @@ -127,7 +127,7 @@ lume_get() { local debug="${4:-false}" local api_host="${LUME_API_HOST:-host.docker.internal}" - local api_port="${LUME_API_PORT:-3000}" + local api_port="${LUME_API_PORT:-7777}" # URL encode the storage path for the query parameter # Replace special characters with their URL encoded equivalents @@ -175,7 +175,7 @@ lume_set() { local display="${5:-1024x768}" local api_host="${LUME_API_HOST:-host.docker.internal}" - local api_port="${LUME_API_PORT:-3000}" + local api_port="${LUME_API_PORT:-7777}" # Handle memory format for the API if [[ "$memory" == *"GB"* ]]; then @@ -258,7 +258,7 @@ lume_stop() { local storage="$2" local api_host="${LUME_API_HOST:-host.docker.internal}" - local api_port="${LUME_API_PORT:-3000}" + local api_port="${LUME_API_PORT:-7777}" # Only log in debug mode if [[ "$LUMIER_DEBUG" == "1" ]]; then @@ -297,7 +297,7 @@ lume_pull() { local organization="${5:-trycua}" # Organization, default is trycua local api_host="${LUME_API_HOST:-host.docker.internal}" - local api_port="${LUME_API_PORT:-3000}" + local api_port="${LUME_API_PORT:-7777}" # Mark that pull is in progress for interrupt handling export PULL_IN_PROGRESS=1 @@ -379,7 +379,7 @@ lume_run() { done local api_host="${LUME_API_HOST:-host.docker.internal}" - local api_port="${LUME_API_PORT:-3000}" + local api_port="${LUME_API_PORT:-7777}" # Only log in debug mode if [[ "$LUMIER_DEBUG" == "1" ]]; then @@ -431,7 +431,7 @@ lume_delete() { local storage="$2" local api_host="${LUME_API_HOST:-host.docker.internal}" - local api_port="${LUME_API_PORT:-3000}" + local api_port="${LUME_API_PORT:-7777}" # URL encode the storage path for the query parameter # Replace special characters with their URL encoded equivalents diff --git a/libs/mcp-server/pyproject.toml b/libs/mcp-server/pyproject.toml index 8b54f322..62fc1a2e 100644 --- a/libs/mcp-server/pyproject.toml +++ b/libs/mcp-server/pyproject.toml @@ -6,15 +6,15 @@ build-backend = "pdm.backend" name = "cua-mcp-server" description = "MCP Server for Computer-Use Agent (CUA)" readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.11" version = "0.1.0" authors = [ {name = "TryCua", email = "gh@trycua.com"} ] dependencies = [ "mcp>=1.6.0,<2.0.0", - "cua-agent[all]>=0.1.0,<0.2.0", - "cua-computer>=0.1.0,<0.2.0", + "cua-agent[all]>=0.2.0,<0.3.0", + "cua-computer>=0.2.0,<0.3.0", ] [project.scripts] @@ -31,10 +31,10 @@ dev = [ [tool.black] line-length = 100 -target-version = ["py310"] +target-version = ["py311"] [tool.ruff] line-length = 100 -target-version = "py310" +target-version = "py311" select = ["E", "F", "B", "I"] fix = true diff --git a/libs/pylume/pyproject.toml b/libs/pylume/pyproject.toml index 21cdc9b7..f21f2bb2 100644 --- a/libs/pylume/pyproject.toml +++ b/libs/pylume/pyproject.toml @@ -43,13 +43,13 @@ dev = [ [tool.black] line-length = 100 -target-version = ["py310"] +target-version = ["py311"] [tool.ruff] fix = true line-length = 100 select = ["B", "E", "F", "I"] -target-version = "py310" +target-version = "py311" [tool.ruff.format] docstring-code-format = true @@ -58,7 +58,7 @@ docstring-code-format = true check_untyped_defs = true disallow_untyped_defs = true ignore_missing_imports = true -python_version = "3.10" +python_version = "3.11" show_error_codes = true strict = true warn_return_any = true diff --git a/libs/som/pyproject.toml b/libs/som/pyproject.toml index f18be609..0bae7ea2 100644 --- a/libs/som/pyproject.toml +++ b/libs/som/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "typing-extensions>=4.9.0", "pydantic>=2.6.3" ] -requires-python = ">=3.10" +requires-python = ">=3.11" readme = "README.md" license = {text = "MIT"} keywords = ["computer-vision", "ocr", "ui-analysis", "icon-detection"] diff --git a/notebooks/agent_nb.ipynb b/notebooks/agent_nb.ipynb index 925810be..84d67574 100644 --- a/notebooks/agent_nb.ipynb +++ b/notebooks/agent_nb.ipynb @@ -6,7 +6,7 @@ "source": [ "## Agent\n", "\n", - "This notebook demonstrates how to use Cua's Agent to run a workflow in a virtual sandbox on Apple Silicon Macs." + "This notebook demonstrates how to use Cua's Agent to run workflows in virtual sandboxes, either using C/ua Cloud Containers or local VMs on Apple Silicon Macs." ] }, { @@ -68,7 +68,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Agent allows you to run an agentic workflow in a virtual sandbox instances on Apple Silicon. Here's a basic example:" + "Agent allows you to run an agentic workflow in virtual sandbox instances. You can choose between cloud containers or local VMs." ] }, { @@ -77,21 +77,23 @@ "metadata": {}, "outputs": [], "source": [ - "from computer import Computer\n", + "from computer import Computer, VMProviderType\n", "from agent import ComputerAgent, LLM, AgentLoop, LLMProvider" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os\n", "\n", "# Get API keys from environment or prompt user\n", - "anthropic_key = os.getenv(\"ANTHROPIC_API_KEY\") or input(\"Enter your Anthropic API key: \")\n", - "openai_key = os.getenv(\"OPENAI_API_KEY\") or input(\"Enter your OpenAI API key: \")\n", + "anthropic_key = os.getenv(\"ANTHROPIC_API_KEY\") or \\\n", + " input(\"Enter your Anthropic API key: \")\n", + "openai_key = os.getenv(\"OPENAI_API_KEY\") or \\\n", + " input(\"Enter your OpenAI API key: \")\n", "\n", "os.environ[\"ANTHROPIC_API_KEY\"] = anthropic_key\n", "os.environ[\"OPENAI_API_KEY\"] = openai_key" @@ -101,14 +103,65 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Similar to Computer, you can either use the async context manager pattern or initialize the ComputerAgent instance directly." + "## Option 1: Agent with C/ua Cloud Containers\n", + "\n", + "Use cloud containers for running agents from any system without local setup." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's start by creating an agent that relies on the OpenAI API computer-use-preview model." + "### Prerequisites for Cloud Containers\n", + "\n", + "To use C/ua Cloud Containers, you need to:\n", + "1. Sign up at https://trycua.com\n", + "2. Create a Cloud Container\n", + "3. Generate an API Key\n", + "\n", + "Once you have these, you can connect to your cloud container and run agents on it." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Get C/ua API credentials and container details" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cua_api_key = os.getenv(\"CUA_API_KEY\") or \\\n", + " input(\"Enter your C/ua API Key: \")\n", + "container_name = os.getenv(\"CONTAINER_NAME\") or \\\n", + " input(\"Enter your Cloud Container name: \")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Choose the OS type for your container (linux or macos)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "os_type = input(\"Enter the OS type of your container (linux/macos) [default: linux]: \").lower() or \"linux\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create an agent with cloud container" ] }, { @@ -120,19 +173,135 @@ "import logging\n", "from pathlib import Path\n", "\n", - "computer = Computer(verbosity=logging.INFO)\n", + "# Connect to your existing cloud container\n", + "computer = Computer(\n", + " os_type=os_type,\n", + " api_key=cua_api_key,\n", + " name=container_name,\n", + " provider_type=VMProviderType.CLOUD,\n", + " verbosity=logging.INFO\n", + ")\n", "\n", - "# Create agent with Anthropic loop and provider\n", + "# Create agent\n", "agent = ComputerAgent(\n", - " computer=computer,\n", - " loop=AgentLoop.OPENAI,\n", - " model=LLM(provider=LLMProvider.OPENAI),\n", - " save_trajectory=True,\n", - " trajectory_dir=str(Path(\"trajectories\")),\n", - " only_n_most_recent_images=3,\n", - " verbosity=logging.INFO\n", - " )\n", + " computer=computer,\n", + " loop=AgentLoop.OPENAI,\n", + " model=LLM(provider=LLMProvider.OPENAI),\n", + " save_trajectory=True,\n", + " trajectory_dir=str(Path(\"trajectories\")),\n", + " only_n_most_recent_images=3,\n", + " verbosity=logging.INFO\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Run tasks on cloud container" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tasks = [\n", + " \"Open a web browser and navigate to GitHub\",\n", + " \"Search for the trycua/cua repository\",\n", + " \"Take a screenshot of the repository page\"\n", + "]\n", "\n", + "for i, task in enumerate(tasks):\n", + " print(f\"\\nExecuting task {i+1}/{len(tasks)}: {task}\")\n", + " async for result in cloud_agent.run(task):\n", + " # print(result)\n", + " pass\n", + " print(f\"✅ Task {i+1}/{len(tasks)} completed: {task}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Option 2: Agent with Local VMs (Lume daemon)\n", + "\n", + "For Apple Silicon Macs, run agents on local VMs with near-native performance." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Before we can create an agent, we need to initialize a local computer with Lume." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "from pathlib import Path\n", + "\n", + "\n", + "computer = Computer(\n", + " verbosity=logging.INFO, \n", + " provider_type=VMProviderType.LUME,\n", + " display=\"1024x768\",\n", + " memory=\"8GB\",\n", + " cpu=\"4\",\n", + " os_type=\"macos\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create an agent with local VM" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's start by creating an agent that relies on the OpenAI API computer-use-preview model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create agent with Anthropic loop and provider\n", + "agent = ComputerAgent(\n", + " computer=computer,\n", + " loop=AgentLoop.OPENAI,\n", + " model=LLM(provider=LLMProvider.OPENAI),\n", + " save_trajectory=True,\n", + " trajectory_dir=str(Path(\"trajectories\")),\n", + " only_n_most_recent_images=3,\n", + " verbosity=logging.INFO\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Run tasks on a local Lume VM" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ "tasks = [\n", " \"Look for a repository named trycua/cua on GitHub.\",\n", " \"Check the open issues, open the most recent one and read it.\",\n", @@ -212,18 +381,42 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "import os\n", - "\n", - "# Get API keys from environment or prompt user\n", - "anthropic_key = os.getenv(\"ANTHROPIC_API_KEY\") or input(\"Enter your Anthropic API key: \")\n", - "openai_key = os.getenv(\"OPENAI_API_KEY\") or input(\"Enter your OpenAI API key: \")\n", + "from agent.ui.gradio.app import create_gradio_ui\n", "\n", - "os.environ[\"ANTHROPIC_API_KEY\"] = anthropic_key\n", - "os.environ[\"OPENAI_API_KEY\"] = openai_key" + "app = create_gradio_ui()\n", + "app.launch(share=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Advanced Agent Configurations" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Using different agent loops" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can use different agent loops depending on your needs:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "1. OpenAI Agent Loop" ] }, { @@ -232,16 +425,116 @@ "metadata": {}, "outputs": [], "source": [ - "from agent.ui.gradio.app import create_gradio_ui\n", - "\n", - "app = create_gradio_ui()\n", - "app.launch(share=False)" + "openai_agent = ComputerAgent(\n", + " computer=computer, # Can be cloud or local\n", + " loop=AgentLoop.OPENAI,\n", + " model=LLM(provider=LLMProvider.OPENAI),\n", + " save_trajectory=True,\n", + " trajectory_dir=str(Path(\"trajectories\")),\n", + " verbosity=logging.INFO\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "2. Anthropic Agent Loop" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "anthropic_agent = ComputerAgent(\n", + " computer=computer,\n", + " loop=AgentLoop.ANTHROPIC,\n", + " model=LLM(provider=LLMProvider.ANTHROPIC),\n", + " save_trajectory=True,\n", + " trajectory_dir=str(Path(\"trajectories\")),\n", + " verbosity=logging.INFO\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "3. Omni Agent Loop (supports multiple providers)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "omni_agent = ComputerAgent(\n", + " computer=computer,\n", + " loop=AgentLoop.OMNI,\n", + " model=LLM(provider=LLMProvider.ANTHROPIC, name=\"claude-3-7-sonnet-20250219\"),\n", + " # model=LLM(provider=LLMProvider.OPENAI, name=\"gpt-4.5-preview\"),\n", + " # model=LLM(provider=LLMProvider.OLLAMA, name=\"gemma3:12b-it-q4_K_M\"),\n", + " save_trajectory=True,\n", + " trajectory_dir=str(Path(\"trajectories\")),\n", + " only_n_most_recent_images=3,\n", + " verbosity=logging.INFO\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "4. UITARS Agent Loop (for local inference on Apple Silicon)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "uitars_agent = ComputerAgent(\n", + " computer=computer,\n", + " loop=AgentLoop.UITARS,\n", + " model=LLM(provider=LLMProvider.UITARS),\n", + " save_trajectory=True,\n", + " trajectory_dir=str(Path(\"trajectories\")),\n", + " verbosity=logging.INFO\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Trajectory viewing" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All agent runs save trajectories that can be viewed at https://trycua.com/trajectory-viewer" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"Trajectories saved to: {Path('trajectories').absolute()}\")\n", + "print(\"Upload trajectory files to https://trycua.com/trajectory-viewer to visualize agent actions\")\n" ] } ], "metadata": { "kernelspec": { - "display_name": "cua313", + "display_name": "cua312", "language": "python", "name": "python3" }, @@ -255,7 +548,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.2" + "version": "3.12.9" } }, "nbformat": 4, diff --git a/notebooks/blog/build-your-own-operator-on-macos-1.ipynb b/notebooks/blog/build-your-own-operator-on-macos-1.ipynb index e39ce336..da7245e6 100644 --- a/notebooks/blog/build-your-own-operator-on-macos-1.ipynb +++ b/notebooks/blog/build-your-own-operator-on-macos-1.ipynb @@ -54,7 +54,7 @@ "metadata": {}, "outputs": [], "source": [ - "!pip install cua-computer" + "!pip install \"cua-computer[all]\"" ] }, { diff --git a/notebooks/computer_nb.ipynb b/notebooks/computer_nb.ipynb index 542c6c4b..c0bd8460 100644 --- a/notebooks/computer_nb.ipynb +++ b/notebooks/computer_nb.ipynb @@ -6,7 +6,7 @@ "source": [ "## Computer\n", "\n", - "This notebook demonstrates how to use Computer to operate a Lume sandbox VMs programmatically on Apple Silicon macOS systems." + "This notebook demonstrates how to use Computer to operate sandbox VMs programmatically, either using C/ua Cloud Containers or local Lume VMs on Apple Silicon macOS systems." ] }, { @@ -22,16 +22,15 @@ "metadata": {}, "outputs": [], "source": [ - "!pip uninstall -y cua-computer" + "!pip uninstall -y cua-computer\n", + "!pip install \"cua-computer[all]\"" ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "!pip install cua-computer" + "If locally installed, use this instead:" ] }, { @@ -40,7 +39,6 @@ "metadata": {}, "outputs": [], "source": [ - "# If locally installed, use this instead:\n", "import os\n", "\n", "os.chdir('../libs/computer')\n", @@ -55,7 +53,126 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Lume daemon\n", + "## Option 1: C/ua Cloud Containers\n", + "\n", + "C/ua Cloud Containers provide remote VMs that can be accessed from any system without local setup." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Prerequisites for Cloud Containers\n", + "\n", + "To use C/ua Cloud Containers, you need to:\n", + "1. Sign up at https://trycua.com\n", + "2. Create a Cloud Container\n", + "3. Generate an API Key\n", + "\n", + "Once you have these, you can connect to your cloud container using its name." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get API key and container name from environment or prompt user\n", + "import os\n", + "\n", + "cua_api_key = os.getenv(\"CUA_API_KEY\") or \\\n", + " input(\"Enter your C/ua API Key: \")\n", + "container_name = os.getenv(\"CONTAINER_NAME\") or \\\n", + " input(\"Enter your Cloud Container name: \")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Choose the OS type for your container (linux or macos)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "os_type = input(\"Enter the OS type of your container (linux/macos) [default: linux]: \").lower() or \"linux\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Connect to your Cloud Container" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from computer import Computer, VMProviderType" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Connect to your existing C/ua Cloud Container" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "computer = Computer(\n", + " os_type=os_type, # Must match the OS type of your cloud container\n", + " api_key=cua_api_key,\n", + " name=container_name,\n", + " provider_type=VMProviderType.CLOUD,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Take a screenshot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "screenshot = await computer.interface.screenshot()\n", + "\n", + "with open(\"screenshot.png\", \"wb\") as f:\n", + " f.write(screenshot)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Option 2: Local VMs (Lume daemon)\n", + "\n", + "For Apple Silicon Macs, you can run VMs locally using the Lume daemon." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Lume daemon setup\n", "\n", "Refer to [../libs/lume/README.md](../libs/lume/README.md) for more details on the lume cli." ] @@ -143,7 +260,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Initialize a Computer instance" + "### Initialize a Local Computer instance" ] }, { @@ -155,11 +272,11 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 23, "metadata": {}, "outputs": [], "source": [ - "from computer import Computer" + "from computer import Computer, VMProviderType" ] }, { @@ -187,9 +304,10 @@ " display=\"1024x768\",\n", " memory=\"8GB\",\n", " cpu=\"4\",\n", - " os_type=\"macos\"\n", + " os_type=\"macos\",\n", + " provider_type=VMProviderType.LUME,\n", ") as computer:\n", - " await computer.run()\n", + " pass\n", " # ... do something with the computer interface" ] }, @@ -216,6 +334,15 @@ "await computer.run()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Computer Interface\n", + "\n", + "Both cloud and local computers provide the same interface for interaction." + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -460,7 +587,7 @@ ], "metadata": { "kernelspec": { - "display_name": "cua313", + "display_name": ".venv", "language": "python", "name": "python3" }, @@ -474,7 +601,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.2" + "version": "3.12.2" } }, "nbformat": 4, diff --git a/notebooks/pylume_nb.ipynb b/notebooks/pylume_nb.ipynb index 2e1e08f6..1b504417 100644 --- a/notebooks/pylume_nb.ipynb +++ b/notebooks/pylume_nb.ipynb @@ -81,7 +81,7 @@ "outputs": [], "source": [ "async def get_ipsw():\n", - " async with PyLume(port=3000) as pylume:\n", + " async with PyLume(port=7777) as pylume:\n", " url = await pylume.get_latest_ipsw_url()\n", " print(f\"Latest IPSW URL: {url}\")\n", "\n", diff --git a/pdm.lock b/pdm.lock deleted file mode 100644 index d057d701..00000000 --- a/pdm.lock +++ /dev/null @@ -1,6021 +0,0 @@ -# This file is @generated by PDM. -# It is not intended for manual editing. - -[metadata] -groups = ["default", "dev", "docs", "examples", "test"] -strategy = ["inherit_metadata"] -lock_version = "4.5.0" -content_hash = "sha256:17e01be328c95e809d9881f426871a494441f1c90bbb91e83680f67787d14204" - -[[metadata.targets]] -requires_python = ">=3.10" - -[[package]] -name = "aiofiles" -version = "24.1.0" -requires_python = ">=3.8" -summary = "File support for asyncio." -groups = ["dev"] -files = [ - {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, - {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, -] - -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -requires_python = ">=3.9" -summary = "Happy Eyeballs for asyncio" -groups = ["dev", "test"] -files = [ - {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, - {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, -] - -[[package]] -name = "aiohttp" -version = "3.11.18" -requires_python = ">=3.9" -summary = "Async http client/server framework (asyncio)" -groups = ["dev", "test"] -dependencies = [ - "aiohappyeyeballs>=2.3.0", - "aiosignal>=1.1.2", - "async-timeout<6.0,>=4.0; python_version < \"3.11\"", - "attrs>=17.3.0", - "frozenlist>=1.1.1", - "multidict<7.0,>=4.5", - "propcache>=0.2.0", - "yarl<2.0,>=1.17.0", -] -files = [ - {file = "aiohttp-3.11.18-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:96264854fedbea933a9ca4b7e0c745728f01380691687b7365d18d9e977179c4"}, - {file = "aiohttp-3.11.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9602044ff047043430452bc3a2089743fa85da829e6fc9ee0025351d66c332b6"}, - {file = "aiohttp-3.11.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5691dc38750fcb96a33ceef89642f139aa315c8a193bbd42a0c33476fd4a1609"}, - {file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554c918ec43f8480b47a5ca758e10e793bd7410b83701676a4782672d670da55"}, - {file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a4076a2b3ba5b004b8cffca6afe18a3b2c5c9ef679b4d1e9859cf76295f8d4f"}, - {file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:767a97e6900edd11c762be96d82d13a1d7c4fc4b329f054e88b57cdc21fded94"}, - {file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0ddc9337a0fb0e727785ad4f41163cc314376e82b31846d3835673786420ef1"}, - {file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f414f37b244f2a97e79b98d48c5ff0789a0b4b4609b17d64fa81771ad780e415"}, - {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fdb239f47328581e2ec7744ab5911f97afb10752332a6dd3d98e14e429e1a9e7"}, - {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f2c50bad73ed629cc326cc0f75aed8ecfb013f88c5af116f33df556ed47143eb"}, - {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a8d8f20c39d3fa84d1c28cdb97f3111387e48209e224408e75f29c6f8e0861d"}, - {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:106032eaf9e62fd6bc6578c8b9e6dc4f5ed9a5c1c7fb2231010a1b4304393421"}, - {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:b491e42183e8fcc9901d8dcd8ae644ff785590f1727f76ca86e731c61bfe6643"}, - {file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad8c745ff9460a16b710e58e06a9dec11ebc0d8f4dd82091cefb579844d69868"}, - {file = "aiohttp-3.11.18-cp310-cp310-win32.whl", hash = "sha256:8e57da93e24303a883146510a434f0faf2f1e7e659f3041abc4e3fb3f6702a9f"}, - {file = "aiohttp-3.11.18-cp310-cp310-win_amd64.whl", hash = "sha256:cc93a4121d87d9f12739fc8fab0a95f78444e571ed63e40bfc78cd5abe700ac9"}, - {file = "aiohttp-3.11.18-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:427fdc56ccb6901ff8088544bde47084845ea81591deb16f957897f0f0ba1be9"}, - {file = "aiohttp-3.11.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c828b6d23b984255b85b9b04a5b963a74278b7356a7de84fda5e3b76866597b"}, - {file = "aiohttp-3.11.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5c2eaa145bb36b33af1ff2860820ba0589e165be4ab63a49aebfd0981c173b66"}, - {file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d518ce32179f7e2096bf4e3e8438cf445f05fedd597f252de9f54c728574756"}, - {file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700055a6e05c2f4711011a44364020d7a10fbbcd02fbf3e30e8f7e7fddc8717"}, - {file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bd1cde83e4684324e6ee19adfc25fd649d04078179890be7b29f76b501de8e4"}, - {file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73b8870fe1c9a201b8c0d12c94fe781b918664766728783241a79e0468427e4f"}, - {file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25557982dd36b9e32c0a3357f30804e80790ec2c4d20ac6bcc598533e04c6361"}, - {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e889c9df381a2433802991288a61e5a19ceb4f61bd14f5c9fa165655dcb1fd1"}, - {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9ea345fda05bae217b6cce2acf3682ce3b13d0d16dd47d0de7080e5e21362421"}, - {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9f26545b9940c4b46f0a9388fd04ee3ad7064c4017b5a334dd450f616396590e"}, - {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3a621d85e85dccabd700294494d7179ed1590b6d07a35709bb9bd608c7f5dd1d"}, - {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9c23fd8d08eb9c2af3faeedc8c56e134acdaf36e2117ee059d7defa655130e5f"}, - {file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9e6b0e519067caa4fd7fb72e3e8002d16a68e84e62e7291092a5433763dc0dd"}, - {file = "aiohttp-3.11.18-cp311-cp311-win32.whl", hash = "sha256:122f3e739f6607e5e4c6a2f8562a6f476192a682a52bda8b4c6d4254e1138f4d"}, - {file = "aiohttp-3.11.18-cp311-cp311-win_amd64.whl", hash = "sha256:e6f3c0a3a1e73e88af384b2e8a0b9f4fb73245afd47589df2afcab6b638fa0e6"}, - {file = "aiohttp-3.11.18-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:63d71eceb9cad35d47d71f78edac41fcd01ff10cacaa64e473d1aec13fa02df2"}, - {file = "aiohttp-3.11.18-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d1929da615840969929e8878d7951b31afe0bac883d84418f92e5755d7b49508"}, - {file = "aiohttp-3.11.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d0aebeb2392f19b184e3fdd9e651b0e39cd0f195cdb93328bd124a1d455cd0e"}, - {file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3849ead845e8444f7331c284132ab314b4dac43bfae1e3cf350906d4fff4620f"}, - {file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e8452ad6b2863709f8b3d615955aa0807bc093c34b8e25b3b52097fe421cb7f"}, - {file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b8d2b42073611c860a37f718b3d61ae8b4c2b124b2e776e2c10619d920350ec"}, - {file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fbf91f6a0ac317c0a07eb328a1384941872f6761f2e6f7208b63c4cc0a7ff6"}, - {file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ff5625413fec55216da5eaa011cf6b0a2ed67a565914a212a51aa3755b0009"}, - {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7f33a92a2fde08e8c6b0c61815521324fc1612f397abf96eed86b8e31618fdb4"}, - {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:11d5391946605f445ddafda5eab11caf310f90cdda1fd99865564e3164f5cff9"}, - {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3cc314245deb311364884e44242e00c18b5896e4fe6d5f942e7ad7e4cb640adb"}, - {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0f421843b0f70740772228b9e8093289924359d306530bcd3926f39acbe1adda"}, - {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e220e7562467dc8d589e31c1acd13438d82c03d7f385c9cd41a3f6d1d15807c1"}, - {file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ab2ef72f8605046115bc9aa8e9d14fd49086d405855f40b79ed9e5c1f9f4faea"}, - {file = "aiohttp-3.11.18-cp312-cp312-win32.whl", hash = "sha256:12a62691eb5aac58d65200c7ae94d73e8a65c331c3a86a2e9670927e94339ee8"}, - {file = "aiohttp-3.11.18-cp312-cp312-win_amd64.whl", hash = "sha256:364329f319c499128fd5cd2d1c31c44f234c58f9b96cc57f743d16ec4f3238c8"}, - {file = "aiohttp-3.11.18-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:474215ec618974054cf5dc465497ae9708543cbfc312c65212325d4212525811"}, - {file = "aiohttp-3.11.18-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ced70adf03920d4e67c373fd692123e34d3ac81dfa1c27e45904a628567d804"}, - {file = "aiohttp-3.11.18-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d9f6c0152f8d71361905aaf9ed979259537981f47ad099c8b3d81e0319814bd"}, - {file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a35197013ed929c0aed5c9096de1fc5a9d336914d73ab3f9df14741668c0616c"}, - {file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:540b8a1f3a424f1af63e0af2d2853a759242a1769f9f1ab053996a392bd70118"}, - {file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9e6710ebebfce2ba21cee6d91e7452d1125100f41b906fb5af3da8c78b764c1"}, - {file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8af2ef3b4b652ff109f98087242e2ab974b2b2b496304063585e3d78de0b000"}, - {file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28c3f975e5ae3dbcbe95b7e3dcd30e51da561a0a0f2cfbcdea30fc1308d72137"}, - {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c28875e316c7b4c3e745172d882d8a5c835b11018e33432d281211af35794a93"}, - {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:13cd38515568ae230e1ef6919e2e33da5d0f46862943fcda74e7e915096815f3"}, - {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0e2a92101efb9f4c2942252c69c63ddb26d20f46f540c239ccfa5af865197bb8"}, - {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e6d3e32b8753c8d45ac550b11a1090dd66d110d4ef805ffe60fa61495360b3b2"}, - {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ea4cf2488156e0f281f93cc2fd365025efcba3e2d217cbe3df2840f8c73db261"}, - {file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d4df95ad522c53f2b9ebc07f12ccd2cb15550941e11a5bbc5ddca2ca56316d7"}, - {file = "aiohttp-3.11.18-cp313-cp313-win32.whl", hash = "sha256:cdd1bbaf1e61f0d94aced116d6e95fe25942f7a5f42382195fd9501089db5d78"}, - {file = "aiohttp-3.11.18-cp313-cp313-win_amd64.whl", hash = "sha256:bdd619c27e44382cf642223f11cfd4d795161362a5a1fc1fa3940397bc89db01"}, - {file = "aiohttp-3.11.18.tar.gz", hash = "sha256:ae856e1138612b7e412db63b7708735cff4d38d0399f6a5435d3dac2669f558a"}, -] - -[[package]] -name = "aioresponses" -version = "0.7.8" -summary = "Mock out requests made by ClientSession from aiohttp package" -groups = ["test"] -dependencies = [ - "aiohttp<4.0.0,>=3.3.0", - "packaging>=22.0", -] -files = [ - {file = "aioresponses-0.7.8-py2.py3-none-any.whl", hash = "sha256:b73bd4400d978855e55004b23a3a84cb0f018183bcf066a85ad392800b5b9a94"}, - {file = "aioresponses-0.7.8.tar.gz", hash = "sha256:b861cdfe5dc58f3b8afac7b0a6973d5d7b2cb608dd0f6253d16b8ee8eaf6df11"}, -] - -[[package]] -name = "aiosignal" -version = "1.3.2" -requires_python = ">=3.9" -summary = "aiosignal: a list of registered asynchronous callbacks" -groups = ["dev", "test"] -dependencies = [ - "frozenlist>=1.1.0", -] -files = [ - {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"}, - {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"}, -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -requires_python = ">=3.8" -summary = "Reusable constraint types to use with typing.Annotated" -groups = ["dev"] -dependencies = [ - "typing-extensions>=4.0.0; python_version < \"3.9\"", -] -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "anthropic" -version = "0.46.0" -requires_python = ">=3.8" -summary = "The official Python library for the anthropic API" -groups = ["dev"] -dependencies = [ - "anyio<5,>=3.5.0", - "distro<2,>=1.7.0", - "httpx<1,>=0.23.0", - "jiter<1,>=0.4.0", - "pydantic<3,>=1.9.0", - "sniffio", - "typing-extensions<5,>=4.10", -] -files = [ - {file = "anthropic-0.46.0-py3-none-any.whl", hash = "sha256:1445ec9be78d2de7ea51b4d5acd3574e414aea97ef903d0ecbb57bec806aaa49"}, - {file = "anthropic-0.46.0.tar.gz", hash = "sha256:eac3d43271d02321a57c3ca68aca84c3d58873e8e72d1433288adee2d46b745b"}, -] - -[[package]] -name = "anyio" -version = "4.9.0" -requires_python = ">=3.9" -summary = "High level compatibility layer for multiple asynchronous event loop implementations" -groups = ["dev"] -dependencies = [ - "exceptiongroup>=1.0.2; python_version < \"3.11\"", - "idna>=2.8", - "sniffio>=1.1", - "typing-extensions>=4.5; python_version < \"3.13\"", -] -files = [ - {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, - {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, -] - -[[package]] -name = "appnope" -version = "0.1.4" -requires_python = ">=3.6" -summary = "Disable App Nap on macOS >= 10.9" -groups = ["dev"] -marker = "platform_system == \"Darwin\"" -files = [ - {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, - {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, -] - -[[package]] -name = "argon2-cffi" -version = "23.1.0" -requires_python = ">=3.7" -summary = "Argon2 for Python" -groups = ["dev"] -dependencies = [ - "argon2-cffi-bindings", - "typing-extensions; python_version < \"3.8\"", -] -files = [ - {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, - {file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"}, -] - -[[package]] -name = "argon2-cffi-bindings" -version = "21.2.0" -requires_python = ">=3.6" -summary = "Low-level CFFI bindings for Argon2" -groups = ["dev"] -dependencies = [ - "cffi>=1.0.1", -] -files = [ - {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"}, - {file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"}, -] - -[[package]] -name = "arrow" -version = "1.3.0" -requires_python = ">=3.8" -summary = "Better dates & times for Python" -groups = ["dev"] -dependencies = [ - "python-dateutil>=2.7.0", - "types-python-dateutil>=2.8.10", -] -files = [ - {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, - {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, -] - -[[package]] -name = "asttokens" -version = "3.0.0" -requires_python = ">=3.8" -summary = "Annotate AST trees with source code positions" -groups = ["dev"] -files = [ - {file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"}, - {file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"}, -] - -[[package]] -name = "async-lru" -version = "2.0.5" -requires_python = ">=3.9" -summary = "Simple LRU cache for asyncio" -groups = ["dev"] -dependencies = [ - "typing-extensions>=4.0.0; python_version < \"3.11\"", -] -files = [ - {file = "async_lru-2.0.5-py3-none-any.whl", hash = "sha256:ab95404d8d2605310d345932697371a5f40def0487c03d6d0ad9138de52c9943"}, - {file = "async_lru-2.0.5.tar.gz", hash = "sha256:481d52ccdd27275f42c43a928b4a50c3bfb2d67af4e78b170e3e0bb39c66e5bb"}, -] - -[[package]] -name = "async-timeout" -version = "5.0.1" -requires_python = ">=3.8" -summary = "Timeout context manager for asyncio programs" -groups = ["dev", "test"] -marker = "python_version < \"3.11\"" -files = [ - {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, - {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, -] - -[[package]] -name = "asyncio" -version = "3.4.3" -summary = "reference implementation of PEP 3156" -groups = ["dev"] -files = [ - {file = "asyncio-3.4.3-py3-none-any.whl", hash = "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d"}, - {file = "asyncio-3.4.3.tar.gz", hash = "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41"}, -] - -[[package]] -name = "attrs" -version = "25.3.0" -requires_python = ">=3.8" -summary = "Classes Without Boilerplate" -groups = ["dev", "test"] -files = [ - {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, - {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, -] - -[[package]] -name = "audioop-lts" -version = "0.2.1" -requires_python = ">=3.13" -summary = "LTS Port of Python audioop" -groups = ["dev"] -marker = "python_version >= \"3.13\"" -files = [ - {file = "audioop_lts-0.2.1-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd1345ae99e17e6910f47ce7d52673c6a1a70820d78b67de1b7abb3af29c426a"}, - {file = "audioop_lts-0.2.1-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:e175350da05d2087e12cea8e72a70a1a8b14a17e92ed2022952a4419689ede5e"}, - {file = "audioop_lts-0.2.1-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:4a8dd6a81770f6ecf019c4b6d659e000dc26571b273953cef7cd1d5ce2ff3ae6"}, - {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cd3c0b6f2ca25c7d2b1c3adeecbe23e65689839ba73331ebc7d893fcda7ffe"}, - {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff3f97b3372c97782e9c6d3d7fdbe83bce8f70de719605bd7ee1839cd1ab360a"}, - {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a351af79edefc2a1bd2234bfd8b339935f389209943043913a919df4b0f13300"}, - {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aeb6f96f7f6da80354330470b9134d81b4cf544cdd1c549f2f45fe964d28059"}, - {file = "audioop_lts-0.2.1-cp313-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c589f06407e8340e81962575fcffbba1e92671879a221186c3d4662de9fe804e"}, - {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fbae5d6925d7c26e712f0beda5ed69ebb40e14212c185d129b8dfbfcc335eb48"}, - {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_i686.whl", hash = "sha256:d2d5434717f33117f29b5691fbdf142d36573d751716249a288fbb96ba26a281"}, - {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:f626a01c0a186b08f7ff61431c01c055961ee28769591efa8800beadd27a2959"}, - {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:05da64e73837f88ee5c6217d732d2584cf638003ac72df124740460531e95e47"}, - {file = "audioop_lts-0.2.1-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:56b7a0a4dba8e353436f31a932f3045d108a67b5943b30f85a5563f4d8488d77"}, - {file = "audioop_lts-0.2.1-cp313-abi3-win32.whl", hash = "sha256:6e899eb8874dc2413b11926b5fb3857ec0ab55222840e38016a6ba2ea9b7d5e3"}, - {file = "audioop_lts-0.2.1-cp313-abi3-win_amd64.whl", hash = "sha256:64562c5c771fb0a8b6262829b9b4f37a7b886c01b4d3ecdbae1d629717db08b4"}, - {file = "audioop_lts-0.2.1-cp313-abi3-win_arm64.whl", hash = "sha256:c45317debeb64002e980077642afbd977773a25fa3dfd7ed0c84dccfc1fafcb0"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3827e3fce6fee4d69d96a3d00cd2ab07f3c0d844cb1e44e26f719b34a5b15455"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:161249db9343b3c9780ca92c0be0d1ccbfecdbccac6844f3d0d44b9c4a00a17f"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5b7b4ff9de7a44e0ad2618afdc2ac920b91f4a6d3509520ee65339d4acde5abf"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72e37f416adb43b0ced93419de0122b42753ee74e87070777b53c5d2241e7fab"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534ce808e6bab6adb65548723c8cbe189a3379245db89b9d555c4210b4aaa9b6"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2de9b6fb8b1cf9f03990b299a9112bfdf8b86b6987003ca9e8a6c4f56d39543"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f24865991b5ed4b038add5edbf424639d1358144f4e2a3e7a84bc6ba23e35074"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bdb3b7912ccd57ea53197943f1bbc67262dcf29802c4a6df79ec1c715d45a78"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:120678b208cca1158f0a12d667af592e067f7a50df9adc4dc8f6ad8d065a93fb"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:54cd4520fc830b23c7d223693ed3e1b4d464997dd3abc7c15dce9a1f9bd76ab2"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:d6bd20c7a10abcb0fb3d8aaa7508c0bf3d40dfad7515c572014da4b979d3310a"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:f0ed1ad9bd862539ea875fb339ecb18fcc4148f8d9908f4502df28f94d23491a"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e1af3ff32b8c38a7d900382646e91f2fc515fd19dea37e9392275a5cbfdbff63"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-win32.whl", hash = "sha256:f51bb55122a89f7a0817d7ac2319744b4640b5b446c4c3efcea5764ea99ae509"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f0f2f336aa2aee2bce0b0dcc32bbba9178995454c7b979cf6ce086a8801e14c7"}, - {file = "audioop_lts-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:78bfb3703388c780edf900be66e07de5a3d4105ca8e8720c5c4d67927e0b15d0"}, - {file = "audioop_lts-0.2.1.tar.gz", hash = "sha256:e81268da0baa880431b68b1308ab7257eb33f356e57a5f9b1f915dfb13dd1387"}, -] - -[[package]] -name = "babel" -version = "2.17.0" -requires_python = ">=3.8" -summary = "Internationalization utilities" -groups = ["dev", "docs"] -dependencies = [ - "pytz>=2015.7; python_version < \"3.9\"", -] -files = [ - {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, - {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, -] - -[[package]] -name = "backoff" -version = "2.2.1" -requires_python = ">=3.7,<4.0" -summary = "Function decoration for backoff and retry" -groups = ["dev"] -files = [ - {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, - {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, -] - -[[package]] -name = "backrefs" -version = "5.8" -requires_python = ">=3.9" -summary = "A wrapper around re and regex that adds additional back references." -groups = ["docs"] -files = [ - {file = "backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d"}, - {file = "backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b"}, - {file = "backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486"}, - {file = "backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585"}, - {file = "backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd"}, -] - -[[package]] -name = "beautifulsoup4" -version = "4.13.4" -requires_python = ">=3.7.0" -summary = "Screen-scraping library" -groups = ["dev"] -dependencies = [ - "soupsieve>1.2", - "typing-extensions>=4.0.0", -] -files = [ - {file = "beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b"}, - {file = "beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195"}, -] - -[[package]] -name = "black" -version = "25.1.0" -requires_python = ">=3.9" -summary = "The uncompromising code formatter." -groups = ["dev"] -dependencies = [ - "click>=8.0.0", - "mypy-extensions>=0.4.3", - "packaging>=22.0", - "pathspec>=0.9.0", - "platformdirs>=2", - "tomli>=1.1.0; python_version < \"3.11\"", - "typing-extensions>=4.0.1; python_version < \"3.11\"", -] -files = [ - {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, - {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, - {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"}, - {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"}, - {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"}, - {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"}, - {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"}, - {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"}, - {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, - {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, - {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, - {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, - {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, - {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, - {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, - {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, - {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, - {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, -] - -[[package]] -name = "bleach" -version = "6.2.0" -requires_python = ">=3.9" -summary = "An easy safelist-based HTML-sanitizing tool." -groups = ["dev"] -dependencies = [ - "webencodings", -] -files = [ - {file = "bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e"}, - {file = "bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f"}, -] - -[[package]] -name = "bleach" -version = "6.2.0" -extras = ["css"] -requires_python = ">=3.9" -summary = "An easy safelist-based HTML-sanitizing tool." -groups = ["dev"] -dependencies = [ - "bleach==6.2.0", - "tinycss2<1.5,>=1.1.0", -] -files = [ - {file = "bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e"}, - {file = "bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f"}, -] - -[[package]] -name = "boto3" -version = "1.38.3" -requires_python = ">=3.9" -summary = "The AWS SDK for Python" -groups = ["dev"] -dependencies = [ - "botocore<1.39.0,>=1.38.3", - "jmespath<2.0.0,>=0.7.1", - "s3transfer<0.13.0,>=0.12.0", -] -files = [ - {file = "boto3-1.38.3-py3-none-any.whl", hash = "sha256:9218f86e2164e1bddb75d435bbde4fa651aa58687213d7e3e1b50f7eb8868f66"}, - {file = "boto3-1.38.3.tar.gz", hash = "sha256:655d51abcd68a40a33c52dbaa2ca73fc63c746b894e2ae22ed8ddc1912ddd93f"}, -] - -[[package]] -name = "botocore" -version = "1.38.3" -requires_python = ">=3.9" -summary = "Low-level, data-driven core of boto 3." -groups = ["dev"] -dependencies = [ - "jmespath<2.0.0,>=0.7.1", - "python-dateutil<3.0.0,>=2.1", - "urllib3!=2.2.0,<3,>=1.25.4; python_version >= \"3.10\"", - "urllib3<1.27,>=1.25.4; python_version < \"3.10\"", -] -files = [ - {file = "botocore-1.38.3-py3-none-any.whl", hash = "sha256:96f823240fe3704b99c17d1d1b2fd2d1679cf56d2a55b095f00255b76087cbf0"}, - {file = "botocore-1.38.3.tar.gz", hash = "sha256:790f8f966201781f5fcf486d48b4492e9f734446bbf9d19ef8159d08be854243"}, -] - -[[package]] -name = "certifi" -version = "2025.1.31" -requires_python = ">=3.6" -summary = "Python package for providing Mozilla's CA Bundle." -groups = ["dev", "docs"] -files = [ - {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, - {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, -] - -[[package]] -name = "cffi" -version = "1.17.1" -requires_python = ">=3.8" -summary = "Foreign Function Interface for Python calling C code." -groups = ["dev"] -dependencies = [ - "pycparser", -] -files = [ - {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, - {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, - {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, - {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, - {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, - {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, - {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, - {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, - {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, - {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, - {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.1" -requires_python = ">=3.7" -summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -groups = ["dev", "docs"] -files = [ - {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, - {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, - {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, -] - -[[package]] -name = "click" -version = "8.1.8" -requires_python = ">=3.7" -summary = "Composable command line interface toolkit" -groups = ["dev", "docs"] -dependencies = [ - "colorama; platform_system == \"Windows\"", - "importlib-metadata; python_version < \"3.8\"", -] -files = [ - {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, - {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -summary = "Cross-platform colored terminal text." -groups = ["dev", "docs", "test"] -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "comm" -version = "0.2.2" -requires_python = ">=3.8" -summary = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." -groups = ["dev"] -dependencies = [ - "traitlets>=4", -] -files = [ - {file = "comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3"}, - {file = "comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e"}, -] - -[[package]] -name = "contourpy" -version = "1.3.2" -requires_python = ">=3.10" -summary = "Python library for calculating contours of 2D quadrilateral grids" -groups = ["dev"] -dependencies = [ - "numpy>=1.23", -] -files = [ - {file = "contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934"}, - {file = "contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989"}, - {file = "contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d"}, - {file = "contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9"}, - {file = "contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512"}, - {file = "contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631"}, - {file = "contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f"}, - {file = "contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2"}, - {file = "contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0"}, - {file = "contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a"}, - {file = "contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445"}, - {file = "contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773"}, - {file = "contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1"}, - {file = "contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43"}, - {file = "contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab"}, - {file = "contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7"}, - {file = "contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83"}, - {file = "contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd"}, - {file = "contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f"}, - {file = "contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878"}, - {file = "contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2"}, - {file = "contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15"}, - {file = "contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92"}, - {file = "contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87"}, - {file = "contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415"}, - {file = "contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe"}, - {file = "contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441"}, - {file = "contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e"}, - {file = "contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912"}, - {file = "contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73"}, - {file = "contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb"}, - {file = "contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08"}, - {file = "contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c"}, - {file = "contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f"}, - {file = "contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85"}, - {file = "contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841"}, - {file = "contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422"}, - {file = "contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef"}, - {file = "contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f"}, - {file = "contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9"}, - {file = "contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f"}, - {file = "contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739"}, - {file = "contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823"}, - {file = "contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5"}, - {file = "contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532"}, - {file = "contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b"}, - {file = "contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52"}, - {file = "contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd"}, - {file = "contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1"}, - {file = "contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69"}, - {file = "contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c"}, - {file = "contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16"}, - {file = "contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad"}, - {file = "contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0"}, - {file = "contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5"}, - {file = "contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5"}, - {file = "contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54"}, -] - -[[package]] -name = "coverage" -version = "7.8.0" -requires_python = ">=3.9" -summary = "Code coverage measurement for Python" -groups = ["test"] -files = [ - {file = "coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe"}, - {file = "coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28"}, - {file = "coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3"}, - {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676"}, - {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d"}, - {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a"}, - {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c"}, - {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f"}, - {file = "coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f"}, - {file = "coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23"}, - {file = "coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27"}, - {file = "coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea"}, - {file = "coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7"}, - {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040"}, - {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543"}, - {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2"}, - {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318"}, - {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9"}, - {file = "coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c"}, - {file = "coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78"}, - {file = "coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc"}, - {file = "coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6"}, - {file = "coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d"}, - {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05"}, - {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a"}, - {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6"}, - {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47"}, - {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe"}, - {file = "coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545"}, - {file = "coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b"}, - {file = "coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd"}, - {file = "coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00"}, - {file = "coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64"}, - {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067"}, - {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008"}, - {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733"}, - {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323"}, - {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3"}, - {file = "coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d"}, - {file = "coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487"}, - {file = "coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25"}, - {file = "coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42"}, - {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502"}, - {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1"}, - {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4"}, - {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73"}, - {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a"}, - {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883"}, - {file = "coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada"}, - {file = "coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257"}, - {file = "coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd"}, - {file = "coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7"}, - {file = "coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501"}, -] - -[[package]] -name = "coverage" -version = "7.8.0" -extras = ["toml"] -requires_python = ">=3.9" -summary = "Code coverage measurement for Python" -groups = ["test"] -dependencies = [ - "coverage==7.8.0", - "tomli; python_full_version <= \"3.11.0a6\"", -] -files = [ - {file = "coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe"}, - {file = "coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28"}, - {file = "coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3"}, - {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676"}, - {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d"}, - {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a"}, - {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c"}, - {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f"}, - {file = "coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f"}, - {file = "coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23"}, - {file = "coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27"}, - {file = "coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea"}, - {file = "coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7"}, - {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040"}, - {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543"}, - {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2"}, - {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318"}, - {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9"}, - {file = "coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c"}, - {file = "coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78"}, - {file = "coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc"}, - {file = "coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6"}, - {file = "coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d"}, - {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05"}, - {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a"}, - {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6"}, - {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47"}, - {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe"}, - {file = "coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545"}, - {file = "coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b"}, - {file = "coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd"}, - {file = "coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00"}, - {file = "coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64"}, - {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067"}, - {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008"}, - {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733"}, - {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323"}, - {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3"}, - {file = "coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d"}, - {file = "coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487"}, - {file = "coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25"}, - {file = "coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42"}, - {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502"}, - {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1"}, - {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4"}, - {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73"}, - {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a"}, - {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883"}, - {file = "coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada"}, - {file = "coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257"}, - {file = "coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd"}, - {file = "coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7"}, - {file = "coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501"}, -] - -[[package]] -name = "cua-agent" -version = "0.1.0" -requires_python = ">=3.10" -editable = true -path = "./libs/agent" -summary = "CUA (Computer Use) Agent for AI-driven computer interaction" -groups = ["dev"] -dependencies = [ - "aiohttp<4.0.0,>=3.9.3", - "anyio<5.0.0,>=4.4.1", - "asyncio", - "certifi>=2024.2.2", - "cua-computer<0.2.0,>=0.1.0", - "cua-core<0.2.0,>=0.1.0", - "httpx<0.29.0,>=0.27.0", - "pydantic<3.0.0,>=2.6.4", - "python-dotenv<2.0.0,>=1.0.1", - "rich<14.0.0,>=13.7.1", - "typing-extensions<5.0.0,>=4.12.2", -] - -[[package]] -name = "cua-agent" -version = "0.1.0" -extras = ["all"] -requires_python = ">=3.10" -summary = "CUA (Computer Use) Agent for AI-driven computer interaction" -groups = ["dev"] -dependencies = [ - "anthropic<0.47.0,>=0.46.0", - "boto3<2.0.0,>=1.35.81", - "cua-agent==0.1.0", - "cua-som<0.2.0,>=0.1.0", - "dashscope<2.0.0,>=1.13.0", - "gradio<6.0.0,>=5.23.3", - "groq<0.5.0,>=0.4.0", - "ollama<0.5.0,>=0.4.7", - "openai<2.0.0,>=1.14.0", - "python-dotenv<2.0.0,>=1.0.1", - "requests<3.0.0,>=2.31.0", - "torch>=2.2.1", - "torchvision>=0.17.1", - "transformers>=4.38.2", - "ultralytics>=8.0.0", -] -files = [ - {file = "cua_agent-0.1.0-py3-none-any.whl", hash = "sha256:1f9f1497c19b1a4f49ba82481516cd9f7df92e75386c72a575b5cd6e0145c94f"}, - {file = "cua_agent-0.1.0.tar.gz", hash = "sha256:49dde3b88d22e979e39376fb82f42fc9d74c8a05eb8cde6a63247541bcf34e3e"}, -] - -[[package]] -name = "cua-computer" -version = "0.1.0" -requires_python = ">=3.10" -editable = true -path = "./libs/computer" -summary = "Computer-Use Interface (CUI) framework powering Cua" -groups = ["dev"] -dependencies = [ - "aiohttp>=3.9.0", - "cua-core<0.2.0,>=0.1.0", - "pillow>=10.0.0", - "pydantic>=2.11.1", - "pylume>=0.1.8", - "websocket-client>=1.8.0", - "websockets>=12.0", -] - -[[package]] -name = "cua-computer-server" -version = "0.1.0" -requires_python = ">=3.10" -editable = true -path = "./libs/computer-server" -summary = "Server component for the Computer-Use Interface (CUI) framework powering Cua" -groups = ["dev"] -dependencies = [ - "fastapi>=0.111.0", - "pillow>=10.2.0", - "pyautogui>=0.9.54", - "pydantic>=2.0.0", - "pyobjc-framework-ApplicationServices>=10.1; sys_platform == \"darwin\"", - "pyobjc-framework-Cocoa>=10.1; sys_platform == \"darwin\"", - "pyobjc-framework-Quartz>=10.1; sys_platform == \"darwin\"", - "python-xlib>=0.33; sys_platform == \"linux\"", - "uvicorn[standard]>=0.27.0", -] - -[[package]] -name = "cua-core" -version = "0.1.5" -requires_python = ">=3.10" -summary = "Core functionality for Cua including telemetry and shared utilities" -groups = ["dev"] -dependencies = [ - "httpx>=0.24.0", - "posthog>=3.20.0", - "pydantic>=2.0.0", -] -files = [ - {file = "cua_core-0.1.5-py3-none-any.whl", hash = "sha256:d43b21e477f76fc340689596630356fbf815f8990dfab48241fdccfc45232b9c"}, - {file = "cua_core-0.1.5.tar.gz", hash = "sha256:96339f47e34ef95a6d000aab9dc50212963ce1feab42ee63886a71dbb14bfca5"}, -] - -[[package]] -name = "cua-mcp-server" -version = "0.1.0" -requires_python = ">=3.10" -editable = true -path = "./libs/mcp-server" -summary = "MCP Server for Computer-Use Agent (CUA)" -groups = ["dev"] -dependencies = [ - "cua-agent[all]<0.2.0,>=0.1.0", - "cua-computer<0.2.0,>=0.1.0", - "mcp<2.0.0,>=1.6.0", -] - -[[package]] -name = "cua-som" -version = "0.1.0" -requires_python = ">=3.10" -editable = true -path = "./libs/som" -summary = "Computer Vision and OCR library for detecting and analyzing UI elements" -groups = ["dev"] -dependencies = [ - "easyocr>=1.7.1", - "huggingface-hub>=0.21.4", - "matplotlib>=3.8.3", - "numpy>=1.26.4", - "opencv-python-headless>=4.11.0.86", - "pillow>=10.2.0", - "pydantic>=2.6.3", - "setuptools>=75.8.1", - "supervision>=0.25.1", - "torch>=2.2.1", - "torchvision>=0.17.1", - "typing-extensions>=4.9.0", - "ultralytics>=8.1.28", -] - -[[package]] -name = "cycler" -version = "0.12.1" -requires_python = ">=3.8" -summary = "Composable style cycles" -groups = ["dev"] -files = [ - {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, - {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, -] - -[[package]] -name = "dashscope" -version = "1.23.1" -requires_python = ">=3.8.0" -summary = "dashscope client sdk library" -groups = ["dev"] -dependencies = [ - "aiohttp", - "requests", - "websocket-client", -] -files = [ - {file = "dashscope-1.23.1-py3-none-any.whl", hash = "sha256:2c3bd6ed909de72cc4833ada0f7fdae670031738d01969a76f3676a6bbb56026"}, -] - -[[package]] -name = "debugpy" -version = "1.8.14" -requires_python = ">=3.8" -summary = "An implementation of the Debug Adapter Protocol for Python" -groups = ["dev"] -files = [ - {file = "debugpy-1.8.14-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:93fee753097e85623cab1c0e6a68c76308cd9f13ffdf44127e6fab4fbf024339"}, - {file = "debugpy-1.8.14-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d937d93ae4fa51cdc94d3e865f535f185d5f9748efb41d0d49e33bf3365bd79"}, - {file = "debugpy-1.8.14-cp310-cp310-win32.whl", hash = "sha256:c442f20577b38cc7a9aafecffe1094f78f07fb8423c3dddb384e6b8f49fd2987"}, - {file = "debugpy-1.8.14-cp310-cp310-win_amd64.whl", hash = "sha256:f117dedda6d969c5c9483e23f573b38f4e39412845c7bc487b6f2648df30fe84"}, - {file = "debugpy-1.8.14-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:1b2ac8c13b2645e0b1eaf30e816404990fbdb168e193322be8f545e8c01644a9"}, - {file = "debugpy-1.8.14-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf431c343a99384ac7eab2f763980724834f933a271e90496944195318c619e2"}, - {file = "debugpy-1.8.14-cp311-cp311-win32.whl", hash = "sha256:c99295c76161ad8d507b413cd33422d7c542889fbb73035889420ac1fad354f2"}, - {file = "debugpy-1.8.14-cp311-cp311-win_amd64.whl", hash = "sha256:7816acea4a46d7e4e50ad8d09d963a680ecc814ae31cdef3622eb05ccacf7b01"}, - {file = "debugpy-1.8.14-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:8899c17920d089cfa23e6005ad9f22582fd86f144b23acb9feeda59e84405b84"}, - {file = "debugpy-1.8.14-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6bb5c0dcf80ad5dbc7b7d6eac484e2af34bdacdf81df09b6a3e62792b722826"}, - {file = "debugpy-1.8.14-cp312-cp312-win32.whl", hash = "sha256:281d44d248a0e1791ad0eafdbbd2912ff0de9eec48022a5bfbc332957487ed3f"}, - {file = "debugpy-1.8.14-cp312-cp312-win_amd64.whl", hash = "sha256:5aa56ef8538893e4502a7d79047fe39b1dae08d9ae257074c6464a7b290b806f"}, - {file = "debugpy-1.8.14-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:329a15d0660ee09fec6786acdb6e0443d595f64f5d096fc3e3ccf09a4259033f"}, - {file = "debugpy-1.8.14-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f920c7f9af409d90f5fd26e313e119d908b0dd2952c2393cd3247a462331f15"}, - {file = "debugpy-1.8.14-cp313-cp313-win32.whl", hash = "sha256:3784ec6e8600c66cbdd4ca2726c72d8ca781e94bce2f396cc606d458146f8f4e"}, - {file = "debugpy-1.8.14-cp313-cp313-win_amd64.whl", hash = "sha256:684eaf43c95a3ec39a96f1f5195a7ff3d4144e4a18d69bb66beeb1a6de605d6e"}, - {file = "debugpy-1.8.14-py2.py3-none-any.whl", hash = "sha256:5cd9a579d553b6cb9759a7908a41988ee6280b961f24f63336835d9418216a20"}, - {file = "debugpy-1.8.14.tar.gz", hash = "sha256:7cd287184318416850aa8b60ac90105837bb1e59531898c07569d197d2ed5322"}, -] - -[[package]] -name = "decorator" -version = "5.2.1" -requires_python = ">=3.8" -summary = "Decorators for Humans" -groups = ["dev"] -files = [ - {file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"}, - {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"}, -] - -[[package]] -name = "defusedxml" -version = "0.7.1" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -summary = "XML bomb protection for Python stdlib modules" -groups = ["dev"] -files = [ - {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, - {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, -] - -[[package]] -name = "distro" -version = "1.9.0" -requires_python = ">=3.6" -summary = "Distro - an OS platform information API" -groups = ["dev"] -files = [ - {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, - {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, -] - -[[package]] -name = "easyocr" -version = "1.7.2" -summary = "End-to-End Multi-Lingual Optical Character Recognition (OCR) Solution" -groups = ["dev"] -dependencies = [ - "Pillow", - "PyYAML", - "Shapely", - "ninja", - "numpy", - "opencv-python-headless", - "pyclipper", - "python-bidi", - "scikit-image", - "scipy", - "torch", - "torchvision>=0.5", -] -files = [ - {file = "easyocr-1.7.2-py3-none-any.whl", hash = "sha256:5be12f9b0e595d443c9c3d10b0542074b50f0ec2d98b141a109cd961fd1c177c"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.2.2" -requires_python = ">=3.7" -summary = "Backport of PEP 654 (exception groups)" -groups = ["dev", "test"] -marker = "python_version < \"3.11\"" -files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, -] - -[[package]] -name = "execnet" -version = "2.1.1" -requires_python = ">=3.8" -summary = "execnet: rapid multi-Python deployment" -groups = ["test"] -files = [ - {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, - {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, -] - -[[package]] -name = "executing" -version = "2.2.0" -requires_python = ">=3.8" -summary = "Get the currently executing AST node of a frame, and other information" -groups = ["dev"] -files = [ - {file = "executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa"}, - {file = "executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755"}, -] - -[[package]] -name = "fastapi" -version = "0.115.12" -requires_python = ">=3.8" -summary = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -groups = ["dev"] -dependencies = [ - "pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4", - "starlette<0.47.0,>=0.40.0", - "typing-extensions>=4.8.0", -] -files = [ - {file = "fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d"}, - {file = "fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681"}, -] - -[[package]] -name = "fastjsonschema" -version = "2.21.1" -summary = "Fastest Python implementation of JSON schema" -groups = ["dev"] -files = [ - {file = "fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667"}, - {file = "fastjsonschema-2.21.1.tar.gz", hash = "sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4"}, -] - -[[package]] -name = "ffmpy" -version = "0.5.0" -requires_python = "<4.0,>=3.8" -summary = "A simple Python wrapper for FFmpeg" -groups = ["dev"] -files = [ - {file = "ffmpy-0.5.0-py3-none-any.whl", hash = "sha256:df3799cf5816daa56d4959a023630ee53c6768b66009dae6d131519ba4b80233"}, - {file = "ffmpy-0.5.0.tar.gz", hash = "sha256:277e131f246d18e9dcfee9bb514c50749031c43582ce5ef82c57b51e3d3955c3"}, -] - -[[package]] -name = "filelock" -version = "3.18.0" -requires_python = ">=3.9" -summary = "A platform independent file lock." -groups = ["dev"] -files = [ - {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, - {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, -] - -[[package]] -name = "fonttools" -version = "4.57.0" -requires_python = ">=3.8" -summary = "Tools to manipulate font files" -groups = ["dev"] -files = [ - {file = "fonttools-4.57.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:babe8d1eb059a53e560e7bf29f8e8f4accc8b6cfb9b5fd10e485bde77e71ef41"}, - {file = "fonttools-4.57.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81aa97669cd726349eb7bd43ca540cf418b279ee3caba5e2e295fb4e8f841c02"}, - {file = "fonttools-4.57.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0e9618630edd1910ad4f07f60d77c184b2f572c8ee43305ea3265675cbbfe7e"}, - {file = "fonttools-4.57.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34687a5d21f1d688d7d8d416cb4c5b9c87fca8a1797ec0d74b9fdebfa55c09ab"}, - {file = "fonttools-4.57.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:69ab81b66ebaa8d430ba56c7a5f9abe0183afefd3a2d6e483060343398b13fb1"}, - {file = "fonttools-4.57.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d639397de852f2ccfb3134b152c741406752640a266d9c1365b0f23d7b88077f"}, - {file = "fonttools-4.57.0-cp310-cp310-win32.whl", hash = "sha256:cc066cb98b912f525ae901a24cd381a656f024f76203bc85f78fcc9e66ae5aec"}, - {file = "fonttools-4.57.0-cp310-cp310-win_amd64.whl", hash = "sha256:7a64edd3ff6a7f711a15bd70b4458611fb240176ec11ad8845ccbab4fe6745db"}, - {file = "fonttools-4.57.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3871349303bdec958360eedb619169a779956503ffb4543bb3e6211e09b647c4"}, - {file = "fonttools-4.57.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c59375e85126b15a90fcba3443eaac58f3073ba091f02410eaa286da9ad80ed8"}, - {file = "fonttools-4.57.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967b65232e104f4b0f6370a62eb33089e00024f2ce143aecbf9755649421c683"}, - {file = "fonttools-4.57.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39acf68abdfc74e19de7485f8f7396fa4d2418efea239b7061d6ed6a2510c746"}, - {file = "fonttools-4.57.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d077f909f2343daf4495ba22bb0e23b62886e8ec7c109ee8234bdbd678cf344"}, - {file = "fonttools-4.57.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:46370ac47a1e91895d40e9ad48effbe8e9d9db1a4b80888095bc00e7beaa042f"}, - {file = "fonttools-4.57.0-cp311-cp311-win32.whl", hash = "sha256:ca2aed95855506b7ae94e8f1f6217b7673c929e4f4f1217bcaa236253055cb36"}, - {file = "fonttools-4.57.0-cp311-cp311-win_amd64.whl", hash = "sha256:17168a4670bbe3775f3f3f72d23ee786bd965395381dfbb70111e25e81505b9d"}, - {file = "fonttools-4.57.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:889e45e976c74abc7256d3064aa7c1295aa283c6bb19810b9f8b604dfe5c7f31"}, - {file = "fonttools-4.57.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0425c2e052a5f1516c94e5855dbda706ae5a768631e9fcc34e57d074d1b65b92"}, - {file = "fonttools-4.57.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44c26a311be2ac130f40a96769264809d3b0cb297518669db437d1cc82974888"}, - {file = "fonttools-4.57.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84c41ba992df5b8d680b89fd84c6a1f2aca2b9f1ae8a67400c8930cd4ea115f6"}, - {file = "fonttools-4.57.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea1e9e43ca56b0c12440a7c689b1350066595bebcaa83baad05b8b2675129d98"}, - {file = "fonttools-4.57.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:84fd56c78d431606332a0627c16e2a63d243d0d8b05521257d77c6529abe14d8"}, - {file = "fonttools-4.57.0-cp312-cp312-win32.whl", hash = "sha256:f4376819c1c778d59e0a31db5dc6ede854e9edf28bbfa5b756604727f7f800ac"}, - {file = "fonttools-4.57.0-cp312-cp312-win_amd64.whl", hash = "sha256:57e30241524879ea10cdf79c737037221f77cc126a8cdc8ff2c94d4a522504b9"}, - {file = "fonttools-4.57.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:408ce299696012d503b714778d89aa476f032414ae57e57b42e4b92363e0b8ef"}, - {file = "fonttools-4.57.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bbceffc80aa02d9e8b99f2a7491ed8c4a783b2fc4020119dc405ca14fb5c758c"}, - {file = "fonttools-4.57.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f022601f3ee9e1f6658ed6d184ce27fa5216cee5b82d279e0f0bde5deebece72"}, - {file = "fonttools-4.57.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dea5893b58d4637ffa925536462ba626f8a1b9ffbe2f5c272cdf2c6ebadb817"}, - {file = "fonttools-4.57.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dff02c5c8423a657c550b48231d0a48d7e2b2e131088e55983cfe74ccc2c7cc9"}, - {file = "fonttools-4.57.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:767604f244dc17c68d3e2dbf98e038d11a18abc078f2d0f84b6c24571d9c0b13"}, - {file = "fonttools-4.57.0-cp313-cp313-win32.whl", hash = "sha256:8e2e12d0d862f43d51e5afb8b9751c77e6bec7d2dc00aad80641364e9df5b199"}, - {file = "fonttools-4.57.0-cp313-cp313-win_amd64.whl", hash = "sha256:f1d6bc9c23356908db712d282acb3eebd4ae5ec6d8b696aa40342b1d84f8e9e3"}, - {file = "fonttools-4.57.0-py3-none-any.whl", hash = "sha256:3122c604a675513c68bd24c6a8f9091f1c2376d18e8f5fe5a101746c81b3e98f"}, - {file = "fonttools-4.57.0.tar.gz", hash = "sha256:727ece10e065be2f9dd239d15dd5d60a66e17eac11aea47d447f9f03fdbc42de"}, -] - -[[package]] -name = "fqdn" -version = "1.5.1" -requires_python = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4, <4" -summary = "Validates fully-qualified domain names against RFC 1123, so that they are acceptable to modern bowsers" -groups = ["dev"] -dependencies = [ - "cached-property>=1.3.0; python_version < \"3.8\"", -] -files = [ - {file = "fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014"}, - {file = "fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f"}, -] - -[[package]] -name = "frozenlist" -version = "1.6.0" -requires_python = ">=3.9" -summary = "A list-like structure which implements collections.abc.MutableSequence" -groups = ["dev", "test"] -files = [ - {file = "frozenlist-1.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e6e558ea1e47fd6fa8ac9ccdad403e5dd5ecc6ed8dda94343056fa4277d5c65e"}, - {file = "frozenlist-1.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4b3cd7334a4bbc0c472164f3744562cb72d05002cc6fcf58adb104630bbc352"}, - {file = "frozenlist-1.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9799257237d0479736e2b4c01ff26b5c7f7694ac9692a426cb717f3dc02fff9b"}, - {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a7bb0fe1f7a70fb5c6f497dc32619db7d2cdd53164af30ade2f34673f8b1fc"}, - {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:36d2fc099229f1e4237f563b2a3e0ff7ccebc3999f729067ce4e64a97a7f2869"}, - {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f27a9f9a86dcf00708be82359db8de86b80d029814e6693259befe82bb58a106"}, - {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75ecee69073312951244f11b8627e3700ec2bfe07ed24e3a685a5979f0412d24"}, - {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2c7d5aa19714b1b01a0f515d078a629e445e667b9da869a3cd0e6fe7dec78bd"}, - {file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69bbd454f0fb23b51cadc9bdba616c9678e4114b6f9fa372d462ff2ed9323ec8"}, - {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7daa508e75613809c7a57136dec4871a21bca3080b3a8fc347c50b187df4f00c"}, - {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:89ffdb799154fd4d7b85c56d5fa9d9ad48946619e0eb95755723fffa11022d75"}, - {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:920b6bd77d209931e4c263223381d63f76828bec574440f29eb497cf3394c249"}, - {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d3ceb265249fb401702fce3792e6b44c1166b9319737d21495d3611028d95769"}, - {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:52021b528f1571f98a7d4258c58aa8d4b1a96d4f01d00d51f1089f2e0323cb02"}, - {file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0f2ca7810b809ed0f1917293050163c7654cefc57a49f337d5cd9de717b8fad3"}, - {file = "frozenlist-1.6.0-cp310-cp310-win32.whl", hash = "sha256:0e6f8653acb82e15e5443dba415fb62a8732b68fe09936bb6d388c725b57f812"}, - {file = "frozenlist-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f1a39819a5a3e84304cd286e3dc62a549fe60985415851b3337b6f5cc91907f1"}, - {file = "frozenlist-1.6.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae8337990e7a45683548ffb2fee1af2f1ed08169284cd829cdd9a7fa7470530d"}, - {file = "frozenlist-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8c952f69dd524558694818a461855f35d36cc7f5c0adddce37e962c85d06eac0"}, - {file = "frozenlist-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f5fef13136c4e2dee91bfb9a44e236fff78fc2cd9f838eddfc470c3d7d90afe"}, - {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:716bbba09611b4663ecbb7cd022f640759af8259e12a6ca939c0a6acd49eedba"}, - {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7b8c4dc422c1a3ffc550b465090e53b0bf4839047f3e436a34172ac67c45d595"}, - {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b11534872256e1666116f6587a1592ef395a98b54476addb5e8d352925cb5d4a"}, - {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c6eceb88aaf7221f75be6ab498dc622a151f5f88d536661af3ffc486245a626"}, - {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62c828a5b195570eb4b37369fcbbd58e96c905768d53a44d13044355647838ff"}, - {file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1c6bd2c6399920c9622362ce95a7d74e7f9af9bfec05fff91b8ce4b9647845a"}, - {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:49ba23817781e22fcbd45fd9ff2b9b8cdb7b16a42a4851ab8025cae7b22e96d0"}, - {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:431ef6937ae0f853143e2ca67d6da76c083e8b1fe3df0e96f3802fd37626e606"}, - {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9d124b38b3c299ca68433597ee26b7819209cb8a3a9ea761dfe9db3a04bba584"}, - {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:118e97556306402e2b010da1ef21ea70cb6d6122e580da64c056b96f524fbd6a"}, - {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fb3b309f1d4086b5533cf7bbcf3f956f0ae6469664522f1bde4feed26fba60f1"}, - {file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54dece0d21dce4fdb188a1ffc555926adf1d1c516e493c2914d7c370e454bc9e"}, - {file = "frozenlist-1.6.0-cp311-cp311-win32.whl", hash = "sha256:654e4ba1d0b2154ca2f096bed27461cf6160bc7f504a7f9a9ef447c293caf860"}, - {file = "frozenlist-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e911391bffdb806001002c1f860787542f45916c3baf764264a52765d5a5603"}, - {file = "frozenlist-1.6.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c5b9e42ace7d95bf41e19b87cec8f262c41d3510d8ad7514ab3862ea2197bfb1"}, - {file = "frozenlist-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ca9973735ce9f770d24d5484dcb42f68f135351c2fc81a7a9369e48cf2998a29"}, - {file = "frozenlist-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ac40ec76041c67b928ca8aaffba15c2b2ee3f5ae8d0cb0617b5e63ec119ca25"}, - {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b7a8a3180dfb280eb044fdec562f9b461614c0ef21669aea6f1d3dac6ee576"}, - {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c444d824e22da6c9291886d80c7d00c444981a72686e2b59d38b285617cb52c8"}, - {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb52c8166499a8150bfd38478248572c924c003cbb45fe3bcd348e5ac7c000f9"}, - {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b35298b2db9c2468106278537ee529719228950a5fdda686582f68f247d1dc6e"}, - {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d108e2d070034f9d57210f22fefd22ea0d04609fc97c5f7f5a686b3471028590"}, - {file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e1be9111cb6756868ac242b3c2bd1f09d9aea09846e4f5c23715e7afb647103"}, - {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:94bb451c664415f02f07eef4ece976a2c65dcbab9c2f1705b7031a3a75349d8c"}, - {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d1a686d0b0949182b8faddea596f3fc11f44768d1f74d4cad70213b2e139d821"}, - {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ea8e59105d802c5a38bdbe7362822c522230b3faba2aa35c0fa1765239b7dd70"}, - {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:abc4e880a9b920bc5020bf6a431a6bb40589d9bca3975c980495f63632e8382f"}, - {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9a79713adfe28830f27a3c62f6b5406c37376c892b05ae070906f07ae4487046"}, - {file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a0318c2068e217a8f5e3b85e35899f5a19e97141a45bb925bb357cfe1daf770"}, - {file = "frozenlist-1.6.0-cp312-cp312-win32.whl", hash = "sha256:853ac025092a24bb3bf09ae87f9127de9fe6e0c345614ac92536577cf956dfcc"}, - {file = "frozenlist-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:2bdfe2d7e6c9281c6e55523acd6c2bf77963cb422fdc7d142fb0cb6621b66878"}, - {file = "frozenlist-1.6.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d7fb014fe0fbfee3efd6a94fc635aeaa68e5e1720fe9e57357f2e2c6e1a647e"}, - {file = "frozenlist-1.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01bcaa305a0fdad12745502bfd16a1c75b14558dabae226852f9159364573117"}, - {file = "frozenlist-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b314faa3051a6d45da196a2c495e922f987dc848e967d8cfeaee8a0328b1cd4"}, - {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da62fecac21a3ee10463d153549d8db87549a5e77eefb8c91ac84bb42bb1e4e3"}, - {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1eb89bf3454e2132e046f9599fbcf0a4483ed43b40f545551a39316d0201cd1"}, - {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18689b40cb3936acd971f663ccb8e2589c45db5e2c5f07e0ec6207664029a9c"}, - {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e67ddb0749ed066b1a03fba812e2dcae791dd50e5da03be50b6a14d0c1a9ee45"}, - {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc5e64626e6682638d6e44398c9baf1d6ce6bc236d40b4b57255c9d3f9761f1f"}, - {file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:437cfd39564744ae32ad5929e55b18ebd88817f9180e4cc05e7d53b75f79ce85"}, - {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62dd7df78e74d924952e2feb7357d826af8d2f307557a779d14ddf94d7311be8"}, - {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a66781d7e4cddcbbcfd64de3d41a61d6bdde370fc2e38623f30b2bd539e84a9f"}, - {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:482fe06e9a3fffbcd41950f9d890034b4a54395c60b5e61fae875d37a699813f"}, - {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e4f9373c500dfc02feea39f7a56e4f543e670212102cc2eeb51d3a99c7ffbde6"}, - {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e69bb81de06827147b7bfbaeb284d85219fa92d9f097e32cc73675f279d70188"}, - {file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7613d9977d2ab4a9141dde4a149f4357e4065949674c5649f920fec86ecb393e"}, - {file = "frozenlist-1.6.0-cp313-cp313-win32.whl", hash = "sha256:4def87ef6d90429f777c9d9de3961679abf938cb6b7b63d4a7eb8a268babfce4"}, - {file = "frozenlist-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:37a8a52c3dfff01515e9bbbee0e6063181362f9de3db2ccf9bc96189b557cbfd"}, - {file = "frozenlist-1.6.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:46138f5a0773d064ff663d273b309b696293d7a7c00a0994c5c13a5078134b64"}, - {file = "frozenlist-1.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f88bc0a2b9c2a835cb888b32246c27cdab5740059fb3688852bf91e915399b91"}, - {file = "frozenlist-1.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:777704c1d7655b802c7850255639672e90e81ad6fa42b99ce5ed3fbf45e338dd"}, - {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ef8d41764c7de0dcdaf64f733a27352248493a85a80661f3c678acd27e31f2"}, - {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:da5cb36623f2b846fb25009d9d9215322318ff1c63403075f812b3b2876c8506"}, - {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cbb56587a16cf0fb8acd19e90ff9924979ac1431baea8681712716a8337577b0"}, - {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6154c3ba59cda3f954c6333025369e42c3acd0c6e8b6ce31eb5c5b8116c07e0"}, - {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e8246877afa3f1ae5c979fe85f567d220f86a50dc6c493b9b7d8191181ae01e"}, - {file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0f6cce16306d2e117cf9db71ab3a9e8878a28176aeaf0dbe35248d97b28d0c"}, - {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1b8e8cd8032ba266f91136d7105706ad57770f3522eac4a111d77ac126a25a9b"}, - {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e2ada1d8515d3ea5378c018a5f6d14b4994d4036591a52ceaf1a1549dec8e1ad"}, - {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:cdb2c7f071e4026c19a3e32b93a09e59b12000751fc9b0b7758da899e657d215"}, - {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:03572933a1969a6d6ab509d509e5af82ef80d4a5d4e1e9f2e1cdd22c77a3f4d2"}, - {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:77effc978947548b676c54bbd6a08992759ea6f410d4987d69feea9cd0919911"}, - {file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a2bda8be77660ad4089caf2223fdbd6db1858462c4b85b67fbfa22102021e497"}, - {file = "frozenlist-1.6.0-cp313-cp313t-win32.whl", hash = "sha256:a4d96dc5bcdbd834ec6b0f91027817214216b5b30316494d2b1aebffb87c534f"}, - {file = "frozenlist-1.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e18036cb4caa17ea151fd5f3d70be9d354c99eb8cf817a3ccde8a7873b074348"}, - {file = "frozenlist-1.6.0-py3-none-any.whl", hash = "sha256:535eec9987adb04701266b92745d6cdcef2e77669299359c3009c3404dd5d191"}, - {file = "frozenlist-1.6.0.tar.gz", hash = "sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68"}, -] - -[[package]] -name = "fsspec" -version = "2025.3.2" -requires_python = ">=3.9" -summary = "File-system specification" -groups = ["dev"] -files = [ - {file = "fsspec-2025.3.2-py3-none-any.whl", hash = "sha256:2daf8dc3d1dfa65b6aa37748d112773a7a08416f6c70d96b264c96476ecaf711"}, - {file = "fsspec-2025.3.2.tar.gz", hash = "sha256:e52c77ef398680bbd6a98c0e628fbc469491282981209907bbc8aea76a04fdc6"}, -] - -[[package]] -name = "ghp-import" -version = "2.1.0" -summary = "Copy your docs directly to the gh-pages branch." -groups = ["docs"] -dependencies = [ - "python-dateutil>=2.8.1", -] -files = [ - {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, - {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, -] - -[[package]] -name = "gradio" -version = "5.27.0" -requires_python = ">=3.10" -summary = "Python library for easily interacting with trained machine learning models" -groups = ["dev"] -dependencies = [ - "aiofiles<25.0,>=22.0", - "anyio<5.0,>=3.0", - "audioop-lts<1.0; python_version >= \"3.13\"", - "fastapi<1.0,>=0.115.2", - "ffmpy", - "gradio-client==1.9.0", - "groovy~=0.1", - "httpx>=0.24.1", - "huggingface-hub>=0.28.1", - "jinja2<4.0", - "markupsafe<4.0,>=2.0", - "numpy<3.0,>=1.0", - "orjson~=3.0", - "packaging", - "pandas<3.0,>=1.0", - "pillow<12.0,>=8.0", - "pydantic<2.12,>=2.0", - "pydub", - "python-multipart>=0.0.18", - "pyyaml<7.0,>=5.0", - "ruff>=0.9.3; sys_platform != \"emscripten\"", - "safehttpx<0.2.0,>=0.1.6", - "semantic-version~=2.0", - "starlette<1.0,>=0.40.0; sys_platform != \"emscripten\"", - "tomlkit<0.14.0,>=0.12.0", - "typer<1.0,>=0.12; sys_platform != \"emscripten\"", - "typing-extensions~=4.0", - "urllib3~=2.0; sys_platform == \"emscripten\"", - "uvicorn>=0.14.0; sys_platform != \"emscripten\"", -] -files = [ - {file = "gradio-5.27.0-py3-none-any.whl", hash = "sha256:68e5873204168c1e0025e1c6dda0adfc7546b2d906a79ad8becfd9864f719cca"}, - {file = "gradio-5.27.0.tar.gz", hash = "sha256:577ee37a94d7d1de8a76c52b1386654d4f9a20694ed9a3ad8238ef90bf5da6e9"}, -] - -[[package]] -name = "gradio-client" -version = "1.9.0" -requires_python = ">=3.10" -summary = "Python library for easily interacting with trained machine learning models" -groups = ["dev"] -dependencies = [ - "fsspec", - "httpx>=0.24.1", - "huggingface-hub>=0.19.3", - "packaging", - "typing-extensions~=4.0", - "websockets<16.0,>=10.0", -] -files = [ - {file = "gradio_client-1.9.0-py3-none-any.whl", hash = "sha256:192a046ed2cff9dfafb40e3b04899ecc1dc16f3ed994119d2b90c6a405c38d3e"}, - {file = "gradio_client-1.9.0.tar.gz", hash = "sha256:eeecec06e4509c32eab5841ba31d34146f50e3f8e0d3fcd3700a8a8f204cc989"}, -] - -[[package]] -name = "groovy" -version = "0.1.2" -requires_python = ">3.9" -summary = "A small Python library created to help developers protect their applications from Server Side Request Forgery (SSRF) attacks." -groups = ["dev"] -files = [ - {file = "groovy-0.1.2-py3-none-any.whl", hash = "sha256:7f7975bab18c729a257a8b1ae9dcd70b7cafb1720481beae47719af57c35fa64"}, - {file = "groovy-0.1.2.tar.gz", hash = "sha256:25c1dc09b3f9d7e292458aa762c6beb96ea037071bf5e917fc81fb78d2231083"}, -] - -[[package]] -name = "groq" -version = "0.4.2" -requires_python = ">=3.7" -summary = "The official Python library for the groq API" -groups = ["dev"] -dependencies = [ - "anyio<5,>=3.5.0", - "cached-property; python_version < \"3.8\"", - "distro<2,>=1.7.0", - "httpx<1,>=0.23.0", - "pydantic<3,>=1.9.0", - "sniffio", - "typing-extensions<5,>=4.7", -] -files = [ - {file = "groq-0.4.2-py3-none-any.whl", hash = "sha256:5b2b472c64d9f35210e0487db465415d47162da3a114031ecbfc8843d26302a5"}, - {file = "groq-0.4.2.tar.gz", hash = "sha256:42e8b0abd0f2b2da024b9a747d28960d62951a5364f078e1537c9fceeca8259d"}, -] - -[[package]] -name = "h11" -version = "0.16.0" -requires_python = ">=3.8" -summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -groups = ["dev"] -files = [ - {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, - {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -requires_python = ">=3.8" -summary = "A minimal low-level HTTP client." -groups = ["dev"] -dependencies = [ - "certifi", - "h11>=0.16", -] -files = [ - {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, - {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, -] - -[[package]] -name = "httptools" -version = "0.6.4" -requires_python = ">=3.8.0" -summary = "A collection of framework independent HTTP protocol utils." -groups = ["dev"] -files = [ - {file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"}, - {file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"}, - {file = "httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1"}, - {file = "httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50"}, - {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959"}, - {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4"}, - {file = "httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c"}, - {file = "httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069"}, - {file = "httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a"}, - {file = "httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975"}, - {file = "httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636"}, - {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721"}, - {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988"}, - {file = "httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17"}, - {file = "httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2"}, - {file = "httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44"}, - {file = "httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1"}, - {file = "httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2"}, - {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81"}, - {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f"}, - {file = "httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970"}, - {file = "httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660"}, - {file = "httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083"}, - {file = "httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3"}, - {file = "httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071"}, - {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5"}, - {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0"}, - {file = "httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8"}, - {file = "httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c"}, -] - -[[package]] -name = "httpx" -version = "0.28.1" -requires_python = ">=3.8" -summary = "The next generation HTTP client." -groups = ["dev"] -dependencies = [ - "anyio", - "certifi", - "httpcore==1.*", - "idna", -] -files = [ - {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, - {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, -] - -[[package]] -name = "httpx-sse" -version = "0.4.0" -requires_python = ">=3.8" -summary = "Consume Server-Sent Event (SSE) messages with HTTPX." -groups = ["dev"] -files = [ - {file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"}, - {file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"}, -] - -[[package]] -name = "huggingface-hub" -version = "0.30.2" -requires_python = ">=3.8.0" -summary = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" -groups = ["dev"] -dependencies = [ - "filelock", - "fsspec>=2023.5.0", - "packaging>=20.9", - "pyyaml>=5.1", - "requests", - "tqdm>=4.42.1", - "typing-extensions>=3.7.4.3", -] -files = [ - {file = "huggingface_hub-0.30.2-py3-none-any.whl", hash = "sha256:68ff05969927058cfa41df4f2155d4bb48f5f54f719dd0390103eefa9b191e28"}, - {file = "huggingface_hub-0.30.2.tar.gz", hash = "sha256:9a7897c5b6fd9dad3168a794a8998d6378210f5b9688d0dfc180b1a228dc2466"}, -] - -[[package]] -name = "idna" -version = "3.10" -requires_python = ">=3.6" -summary = "Internationalized Domain Names in Applications (IDNA)" -groups = ["dev", "docs", "test"] -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] - -[[package]] -name = "imageio" -version = "2.37.0" -requires_python = ">=3.9" -summary = "Library for reading and writing a wide range of image, video, scientific, and volumetric data formats." -groups = ["dev"] -dependencies = [ - "numpy", - "pillow>=8.3.2", -] -files = [ - {file = "imageio-2.37.0-py3-none-any.whl", hash = "sha256:11efa15b87bc7871b61590326b2d635439acc321cf7f8ce996f812543ce10eed"}, - {file = "imageio-2.37.0.tar.gz", hash = "sha256:71b57b3669666272c818497aebba2b4c5f20d5b37c81720e5e1a56d59c492996"}, -] - -[[package]] -name = "iniconfig" -version = "2.1.0" -requires_python = ">=3.8" -summary = "brain-dead simple config-ini parsing" -groups = ["test"] -files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, -] - -[[package]] -name = "ipykernel" -version = "6.29.5" -requires_python = ">=3.8" -summary = "IPython Kernel for Jupyter" -groups = ["dev"] -dependencies = [ - "appnope; platform_system == \"Darwin\"", - "comm>=0.1.1", - "debugpy>=1.6.5", - "ipython>=7.23.1", - "jupyter-client>=6.1.12", - "jupyter-core!=5.0.*,>=4.12", - "matplotlib-inline>=0.1", - "nest-asyncio", - "packaging", - "psutil", - "pyzmq>=24", - "tornado>=6.1", - "traitlets>=5.4.0", -] -files = [ - {file = "ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5"}, - {file = "ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215"}, -] - -[[package]] -name = "ipython" -version = "8.36.0" -requires_python = ">=3.10" -summary = "IPython: Productive Interactive Computing" -groups = ["dev"] -dependencies = [ - "colorama; sys_platform == \"win32\"", - "decorator", - "exceptiongroup; python_version < \"3.11\"", - "jedi>=0.16", - "matplotlib-inline", - "pexpect>4.3; sys_platform != \"win32\" and sys_platform != \"emscripten\"", - "prompt-toolkit<3.1.0,>=3.0.41", - "pygments>=2.4.0", - "stack-data", - "traitlets>=5.13.0", - "typing-extensions>=4.6; python_version < \"3.12\"", -] -files = [ - {file = "ipython-8.36.0-py3-none-any.whl", hash = "sha256:12b913914d010dcffa2711505ec8be4bf0180742d97f1e5175e51f22086428c1"}, - {file = "ipython-8.36.0.tar.gz", hash = "sha256:24658e9fe5c5c819455043235ba59cfffded4a35936eefceceab6b192f7092ff"}, -] - -[[package]] -name = "ipywidgets" -version = "8.1.6" -requires_python = ">=3.7" -summary = "Jupyter interactive widgets" -groups = ["dev"] -dependencies = [ - "comm>=0.1.3", - "ipython>=6.1.0", - "jupyterlab-widgets~=3.0.14", - "traitlets>=4.3.1", - "widgetsnbextension~=4.0.14", -] -files = [ - {file = "ipywidgets-8.1.6-py3-none-any.whl", hash = "sha256:446e7630a1d025bdc7635e1169fcc06f2ce33b5bd41c2003edeb4a47c8d4bbb1"}, - {file = "ipywidgets-8.1.6.tar.gz", hash = "sha256:d8ace49c66f14419fc66071371b99d01bed230bbc15d8a60233b18bfbd782851"}, -] - -[[package]] -name = "isoduration" -version = "20.11.0" -requires_python = ">=3.7" -summary = "Operations with ISO 8601 durations" -groups = ["dev"] -dependencies = [ - "arrow>=0.15.0", -] -files = [ - {file = "isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042"}, - {file = "isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9"}, -] - -[[package]] -name = "jedi" -version = "0.19.2" -requires_python = ">=3.6" -summary = "An autocompletion tool for Python that can be used for text editors." -groups = ["dev"] -dependencies = [ - "parso<0.9.0,>=0.8.4", -] -files = [ - {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, - {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -requires_python = ">=3.7" -summary = "A very fast and expressive template engine." -groups = ["dev", "docs"] -dependencies = [ - "MarkupSafe>=2.0", -] -files = [ - {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, - {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, -] - -[[package]] -name = "jiter" -version = "0.9.0" -requires_python = ">=3.8" -summary = "Fast iterable JSON parser." -groups = ["dev"] -files = [ - {file = "jiter-0.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:816ec9b60fdfd1fec87da1d7ed46c66c44ffec37ab2ef7de5b147b2fce3fd5ad"}, - {file = "jiter-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b1d3086f8a3ee0194ecf2008cf81286a5c3e540d977fa038ff23576c023c0ea"}, - {file = "jiter-0.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1339f839b91ae30b37c409bf16ccd3dc453e8b8c3ed4bd1d6a567193651a4a51"}, - {file = "jiter-0.9.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ffba79584b3b670fefae66ceb3a28822365d25b7bf811e030609a3d5b876f538"}, - {file = "jiter-0.9.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cfc7d0a8e899089d11f065e289cb5b2daf3d82fbe028f49b20d7b809193958d"}, - {file = "jiter-0.9.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e00a1a2bbfaaf237e13c3d1592356eab3e9015d7efd59359ac8b51eb56390a12"}, - {file = "jiter-0.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1d9870561eb26b11448854dce0ff27a9a27cb616b632468cafc938de25e9e51"}, - {file = "jiter-0.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9872aeff3f21e437651df378cb75aeb7043e5297261222b6441a620218b58708"}, - {file = "jiter-0.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1fd19112d1049bdd47f17bfbb44a2c0001061312dcf0e72765bfa8abd4aa30e5"}, - {file = "jiter-0.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6ef5da104664e526836070e4a23b5f68dec1cc673b60bf1edb1bfbe8a55d0678"}, - {file = "jiter-0.9.0-cp310-cp310-win32.whl", hash = "sha256:cb12e6d65ebbefe5518de819f3eda53b73187b7089040b2d17f5b39001ff31c4"}, - {file = "jiter-0.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:c43ca669493626d8672be3b645dbb406ef25af3f4b6384cfd306da7eb2e70322"}, - {file = "jiter-0.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6c4d99c71508912a7e556d631768dcdef43648a93660670986916b297f1c54af"}, - {file = "jiter-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f60fb8ce7df529812bf6c625635a19d27f30806885139e367af93f6e734ef58"}, - {file = "jiter-0.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51c4e1a4f8ea84d98b7b98912aa4290ac3d1eabfde8e3c34541fae30e9d1f08b"}, - {file = "jiter-0.9.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f4c677c424dc76684fea3e7285a7a2a7493424bea89ac441045e6a1fb1d7b3b"}, - {file = "jiter-0.9.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2221176dfec87f3470b21e6abca056e6b04ce9bff72315cb0b243ca9e835a4b5"}, - {file = "jiter-0.9.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c7adb66f899ffa25e3c92bfcb593391ee1947dbdd6a9a970e0d7e713237d572"}, - {file = "jiter-0.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c98d27330fdfb77913c1097a7aab07f38ff2259048949f499c9901700789ac15"}, - {file = "jiter-0.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eda3f8cc74df66892b1d06b5d41a71670c22d95a1ca2cbab73654745ce9d0419"}, - {file = "jiter-0.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dd5ab5ddc11418dce28343123644a100f487eaccf1de27a459ab36d6cca31043"}, - {file = "jiter-0.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:42f8a68a69f047b310319ef8e2f52fdb2e7976fb3313ef27df495cf77bcad965"}, - {file = "jiter-0.9.0-cp311-cp311-win32.whl", hash = "sha256:a25519efb78a42254d59326ee417d6f5161b06f5da827d94cf521fed961b1ff2"}, - {file = "jiter-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:923b54afdd697dfd00d368b7ccad008cccfeb1efb4e621f32860c75e9f25edbd"}, - {file = "jiter-0.9.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7b46249cfd6c48da28f89eb0be3f52d6fdb40ab88e2c66804f546674e539ec11"}, - {file = "jiter-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:609cf3c78852f1189894383cf0b0b977665f54cb38788e3e6b941fa6d982c00e"}, - {file = "jiter-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d726a3890a54561e55a9c5faea1f7655eda7f105bd165067575ace6e65f80bb2"}, - {file = "jiter-0.9.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2e89dc075c1fef8fa9be219e249f14040270dbc507df4215c324a1839522ea75"}, - {file = "jiter-0.9.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04e8ffa3c353b1bc4134f96f167a2082494351e42888dfcf06e944f2729cbe1d"}, - {file = "jiter-0.9.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:203f28a72a05ae0e129b3ed1f75f56bc419d5f91dfacd057519a8bd137b00c42"}, - {file = "jiter-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fca1a02ad60ec30bb230f65bc01f611c8608b02d269f998bc29cca8619a919dc"}, - {file = "jiter-0.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:237e5cee4d5d2659aaf91bbf8ec45052cc217d9446070699441a91b386ae27dc"}, - {file = "jiter-0.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:528b6b71745e7326eed73c53d4aa57e2a522242320b6f7d65b9c5af83cf49b6e"}, - {file = "jiter-0.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9f48e86b57bc711eb5acdfd12b6cb580a59cc9a993f6e7dcb6d8b50522dcd50d"}, - {file = "jiter-0.9.0-cp312-cp312-win32.whl", hash = "sha256:699edfde481e191d81f9cf6d2211debbfe4bd92f06410e7637dffb8dd5dfde06"}, - {file = "jiter-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:099500d07b43f61d8bd780466d429c45a7b25411b334c60ca875fa775f68ccb0"}, - {file = "jiter-0.9.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2764891d3f3e8b18dce2cff24949153ee30c9239da7c00f032511091ba688ff7"}, - {file = "jiter-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:387b22fbfd7a62418d5212b4638026d01723761c75c1c8232a8b8c37c2f1003b"}, - {file = "jiter-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d8da8629ccae3606c61d9184970423655fb4e33d03330bcdfe52d234d32f69"}, - {file = "jiter-0.9.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1be73d8982bdc278b7b9377426a4b44ceb5c7952073dd7488e4ae96b88e1103"}, - {file = "jiter-0.9.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2228eaaaa111ec54b9e89f7481bffb3972e9059301a878d085b2b449fbbde635"}, - {file = "jiter-0.9.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:11509bfecbc319459647d4ac3fd391d26fdf530dad00c13c4dadabf5b81f01a4"}, - {file = "jiter-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f22238da568be8bbd8e0650e12feeb2cfea15eda4f9fc271d3b362a4fa0604d"}, - {file = "jiter-0.9.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17f5d55eb856597607562257c8e36c42bc87f16bef52ef7129b7da11afc779f3"}, - {file = "jiter-0.9.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6a99bed9fbb02f5bed416d137944419a69aa4c423e44189bc49718859ea83bc5"}, - {file = "jiter-0.9.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e057adb0cd1bd39606100be0eafe742de2de88c79df632955b9ab53a086b3c8d"}, - {file = "jiter-0.9.0-cp313-cp313-win32.whl", hash = "sha256:f7e6850991f3940f62d387ccfa54d1a92bd4bb9f89690b53aea36b4364bcab53"}, - {file = "jiter-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:c8ae3bf27cd1ac5e6e8b7a27487bf3ab5f82318211ec2e1346a5b058756361f7"}, - {file = "jiter-0.9.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f0b2827fb88dda2cbecbbc3e596ef08d69bda06c6f57930aec8e79505dc17001"}, - {file = "jiter-0.9.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:062b756ceb1d40b0b28f326cba26cfd575a4918415b036464a52f08632731e5a"}, - {file = "jiter-0.9.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6f7838bc467ab7e8ef9f387bd6de195c43bad82a569c1699cb822f6609dd4cdf"}, - {file = "jiter-0.9.0.tar.gz", hash = "sha256:aadba0964deb424daa24492abc3d229c60c4a31bfee205aedbf1acc7639d7893"}, -] - -[[package]] -name = "jmespath" -version = "1.0.1" -requires_python = ">=3.7" -summary = "JSON Matching Expressions" -groups = ["dev"] -files = [ - {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, - {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, -] - -[[package]] -name = "json5" -version = "0.12.0" -requires_python = ">=3.8.0" -summary = "A Python implementation of the JSON5 data format." -groups = ["dev"] -files = [ - {file = "json5-0.12.0-py3-none-any.whl", hash = "sha256:6d37aa6c08b0609f16e1ec5ff94697e2cbbfbad5ac112afa05794da9ab7810db"}, - {file = "json5-0.12.0.tar.gz", hash = "sha256:0b4b6ff56801a1c7dc817b0241bca4ce474a0e6a163bfef3fc594d3fd263ff3a"}, -] - -[[package]] -name = "jsonpointer" -version = "3.0.0" -requires_python = ">=3.7" -summary = "Identify specific nodes in a JSON document (RFC 6901) " -groups = ["dev"] -files = [ - {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, - {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, -] - -[[package]] -name = "jsonschema" -version = "4.23.0" -requires_python = ">=3.8" -summary = "An implementation of JSON Schema validation for Python" -groups = ["dev"] -dependencies = [ - "attrs>=22.2.0", - "importlib-resources>=1.4.0; python_version < \"3.9\"", - "jsonschema-specifications>=2023.03.6", - "pkgutil-resolve-name>=1.3.10; python_version < \"3.9\"", - "referencing>=0.28.4", - "rpds-py>=0.7.1", -] -files = [ - {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, - {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, -] - -[[package]] -name = "jsonschema-specifications" -version = "2025.4.1" -requires_python = ">=3.9" -summary = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" -groups = ["dev"] -dependencies = [ - "referencing>=0.31.0", -] -files = [ - {file = "jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af"}, - {file = "jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608"}, -] - -[[package]] -name = "jsonschema" -version = "4.23.0" -extras = ["format-nongpl"] -requires_python = ">=3.8" -summary = "An implementation of JSON Schema validation for Python" -groups = ["dev"] -dependencies = [ - "fqdn", - "idna", - "isoduration", - "jsonpointer>1.13", - "jsonschema==4.23.0", - "rfc3339-validator", - "rfc3986-validator>0.1.0", - "uri-template", - "webcolors>=24.6.0", -] -files = [ - {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, - {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, -] - -[[package]] -name = "jupyter" -version = "1.1.1" -summary = "Jupyter metapackage. Install all the Jupyter components in one go." -groups = ["dev"] -dependencies = [ - "ipykernel", - "ipywidgets", - "jupyter-console", - "jupyterlab", - "nbconvert", - "notebook", -] -files = [ - {file = "jupyter-1.1.1-py2.py3-none-any.whl", hash = "sha256:7a59533c22af65439b24bbe60373a4e95af8f16ac65a6c00820ad378e3f7cc83"}, - {file = "jupyter-1.1.1.tar.gz", hash = "sha256:d55467bceabdea49d7e3624af7e33d59c37fff53ed3a350e1ac957bed731de7a"}, -] - -[[package]] -name = "jupyter-client" -version = "8.6.3" -requires_python = ">=3.8" -summary = "Jupyter protocol implementation and client libraries" -groups = ["dev"] -dependencies = [ - "importlib-metadata>=4.8.3; python_version < \"3.10\"", - "jupyter-core!=5.0.*,>=4.12", - "python-dateutil>=2.8.2", - "pyzmq>=23.0", - "tornado>=6.2", - "traitlets>=5.3", -] -files = [ - {file = "jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f"}, - {file = "jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419"}, -] - -[[package]] -name = "jupyter-console" -version = "6.6.3" -requires_python = ">=3.7" -summary = "Jupyter terminal console" -groups = ["dev"] -dependencies = [ - "ipykernel>=6.14", - "ipython", - "jupyter-client>=7.0.0", - "jupyter-core!=5.0.*,>=4.12", - "prompt-toolkit>=3.0.30", - "pygments", - "pyzmq>=17", - "traitlets>=5.4", -] -files = [ - {file = "jupyter_console-6.6.3-py3-none-any.whl", hash = "sha256:309d33409fcc92ffdad25f0bcdf9a4a9daa61b6f341177570fdac03de5352485"}, - {file = "jupyter_console-6.6.3.tar.gz", hash = "sha256:566a4bf31c87adbfadf22cdf846e3069b59a71ed5da71d6ba4d8aaad14a53539"}, -] - -[[package]] -name = "jupyter-core" -version = "5.7.2" -requires_python = ">=3.8" -summary = "Jupyter core package. A base package on which Jupyter projects rely." -groups = ["dev"] -dependencies = [ - "platformdirs>=2.5", - "pywin32>=300; sys_platform == \"win32\" and platform_python_implementation != \"PyPy\"", - "traitlets>=5.3", -] -files = [ - {file = "jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409"}, - {file = "jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9"}, -] - -[[package]] -name = "jupyter-events" -version = "0.12.0" -requires_python = ">=3.9" -summary = "Jupyter Event System library" -groups = ["dev"] -dependencies = [ - "jsonschema[format-nongpl]>=4.18.0", - "packaging", - "python-json-logger>=2.0.4", - "pyyaml>=5.3", - "referencing", - "rfc3339-validator", - "rfc3986-validator>=0.1.1", - "traitlets>=5.3", -] -files = [ - {file = "jupyter_events-0.12.0-py3-none-any.whl", hash = "sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb"}, - {file = "jupyter_events-0.12.0.tar.gz", hash = "sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b"}, -] - -[[package]] -name = "jupyter-lsp" -version = "2.2.5" -requires_python = ">=3.8" -summary = "Multi-Language Server WebSocket proxy for Jupyter Notebook/Lab server" -groups = ["dev"] -dependencies = [ - "importlib-metadata>=4.8.3; python_version < \"3.10\"", - "jupyter-server>=1.1.2", -] -files = [ - {file = "jupyter-lsp-2.2.5.tar.gz", hash = "sha256:793147a05ad446f809fd53ef1cd19a9f5256fd0a2d6b7ce943a982cb4f545001"}, - {file = "jupyter_lsp-2.2.5-py3-none-any.whl", hash = "sha256:45fbddbd505f3fbfb0b6cb2f1bc5e15e83ab7c79cd6e89416b248cb3c00c11da"}, -] - -[[package]] -name = "jupyter-server" -version = "2.15.0" -requires_python = ">=3.9" -summary = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications." -groups = ["dev"] -dependencies = [ - "anyio>=3.1.0", - "argon2-cffi>=21.1", - "jinja2>=3.0.3", - "jupyter-client>=7.4.4", - "jupyter-core!=5.0.*,>=4.12", - "jupyter-events>=0.11.0", - "jupyter-server-terminals>=0.4.4", - "nbconvert>=6.4.4", - "nbformat>=5.3.0", - "overrides>=5.0", - "packaging>=22.0", - "prometheus-client>=0.9", - "pywinpty>=2.0.1; os_name == \"nt\"", - "pyzmq>=24", - "send2trash>=1.8.2", - "terminado>=0.8.3", - "tornado>=6.2.0", - "traitlets>=5.6.0", - "websocket-client>=1.7", -] -files = [ - {file = "jupyter_server-2.15.0-py3-none-any.whl", hash = "sha256:872d989becf83517012ee669f09604aa4a28097c0bd90b2f424310156c2cdae3"}, - {file = "jupyter_server-2.15.0.tar.gz", hash = "sha256:9d446b8697b4f7337a1b7cdcac40778babdd93ba614b6d68ab1c0c918f1c4084"}, -] - -[[package]] -name = "jupyter-server-terminals" -version = "0.5.3" -requires_python = ">=3.8" -summary = "A Jupyter Server Extension Providing Terminals." -groups = ["dev"] -dependencies = [ - "pywinpty>=2.0.3; os_name == \"nt\"", - "terminado>=0.8.3", -] -files = [ - {file = "jupyter_server_terminals-0.5.3-py3-none-any.whl", hash = "sha256:41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa"}, - {file = "jupyter_server_terminals-0.5.3.tar.gz", hash = "sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269"}, -] - -[[package]] -name = "jupyterlab" -version = "4.4.1" -requires_python = ">=3.9" -summary = "JupyterLab computational environment" -groups = ["dev"] -dependencies = [ - "async-lru>=1.0.0", - "httpx>=0.25.0", - "importlib-metadata>=4.8.3; python_version < \"3.10\"", - "ipykernel>=6.5.0", - "jinja2>=3.0.3", - "jupyter-core", - "jupyter-lsp>=2.0.0", - "jupyter-server<3,>=2.4.0", - "jupyterlab-server<3,>=2.27.1", - "notebook-shim>=0.2", - "packaging", - "setuptools>=41.1.0", - "tomli>=1.2.2; python_version < \"3.11\"", - "tornado>=6.2.0", - "traitlets", -] -files = [ - {file = "jupyterlab-4.4.1-py3-none-any.whl", hash = "sha256:989bca3f9cf2d04b2022e7e657e2df6d4aca808b364810d31c4865edd968a5f7"}, - {file = "jupyterlab-4.4.1.tar.gz", hash = "sha256:c75c4f33056fbd84f0b31eb44622a00c7a5f981b85adfeb198a83721f0465808"}, -] - -[[package]] -name = "jupyterlab-pygments" -version = "0.3.0" -requires_python = ">=3.8" -summary = "Pygments theme using JupyterLab CSS variables" -groups = ["dev"] -files = [ - {file = "jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780"}, - {file = "jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d"}, -] - -[[package]] -name = "jupyterlab-server" -version = "2.27.3" -requires_python = ">=3.8" -summary = "A set of server components for JupyterLab and JupyterLab like applications." -groups = ["dev"] -dependencies = [ - "babel>=2.10", - "importlib-metadata>=4.8.3; python_version < \"3.10\"", - "jinja2>=3.0.3", - "json5>=0.9.0", - "jsonschema>=4.18.0", - "jupyter-server<3,>=1.21", - "packaging>=21.3", - "requests>=2.31", -] -files = [ - {file = "jupyterlab_server-2.27.3-py3-none-any.whl", hash = "sha256:e697488f66c3db49df675158a77b3b017520d772c6e1548c7d9bcc5df7944ee4"}, - {file = "jupyterlab_server-2.27.3.tar.gz", hash = "sha256:eb36caca59e74471988f0ae25c77945610b887f777255aa21f8065def9e51ed4"}, -] - -[[package]] -name = "jupyterlab-widgets" -version = "3.0.14" -requires_python = ">=3.7" -summary = "Jupyter interactive widgets for JupyterLab" -groups = ["dev"] -files = [ - {file = "jupyterlab_widgets-3.0.14-py3-none-any.whl", hash = "sha256:54c33e3306b7fca139d165d6190dc6c0627aafa5d14adfc974a4e9a3d26cb703"}, - {file = "jupyterlab_widgets-3.0.14.tar.gz", hash = "sha256:bad03e59546869f026e537e0d170e454259e6dc7048e14041707ca31e523c8a1"}, -] - -[[package]] -name = "kiwisolver" -version = "1.4.8" -requires_python = ">=3.10" -summary = "A fast implementation of the Cassowary constraint solver" -groups = ["dev"] -files = [ - {file = "kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db"}, - {file = "kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b"}, - {file = "kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d"}, - {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d"}, - {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c"}, - {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3"}, - {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed"}, - {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f"}, - {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff"}, - {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d"}, - {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c"}, - {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605"}, - {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e"}, - {file = "kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751"}, - {file = "kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271"}, - {file = "kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84"}, - {file = "kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561"}, - {file = "kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7"}, - {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03"}, - {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954"}, - {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79"}, - {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6"}, - {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0"}, - {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab"}, - {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc"}, - {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25"}, - {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc"}, - {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67"}, - {file = "kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34"}, - {file = "kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2"}, - {file = "kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502"}, - {file = "kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31"}, - {file = "kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb"}, - {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f"}, - {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc"}, - {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a"}, - {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a"}, - {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a"}, - {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3"}, - {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b"}, - {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4"}, - {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d"}, - {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8"}, - {file = "kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50"}, - {file = "kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476"}, - {file = "kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09"}, - {file = "kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1"}, - {file = "kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c"}, - {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b"}, - {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47"}, - {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16"}, - {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc"}, - {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246"}, - {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794"}, - {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b"}, - {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3"}, - {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957"}, - {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb"}, - {file = "kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2"}, - {file = "kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85"}, - {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a"}, - {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8"}, - {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0"}, - {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c"}, - {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b"}, - {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b"}, - {file = "kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e"}, -] - -[[package]] -name = "lazy-loader" -version = "0.4" -requires_python = ">=3.7" -summary = "Makes it easy to load subpackages and functions on demand." -groups = ["dev"] -dependencies = [ - "importlib-metadata; python_version < \"3.8\"", - "packaging", -] -files = [ - {file = "lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc"}, - {file = "lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1"}, -] - -[[package]] -name = "markdown" -version = "3.8" -requires_python = ">=3.9" -summary = "Python implementation of John Gruber's Markdown." -groups = ["docs"] -dependencies = [ - "importlib-metadata>=4.4; python_version < \"3.10\"", -] -files = [ - {file = "markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc"}, - {file = "markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f"}, -] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -requires_python = ">=3.8" -summary = "Python port of markdown-it. Markdown parsing, done right!" -groups = ["dev"] -dependencies = [ - "mdurl~=0.1", -] -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[[package]] -name = "markupsafe" -version = "3.0.2" -requires_python = ">=3.9" -summary = "Safely add untrusted strings to HTML/XML markup." -groups = ["dev", "docs"] -files = [ - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, -] - -[[package]] -name = "matplotlib" -version = "3.10.1" -requires_python = ">=3.10" -summary = "Python plotting package" -groups = ["dev"] -dependencies = [ - "contourpy>=1.0.1", - "cycler>=0.10", - "fonttools>=4.22.0", - "kiwisolver>=1.3.1", - "numpy>=1.23", - "packaging>=20.0", - "pillow>=8", - "pyparsing>=2.3.1", - "python-dateutil>=2.7", -] -files = [ - {file = "matplotlib-3.10.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ff2ae14910be903f4a24afdbb6d7d3a6c44da210fc7d42790b87aeac92238a16"}, - {file = "matplotlib-3.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0721a3fd3d5756ed593220a8b86808a36c5031fce489adb5b31ee6dbb47dd5b2"}, - {file = "matplotlib-3.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0673b4b8f131890eb3a1ad058d6e065fb3c6e71f160089b65f8515373394698"}, - {file = "matplotlib-3.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e875b95ac59a7908978fe307ecdbdd9a26af7fa0f33f474a27fcf8c99f64a19"}, - {file = "matplotlib-3.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2589659ea30726284c6c91037216f64a506a9822f8e50592d48ac16a2f29e044"}, - {file = "matplotlib-3.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a97ff127f295817bc34517255c9db6e71de8eddaab7f837b7d341dee9f2f587f"}, - {file = "matplotlib-3.10.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:057206ff2d6ab82ff3e94ebd94463d084760ca682ed5f150817b859372ec4401"}, - {file = "matplotlib-3.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a144867dd6bf8ba8cb5fc81a158b645037e11b3e5cf8a50bd5f9917cb863adfe"}, - {file = "matplotlib-3.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56c5d9fcd9879aa8040f196a235e2dcbdf7dd03ab5b07c0696f80bc6cf04bedd"}, - {file = "matplotlib-3.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f69dc9713e4ad2fb21a1c30e37bd445d496524257dfda40ff4a8efb3604ab5c"}, - {file = "matplotlib-3.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c59af3e8aca75d7744b68e8e78a669e91ccbcf1ac35d0102a7b1b46883f1dd7"}, - {file = "matplotlib-3.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:11b65088c6f3dae784bc72e8d039a2580186285f87448babb9ddb2ad0082993a"}, - {file = "matplotlib-3.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:66e907a06e68cb6cfd652c193311d61a12b54f56809cafbed9736ce5ad92f107"}, - {file = "matplotlib-3.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b4bb156abb8fa5e5b2b460196f7db7264fc6d62678c03457979e7d5254b7be"}, - {file = "matplotlib-3.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1985ad3d97f51307a2cbfc801a930f120def19ba22864182dacef55277102ba6"}, - {file = "matplotlib-3.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c96f2c2f825d1257e437a1482c5a2cf4fee15db4261bd6fc0750f81ba2b4ba3d"}, - {file = "matplotlib-3.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35e87384ee9e488d8dd5a2dd7baf471178d38b90618d8ea147aced4ab59c9bea"}, - {file = "matplotlib-3.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:cfd414bce89cc78a7e1d25202e979b3f1af799e416010a20ab2b5ebb3a02425c"}, - {file = "matplotlib-3.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42eee41e1b60fd83ee3292ed83a97a5f2a8239b10c26715d8a6172226988d7b"}, - {file = "matplotlib-3.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4f0647b17b667ae745c13721602b540f7aadb2a32c5b96e924cd4fea5dcb90f1"}, - {file = "matplotlib-3.10.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa3854b5f9473564ef40a41bc922be978fab217776e9ae1545c9b3a5cf2092a3"}, - {file = "matplotlib-3.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e496c01441be4c7d5f96d4e40f7fca06e20dcb40e44c8daa2e740e1757ad9e6"}, - {file = "matplotlib-3.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d45d3f5245be5b469843450617dcad9af75ca50568acf59997bed9311131a0b"}, - {file = "matplotlib-3.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:8e8e25b1209161d20dfe93037c8a7f7ca796ec9aa326e6e4588d8c4a5dd1e473"}, - {file = "matplotlib-3.10.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:19b06241ad89c3ae9469e07d77efa87041eac65d78df4fcf9cac318028009b01"}, - {file = "matplotlib-3.10.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01e63101ebb3014e6e9f80d9cf9ee361a8599ddca2c3e166c563628b39305dbb"}, - {file = "matplotlib-3.10.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f06bad951eea6422ac4e8bdebcf3a70c59ea0a03338c5d2b109f57b64eb3972"}, - {file = "matplotlib-3.10.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfb036f34873b46978f55e240cff7a239f6c4409eac62d8145bad3fc6ba5a3"}, - {file = "matplotlib-3.10.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dc6ab14a7ab3b4d813b88ba957fc05c79493a037f54e246162033591e770de6f"}, - {file = "matplotlib-3.10.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bc411ebd5889a78dabbc457b3fa153203e22248bfa6eedc6797be5df0164dbf9"}, - {file = "matplotlib-3.10.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:648406f1899f9a818cef8c0231b44dcfc4ff36f167101c3fd1c9151f24220fdc"}, - {file = "matplotlib-3.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:02582304e352f40520727984a5a18f37e8187861f954fea9be7ef06569cf85b4"}, - {file = "matplotlib-3.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3809916157ba871bcdd33d3493acd7fe3037db5daa917ca6e77975a94cef779"}, - {file = "matplotlib-3.10.1.tar.gz", hash = "sha256:e8d2d0e3881b129268585bf4765ad3ee73a4591d77b9a18c214ac7e3a79fb2ba"}, -] - -[[package]] -name = "matplotlib-inline" -version = "0.1.7" -requires_python = ">=3.8" -summary = "Inline Matplotlib backend for Jupyter" -groups = ["dev"] -dependencies = [ - "traitlets", -] -files = [ - {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, - {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, -] - -[[package]] -name = "mcp" -version = "1.6.0" -requires_python = ">=3.10" -summary = "Model Context Protocol SDK" -groups = ["dev"] -dependencies = [ - "anyio>=4.5", - "httpx-sse>=0.4", - "httpx>=0.27", - "pydantic-settings>=2.5.2", - "pydantic<3.0.0,>=2.7.2", - "sse-starlette>=1.6.1", - "starlette>=0.27", - "uvicorn>=0.23.1", -] -files = [ - {file = "mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0"}, - {file = "mcp-1.6.0.tar.gz", hash = "sha256:d9324876de2c5637369f43161cd71eebfd803df5a95e46225cab8d280e366723"}, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -requires_python = ">=3.7" -summary = "Markdown URL utilities" -groups = ["dev"] -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "mergedeep" -version = "1.3.4" -requires_python = ">=3.6" -summary = "A deep merge function for 🐍." -groups = ["docs"] -files = [ - {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, - {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, -] - -[[package]] -name = "mistune" -version = "3.1.3" -requires_python = ">=3.8" -summary = "A sane and fast Markdown parser with useful plugins and renderers" -groups = ["dev"] -dependencies = [ - "typing-extensions; python_version < \"3.11\"", -] -files = [ - {file = "mistune-3.1.3-py3-none-any.whl", hash = "sha256:1a32314113cff28aa6432e99e522677c8587fd83e3d51c29b82a52409c842bd9"}, - {file = "mistune-3.1.3.tar.gz", hash = "sha256:a7035c21782b2becb6be62f8f25d3df81ccb4d6fa477a6525b15af06539f02a0"}, -] - -[[package]] -name = "mkdocs" -version = "1.6.1" -requires_python = ">=3.8" -summary = "Project documentation with Markdown." -groups = ["docs"] -dependencies = [ - "click>=7.0", - "colorama>=0.4; platform_system == \"Windows\"", - "ghp-import>=1.0", - "importlib-metadata>=4.4; python_version < \"3.10\"", - "jinja2>=2.11.1", - "markdown>=3.3.6", - "markupsafe>=2.0.1", - "mergedeep>=1.3.4", - "mkdocs-get-deps>=0.2.0", - "packaging>=20.5", - "pathspec>=0.11.1", - "pyyaml-env-tag>=0.1", - "pyyaml>=5.1", - "watchdog>=2.0", -] -files = [ - {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, - {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, -] - -[[package]] -name = "mkdocs-get-deps" -version = "0.2.0" -requires_python = ">=3.8" -summary = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" -groups = ["docs"] -dependencies = [ - "importlib-metadata>=4.3; python_version < \"3.10\"", - "mergedeep>=1.3.4", - "platformdirs>=2.2.0", - "pyyaml>=5.1", -] -files = [ - {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, - {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, -] - -[[package]] -name = "mkdocs-material" -version = "9.6.12" -requires_python = ">=3.8" -summary = "Documentation that simply works" -groups = ["docs"] -dependencies = [ - "babel~=2.10", - "backrefs~=5.7.post1", - "colorama~=0.4", - "jinja2~=3.1", - "markdown~=3.2", - "mkdocs-material-extensions~=1.3", - "mkdocs~=1.6", - "paginate~=0.5", - "pygments~=2.16", - "pymdown-extensions~=10.2", - "requests~=2.26", -] -files = [ - {file = "mkdocs_material-9.6.12-py3-none-any.whl", hash = "sha256:92b4fbdc329e4febc267ca6e2c51e8501fa97b2225c5f4deb4d4e43550f8e61e"}, - {file = "mkdocs_material-9.6.12.tar.gz", hash = "sha256:add6a6337b29f9ea7912cb1efc661de2c369060b040eb5119855d794ea85b473"}, -] - -[[package]] -name = "mkdocs-material-extensions" -version = "1.3.1" -requires_python = ">=3.8" -summary = "Extension pack for Python Markdown and MkDocs Material." -groups = ["docs"] -files = [ - {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, - {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, -] - -[[package]] -name = "monotonic" -version = "1.6" -summary = "An implementation of time.monotonic() for Python 2 & < 3.3" -groups = ["dev"] -files = [ - {file = "monotonic-1.6-py2.py3-none-any.whl", hash = "sha256:68687e19a14f11f26d140dd5c86f3dba4bf5df58003000ed467e0e2a69bca96c"}, - {file = "monotonic-1.6.tar.gz", hash = "sha256:3a55207bcfed53ddd5c5bae174524062935efed17792e9de2ad0205ce9ad63f7"}, -] - -[[package]] -name = "mouseinfo" -version = "0.1.3" -summary = "An application to display XY position and RGB color information for the pixel currently under the mouse. Works on Python 2 and 3." -groups = ["dev"] -dependencies = [ - "Pillow<=3.4.2,>=2.0.0; python_version == \"3.2\"", - "Pillow<=4.3.0,>=2.0.0; python_version == \"3.3\"", - "Pillow<=5.4.1,>=2.5.0; python_version == \"3.4\"", - "Pillow>=2.0.0; python_version == \"2.7\"", - "Pillow>=3.2.0; python_version == \"3.5\"", - "Pillow>=4.0.0; python_version == \"3.6\"", - "Pillow>=5.2.0; python_version == \"3.7\"", - "Xlib; platform_system == \"Linux\" and python_version < \"3.0\"", - "pyperclip", - "python3-Xlib; platform_system == \"Linux\" and python_version >= \"3.0\"", - "rubicon-objc; platform_system == \"Darwin\"", -] -files = [ - {file = "MouseInfo-0.1.3.tar.gz", hash = "sha256:2c62fb8885062b8e520a3cce0a297c657adcc08c60952eb05bc8256ef6f7f6e7"}, -] - -[[package]] -name = "mpmath" -version = "1.3.0" -summary = "Python library for arbitrary-precision floating-point arithmetic" -groups = ["dev"] -files = [ - {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, - {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, -] - -[[package]] -name = "multidict" -version = "6.4.3" -requires_python = ">=3.9" -summary = "multidict implementation" -groups = ["dev", "test"] -dependencies = [ - "typing-extensions>=4.1.0; python_version < \"3.11\"", -] -files = [ - {file = "multidict-6.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32a998bd8a64ca48616eac5a8c1cc4fa38fb244a3facf2eeb14abe186e0f6cc5"}, - {file = "multidict-6.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a54ec568f1fc7f3c313c2f3b16e5db346bf3660e1309746e7fccbbfded856188"}, - {file = "multidict-6.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a7be07e5df178430621c716a63151165684d3e9958f2bbfcb644246162007ab7"}, - {file = "multidict-6.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b128dbf1c939674a50dd0b28f12c244d90e5015e751a4f339a96c54f7275e291"}, - {file = "multidict-6.4.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b9cb19dfd83d35b6ff24a4022376ea6e45a2beba8ef3f0836b8a4b288b6ad685"}, - {file = "multidict-6.4.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3cf62f8e447ea2c1395afa289b332e49e13d07435369b6f4e41f887db65b40bf"}, - {file = "multidict-6.4.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:909f7d43ff8f13d1adccb6a397094adc369d4da794407f8dd592c51cf0eae4b1"}, - {file = "multidict-6.4.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0bb8f8302fbc7122033df959e25777b0b7659b1fd6bcb9cb6bed76b5de67afef"}, - {file = "multidict-6.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:224b79471b4f21169ea25ebc37ed6f058040c578e50ade532e2066562597b8a9"}, - {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a7bd27f7ab3204f16967a6f899b3e8e9eb3362c0ab91f2ee659e0345445e0078"}, - {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:99592bd3162e9c664671fd14e578a33bfdba487ea64bcb41d281286d3c870ad7"}, - {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a62d78a1c9072949018cdb05d3c533924ef8ac9bcb06cbf96f6d14772c5cd451"}, - {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3ccdde001578347e877ca4f629450973c510e88e8865d5aefbcb89b852ccc666"}, - {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:eccb67b0e78aa2e38a04c5ecc13bab325a43e5159a181a9d1a6723db913cbb3c"}, - {file = "multidict-6.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8b6fcf6054fc4114a27aa865f8840ef3d675f9316e81868e0ad5866184a6cba5"}, - {file = "multidict-6.4.3-cp310-cp310-win32.whl", hash = "sha256:f92c7f62d59373cd93bc9969d2da9b4b21f78283b1379ba012f7ee8127b3152e"}, - {file = "multidict-6.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:b57e28dbc031d13916b946719f213c494a517b442d7b48b29443e79610acd887"}, - {file = "multidict-6.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f6f19170197cc29baccd33ccc5b5d6a331058796485857cf34f7635aa25fb0cd"}, - {file = "multidict-6.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2882bf27037eb687e49591690e5d491e677272964f9ec7bc2abbe09108bdfb8"}, - {file = "multidict-6.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fbf226ac85f7d6b6b9ba77db4ec0704fde88463dc17717aec78ec3c8546c70ad"}, - {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e329114f82ad4b9dd291bef614ea8971ec119ecd0f54795109976de75c9a852"}, - {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1f4e0334d7a555c63f5c8952c57ab6f1c7b4f8c7f3442df689fc9f03df315c08"}, - {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:740915eb776617b57142ce0bb13b7596933496e2f798d3d15a20614adf30d229"}, - {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255dac25134d2b141c944b59a0d2f7211ca12a6d4779f7586a98b4b03ea80508"}, - {file = "multidict-6.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4e8535bd4d741039b5aad4285ecd9b902ef9e224711f0b6afda6e38d7ac02c7"}, - {file = "multidict-6.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c433a33be000dd968f5750722eaa0991037be0be4a9d453eba121774985bc8"}, - {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4eb33b0bdc50acd538f45041f5f19945a1f32b909b76d7b117c0c25d8063df56"}, - {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:75482f43465edefd8a5d72724887ccdcd0c83778ded8f0cb1e0594bf71736cc0"}, - {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce5b3082e86aee80b3925ab4928198450d8e5b6466e11501fe03ad2191c6d777"}, - {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e413152e3212c4d39f82cf83c6f91be44bec9ddea950ce17af87fbf4e32ca6b2"}, - {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8aac2eeff69b71f229a405c0a4b61b54bade8e10163bc7b44fcd257949620618"}, - {file = "multidict-6.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ab583ac203af1d09034be41458feeab7863c0635c650a16f15771e1386abf2d7"}, - {file = "multidict-6.4.3-cp311-cp311-win32.whl", hash = "sha256:1b2019317726f41e81154df636a897de1bfe9228c3724a433894e44cd2512378"}, - {file = "multidict-6.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:43173924fa93c7486402217fab99b60baf78d33806af299c56133a3755f69589"}, - {file = "multidict-6.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f1c2f58f08b36f8475f3ec6f5aeb95270921d418bf18f90dffd6be5c7b0e676"}, - {file = "multidict-6.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:26ae9ad364fc61b936fb7bf4c9d8bd53f3a5b4417142cd0be5c509d6f767e2f1"}, - {file = "multidict-6.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:659318c6c8a85f6ecfc06b4e57529e5a78dfdd697260cc81f683492ad7e9435a"}, - {file = "multidict-6.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1eb72c741fd24d5a28242ce72bb61bc91f8451877131fa3fe930edb195f7054"}, - {file = "multidict-6.4.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3cd06d88cb7398252284ee75c8db8e680aa0d321451132d0dba12bc995f0adcc"}, - {file = "multidict-6.4.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4543d8dc6470a82fde92b035a92529317191ce993533c3c0c68f56811164ed07"}, - {file = "multidict-6.4.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30a3ebdc068c27e9d6081fca0e2c33fdf132ecea703a72ea216b81a66860adde"}, - {file = "multidict-6.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b038f10e23f277153f86f95c777ba1958bcd5993194fda26a1d06fae98b2f00c"}, - {file = "multidict-6.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c605a2b2dc14282b580454b9b5d14ebe0668381a3a26d0ac39daa0ca115eb2ae"}, - {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8bd2b875f4ca2bb527fe23e318ddd509b7df163407b0fb717df229041c6df5d3"}, - {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c2e98c840c9c8e65c0e04b40c6c5066c8632678cd50c8721fdbcd2e09f21a507"}, - {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66eb80dd0ab36dbd559635e62fba3083a48a252633164857a1d1684f14326427"}, - {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c23831bdee0a2a3cf21be057b5e5326292f60472fb6c6f86392bbf0de70ba731"}, - {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1535cec6443bfd80d028052e9d17ba6ff8a5a3534c51d285ba56c18af97e9713"}, - {file = "multidict-6.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b73e7227681f85d19dec46e5b881827cd354aabe46049e1a61d2f9aaa4e285a"}, - {file = "multidict-6.4.3-cp312-cp312-win32.whl", hash = "sha256:8eac0c49df91b88bf91f818e0a24c1c46f3622978e2c27035bfdca98e0e18124"}, - {file = "multidict-6.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:11990b5c757d956cd1db7cb140be50a63216af32cd6506329c2c59d732d802db"}, - {file = "multidict-6.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a76534263d03ae0cfa721fea40fd2b5b9d17a6f85e98025931d41dc49504474"}, - {file = "multidict-6.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:805031c2f599eee62ac579843555ed1ce389ae00c7e9f74c2a1b45e0564a88dd"}, - {file = "multidict-6.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c56c179839d5dcf51d565132185409d1d5dd8e614ba501eb79023a6cab25576b"}, - {file = "multidict-6.4.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c64f4ddb3886dd8ab71b68a7431ad4aa01a8fa5be5b11543b29674f29ca0ba3"}, - {file = "multidict-6.4.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3002a856367c0b41cad6784f5b8d3ab008eda194ed7864aaa58f65312e2abcac"}, - {file = "multidict-6.4.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d75e621e7d887d539d6e1d789f0c64271c250276c333480a9e1de089611f790"}, - {file = "multidict-6.4.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:995015cf4a3c0d72cbf453b10a999b92c5629eaf3a0c3e1efb4b5c1f602253bb"}, - {file = "multidict-6.4.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b0fabae7939d09d7d16a711468c385272fa1b9b7fb0d37e51143585d8e72e0"}, - {file = "multidict-6.4.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:61ed4d82f8a1e67eb9eb04f8587970d78fe7cddb4e4d6230b77eda23d27938f9"}, - {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:062428944a8dc69df9fdc5d5fc6279421e5f9c75a9ee3f586f274ba7b05ab3c8"}, - {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b90e27b4674e6c405ad6c64e515a505c6d113b832df52fdacb6b1ffd1fa9a1d1"}, - {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7d50d4abf6729921e9613d98344b74241572b751c6b37feed75fb0c37bd5a817"}, - {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:43fe10524fb0a0514be3954be53258e61d87341008ce4914f8e8b92bee6f875d"}, - {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:236966ca6c472ea4e2d3f02f6673ebfd36ba3f23159c323f5a496869bc8e47c9"}, - {file = "multidict-6.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:422a5ec315018e606473ba1f5431e064cf8b2a7468019233dcf8082fabad64c8"}, - {file = "multidict-6.4.3-cp313-cp313-win32.whl", hash = "sha256:f901a5aace8e8c25d78960dcc24c870c8d356660d3b49b93a78bf38eb682aac3"}, - {file = "multidict-6.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:1c152c49e42277bc9a2f7b78bd5fa10b13e88d1b0328221e7aef89d5c60a99a5"}, - {file = "multidict-6.4.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:be8751869e28b9c0d368d94f5afcb4234db66fe8496144547b4b6d6a0645cfc6"}, - {file = "multidict-6.4.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d4b31f8a68dccbcd2c0ea04f0e014f1defc6b78f0eb8b35f2265e8716a6df0c"}, - {file = "multidict-6.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:032efeab3049e37eef2ff91271884303becc9e54d740b492a93b7e7266e23756"}, - {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e78006af1a7c8a8007e4f56629d7252668344442f66982368ac06522445e375"}, - {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:daeac9dd30cda8703c417e4fddccd7c4dc0c73421a0b54a7da2713be125846be"}, - {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f6f90700881438953eae443a9c6f8a509808bc3b185246992c4233ccee37fea"}, - {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f84627997008390dd15762128dcf73c3365f4ec0106739cde6c20a07ed198ec8"}, - {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3307b48cd156153b117c0ea54890a3bdbf858a5b296ddd40dc3852e5f16e9b02"}, - {file = "multidict-6.4.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ead46b0fa1dcf5af503a46e9f1c2e80b5d95c6011526352fa5f42ea201526124"}, - {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1748cb2743bedc339d63eb1bca314061568793acd603a6e37b09a326334c9f44"}, - {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:acc9fa606f76fc111b4569348cc23a771cb52c61516dcc6bcef46d612edb483b"}, - {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:31469d5832b5885adeb70982e531ce86f8c992334edd2f2254a10fa3182ac504"}, - {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ba46b51b6e51b4ef7bfb84b82f5db0dc5e300fb222a8a13b8cd4111898a869cf"}, - {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:389cfefb599edf3fcfd5f64c0410da686f90f5f5e2c4d84e14f6797a5a337af4"}, - {file = "multidict-6.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:64bc2bbc5fba7b9db5c2c8d750824f41c6994e3882e6d73c903c2afa78d091e4"}, - {file = "multidict-6.4.3-cp313-cp313t-win32.whl", hash = "sha256:0ecdc12ea44bab2807d6b4a7e5eef25109ab1c82a8240d86d3c1fc9f3b72efd5"}, - {file = "multidict-6.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7146a8742ea71b5d7d955bffcef58a9e6e04efba704b52a460134fefd10a8208"}, - {file = "multidict-6.4.3-py3-none-any.whl", hash = "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9"}, - {file = "multidict-6.4.3.tar.gz", hash = "sha256:3ada0b058c9f213c5f95ba301f922d402ac234f1111a7d8fd70f1b99f3c281ec"}, -] - -[[package]] -name = "mypy" -version = "1.15.0" -requires_python = ">=3.9" -summary = "Optional static typing for Python" -groups = ["dev"] -dependencies = [ - "mypy-extensions>=1.0.0", - "tomli>=1.1.0; python_version < \"3.11\"", - "typing-extensions>=4.6.0", -] -files = [ - {file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"}, - {file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"}, - {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"}, - {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"}, - {file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"}, - {file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"}, - {file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"}, - {file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"}, - {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"}, - {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"}, - {file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"}, - {file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"}, - {file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"}, - {file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"}, - {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"}, - {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"}, - {file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"}, - {file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"}, - {file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"}, - {file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"}, - {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"}, - {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"}, - {file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"}, - {file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"}, - {file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"}, - {file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"}, -] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -requires_python = ">=3.8" -summary = "Type system extensions for programs checked with the mypy type checker." -groups = ["dev"] -files = [ - {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, - {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, -] - -[[package]] -name = "nbclient" -version = "0.10.2" -requires_python = ">=3.9.0" -summary = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." -groups = ["dev"] -dependencies = [ - "jupyter-client>=6.1.12", - "jupyter-core!=5.0.*,>=4.12", - "nbformat>=5.1", - "traitlets>=5.4", -] -files = [ - {file = "nbclient-0.10.2-py3-none-any.whl", hash = "sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d"}, - {file = "nbclient-0.10.2.tar.gz", hash = "sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193"}, -] - -[[package]] -name = "nbconvert" -version = "7.16.6" -requires_python = ">=3.8" -summary = "Converting Jupyter Notebooks (.ipynb files) to other formats. Output formats include asciidoc, html, latex, markdown, pdf, py, rst, script. nbconvert can be used both as a Python library (`import nbconvert`) or as a command line tool (invoked as `jupyter nbconvert ...`)." -groups = ["dev"] -dependencies = [ - "beautifulsoup4", - "bleach[css]!=5.0.0", - "defusedxml", - "importlib-metadata>=3.6; python_version < \"3.10\"", - "jinja2>=3.0", - "jupyter-core>=4.7", - "jupyterlab-pygments", - "markupsafe>=2.0", - "mistune<4,>=2.0.3", - "nbclient>=0.5.0", - "nbformat>=5.7", - "packaging", - "pandocfilters>=1.4.1", - "pygments>=2.4.1", - "traitlets>=5.1", -] -files = [ - {file = "nbconvert-7.16.6-py3-none-any.whl", hash = "sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b"}, - {file = "nbconvert-7.16.6.tar.gz", hash = "sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582"}, -] - -[[package]] -name = "nbformat" -version = "5.10.4" -requires_python = ">=3.8" -summary = "The Jupyter Notebook format" -groups = ["dev"] -dependencies = [ - "fastjsonschema>=2.15", - "jsonschema>=2.6", - "jupyter-core!=5.0.*,>=4.12", - "traitlets>=5.1", -] -files = [ - {file = "nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b"}, - {file = "nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a"}, -] - -[[package]] -name = "nest-asyncio" -version = "1.6.0" -requires_python = ">=3.5" -summary = "Patch asyncio to allow nested event loops" -groups = ["dev"] -files = [ - {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, - {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, -] - -[[package]] -name = "networkx" -version = "3.4.2" -requires_python = ">=3.10" -summary = "Python package for creating and manipulating graphs and networks" -groups = ["dev"] -files = [ - {file = "networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f"}, - {file = "networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1"}, -] - -[[package]] -name = "ninja" -version = "1.11.1.4" -requires_python = ">=3.7" -summary = "Ninja is a small build system with a focus on speed" -groups = ["dev"] -files = [ - {file = "ninja-1.11.1.4-py3-none-macosx_10_9_universal2.whl", hash = "sha256:b33923c8da88e8da20b6053e38deb433f53656441614207e01d283ad02c5e8e7"}, - {file = "ninja-1.11.1.4-py3-none-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cede0af00b58e27b31f2482ba83292a8e9171cdb9acc2c867a3b6e40b3353e43"}, - {file = "ninja-1.11.1.4-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:096487995473320de7f65d622c3f1d16c3ad174797602218ca8c967f51ec38a0"}, - {file = "ninja-1.11.1.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3090d4488fadf6047d0d7a1db0c9643a8d391f0d94729554dbb89b5bdc769d7"}, - {file = "ninja-1.11.1.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecce44a00325a93631792974659cf253a815cc6da4ec96f89742925dfc295a0d"}, - {file = "ninja-1.11.1.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c29bb66d2aa46a2409ab369ea804c730faec7652e8c22c1e428cc09216543e5"}, - {file = "ninja-1.11.1.4-py3-none-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:055f386fb550c2c9d6157e45e20a84d29c47968876b9c5794ae2aec46f952306"}, - {file = "ninja-1.11.1.4-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:f6186d7607bb090c3be1e10c8a56b690be238f953616626f5032238c66e56867"}, - {file = "ninja-1.11.1.4-py3-none-musllinux_1_1_i686.whl", hash = "sha256:cf4453679d15babc04ba023d68d091bb613091b67101c88f85d2171c6621c6eb"}, - {file = "ninja-1.11.1.4-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:d4a6f159b08b0ac4aca5ee1572e3e402f969139e71d85d37c0e2872129098749"}, - {file = "ninja-1.11.1.4-py3-none-musllinux_1_1_s390x.whl", hash = "sha256:c3b96bd875f3ef1db782470e9e41d7508905a0986571f219d20ffed238befa15"}, - {file = "ninja-1.11.1.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:cf554e73f72c04deb04d0cf51f5fdb1903d9c9ca3d2344249c8ce3bd616ebc02"}, - {file = "ninja-1.11.1.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:cfdd09776436a1ff3c4a2558d3fc50a689fb9d7f1bdbc3e6f7b8c2991341ddb3"}, - {file = "ninja-1.11.1.4-py3-none-win32.whl", hash = "sha256:2ab67a41c90bea5ec4b795bab084bc0b3b3bb69d3cd21ca0294fc0fc15a111eb"}, - {file = "ninja-1.11.1.4-py3-none-win_amd64.whl", hash = "sha256:4617b3c12ff64b611a7d93fd9e378275512bb36eff8babff7c83f5116b4f8d66"}, - {file = "ninja-1.11.1.4-py3-none-win_arm64.whl", hash = "sha256:5713cf50c5be50084a8693308a63ecf9e55c3132a78a41ab1363a28b6caaaee1"}, - {file = "ninja-1.11.1.4.tar.gz", hash = "sha256:6aa39f6e894e0452e5b297327db00019383ae55d5d9c57c73b04f13bf79d438a"}, -] - -[[package]] -name = "notebook" -version = "7.4.1" -requires_python = ">=3.8" -summary = "Jupyter Notebook - A web-based notebook environment for interactive computing" -groups = ["dev"] -dependencies = [ - "jupyter-server<3,>=2.4.0", - "jupyterlab-server<3,>=2.27.1", - "jupyterlab<4.5,>=4.4.0rc0", - "notebook-shim<0.3,>=0.2", - "tornado>=6.2.0", -] -files = [ - {file = "notebook-7.4.1-py3-none-any.whl", hash = "sha256:498f12cf567d95b20e780d62d52564ee4310248b3175e996b667b5808028e5d3"}, - {file = "notebook-7.4.1.tar.gz", hash = "sha256:96894962b230013ea0c0a466e4e642c5aace25ba8c86686175b69990ef628ff9"}, -] - -[[package]] -name = "notebook-shim" -version = "0.2.4" -requires_python = ">=3.7" -summary = "A shim layer for notebook traits and config" -groups = ["dev"] -dependencies = [ - "jupyter-server<3,>=1.8", -] -files = [ - {file = "notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef"}, - {file = "notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb"}, -] - -[[package]] -name = "numpy" -version = "2.1.1" -requires_python = ">=3.10" -summary = "Fundamental package for array computing in Python" -groups = ["dev"] -files = [ - {file = "numpy-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8a0e34993b510fc19b9a2ce7f31cb8e94ecf6e924a40c0c9dd4f62d0aac47d9"}, - {file = "numpy-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7dd86dfaf7c900c0bbdcb8b16e2f6ddf1eb1fe39c6c8cca6e94844ed3152a8fd"}, - {file = "numpy-2.1.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:5889dd24f03ca5a5b1e8a90a33b5a0846d8977565e4ae003a63d22ecddf6782f"}, - {file = "numpy-2.1.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:59ca673ad11d4b84ceb385290ed0ebe60266e356641428c845b39cd9df6713ab"}, - {file = "numpy-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ce49a34c44b6de5241f0b38b07e44c1b2dcacd9e36c30f9c2fcb1bb5135db7"}, - {file = "numpy-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913cc1d311060b1d409e609947fa1b9753701dac96e6581b58afc36b7ee35af6"}, - {file = "numpy-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:caf5d284ddea7462c32b8d4a6b8af030b6c9fd5332afb70e7414d7fdded4bfd0"}, - {file = "numpy-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:57eb525e7c2a8fdee02d731f647146ff54ea8c973364f3b850069ffb42799647"}, - {file = "numpy-2.1.1-cp310-cp310-win32.whl", hash = "sha256:9a8e06c7a980869ea67bbf551283bbed2856915f0a792dc32dd0f9dd2fb56728"}, - {file = "numpy-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:d10c39947a2d351d6d466b4ae83dad4c37cd6c3cdd6d5d0fa797da56f710a6ae"}, - {file = "numpy-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0d07841fd284718feffe7dd17a63a2e6c78679b2d386d3e82f44f0108c905550"}, - {file = "numpy-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b5613cfeb1adfe791e8e681128f5f49f22f3fcaa942255a6124d58ca59d9528f"}, - {file = "numpy-2.1.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0b8cc2715a84b7c3b161f9ebbd942740aaed913584cae9cdc7f8ad5ad41943d0"}, - {file = "numpy-2.1.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:b49742cdb85f1f81e4dc1b39dcf328244f4d8d1ded95dea725b316bd2cf18c95"}, - {file = "numpy-2.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8d5f8a8e3bc87334f025194c6193e408903d21ebaeb10952264943a985066ca"}, - {file = "numpy-2.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d51fc141ddbe3f919e91a096ec739f49d686df8af254b2053ba21a910ae518bf"}, - {file = "numpy-2.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:98ce7fb5b8063cfdd86596b9c762bf2b5e35a2cdd7e967494ab78a1fa7f8b86e"}, - {file = "numpy-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:24c2ad697bd8593887b019817ddd9974a7f429c14a5469d7fad413f28340a6d2"}, - {file = "numpy-2.1.1-cp311-cp311-win32.whl", hash = "sha256:397bc5ce62d3fb73f304bec332171535c187e0643e176a6e9421a6e3eacef06d"}, - {file = "numpy-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:ae8ce252404cdd4de56dcfce8b11eac3c594a9c16c231d081fb705cf23bd4d9e"}, - {file = "numpy-2.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c803b7934a7f59563db459292e6aa078bb38b7ab1446ca38dd138646a38203e"}, - {file = "numpy-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6435c48250c12f001920f0751fe50c0348f5f240852cfddc5e2f97e007544cbe"}, - {file = "numpy-2.1.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3269c9eb8745e8d975980b3a7411a98976824e1fdef11f0aacf76147f662b15f"}, - {file = "numpy-2.1.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:fac6e277a41163d27dfab5f4ec1f7a83fac94e170665a4a50191b545721c6521"}, - {file = "numpy-2.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcd8f556cdc8cfe35e70efb92463082b7f43dd7e547eb071ffc36abc0ca4699b"}, - {file = "numpy-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b9cd92c8f8e7b313b80e93cedc12c0112088541dcedd9197b5dee3738c1201"}, - {file = "numpy-2.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:afd9c680df4de71cd58582b51e88a61feed4abcc7530bcd3d48483f20fc76f2a"}, - {file = "numpy-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8661c94e3aad18e1ea17a11f60f843a4933ccaf1a25a7c6a9182af70610b2313"}, - {file = "numpy-2.1.1-cp312-cp312-win32.whl", hash = "sha256:950802d17a33c07cba7fd7c3dcfa7d64705509206be1606f196d179e539111ed"}, - {file = "numpy-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:3fc5eabfc720db95d68e6646e88f8b399bfedd235994016351b1d9e062c4b270"}, - {file = "numpy-2.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:046356b19d7ad1890c751b99acad5e82dc4a02232013bd9a9a712fddf8eb60f5"}, - {file = "numpy-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6e5a9cb2be39350ae6c8f79410744e80154df658d5bea06e06e0ac5bb75480d5"}, - {file = "numpy-2.1.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:d4c57b68c8ef5e1ebf47238e99bf27657511ec3f071c465f6b1bccbef12d4136"}, - {file = "numpy-2.1.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:8ae0fd135e0b157365ac7cc31fff27f07a5572bdfc38f9c2d43b2aff416cc8b0"}, - {file = "numpy-2.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:981707f6b31b59c0c24bcda52e5605f9701cb46da4b86c2e8023656ad3e833cb"}, - {file = "numpy-2.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ca4b53e1e0b279142113b8c5eb7d7a877e967c306edc34f3b58e9be12fda8df"}, - {file = "numpy-2.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e097507396c0be4e547ff15b13dc3866f45f3680f789c1a1301b07dadd3fbc78"}, - {file = "numpy-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7506387e191fe8cdb267f912469a3cccc538ab108471291636a96a54e599556"}, - {file = "numpy-2.1.1-cp313-cp313-win32.whl", hash = "sha256:251105b7c42abe40e3a689881e1793370cc9724ad50d64b30b358bbb3a97553b"}, - {file = "numpy-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:f212d4f46b67ff604d11fff7cc62d36b3e8714edf68e44e9760e19be38c03eb0"}, - {file = "numpy-2.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:920b0911bb2e4414c50e55bd658baeb78281a47feeb064ab40c2b66ecba85553"}, - {file = "numpy-2.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bab7c09454460a487e631ffc0c42057e3d8f2a9ddccd1e60c7bb8ed774992480"}, - {file = "numpy-2.1.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:cea427d1350f3fd0d2818ce7350095c1a2ee33e30961d2f0fef48576ddbbe90f"}, - {file = "numpy-2.1.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:e30356d530528a42eeba51420ae8bf6c6c09559051887196599d96ee5f536468"}, - {file = "numpy-2.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8dfa9e94fc127c40979c3eacbae1e61fda4fe71d84869cc129e2721973231ef"}, - {file = "numpy-2.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910b47a6d0635ec1bd53b88f86120a52bf56dcc27b51f18c7b4a2e2224c29f0f"}, - {file = "numpy-2.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:13cc11c00000848702322af4de0147ced365c81d66053a67c2e962a485b3717c"}, - {file = "numpy-2.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53e27293b3a2b661c03f79aa51c3987492bd4641ef933e366e0f9f6c9bf257ec"}, - {file = "numpy-2.1.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7be6a07520b88214ea85d8ac8b7d6d8a1839b0b5cb87412ac9f49fa934eb15d5"}, - {file = "numpy-2.1.1-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:52ac2e48f5ad847cd43c4755520a2317f3380213493b9d8a4c5e37f3b87df504"}, - {file = "numpy-2.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50a95ca3560a6058d6ea91d4629a83a897ee27c00630aed9d933dff191f170cd"}, - {file = "numpy-2.1.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:99f4a9ee60eed1385a86e82288971a51e71df052ed0b2900ed30bc840c0f2e39"}, - {file = "numpy-2.1.1.tar.gz", hash = "sha256:d0cf7d55b1051387807405b3898efafa862997b4cba8aa5dbe657be794afeafd"}, -] - -[[package]] -name = "nvidia-cublas-cu12" -version = "12.6.4.1" -requires_python = ">=3" -summary = "CUBLAS native runtime libraries" -groups = ["dev"] -marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" -files = [ - {file = "nvidia_cublas_cu12-12.6.4.1-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08ed2686e9875d01b58e3cb379c6896df8e76c75e0d4a7f7dace3d7b6d9ef8eb"}, - {file = "nvidia_cublas_cu12-12.6.4.1-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:235f728d6e2a409eddf1df58d5b0921cf80cfa9e72b9f2775ccb7b4a87984668"}, - {file = "nvidia_cublas_cu12-12.6.4.1-py3-none-win_amd64.whl", hash = "sha256:9e4fa264f4d8a4eb0cdbd34beadc029f453b3bafae02401e999cf3d5a5af75f8"}, -] - -[[package]] -name = "nvidia-cuda-cupti-cu12" -version = "12.6.80" -requires_python = ">=3" -summary = "CUDA profiling tools runtime libs." -groups = ["dev"] -marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" -files = [ - {file = "nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:166ee35a3ff1587f2490364f90eeeb8da06cd867bd5b701bf7f9a02b78bc63fc"}, - {file = "nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_aarch64.whl", hash = "sha256:358b4a1d35370353d52e12f0a7d1769fc01ff74a191689d3870b2123156184c4"}, - {file = "nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6768bad6cab4f19e8292125e5f1ac8aa7d1718704012a0e3272a6f61c4bce132"}, - {file = "nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a3eff6cdfcc6a4c35db968a06fcadb061cbc7d6dde548609a941ff8701b98b73"}, - {file = "nvidia_cuda_cupti_cu12-12.6.80-py3-none-win_amd64.whl", hash = "sha256:bbe6ae76e83ce5251b56e8c8e61a964f757175682bbad058b170b136266ab00a"}, -] - -[[package]] -name = "nvidia-cuda-nvrtc-cu12" -version = "12.6.77" -requires_python = ">=3" -summary = "NVRTC native runtime libraries" -groups = ["dev"] -marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" -files = [ - {file = "nvidia_cuda_nvrtc_cu12-12.6.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:5847f1d6e5b757f1d2b3991a01082a44aad6f10ab3c5c0213fa3e25bddc25a13"}, - {file = "nvidia_cuda_nvrtc_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:35b0cc6ee3a9636d5409133e79273ce1f3fd087abb0532d2d2e8fff1fe9efc53"}, - {file = "nvidia_cuda_nvrtc_cu12-12.6.77-py3-none-win_amd64.whl", hash = "sha256:f7007dbd914c56bd80ea31bc43e8e149da38f68158f423ba845fc3292684e45a"}, -] - -[[package]] -name = "nvidia-cuda-runtime-cu12" -version = "12.6.77" -requires_python = ">=3" -summary = "CUDA Runtime native Libraries" -groups = ["dev"] -marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" -files = [ - {file = "nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6116fad3e049e04791c0256a9778c16237837c08b27ed8c8401e2e45de8d60cd"}, - {file = "nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:d461264ecb429c84c8879a7153499ddc7b19b5f8d84c204307491989a365588e"}, - {file = "nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba3b56a4f896141e25e19ab287cd71e52a6a0f4b29d0d31609f60e3b4d5219b7"}, - {file = "nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a84d15d5e1da416dd4774cb42edf5e954a3e60cc945698dc1d5be02321c44dc8"}, - {file = "nvidia_cuda_runtime_cu12-12.6.77-py3-none-win_amd64.whl", hash = "sha256:86c58044c824bf3c173c49a2dbc7a6c8b53cb4e4dca50068be0bf64e9dab3f7f"}, -] - -[[package]] -name = "nvidia-cudnn-cu12" -version = "9.5.1.17" -requires_python = ">=3" -summary = "cuDNN runtime libraries" -groups = ["dev"] -marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" -dependencies = [ - "nvidia-cublas-cu12", -] -files = [ - {file = "nvidia_cudnn_cu12-9.5.1.17-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9fd4584468533c61873e5fda8ca41bac3a38bcb2d12350830c69b0a96a7e4def"}, - {file = "nvidia_cudnn_cu12-9.5.1.17-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:30ac3869f6db17d170e0e556dd6cc5eee02647abc31ca856634d5a40f82c15b2"}, - {file = "nvidia_cudnn_cu12-9.5.1.17-py3-none-win_amd64.whl", hash = "sha256:d7af0f8a4f3b4b9dbb3122f2ef553b45694ed9c384d5a75bab197b8eefb79ab8"}, -] - -[[package]] -name = "nvidia-cufft-cu12" -version = "11.3.0.4" -requires_python = ">=3" -summary = "CUFFT native runtime libraries" -groups = ["dev"] -marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" -dependencies = [ - "nvidia-nvjitlink-cu12", -] -files = [ - {file = "nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d16079550df460376455cba121db6564089176d9bac9e4f360493ca4741b22a6"}, - {file = "nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8510990de9f96c803a051822618d42bf6cb8f069ff3f48d93a8486efdacb48fb"}, - {file = "nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ccba62eb9cef5559abd5e0d54ceed2d9934030f51163df018532142a8ec533e5"}, - {file = "nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_x86_64.whl", hash = "sha256:768160ac89f6f7b459bee747e8d175dbf53619cfe74b2a5636264163138013ca"}, - {file = "nvidia_cufft_cu12-11.3.0.4-py3-none-win_amd64.whl", hash = "sha256:6048ebddfb90d09d2707efb1fd78d4e3a77cb3ae4dc60e19aab6be0ece2ae464"}, -] - -[[package]] -name = "nvidia-cufile-cu12" -version = "1.11.1.6" -requires_python = ">=3" -summary = "cuFile GPUDirect libraries" -groups = ["dev"] -marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" -files = [ - {file = "nvidia_cufile_cu12-1.11.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc23469d1c7e52ce6c1d55253273d32c565dd22068647f3aa59b3c6b005bf159"}, - {file = "nvidia_cufile_cu12-1.11.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:8f57a0051dcf2543f6dc2b98a98cb2719c37d3cee1baba8965d57f3bbc90d4db"}, -] - -[[package]] -name = "nvidia-curand-cu12" -version = "10.3.7.77" -requires_python = ">=3" -summary = "CURAND native runtime libraries" -groups = ["dev"] -marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" -files = [ - {file = "nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:6e82df077060ea28e37f48a3ec442a8f47690c7499bff392a5938614b56c98d8"}, - {file = "nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a42cd1344297f70b9e39a1e4f467a4e1c10f1da54ff7a85c12197f6c652c8bdf"}, - {file = "nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:99f1a32f1ac2bd134897fc7a203f779303261268a65762a623bf30cc9fe79117"}, - {file = "nvidia_curand_cu12-10.3.7.77-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:7b2ed8e95595c3591d984ea3603dd66fe6ce6812b886d59049988a712ed06b6e"}, - {file = "nvidia_curand_cu12-10.3.7.77-py3-none-win_amd64.whl", hash = "sha256:6d6d935ffba0f3d439b7cd968192ff068fafd9018dbf1b85b37261b13cfc9905"}, -] - -[[package]] -name = "nvidia-cusolver-cu12" -version = "11.7.1.2" -requires_python = ">=3" -summary = "CUDA solver native runtime libraries" -groups = ["dev"] -marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" -dependencies = [ - "nvidia-cublas-cu12", - "nvidia-cusparse-cu12", - "nvidia-nvjitlink-cu12", -] -files = [ - {file = "nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0ce237ef60acde1efc457335a2ddadfd7610b892d94efee7b776c64bb1cac9e0"}, - {file = "nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9e49843a7707e42022babb9bcfa33c29857a93b88020c4e4434656a655b698c"}, - {file = "nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6cf28f17f64107a0c4d7802be5ff5537b2130bfc112f25d5a30df227058ca0e6"}, - {file = "nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:dbbe4fc38ec1289c7e5230e16248365e375c3673c9c8bac5796e2e20db07f56e"}, - {file = "nvidia_cusolver_cu12-11.7.1.2-py3-none-win_amd64.whl", hash = "sha256:6813f9d8073f555444a8705f3ab0296d3e1cb37a16d694c5fc8b862a0d8706d7"}, -] - -[[package]] -name = "nvidia-cusparse-cu12" -version = "12.5.4.2" -requires_python = ">=3" -summary = "CUSPARSE native runtime libraries" -groups = ["dev"] -marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" -dependencies = [ - "nvidia-nvjitlink-cu12", -] -files = [ - {file = "nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d25b62fb18751758fe3c93a4a08eff08effedfe4edf1c6bb5afd0890fe88f887"}, - {file = "nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7aa32fa5470cf754f72d1116c7cbc300b4e638d3ae5304cfa4a638a5b87161b1"}, - {file = "nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7556d9eca156e18184b94947ade0fba5bb47d69cec46bf8660fd2c71a4b48b73"}, - {file = "nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:23749a6571191a215cb74d1cdbff4a86e7b19f1200c071b3fcf844a5bea23a2f"}, - {file = "nvidia_cusparse_cu12-12.5.4.2-py3-none-win_amd64.whl", hash = "sha256:4acb8c08855a26d737398cba8fb6f8f5045d93f82612b4cfd84645a2332ccf20"}, -] - -[[package]] -name = "nvidia-cusparselt-cu12" -version = "0.6.3" -summary = "NVIDIA cuSPARSELt" -groups = ["dev"] -marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" -files = [ - {file = "nvidia_cusparselt_cu12-0.6.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8371549623ba601a06322af2133c4a44350575f5a3108fb75f3ef20b822ad5f1"}, - {file = "nvidia_cusparselt_cu12-0.6.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e5c8a26c36445dd2e6812f1177978a24e2d37cacce7e090f297a688d1ec44f46"}, - {file = "nvidia_cusparselt_cu12-0.6.3-py3-none-win_amd64.whl", hash = "sha256:3b325bcbd9b754ba43df5a311488fca11a6b5dc3d11df4d190c000cf1a0765c7"}, -] - -[[package]] -name = "nvidia-nccl-cu12" -version = "2.26.2" -requires_python = ">=3" -summary = "NVIDIA Collective Communication Library (NCCL) Runtime" -groups = ["dev"] -marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" -files = [ - {file = "nvidia_nccl_cu12-2.26.2-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c196e95e832ad30fbbb50381eb3cbd1fadd5675e587a548563993609af19522"}, - {file = "nvidia_nccl_cu12-2.26.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:694cf3879a206553cc9d7dbda76b13efaf610fdb70a50cba303de1b0d1530ac6"}, -] - -[[package]] -name = "nvidia-nvjitlink-cu12" -version = "12.6.85" -requires_python = ">=3" -summary = "Nvidia JIT LTO Library" -groups = ["dev"] -marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" -files = [ - {file = "nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:eedc36df9e88b682efe4309aa16b5b4e78c2407eac59e8c10a6a47535164369a"}, - {file = "nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cf4eaa7d4b6b543ffd69d6abfb11efdeb2db48270d94dfd3a452c24150829e41"}, - {file = "nvidia_nvjitlink_cu12-12.6.85-py3-none-win_amd64.whl", hash = "sha256:e61120e52ed675747825cdd16febc6a0730537451d867ee58bee3853b1b13d1c"}, -] - -[[package]] -name = "nvidia-nvtx-cu12" -version = "12.6.77" -requires_python = ">=3" -summary = "NVIDIA Tools Extension" -groups = ["dev"] -marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" -files = [ - {file = "nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f44f8d86bb7d5629988d61c8d3ae61dddb2015dee142740536bc7481b022fe4b"}, - {file = "nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_aarch64.whl", hash = "sha256:adcaabb9d436c9761fca2b13959a2d237c5f9fd406c8e4b723c695409ff88059"}, - {file = "nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b90bed3df379fa79afbd21be8e04a0314336b8ae16768b58f2d34cb1d04cd7d2"}, - {file = "nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6574241a3ec5fdc9334353ab8c479fe75841dbe8f4532a8fc97ce63503330ba1"}, - {file = "nvidia_nvtx_cu12-12.6.77-py3-none-win_amd64.whl", hash = "sha256:2fb11a4af04a5e6c84073e6404d26588a34afd35379f0855a99797897efa75c0"}, -] - -[[package]] -name = "ollama" -version = "0.4.8" -requires_python = "<4.0,>=3.8" -summary = "The official Python client for Ollama." -groups = ["dev"] -dependencies = [ - "httpx<0.29,>=0.27", - "pydantic<3.0.0,>=2.9.0", -] -files = [ - {file = "ollama-0.4.8-py3-none-any.whl", hash = "sha256:04312af2c5e72449aaebac4a2776f52ef010877c554103419d3f36066fe8af4c"}, - {file = "ollama-0.4.8.tar.gz", hash = "sha256:1121439d49b96fa8339842965d0616eba5deb9f8c790786cdf4c0b3df4833802"}, -] - -[[package]] -name = "openai" -version = "1.76.0" -requires_python = ">=3.8" -summary = "The official Python library for the openai API" -groups = ["dev"] -dependencies = [ - "anyio<5,>=3.5.0", - "distro<2,>=1.7.0", - "httpx<1,>=0.23.0", - "jiter<1,>=0.4.0", - "pydantic<3,>=1.9.0", - "sniffio", - "tqdm>4", - "typing-extensions<5,>=4.11", -] -files = [ - {file = "openai-1.76.0-py3-none-any.whl", hash = "sha256:a712b50e78cf78e6d7b2a8f69c4978243517c2c36999756673e07a14ce37dc0a"}, - {file = "openai-1.76.0.tar.gz", hash = "sha256:fd2bfaf4608f48102d6b74f9e11c5ecaa058b60dad9c36e409c12477dfd91fb2"}, -] - -[[package]] -name = "opencv-python" -version = "4.11.0.86" -requires_python = ">=3.6" -summary = "Wrapper package for OpenCV python bindings." -groups = ["dev"] -dependencies = [ - "numpy>=1.13.3; python_version < \"3.7\"", - "numpy>=1.17.0; python_version >= \"3.7\"", - "numpy>=1.17.3; python_version >= \"3.8\"", - "numpy>=1.19.3; python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\"", - "numpy>=1.19.3; python_version >= \"3.9\"", - "numpy>=1.21.0; python_version <= \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\"", - "numpy>=1.21.2; python_version >= \"3.10\"", - "numpy>=1.21.4; python_version >= \"3.10\" and platform_system == \"Darwin\"", - "numpy>=1.23.5; python_version >= \"3.11\"", - "numpy>=1.26.0; python_version >= \"3.12\"", -] -files = [ - {file = "opencv-python-4.11.0.86.tar.gz", hash = "sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4"}, - {file = "opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a"}, - {file = "opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66"}, - {file = "opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202"}, - {file = "opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d"}, - {file = "opencv_python-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b"}, - {file = "opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec"}, -] - -[[package]] -name = "opencv-python-headless" -version = "4.11.0.86" -requires_python = ">=3.6" -summary = "Wrapper package for OpenCV python bindings." -groups = ["dev"] -dependencies = [ - "numpy>=1.13.3; python_version < \"3.7\"", - "numpy>=1.17.0; python_version >= \"3.7\"", - "numpy>=1.17.3; python_version >= \"3.8\"", - "numpy>=1.19.3; python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\"", - "numpy>=1.19.3; python_version >= \"3.9\"", - "numpy>=1.21.0; python_version <= \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\"", - "numpy>=1.21.2; python_version >= \"3.10\"", - "numpy>=1.21.4; python_version >= \"3.10\" and platform_system == \"Darwin\"", - "numpy>=1.23.5; python_version >= \"3.11\"", - "numpy>=1.26.0; python_version >= \"3.12\"", -] -files = [ - {file = "opencv-python-headless-4.11.0.86.tar.gz", hash = "sha256:996eb282ca4b43ec6a3972414de0e2331f5d9cda2b41091a49739c19fb843798"}, - {file = "opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:48128188ade4a7e517237c8e1e11a9cdf5c282761473383e77beb875bb1e61ca"}, - {file = "opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:a66c1b286a9de872c343ee7c3553b084244299714ebb50fbdcd76f07ebbe6c81"}, - {file = "opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6efabcaa9df731f29e5ea9051776715b1bdd1845d7c9530065c7951d2a2899eb"}, - {file = "opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e0a27c19dd1f40ddff94976cfe43066fbbe9dfbb2ec1907d66c19caef42a57b"}, - {file = "opencv_python_headless-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:f447d8acbb0b6f2808da71fddd29c1cdd448d2bc98f72d9bb78a7a898fc9621b"}, - {file = "opencv_python_headless-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:6c304df9caa7a6a5710b91709dd4786bf20a74d57672b3c31f7033cc638174ca"}, -] - -[[package]] -name = "orjson" -version = "3.10.16" -requires_python = ">=3.9" -summary = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" -groups = ["dev"] -files = [ - {file = "orjson-3.10.16-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4cb473b8e79154fa778fb56d2d73763d977be3dcc140587e07dbc545bbfc38f8"}, - {file = "orjson-3.10.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:622a8e85eeec1948690409a19ca1c7d9fd8ff116f4861d261e6ae2094fe59a00"}, - {file = "orjson-3.10.16-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c682d852d0ce77613993dc967e90e151899fe2d8e71c20e9be164080f468e370"}, - {file = "orjson-3.10.16-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c520ae736acd2e32df193bcff73491e64c936f3e44a2916b548da048a48b46b"}, - {file = "orjson-3.10.16-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:134f87c76bfae00f2094d85cfab261b289b76d78c6da8a7a3b3c09d362fd1e06"}, - {file = "orjson-3.10.16-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b59afde79563e2cf37cfe62ee3b71c063fd5546c8e662d7fcfc2a3d5031a5c4c"}, - {file = "orjson-3.10.16-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:113602f8241daaff05d6fad25bd481d54c42d8d72ef4c831bb3ab682a54d9e15"}, - {file = "orjson-3.10.16-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4fc0077d101f8fab4031e6554fc17b4c2ad8fdbc56ee64a727f3c95b379e31da"}, - {file = "orjson-3.10.16-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9c6bf6ff180cd69e93f3f50380224218cfab79953a868ea3908430bcfaf9cb5e"}, - {file = "orjson-3.10.16-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5673eadfa952f95a7cd76418ff189df11b0a9c34b1995dff43a6fdbce5d63bf4"}, - {file = "orjson-3.10.16-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5fe638a423d852b0ae1e1a79895851696cb0d9fa0946fdbfd5da5072d9bb9551"}, - {file = "orjson-3.10.16-cp310-cp310-win32.whl", hash = "sha256:33af58f479b3c6435ab8f8b57999874b4b40c804c7a36b5cc6b54d8f28e1d3dd"}, - {file = "orjson-3.10.16-cp310-cp310-win_amd64.whl", hash = "sha256:0338356b3f56d71293c583350af26f053017071836b07e064e92819ecf1aa055"}, - {file = "orjson-3.10.16-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44fcbe1a1884f8bc9e2e863168b0f84230c3d634afe41c678637d2728ea8e739"}, - {file = "orjson-3.10.16-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78177bf0a9d0192e0b34c3d78bcff7fe21d1b5d84aeb5ebdfe0dbe637b885225"}, - {file = "orjson-3.10.16-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12824073a010a754bb27330cad21d6e9b98374f497f391b8707752b96f72e741"}, - {file = "orjson-3.10.16-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddd41007e56284e9867864aa2f29f3136bb1dd19a49ca43c0b4eda22a579cf53"}, - {file = "orjson-3.10.16-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0877c4d35de639645de83666458ca1f12560d9fa7aa9b25d8bb8f52f61627d14"}, - {file = "orjson-3.10.16-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a09a539e9cc3beead3e7107093b4ac176d015bec64f811afb5965fce077a03c"}, - {file = "orjson-3.10.16-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31b98bc9b40610fec971d9a4d67bb2ed02eec0a8ae35f8ccd2086320c28526ca"}, - {file = "orjson-3.10.16-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0ce243f5a8739f3a18830bc62dc2e05b69a7545bafd3e3249f86668b2bcd8e50"}, - {file = "orjson-3.10.16-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:64792c0025bae049b3074c6abe0cf06f23c8e9f5a445f4bab31dc5ca23dbf9e1"}, - {file = "orjson-3.10.16-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ea53f7e68eec718b8e17e942f7ca56c6bd43562eb19db3f22d90d75e13f0431d"}, - {file = "orjson-3.10.16-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a741ba1a9488c92227711bde8c8c2b63d7d3816883268c808fbeada00400c164"}, - {file = "orjson-3.10.16-cp311-cp311-win32.whl", hash = "sha256:c7ed2c61bb8226384c3fdf1fb01c51b47b03e3f4536c985078cccc2fd19f1619"}, - {file = "orjson-3.10.16-cp311-cp311-win_amd64.whl", hash = "sha256:cd67d8b3e0e56222a2e7b7f7da9031e30ecd1fe251c023340b9f12caca85ab60"}, - {file = "orjson-3.10.16-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6d3444abbfa71ba21bb042caa4b062535b122248259fdb9deea567969140abca"}, - {file = "orjson-3.10.16-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:30245c08d818fdcaa48b7d5b81499b8cae09acabb216fe61ca619876b128e184"}, - {file = "orjson-3.10.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0ba1d0baa71bf7579a4ccdcf503e6f3098ef9542106a0eca82395898c8a500a"}, - {file = "orjson-3.10.16-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb0beefa5ef3af8845f3a69ff2a4aa62529b5acec1cfe5f8a6b4141033fd46ef"}, - {file = "orjson-3.10.16-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6daa0e1c9bf2e030e93c98394de94506f2a4d12e1e9dadd7c53d5e44d0f9628e"}, - {file = "orjson-3.10.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9da9019afb21e02410ef600e56666652b73eb3e4d213a0ec919ff391a7dd52aa"}, - {file = "orjson-3.10.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:daeb3a1ee17b69981d3aae30c3b4e786b0f8c9e6c71f2b48f1aef934f63f38f4"}, - {file = "orjson-3.10.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fed80eaf0e20a31942ae5d0728849862446512769692474be5e6b73123a23b"}, - {file = "orjson-3.10.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73390ed838f03764540a7bdc4071fe0123914c2cc02fb6abf35182d5fd1b7a42"}, - {file = "orjson-3.10.16-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a22bba012a0c94ec02a7768953020ab0d3e2b884760f859176343a36c01adf87"}, - {file = "orjson-3.10.16-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5385bbfdbc90ff5b2635b7e6bebf259652db00a92b5e3c45b616df75b9058e88"}, - {file = "orjson-3.10.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:02c6279016346e774dd92625d46c6c40db687b8a0d685aadb91e26e46cc33e1e"}, - {file = "orjson-3.10.16-cp312-cp312-win32.whl", hash = "sha256:7ca55097a11426db80f79378e873a8c51f4dde9ffc22de44850f9696b7eb0e8c"}, - {file = "orjson-3.10.16-cp312-cp312-win_amd64.whl", hash = "sha256:86d127efdd3f9bf5f04809b70faca1e6836556ea3cc46e662b44dab3fe71f3d6"}, - {file = "orjson-3.10.16-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:148a97f7de811ba14bc6dbc4a433e0341ffd2cc285065199fb5f6a98013744bd"}, - {file = "orjson-3.10.16-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1d960c1bf0e734ea36d0adc880076de3846aaec45ffad29b78c7f1b7962516b8"}, - {file = "orjson-3.10.16-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a318cd184d1269f68634464b12871386808dc8b7c27de8565234d25975a7a137"}, - {file = "orjson-3.10.16-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df23f8df3ef9223d1d6748bea63fca55aae7da30a875700809c500a05975522b"}, - {file = "orjson-3.10.16-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b94dda8dd6d1378f1037d7f3f6b21db769ef911c4567cbaa962bb6dc5021cf90"}, - {file = "orjson-3.10.16-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f12970a26666a8775346003fd94347d03ccb98ab8aa063036818381acf5f523e"}, - {file = "orjson-3.10.16-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15a1431a245d856bd56e4d29ea0023eb4d2c8f71efe914beb3dee8ab3f0cd7fb"}, - {file = "orjson-3.10.16-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c83655cfc247f399a222567d146524674a7b217af7ef8289c0ff53cfe8db09f0"}, - {file = "orjson-3.10.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fa59ae64cb6ddde8f09bdbf7baf933c4cd05734ad84dcf4e43b887eb24e37652"}, - {file = "orjson-3.10.16-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ca5426e5aacc2e9507d341bc169d8af9c3cbe88f4cd4c1cf2f87e8564730eb56"}, - {file = "orjson-3.10.16-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6fd5da4edf98a400946cd3a195680de56f1e7575109b9acb9493331047157430"}, - {file = "orjson-3.10.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:980ecc7a53e567169282a5e0ff078393bac78320d44238da4e246d71a4e0e8f5"}, - {file = "orjson-3.10.16-cp313-cp313-win32.whl", hash = "sha256:28f79944dd006ac540a6465ebd5f8f45dfdf0948ff998eac7a908275b4c1add6"}, - {file = "orjson-3.10.16-cp313-cp313-win_amd64.whl", hash = "sha256:fe0a145e96d51971407cb8ba947e63ead2aa915db59d6631a355f5f2150b56b7"}, - {file = "orjson-3.10.16.tar.gz", hash = "sha256:d2aaa5c495e11d17b9b93205f5fa196737ee3202f000aaebf028dc9a73750f10"}, -] - -[[package]] -name = "overrides" -version = "7.7.0" -requires_python = ">=3.6" -summary = "A decorator to automatically detect mismatch when overriding a method." -groups = ["dev"] -dependencies = [ - "typing; python_version < \"3.5\"", -] -files = [ - {file = "overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49"}, - {file = "overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a"}, -] - -[[package]] -name = "packaging" -version = "25.0" -requires_python = ">=3.8" -summary = "Core utilities for Python packages" -groups = ["dev", "docs", "test"] -files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, -] - -[[package]] -name = "paginate" -version = "0.5.7" -summary = "Divides large result sets into pages for easier browsing" -groups = ["docs"] -files = [ - {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, - {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, -] - -[[package]] -name = "pandas" -version = "2.2.3" -requires_python = ">=3.9" -summary = "Powerful data structures for data analysis, time series, and statistics" -groups = ["dev"] -dependencies = [ - "numpy>=1.22.4; python_version < \"3.11\"", - "numpy>=1.23.2; python_version == \"3.11\"", - "numpy>=1.26.0; python_version >= \"3.12\"", - "python-dateutil>=2.8.2", - "pytz>=2020.1", - "tzdata>=2022.7", -] -files = [ - {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, - {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, - {file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"}, - {file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"}, - {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"}, - {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"}, - {file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"}, - {file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"}, - {file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"}, - {file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"}, - {file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"}, - {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"}, - {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"}, - {file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"}, - {file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"}, - {file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"}, - {file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"}, - {file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"}, - {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"}, - {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"}, - {file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"}, - {file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"}, - {file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"}, - {file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"}, - {file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"}, - {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"}, - {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"}, - {file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"}, - {file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"}, - {file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"}, - {file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"}, - {file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"}, - {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"}, - {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"}, - {file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"}, -] - -[[package]] -name = "pandocfilters" -version = "1.5.1" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -summary = "Utilities for writing pandoc filters in python" -groups = ["dev"] -files = [ - {file = "pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc"}, - {file = "pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e"}, -] - -[[package]] -name = "parso" -version = "0.8.4" -requires_python = ">=3.6" -summary = "A Python Parser" -groups = ["dev"] -files = [ - {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, - {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -requires_python = ">=3.8" -summary = "Utility library for gitignore style pattern matching of file paths." -groups = ["dev", "docs"] -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "pexpect" -version = "4.9.0" -summary = "Pexpect allows easy control of interactive console applications." -groups = ["dev"] -marker = "sys_platform != \"win32\" and sys_platform != \"emscripten\"" -dependencies = [ - "ptyprocess>=0.5", -] -files = [ - {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, - {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, -] - -[[package]] -name = "pillow" -version = "11.2.1" -requires_python = ">=3.9" -summary = "Python Imaging Library (Fork)" -groups = ["dev"] -files = [ - {file = "pillow-11.2.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047"}, - {file = "pillow-11.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95"}, - {file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61"}, - {file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1"}, - {file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c"}, - {file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d"}, - {file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97"}, - {file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579"}, - {file = "pillow-11.2.1-cp310-cp310-win32.whl", hash = "sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d"}, - {file = "pillow-11.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad"}, - {file = "pillow-11.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2"}, - {file = "pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70"}, - {file = "pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf"}, - {file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7"}, - {file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8"}, - {file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600"}, - {file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788"}, - {file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e"}, - {file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e"}, - {file = "pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6"}, - {file = "pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193"}, - {file = "pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7"}, - {file = "pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f"}, - {file = "pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b"}, - {file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d"}, - {file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4"}, - {file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d"}, - {file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4"}, - {file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443"}, - {file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c"}, - {file = "pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3"}, - {file = "pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941"}, - {file = "pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb"}, - {file = "pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28"}, - {file = "pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830"}, - {file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0"}, - {file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1"}, - {file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f"}, - {file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155"}, - {file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14"}, - {file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b"}, - {file = "pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2"}, - {file = "pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691"}, - {file = "pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c"}, - {file = "pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22"}, - {file = "pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7"}, - {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16"}, - {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b"}, - {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406"}, - {file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91"}, - {file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751"}, - {file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9"}, - {file = "pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd"}, - {file = "pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e"}, - {file = "pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681"}, - {file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156"}, - {file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772"}, - {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363"}, - {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0"}, - {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01"}, - {file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193"}, - {file = "pillow-11.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013"}, - {file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed"}, - {file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c"}, - {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd"}, - {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076"}, - {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b"}, - {file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f"}, - {file = "pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044"}, - {file = "pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6"}, -] - -[[package]] -name = "platformdirs" -version = "4.3.7" -requires_python = ">=3.9" -summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -groups = ["dev", "docs"] -files = [ - {file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"}, - {file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"}, -] - -[[package]] -name = "pluggy" -version = "1.5.0" -requires_python = ">=3.8" -summary = "plugin and hook calling mechanisms for python" -groups = ["test"] -files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, -] - -[[package]] -name = "posthog" -version = "4.0.0" -summary = "Integrate PostHog into any python application." -groups = ["dev"] -dependencies = [ - "backoff>=1.10.0", - "distro>=1.5.0", - "monotonic>=1.5", - "python-dateutil>2.1", - "requests<3.0,>=2.7", - "six>=1.5", -] -files = [ - {file = "posthog-4.0.0-py2.py3-none-any.whl", hash = "sha256:8dfc160510a3eddbaac11991e255bba89bf38b630c0958e68279aa4a12fe465b"}, - {file = "posthog-4.0.0.tar.gz", hash = "sha256:f6310a74924f9897a2a07cefb432196e48feb9e923522a48a3d98274bf354b21"}, -] - -[[package]] -name = "prometheus-client" -version = "0.21.1" -requires_python = ">=3.8" -summary = "Python client for the Prometheus monitoring system." -groups = ["dev"] -files = [ - {file = "prometheus_client-0.21.1-py3-none-any.whl", hash = "sha256:594b45c410d6f4f8888940fe80b5cc2521b305a1fafe1c58609ef715a001f301"}, - {file = "prometheus_client-0.21.1.tar.gz", hash = "sha256:252505a722ac04b0456be05c05f75f45d760c2911ffc45f2a06bcaed9f3ae3fb"}, -] - -[[package]] -name = "prompt-toolkit" -version = "3.0.51" -requires_python = ">=3.8" -summary = "Library for building powerful interactive command lines in Python" -groups = ["dev"] -dependencies = [ - "wcwidth", -] -files = [ - {file = "prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07"}, - {file = "prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed"}, -] - -[[package]] -name = "propcache" -version = "0.3.1" -requires_python = ">=3.9" -summary = "Accelerated property cache" -groups = ["dev", "test"] -files = [ - {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98"}, - {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180"}, - {file = "propcache-0.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:730178f476ef03d3d4d255f0c9fa186cb1d13fd33ffe89d39f2cda4da90ceb71"}, - {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967a8eec513dbe08330f10137eacb427b2ca52118769e82ebcfcab0fba92a649"}, - {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b9145c35cc87313b5fd480144f8078716007656093d23059e8993d3a8fa730f"}, - {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e64e948ab41411958670f1093c0a57acfdc3bee5cf5b935671bbd5313bcf229"}, - {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:319fa8765bfd6a265e5fa661547556da381e53274bc05094fc9ea50da51bfd46"}, - {file = "propcache-0.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66d8ccbc902ad548312b96ed8d5d266d0d2c6d006fd0f66323e9d8f2dd49be7"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2d219b0dbabe75e15e581fc1ae796109b07c8ba7d25b9ae8d650da582bed01b0"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd6a55f65241c551eb53f8cf4d2f4af33512c39da5d9777694e9d9c60872f519"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9979643ffc69b799d50d3a7b72b5164a2e97e117009d7af6dfdd2ab906cb72cd"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4cf9e93a81979f1424f1a3d155213dc928f1069d697e4353edb8a5eba67c6259"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2fce1df66915909ff6c824bbb5eb403d2d15f98f1518e583074671a30fe0c21e"}, - {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4d0dfdd9a2ebc77b869a0b04423591ea8823f791293b527dc1bb896c1d6f1136"}, - {file = "propcache-0.3.1-cp310-cp310-win32.whl", hash = "sha256:1f6cc0ad7b4560e5637eb2c994e97b4fa41ba8226069c9277eb5ea7101845b42"}, - {file = "propcache-0.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:47ef24aa6511e388e9894ec16f0fbf3313a53ee68402bc428744a367ec55b833"}, - {file = "propcache-0.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5"}, - {file = "propcache-0.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371"}, - {file = "propcache-0.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da"}, - {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744"}, - {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0"}, - {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5"}, - {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256"}, - {file = "propcache-0.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a"}, - {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9"}, - {file = "propcache-0.3.1-cp311-cp311-win32.whl", hash = "sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005"}, - {file = "propcache-0.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7"}, - {file = "propcache-0.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723"}, - {file = "propcache-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976"}, - {file = "propcache-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b"}, - {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f"}, - {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70"}, - {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7"}, - {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25"}, - {file = "propcache-0.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5"}, - {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7"}, - {file = "propcache-0.3.1-cp312-cp312-win32.whl", hash = "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b"}, - {file = "propcache-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3"}, - {file = "propcache-0.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8"}, - {file = "propcache-0.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f"}, - {file = "propcache-0.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111"}, - {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5"}, - {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb"}, - {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7"}, - {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120"}, - {file = "propcache-0.3.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7"}, - {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef"}, - {file = "propcache-0.3.1-cp313-cp313-win32.whl", hash = "sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24"}, - {file = "propcache-0.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037"}, - {file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f"}, - {file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c"}, - {file = "propcache-0.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc"}, - {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de"}, - {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6"}, - {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7"}, - {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458"}, - {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18"}, - {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a"}, - {file = "propcache-0.3.1-cp313-cp313t-win32.whl", hash = "sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d"}, - {file = "propcache-0.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e"}, - {file = "propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40"}, - {file = "propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf"}, -] - -[[package]] -name = "psutil" -version = "7.0.0" -requires_python = ">=3.6" -summary = "Cross-platform lib for process and system monitoring in Python. NOTE: the syntax of this script MUST be kept compatible with Python 2.7." -groups = ["dev"] -files = [ - {file = "psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25"}, - {file = "psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da"}, - {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91"}, - {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34"}, - {file = "psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993"}, - {file = "psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99"}, - {file = "psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553"}, - {file = "psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456"}, -] - -[[package]] -name = "ptyprocess" -version = "0.7.0" -summary = "Run a subprocess in a pseudo terminal" -groups = ["dev"] -marker = "sys_platform != \"win32\" and sys_platform != \"emscripten\" or os_name != \"nt\"" -files = [ - {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, - {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, -] - -[[package]] -name = "pure-eval" -version = "0.2.3" -summary = "Safely evaluate AST nodes without side effects" -groups = ["dev"] -files = [ - {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, - {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, -] - -[[package]] -name = "py-cpuinfo" -version = "9.0.0" -summary = "Get CPU info with pure Python" -groups = ["dev"] -files = [ - {file = "py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690"}, - {file = "py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"}, -] - -[[package]] -name = "pyautogui" -version = "0.9.54" -summary = "PyAutoGUI lets Python control the mouse and keyboard, and other GUI automation tasks. For Windows, macOS, and Linux, on Python 3 and 2." -groups = ["dev"] -dependencies = [ - "mouseinfo", - "pygetwindow>=0.0.5", - "pymsgbox", - "pyobjc-core; platform_system == \"Darwin\"", - "pyobjc-framework-quartz; platform_system == \"Darwin\"", - "pyscreeze>=0.1.21", - "python-xlib; platform_system == \"Linux\" and python_version < \"3.0\"", - "python3-Xlib; platform_system == \"Linux\" and python_version >= \"3.0\"", - "pytweening>=1.0.4", -] -files = [ - {file = "PyAutoGUI-0.9.54.tar.gz", hash = "sha256:dd1d29e8fd118941cb193f74df57e5c6ff8e9253b99c7b04f39cfc69f3ae04b2"}, -] - -[[package]] -name = "pyclipper" -version = "1.3.0.post6" -summary = "Cython wrapper for the C++ translation of the Angus Johnson's Clipper library (ver. 6.4.2)" -groups = ["dev"] -files = [ - {file = "pyclipper-1.3.0.post6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fa0f5e78cfa8262277bb3d0225537b3c2a90ef68fd90a229d5d24cf49955dcf4"}, - {file = "pyclipper-1.3.0.post6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a01f182d8938c1dc515e8508ed2442f7eebd2c25c7d5cb29281f583c1a8008a4"}, - {file = "pyclipper-1.3.0.post6-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:640f20975727994d4abacd07396f564e9e5665ba5cb66ceb36b300c281f84fa4"}, - {file = "pyclipper-1.3.0.post6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a63002f6bb0f1efa87c0b81634cbb571066f237067e23707dabf746306c92ba5"}, - {file = "pyclipper-1.3.0.post6-cp310-cp310-win32.whl", hash = "sha256:106b8622cd9fb07d80cbf9b1d752334c55839203bae962376a8c59087788af26"}, - {file = "pyclipper-1.3.0.post6-cp310-cp310-win_amd64.whl", hash = "sha256:9699e98862dadefd0bea2360c31fa61ca553c660cbf6fb44993acde1b959f58f"}, - {file = "pyclipper-1.3.0.post6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4247e7c44b34c87acbf38f99d48fb1acaf5da4a2cf4dcd601a9b24d431be4ef"}, - {file = "pyclipper-1.3.0.post6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:851b3e58106c62a5534a1201295fe20c21714dee2eda68081b37ddb0367e6caa"}, - {file = "pyclipper-1.3.0.post6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16cc1705a915896d2aff52131c427df02265631279eac849ebda766432714cc0"}, - {file = "pyclipper-1.3.0.post6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ace1f0753cf71c5c5f6488b8feef5dd0fa8b976ad86b24bb51f708f513df4aac"}, - {file = "pyclipper-1.3.0.post6-cp311-cp311-win32.whl", hash = "sha256:dbc828641667142751b1127fd5c4291663490cf05689c85be4c5bcc89aaa236a"}, - {file = "pyclipper-1.3.0.post6-cp311-cp311-win_amd64.whl", hash = "sha256:1c03f1ae43b18ee07730c3c774cc3cf88a10c12a4b097239b33365ec24a0a14a"}, - {file = "pyclipper-1.3.0.post6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6363b9d79ba1b5d8f32d1623e797c1e9f994600943402e68d5266067bdde173e"}, - {file = "pyclipper-1.3.0.post6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32cd7fb9c1c893eb87f82a072dbb5e26224ea7cebbad9dc306d67e1ac62dd229"}, - {file = "pyclipper-1.3.0.post6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3aab10e3c10ed8fa60c608fb87c040089b83325c937f98f06450cf9fcfdaf1d"}, - {file = "pyclipper-1.3.0.post6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58eae2ff92a8cae1331568df076c4c5775bf946afab0068b217f0cf8e188eb3c"}, - {file = "pyclipper-1.3.0.post6-cp312-cp312-win32.whl", hash = "sha256:793b0aa54b914257aa7dc76b793dd4dcfb3c84011d48df7e41ba02b571616eaf"}, - {file = "pyclipper-1.3.0.post6-cp312-cp312-win_amd64.whl", hash = "sha256:d3f9da96f83b8892504923beb21a481cd4516c19be1d39eb57a92ef1c9a29548"}, - {file = "pyclipper-1.3.0.post6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f129284d2c7bcd213d11c0f35e1ae506a1144ce4954e9d1734d63b120b0a1b58"}, - {file = "pyclipper-1.3.0.post6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:188fbfd1d30d02247f92c25ce856f5f3c75d841251f43367dbcf10935bc48f38"}, - {file = "pyclipper-1.3.0.post6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6d129d0c2587f2f5904d201a4021f859afbb45fada4261c9fdedb2205b09d23"}, - {file = "pyclipper-1.3.0.post6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c9c80b5c46eef38ba3f12dd818dc87f5f2a0853ba914b6f91b133232315f526"}, - {file = "pyclipper-1.3.0.post6-cp313-cp313-win32.whl", hash = "sha256:b15113ec4fc423b58e9ae80aa95cf5a0802f02d8f02a98a46af3d7d66ff0cc0e"}, - {file = "pyclipper-1.3.0.post6-cp313-cp313-win_amd64.whl", hash = "sha256:e5ff68fa770ac654c7974fc78792978796f068bd274e95930c0691c31e192889"}, - {file = "pyclipper-1.3.0.post6.tar.gz", hash = "sha256:42bff0102fa7a7f2abdd795a2594654d62b786d0c6cd67b72d469114fdeb608c"}, -] - -[[package]] -name = "pycparser" -version = "2.22" -requires_python = ">=3.8" -summary = "C parser in Python" -groups = ["dev"] -files = [ - {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, - {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, -] - -[[package]] -name = "pydantic" -version = "2.11.3" -requires_python = ">=3.9" -summary = "Data validation using Python type hints" -groups = ["dev"] -dependencies = [ - "annotated-types>=0.6.0", - "pydantic-core==2.33.1", - "typing-extensions>=4.12.2", - "typing-inspection>=0.4.0", -] -files = [ - {file = "pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f"}, - {file = "pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3"}, -] - -[[package]] -name = "pydantic-core" -version = "2.33.1" -requires_python = ">=3.9" -summary = "Core functionality for Pydantic validation and serialization" -groups = ["dev"] -dependencies = [ - "typing-extensions!=4.7.0,>=4.6.0", -] -files = [ - {file = "pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26"}, - {file = "pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927"}, - {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5183e4f6a2d468787243ebcd70cf4098c247e60d73fb7d68d5bc1e1beaa0c4db"}, - {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:398a38d323f37714023be1e0285765f0a27243a8b1506b7b7de87b647b517e48"}, - {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3776f0001b43acebfa86f8c64019c043b55cc5a6a2e313d728b5c95b46969"}, - {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c566dd9c5f63d22226409553531f89de0cac55397f2ab8d97d6f06cfce6d947e"}, - {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d5f3acc81452c56895e90643a625302bd6be351e7010664151cc55b7b97f89"}, - {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3a07fadec2a13274a8d861d3d37c61e97a816beae717efccaa4b36dfcaadcde"}, - {file = "pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f99aeda58dce827f76963ee87a0ebe75e648c72ff9ba1174a253f6744f518f65"}, - {file = "pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:902dbc832141aa0ec374f4310f1e4e7febeebc3256f00dc359a9ac3f264a45dc"}, - {file = "pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fe44d56aa0b00d66640aa84a3cbe80b7a3ccdc6f0b1ca71090696a6d4777c091"}, - {file = "pydantic_core-2.33.1-cp310-cp310-win32.whl", hash = "sha256:ed3eb16d51257c763539bde21e011092f127a2202692afaeaccb50db55a31383"}, - {file = "pydantic_core-2.33.1-cp310-cp310-win_amd64.whl", hash = "sha256:694ad99a7f6718c1a498dc170ca430687a39894a60327f548e02a9c7ee4b6504"}, - {file = "pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24"}, - {file = "pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30"}, - {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595"}, - {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e"}, - {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a"}, - {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505"}, - {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f"}, - {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77"}, - {file = "pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961"}, - {file = "pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1"}, - {file = "pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c"}, - {file = "pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896"}, - {file = "pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83"}, - {file = "pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89"}, - {file = "pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8"}, - {file = "pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498"}, - {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939"}, - {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d"}, - {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e"}, - {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3"}, - {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d"}, - {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b"}, - {file = "pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39"}, - {file = "pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a"}, - {file = "pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db"}, - {file = "pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda"}, - {file = "pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4"}, - {file = "pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea"}, - {file = "pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a"}, - {file = "pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266"}, - {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3"}, - {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a"}, - {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516"}, - {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764"}, - {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d"}, - {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4"}, - {file = "pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde"}, - {file = "pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e"}, - {file = "pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd"}, - {file = "pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f"}, - {file = "pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40"}, - {file = "pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523"}, - {file = "pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d"}, - {file = "pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c"}, - {file = "pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18"}, - {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c834f54f8f4640fd7e4b193f80eb25a0602bba9e19b3cd2fc7ffe8199f5ae02"}, - {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:049e0de24cf23766f12cc5cc71d8abc07d4a9deb9061b334b62093dedc7cb068"}, - {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a28239037b3d6f16916a4c831a5a0eadf856bdd6d2e92c10a0da3a59eadcf3e"}, - {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d3da303ab5f378a268fa7d45f37d7d85c3ec19769f28d2cc0c61826a8de21fe"}, - {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25626fb37b3c543818c14821afe0fd3830bc327a43953bc88db924b68c5723f1"}, - {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3ab2d36e20fbfcce8f02d73c33a8a7362980cff717926bbae030b93ae46b56c7"}, - {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2f9284e11c751b003fd4215ad92d325d92c9cb19ee6729ebd87e3250072cdcde"}, - {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:048c01eee07d37cbd066fc512b9d8b5ea88ceeb4e629ab94b3e56965ad655add"}, - {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5ccd429694cf26af7997595d627dd2637e7932214486f55b8a357edaac9dae8c"}, - {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a"}, - {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc"}, - {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b"}, - {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe"}, - {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5"}, - {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761"}, - {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850"}, - {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544"}, - {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f995719707e0e29f0f41a8aa3bcea6e761a36c9136104d3189eafb83f5cec5e5"}, - {file = "pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df"}, -] - -[[package]] -name = "pydantic-settings" -version = "2.9.1" -requires_python = ">=3.9" -summary = "Settings management using Pydantic" -groups = ["dev"] -dependencies = [ - "pydantic>=2.7.0", - "python-dotenv>=0.21.0", - "typing-inspection>=0.4.0", -] -files = [ - {file = "pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef"}, - {file = "pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268"}, -] - -[[package]] -name = "pydub" -version = "0.25.1" -summary = "Manipulate audio with an simple and easy high level interface" -groups = ["dev"] -files = [ - {file = "pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6"}, - {file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"}, -] - -[[package]] -name = "pygetwindow" -version = "0.0.9" -summary = "A simple, cross-platform module for obtaining GUI information on application's windows." -groups = ["dev"] -dependencies = [ - "pyrect", -] -files = [ - {file = "PyGetWindow-0.0.9.tar.gz", hash = "sha256:17894355e7d2b305cd832d717708384017c1698a90ce24f6f7fbf0242dd0a688"}, -] - -[[package]] -name = "pygments" -version = "2.19.1" -requires_python = ">=3.8" -summary = "Pygments is a syntax highlighting package written in Python." -groups = ["dev", "docs"] -files = [ - {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, - {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, -] - -[[package]] -name = "pylume" -version = "0.1.8" -requires_python = ">=3.9" -editable = true -path = "./libs/pylume" -summary = "Python SDK for lume - run macOS and Linux VMs on Apple Silicon" -groups = ["dev"] -dependencies = [ - "pydantic>=2.11.1", -] - -[[package]] -name = "pymdown-extensions" -version = "10.14.3" -requires_python = ">=3.8" -summary = "Extension pack for Python Markdown." -groups = ["docs"] -dependencies = [ - "markdown>=3.6", - "pyyaml", -] -files = [ - {file = "pymdown_extensions-10.14.3-py3-none-any.whl", hash = "sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9"}, - {file = "pymdown_extensions-10.14.3.tar.gz", hash = "sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b"}, -] - -[[package]] -name = "pymsgbox" -version = "1.0.9" -summary = "A simple, cross-platform, pure Python module for JavaScript-like message boxes." -groups = ["dev"] -files = [ - {file = "PyMsgBox-1.0.9.tar.gz", hash = "sha256:2194227de8bff7a3d6da541848705a155dcbb2a06ee120d9f280a1d7f51263ff"}, -] - -[[package]] -name = "pyobjc-core" -version = "11.0" -requires_python = ">=3.8" -summary = "Python<->ObjC Interoperability Module" -groups = ["dev"] -marker = "platform_system == \"Darwin\" or sys_platform == \"darwin\"" -files = [ - {file = "pyobjc_core-11.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:10866b3a734d47caf48e456eea0d4815c2c9b21856157db5917b61dee06893a1"}, - {file = "pyobjc_core-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:50675c0bb8696fe960a28466f9baf6943df2928a1fd85625d678fa2f428bd0bd"}, - {file = "pyobjc_core-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a03061d4955c62ddd7754224a80cdadfdf17b6b5f60df1d9169a3b1b02923f0b"}, - {file = "pyobjc_core-11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c338c1deb7ab2e9436d4175d1127da2eeed4a1b564b3d83b9f3ae4844ba97e86"}, - {file = "pyobjc_core-11.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b4e9dc4296110f251a4033ff3f40320b35873ea7f876bd29a1c9705bb5e08c59"}, - {file = "pyobjc_core-11.0.tar.gz", hash = "sha256:63bced211cb8a8fb5c8ff46473603da30e51112861bd02c438fbbbc8578d9a70"}, -] - -[[package]] -name = "pyobjc-framework-applicationservices" -version = "11.0" -requires_python = ">=3.9" -summary = "Wrappers for the framework ApplicationServices on macOS" -groups = ["dev"] -marker = "sys_platform == \"darwin\"" -dependencies = [ - "pyobjc-core>=11.0", - "pyobjc-framework-Cocoa>=11.0", - "pyobjc-framework-CoreText>=11.0", - "pyobjc-framework-Quartz>=11.0", -] -files = [ - {file = "pyobjc_framework_ApplicationServices-11.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bc8f34b5b59ffd3c210ae883d794345c1197558ff3da0f5800669cf16435271e"}, - {file = "pyobjc_framework_ApplicationServices-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:61a99eef23abb704257310db4f5271137707e184768f6407030c01de4731b67b"}, - {file = "pyobjc_framework_ApplicationServices-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5fbeb425897d6129471d451ec61a29ddd5b1386eb26b1dd49cb313e34616ee21"}, - {file = "pyobjc_framework_ApplicationServices-11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:59becf3cd87a4f4cedf4be02ff6cf46ed736f5c1123ce629f788aaafad91eff0"}, - {file = "pyobjc_framework_ApplicationServices-11.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:44b466e8745fb49e8ac20f29f2ffd7895b45e97aa63a844b2a80a97c3a34346f"}, - {file = "pyobjc_framework_applicationservices-11.0.tar.gz", hash = "sha256:d6ea18dfc7d5626a3ecf4ac72d510405c0d3a648ca38cae8db841acdebecf4d2"}, -] - -[[package]] -name = "pyobjc-framework-cocoa" -version = "11.0" -requires_python = ">=3.9" -summary = "Wrappers for the Cocoa frameworks on macOS" -groups = ["dev"] -marker = "sys_platform == \"darwin\" or platform_system == \"Darwin\"" -dependencies = [ - "pyobjc-core>=11.0", -] -files = [ - {file = "pyobjc_framework_Cocoa-11.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fbc65f260d617d5463c7fb9dbaaffc23c9a4fabfe3b1a50b039b61870b8daefd"}, - {file = "pyobjc_framework_Cocoa-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3ea7be6e6dd801b297440de02d312ba3fa7fd3c322db747ae1cb237e975f5d33"}, - {file = "pyobjc_framework_Cocoa-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:280a577b83c68175a28b2b7138d1d2d3111f2b2b66c30e86f81a19c2b02eae71"}, - {file = "pyobjc_framework_Cocoa-11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:15b2bd977ed340074f930f1330f03d42912d5882b697d78bd06f8ebe263ef92e"}, - {file = "pyobjc_framework_Cocoa-11.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5750001db544e67f2b66f02067d8f0da96bb2ef71732bde104f01b8628f9d7ea"}, - {file = "pyobjc_framework_cocoa-11.0.tar.gz", hash = "sha256:00346a8cb81ad7b017b32ff7bf596000f9faa905807b1bd234644ebd47f692c5"}, -] - -[[package]] -name = "pyobjc-framework-coretext" -version = "11.0" -requires_python = ">=3.9" -summary = "Wrappers for the framework CoreText on macOS" -groups = ["dev"] -marker = "sys_platform == \"darwin\"" -dependencies = [ - "pyobjc-core>=11.0", - "pyobjc-framework-Cocoa>=11.0", - "pyobjc-framework-Quartz>=11.0", -] -files = [ - {file = "pyobjc_framework_CoreText-11.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6939b4ea745b349b5c964823a2071f155f5defdc9b9fc3a13f036d859d7d0439"}, - {file = "pyobjc_framework_CoreText-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:56a4889858308b0d9f147d568b4d91c441cc0ffd332497cb4f709bb1990450c1"}, - {file = "pyobjc_framework_CoreText-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb90e7f370b3fd7cb2fb442e3dc63fedf0b4af6908db1c18df694d10dc94669d"}, - {file = "pyobjc_framework_CoreText-11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7947f755782456bd663e0b00c7905eeffd10f839f0bf2af031f68ded6a1ea360"}, - {file = "pyobjc_framework_CoreText-11.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5356116bae33ec49f1f212c301378a7d08000440a2d6a7281aab351945528ab9"}, - {file = "pyobjc_framework_coretext-11.0.tar.gz", hash = "sha256:a68437153e627847e3898754dd3f13ae0cb852246b016a91f9c9cbccb9f91a43"}, -] - -[[package]] -name = "pyobjc-framework-quartz" -version = "11.0" -requires_python = ">=3.9" -summary = "Wrappers for the Quartz frameworks on macOS" -groups = ["dev"] -marker = "sys_platform == \"darwin\" or platform_system == \"Darwin\"" -dependencies = [ - "pyobjc-core>=11.0", - "pyobjc-framework-Cocoa>=11.0", -] -files = [ - {file = "pyobjc_framework_Quartz-11.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da3ab13c9f92361959b41b0ad4cdd41ae872f90a6d8c58a9ed699bc08ab1c45c"}, - {file = "pyobjc_framework_Quartz-11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d251696bfd8e8ef72fbc90eb29fec95cb9d1cc409008a183d5cc3246130ae8c2"}, - {file = "pyobjc_framework_Quartz-11.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cb4a9f2d9d580ea15e25e6b270f47681afb5689cafc9e25712445ce715bcd18e"}, - {file = "pyobjc_framework_Quartz-11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:973b4f9b8ab844574461a038bd5269f425a7368d6e677e3cc81fcc9b27b65498"}, - {file = "pyobjc_framework_Quartz-11.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:66ab58d65348863b8707e63b2ec5cdc54569ee8189d1af90d52f29f5fdf6272c"}, - {file = "pyobjc_framework_quartz-11.0.tar.gz", hash = "sha256:3205bf7795fb9ae34747f701486b3db6dfac71924894d1f372977c4d70c3c619"}, -] - -[[package]] -name = "pyparsing" -version = "3.2.3" -requires_python = ">=3.9" -summary = "pyparsing module - Classes and methods to define and execute parsing grammars" -groups = ["dev"] -files = [ - {file = "pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf"}, - {file = "pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be"}, -] - -[[package]] -name = "pyperclip" -version = "1.9.0" -summary = "A cross-platform clipboard module for Python. (Only handles plain text for now.)" -groups = ["dev"] -files = [ - {file = "pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310"}, -] - -[[package]] -name = "pyrect" -version = "0.2.0" -summary = "PyRect is a simple module with a Rect class for Pygame-like rectangular areas." -groups = ["dev"] -files = [ - {file = "PyRect-0.2.0.tar.gz", hash = "sha256:f65155f6df9b929b67caffbd57c0947c5ae5449d3b580d178074bffb47a09b78"}, -] - -[[package]] -name = "pyscreeze" -version = "1.0.1" -summary = "A simple, cross-platform screenshot module for Python 2 and 3." -groups = ["dev"] -dependencies = [ - "Pillow<6.0.0,>=2.5.0; python_version == \"3.4\"", - "Pillow<8.0.0,>=3.2.0; python_version == \"3.5\"", - "Pillow<9.0.0,>=8.3.2; python_version == \"3.6\"", - "Pillow>=9.2.0; python_version == \"3.10\"", - "Pillow>=9.2.0; python_version == \"3.7\"", - "Pillow>=9.2.0; python_version == \"3.8\"", - "Pillow>=9.2.0; python_version == \"3.9\"", - "Pillow>=9.3.0; python_version == \"3.11\"", -] -files = [ - {file = "pyscreeze-1.0.1.tar.gz", hash = "sha256:cf1662710f1b46aa5ff229ee23f367da9e20af4a78e6e365bee973cad0ead4be"}, -] - -[[package]] -name = "pytest" -version = "8.3.5" -requires_python = ">=3.8" -summary = "pytest: simple powerful testing with Python" -groups = ["test"] -dependencies = [ - "colorama; sys_platform == \"win32\"", - "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", - "iniconfig", - "packaging", - "pluggy<2,>=1.5", - "tomli>=1; python_version < \"3.11\"", -] -files = [ - {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, - {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, -] - -[[package]] -name = "pytest-asyncio" -version = "0.26.0" -requires_python = ">=3.9" -summary = "Pytest support for asyncio" -groups = ["test"] -dependencies = [ - "pytest<9,>=8.2", - "typing-extensions>=4.12; python_version < \"3.10\"", -] -files = [ - {file = "pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0"}, - {file = "pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f"}, -] - -[[package]] -name = "pytest-cov" -version = "6.1.1" -requires_python = ">=3.9" -summary = "Pytest plugin for measuring coverage." -groups = ["test"] -dependencies = [ - "coverage[toml]>=7.5", - "pytest>=4.6", -] -files = [ - {file = "pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde"}, - {file = "pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a"}, -] - -[[package]] -name = "pytest-mock" -version = "3.14.0" -requires_python = ">=3.8" -summary = "Thin-wrapper around the mock package for easier use with pytest" -groups = ["test"] -dependencies = [ - "pytest>=6.2.5", -] -files = [ - {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, - {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, -] - -[[package]] -name = "pytest-xdist" -version = "3.6.1" -requires_python = ">=3.8" -summary = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" -groups = ["test"] -dependencies = [ - "execnet>=2.1", - "pytest>=7.0.0", -] -files = [ - {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, - {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, -] - -[[package]] -name = "python-bidi" -version = "0.6.6" -summary = "Python Bidi layout wrapping the Rust crate unicode-bidi" -groups = ["dev"] -files = [ - {file = "python_bidi-0.6.6-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:09d4da6b5851d0df01d7313a11d22f308fdfb0e12461f7262e0f55c521ccc0f1"}, - {file = "python_bidi-0.6.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:493a844891e23264411b01df58ba77d5dbb0045da3787f4195f50a56bfb847d9"}, - {file = "python_bidi-0.6.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a4f4c664b2594d2d6be6a31c9254e784d6d5c1b17edfdccb5f0fac317a1cd5e"}, - {file = "python_bidi-0.6.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b53b8b061b67908b5b436abede8c450c8d2fa965cb713d541688f552b4cfa3d3"}, - {file = "python_bidi-0.6.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b144a1b8766fa6a536cc0feb6fdd29d91af7a82a0c09d89db5fc0b79d5678d7d"}, - {file = "python_bidi-0.6.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:41fde9b4bb45c0e1b3283599e7539c82624ef8a8d3115da76b06160d923aab09"}, - {file = "python_bidi-0.6.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de020488c334c31916ee7526c1a867bf632516c1c2a0420d14d10b79f00761c7"}, - {file = "python_bidi-0.6.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:27cf629a0ef983a25cfd62c6238ee1e742e35552409d5c1b43f6d22945adc4c2"}, - {file = "python_bidi-0.6.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9a9de76229ac22cb6bd40b56a8f7f0c42cbdff985dbd14b65bac955acf070594"}, - {file = "python_bidi-0.6.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:2150ac84f7b15f00f8cd9e29fee7edb4639b7ed2cd9e3d23e2dfd83098f719b7"}, - {file = "python_bidi-0.6.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dc8b0566cef5277f127a80e7546b52393050e5a572f08a352ca220e3f94807cf"}, - {file = "python_bidi-0.6.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3564e574db1a0b3826ed6e646dc7206602189c31194d8da412007477ce653174"}, - {file = "python_bidi-0.6.6-cp310-cp310-win32.whl", hash = "sha256:92eb89f9d8aa0c877cb49fc6356c7f5566e819ea29306992e26be59a5ce468d7"}, - {file = "python_bidi-0.6.6-cp310-cp310-win_amd64.whl", hash = "sha256:1d627f8cfeba70fe4e0ec27b35615c938a483cbef2d9eb7e1e42400d2196019e"}, - {file = "python_bidi-0.6.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:da4949496e563b51f53ff34aad5a9f4c3aaf06f4180cf3bcb42bec649486c8f1"}, - {file = "python_bidi-0.6.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c48a755ca8ba3f2b242d6795d4a60e83ca580cc4fa270a3aaa8af05d93b7ba7f"}, - {file = "python_bidi-0.6.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76a1cd320993ba3e91a567e97f057a03f2c6b493096b3fff8b5630f51a38e7eb"}, - {file = "python_bidi-0.6.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e8bf3e396f9ebe8f4f81e92fa4c98c50160d60c58964b89c8ff4ee0c482befaa"}, - {file = "python_bidi-0.6.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2a49b506ed21f762ebf332de6de689bc4912e24dcc3b85f120b34e5f01e541a"}, - {file = "python_bidi-0.6.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3428331e7ce0d58c15b5a57e18a43a12e28f8733086066e6fd75b0ded80e1cae"}, - {file = "python_bidi-0.6.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35adfb9fed3e72b9043a5c00b6ab69e4b33d53d2d8f8b9f60d4df700f77bc2c0"}, - {file = "python_bidi-0.6.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:589c5b24a8c4b5e07a1e97654020734bf16ed01a4353911ab663a37aaf1c281d"}, - {file = "python_bidi-0.6.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:994534e47260d712c3b3291a6ab55b46cdbfd78a879ef95d14b27bceebfd4049"}, - {file = "python_bidi-0.6.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:00622f54a80826a918b22a2d6d5481bb3f669147e17bac85c81136b6ffbe7c06"}, - {file = "python_bidi-0.6.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:965e6f2182e7b9352f2d79221f6c49502a307a9778d7d87d82dc36bb1ffecbab"}, - {file = "python_bidi-0.6.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:53d7d3a550d176df99dd0bb0cc2da16b40634f11c8b9f5715777441d679c0a62"}, - {file = "python_bidi-0.6.6-cp311-cp311-win32.whl", hash = "sha256:b271cd05cb40f47eb4600de79a8e47f8579d81ce35f5650b39b7860d018c3ece"}, - {file = "python_bidi-0.6.6-cp311-cp311-win_amd64.whl", hash = "sha256:4ff1eba0ff87e04bd35d7e164203ad6e5ce19f0bac0bdf673134c0b78d919608"}, - {file = "python_bidi-0.6.6-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:166060a31c10aa3ffadd52cf10a3c9c2b8d78d844e0f2c5801e2ed511d3ec316"}, - {file = "python_bidi-0.6.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8706addd827840c2c3b3a9963060d9b979b43801cc9be982efa9644facd3ed26"}, - {file = "python_bidi-0.6.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69c02316a4f72a168ea6f66b90d845086e2f2d2de6b08eb32c576db36582177c"}, - {file = "python_bidi-0.6.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a525bcb77b8edbfdcf8b199dbed24556e6d1436af8f5fa392f6cdc93ed79b4af"}, - {file = "python_bidi-0.6.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb186c8da4bdc953893504bba93f41d5b412fd767ba5661ff606f22950ec609"}, - {file = "python_bidi-0.6.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25fa21b46dc80ac7099d2dee424b634eb1f76b2308d518e505a626c55cdbf7b1"}, - {file = "python_bidi-0.6.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b31f5562839e7ecea881ba337f9d39716e2e0e6b3ba395e824620ee5060050ff"}, - {file = "python_bidi-0.6.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fb750d3d5ac028e8afd62d000928a2110dbca012fee68b1a325a38caa03dc50b"}, - {file = "python_bidi-0.6.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b5f648ee8e9f4ac0400f71e671934b39837d7031496e0edde867a303344d758"}, - {file = "python_bidi-0.6.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c4c0255940e6ff98fb05f9d5de3ffcaab7b60d821d4ca072b50c4f871b036562"}, - {file = "python_bidi-0.6.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e7e36601edda15e67527560b1c00108b0d27831260b6b251cf7c6dd110645c03"}, - {file = "python_bidi-0.6.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07c9f000671b187319bacebb9e98d8b75005ccd16aa41b9d4411e66813c467bb"}, - {file = "python_bidi-0.6.6-cp312-cp312-win32.whl", hash = "sha256:57c0ca449a116c4f804422111b3345281c4e69c733c4556fa216644ec9907078"}, - {file = "python_bidi-0.6.6-cp312-cp312-win_amd64.whl", hash = "sha256:f60afe457a37bd908fdc7b520c07620b1a7cc006e08b6e3e70474025b4f5e5c7"}, - {file = "python_bidi-0.6.6-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:61cf12f6b7d0b9bb37838a5f045e6acbd91e838b57f0369c55319bb3969ffa4d"}, - {file = "python_bidi-0.6.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:33bd0ba5eedf18315a1475ac0f215b5134e48011b7320aedc2fb97df31d4e5bf"}, - {file = "python_bidi-0.6.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c9f798dd49b24bb1a9d90f065ef25c7bffa94c04c554f1fc02d0aea0a9b10b0"}, - {file = "python_bidi-0.6.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43a0409570c618d93706dc875b1d33b4adfe67144f6f2ebeb32d85d8bbdb85ed"}, - {file = "python_bidi-0.6.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ada1aecd32773c61b16f7c9f74d9ec1b57ea433e2083e08ca387c5cd4b0ceaed"}, - {file = "python_bidi-0.6.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:125a815f2b20313a2f6d331aa84abdd07de7d270985b056e6729390a4cda90df"}, - {file = "python_bidi-0.6.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:183fee39bd2de787f632376bd5ba0d5f1daf6a09d3ebfaa211df25d62223e531"}, - {file = "python_bidi-0.6.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4e08753d32d633f5ecb5eb02624272eeffaa6d5c6f4f9ddf012637bcaabfc0a"}, - {file = "python_bidi-0.6.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d1dcd7a82ae00b86821fce627e310791f56da90924f15877cfda844e340679de"}, - {file = "python_bidi-0.6.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5506ba56380140b3cb3504029de014d21eb8874c5e081d88495f8775f6ed90bc"}, - {file = "python_bidi-0.6.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:207b0a7082ec38045910d37700a0dd73c10d4ffccb22a4fd0391d7e9ce241672"}, - {file = "python_bidi-0.6.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:686642a52acdeffb1d9a593a284d07b175c63877c596fa3ccceeb2649ced1dd8"}, - {file = "python_bidi-0.6.6-cp313-cp313-win32.whl", hash = "sha256:485f2ee109e7aa73efc165b90a6d90da52546801413540c08b7133fe729d5e0a"}, - {file = "python_bidi-0.6.6-cp313-cp313-win_amd64.whl", hash = "sha256:63f7a9eaec31078e7611ab958b6e18e796c05b63ca50c1f7298311dc1e15ac3e"}, - {file = "python_bidi-0.6.6-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fd9bf9736269ad5cb0d215308fd44e1e02fe591cb9fbb7927d83492358c7ed5f"}, - {file = "python_bidi-0.6.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d941a6a8a7159982d904982cfe0feb0a794913c5592d8137ccae0d518b2575e4"}, - {file = "python_bidi-0.6.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0e715b500b09cefccaddb7087978dcd755443b9620aa1cc7b441824253cf2b8"}, - {file = "python_bidi-0.6.6-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4142467ec0caa063aca894ca8f1e8a4d9ca6834093c06b0ad5e7aa98dc801079"}, - {file = "python_bidi-0.6.6-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2f227ee564e0241e57269043bdfa13025d08d0919b349f5c686e8cfc0540dbf"}, - {file = "python_bidi-0.6.6-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00081439e969c9d9d2ede8eccef4e91397f601931c4f02864edccb760c8f1db5"}, - {file = "python_bidi-0.6.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:804c74d070f4e85c6976e55cdbb3f4ead5ec5d7ea0cfad8f18f5464be5174ec9"}, - {file = "python_bidi-0.6.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0781c3c63b4bc3b37273de2076cb9b875436ae19be0ff04752914d02a4375790"}, - {file = "python_bidi-0.6.6-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:39eed023add8c53684f1de96cb72b4309cc4d412745f59b5d0dab48e6b88317b"}, - {file = "python_bidi-0.6.6-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:91a8cb8feac5d0042e2897042fe7bbbeab5dea1ab785f4b7d0c0bbbf6bc7aefd"}, - {file = "python_bidi-0.6.6-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:a6ac2a3ec5ccc3736e29bb201f27bd33707bfde774d3d222826aa181552590b2"}, - {file = "python_bidi-0.6.6-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6dfa55611022f95058bb7deb2ac20755ae8abbe1104f87515f561e4a56944ba1"}, - {file = "python_bidi-0.6.6.tar.gz", hash = "sha256:07db4c7da502593bd6e39c07b3a38733704070de0cbf92a7b7277b7be8867dd9"}, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -summary = "Extensions to the standard Python datetime module" -groups = ["dev", "docs"] -dependencies = [ - "six>=1.5", -] -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[[package]] -name = "python-dotenv" -version = "1.1.0" -requires_python = ">=3.9" -summary = "Read key-value pairs from a .env file and set them as environment variables" -groups = ["dev"] -files = [ - {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, - {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, -] - -[[package]] -name = "python-json-logger" -version = "3.3.0" -requires_python = ">=3.8" -summary = "JSON Log Formatter for the Python Logging Package" -groups = ["dev"] -dependencies = [ - "typing-extensions; python_version < \"3.10\"", -] -files = [ - {file = "python_json_logger-3.3.0-py3-none-any.whl", hash = "sha256:dd980fae8cffb24c13caf6e158d3d61c0d6d22342f932cb6e9deedab3d35eec7"}, - {file = "python_json_logger-3.3.0.tar.gz", hash = "sha256:12b7e74b17775e7d565129296105bbe3910842d9d0eb083fc83a6a617aa8df84"}, -] - -[[package]] -name = "python-multipart" -version = "0.0.20" -requires_python = ">=3.8" -summary = "A streaming multipart parser for Python" -groups = ["dev"] -files = [ - {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, - {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, -] - -[[package]] -name = "python-xlib" -version = "0.33" -summary = "Python X Library" -groups = ["dev"] -marker = "sys_platform == \"linux\"" -dependencies = [ - "six>=1.10.0", -] -files = [ - {file = "python-xlib-0.33.tar.gz", hash = "sha256:55af7906a2c75ce6cb280a584776080602444f75815a7aff4d287bb2d7018b32"}, - {file = "python_xlib-0.33-py2.py3-none-any.whl", hash = "sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398"}, -] - -[[package]] -name = "python3-xlib" -version = "0.15" -summary = "Python3 X Library" -groups = ["dev"] -marker = "platform_system == \"Linux\" and python_version >= \"3.0\"" -files = [ - {file = "python3-xlib-0.15.tar.gz", hash = "sha256:dc4245f3ae4aa5949c1d112ee4723901ade37a96721ba9645f2bfa56e5b383f8"}, -] - -[[package]] -name = "pytweening" -version = "1.2.0" -summary = "A collection of tweening (aka easing) functions." -groups = ["dev"] -files = [ - {file = "pytweening-1.2.0.tar.gz", hash = "sha256:243318b7736698066c5f362ec5c2b6434ecf4297c3c8e7caa8abfe6af4cac71b"}, -] - -[[package]] -name = "pytz" -version = "2025.2" -summary = "World timezone definitions, modern and historical" -groups = ["dev"] -files = [ - {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, - {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, -] - -[[package]] -name = "pywin32" -version = "310" -summary = "Python for Window Extensions" -groups = ["dev"] -marker = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\"" -files = [ - {file = "pywin32-310-cp310-cp310-win32.whl", hash = "sha256:6dd97011efc8bf51d6793a82292419eba2c71cf8e7250cfac03bba284454abc1"}, - {file = "pywin32-310-cp310-cp310-win_amd64.whl", hash = "sha256:c3e78706e4229b915a0821941a84e7ef420bf2b77e08c9dae3c76fd03fd2ae3d"}, - {file = "pywin32-310-cp310-cp310-win_arm64.whl", hash = "sha256:33babed0cf0c92a6f94cc6cc13546ab24ee13e3e800e61ed87609ab91e4c8213"}, - {file = "pywin32-310-cp311-cp311-win32.whl", hash = "sha256:1e765f9564e83011a63321bb9d27ec456a0ed90d3732c4b2e312b855365ed8bd"}, - {file = "pywin32-310-cp311-cp311-win_amd64.whl", hash = "sha256:126298077a9d7c95c53823934f000599f66ec9296b09167810eb24875f32689c"}, - {file = "pywin32-310-cp311-cp311-win_arm64.whl", hash = "sha256:19ec5fc9b1d51c4350be7bb00760ffce46e6c95eaf2f0b2f1150657b1a43c582"}, - {file = "pywin32-310-cp312-cp312-win32.whl", hash = "sha256:8a75a5cc3893e83a108c05d82198880704c44bbaee4d06e442e471d3c9ea4f3d"}, - {file = "pywin32-310-cp312-cp312-win_amd64.whl", hash = "sha256:bf5c397c9a9a19a6f62f3fb821fbf36cac08f03770056711f765ec1503972060"}, - {file = "pywin32-310-cp312-cp312-win_arm64.whl", hash = "sha256:2349cc906eae872d0663d4d6290d13b90621eaf78964bb1578632ff20e152966"}, - {file = "pywin32-310-cp313-cp313-win32.whl", hash = "sha256:5d241a659c496ada3253cd01cfaa779b048e90ce4b2b38cd44168ad555ce74ab"}, - {file = "pywin32-310-cp313-cp313-win_amd64.whl", hash = "sha256:667827eb3a90208ddbdcc9e860c81bde63a135710e21e4cb3348968e4bd5249e"}, - {file = "pywin32-310-cp313-cp313-win_arm64.whl", hash = "sha256:e308f831de771482b7cf692a1f308f8fca701b2d8f9dde6cc440c7da17e47b33"}, -] - -[[package]] -name = "pywinpty" -version = "2.0.15" -requires_python = ">=3.9" -summary = "Pseudo terminal support for Windows from Python." -groups = ["dev"] -marker = "os_name == \"nt\"" -files = [ - {file = "pywinpty-2.0.15-cp310-cp310-win_amd64.whl", hash = "sha256:8e7f5de756a615a38b96cd86fa3cd65f901ce54ce147a3179c45907fa11b4c4e"}, - {file = "pywinpty-2.0.15-cp311-cp311-win_amd64.whl", hash = "sha256:9a6bcec2df2707aaa9d08b86071970ee32c5026e10bcc3cc5f6f391d85baf7ca"}, - {file = "pywinpty-2.0.15-cp312-cp312-win_amd64.whl", hash = "sha256:83a8f20b430bbc5d8957249f875341a60219a4e971580f2ba694fbfb54a45ebc"}, - {file = "pywinpty-2.0.15-cp313-cp313-win_amd64.whl", hash = "sha256:ab5920877dd632c124b4ed17bc6dd6ef3b9f86cd492b963ffdb1a67b85b0f408"}, - {file = "pywinpty-2.0.15-cp313-cp313t-win_amd64.whl", hash = "sha256:a4560ad8c01e537708d2790dbe7da7d986791de805d89dd0d3697ca59e9e4901"}, - {file = "pywinpty-2.0.15.tar.gz", hash = "sha256:312cf39153a8736c617d45ce8b6ad6cd2107de121df91c455b10ce6bba7a39b2"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -requires_python = ">=3.8" -summary = "YAML parser and emitter for Python" -groups = ["dev", "docs"] -files = [ - {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, - {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, - {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, - {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - -[[package]] -name = "pyyaml-env-tag" -version = "0.1" -requires_python = ">=3.6" -summary = "A custom YAML tag for referencing environment variables in YAML files. " -groups = ["docs"] -dependencies = [ - "pyyaml", -] -files = [ - {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, - {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, -] - -[[package]] -name = "pyzmq" -version = "26.4.0" -requires_python = ">=3.8" -summary = "Python bindings for 0MQ" -groups = ["dev"] -dependencies = [ - "cffi; implementation_name == \"pypy\"", -] -files = [ - {file = "pyzmq-26.4.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:0329bdf83e170ac133f44a233fc651f6ed66ef8e66693b5af7d54f45d1ef5918"}, - {file = "pyzmq-26.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:398a825d2dea96227cf6460ce0a174cf7657d6f6827807d4d1ae9d0f9ae64315"}, - {file = "pyzmq-26.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d52d62edc96787f5c1dfa6c6ccff9b581cfae5a70d94ec4c8da157656c73b5b"}, - {file = "pyzmq-26.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1410c3a3705db68d11eb2424d75894d41cff2f64d948ffe245dd97a9debfebf4"}, - {file = "pyzmq-26.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:7dacb06a9c83b007cc01e8e5277f94c95c453c5851aac5e83efe93e72226353f"}, - {file = "pyzmq-26.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6bab961c8c9b3a4dc94d26e9b2cdf84de9918931d01d6ff38c721a83ab3c0ef5"}, - {file = "pyzmq-26.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7a5c09413b924d96af2aa8b57e76b9b0058284d60e2fc3730ce0f979031d162a"}, - {file = "pyzmq-26.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7d489ac234d38e57f458fdbd12a996bfe990ac028feaf6f3c1e81ff766513d3b"}, - {file = "pyzmq-26.4.0-cp310-cp310-win32.whl", hash = "sha256:dea1c8db78fb1b4b7dc9f8e213d0af3fc8ecd2c51a1d5a3ca1cde1bda034a980"}, - {file = "pyzmq-26.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:fa59e1f5a224b5e04dc6c101d7186058efa68288c2d714aa12d27603ae93318b"}, - {file = "pyzmq-26.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:a651fe2f447672f4a815e22e74630b6b1ec3a1ab670c95e5e5e28dcd4e69bbb5"}, - {file = "pyzmq-26.4.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:bfcf82644c9b45ddd7cd2a041f3ff8dce4a0904429b74d73a439e8cab1bd9e54"}, - {file = "pyzmq-26.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9bcae3979b2654d5289d3490742378b2f3ce804b0b5fd42036074e2bf35b030"}, - {file = "pyzmq-26.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccdff8ac4246b6fb60dcf3982dfaeeff5dd04f36051fe0632748fc0aa0679c01"}, - {file = "pyzmq-26.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4550af385b442dc2d55ab7717837812799d3674cb12f9a3aa897611839c18e9e"}, - {file = "pyzmq-26.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:2f9f7ffe9db1187a253fca95191854b3fda24696f086e8789d1d449308a34b88"}, - {file = "pyzmq-26.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3709c9ff7ba61589b7372923fd82b99a81932b592a5c7f1a24147c91da9a68d6"}, - {file = "pyzmq-26.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f8f3c30fb2d26ae5ce36b59768ba60fb72507ea9efc72f8f69fa088450cff1df"}, - {file = "pyzmq-26.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:382a4a48c8080e273427fc692037e3f7d2851959ffe40864f2db32646eeb3cef"}, - {file = "pyzmq-26.4.0-cp311-cp311-win32.whl", hash = "sha256:d56aad0517d4c09e3b4f15adebba8f6372c5102c27742a5bdbfc74a7dceb8fca"}, - {file = "pyzmq-26.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:963977ac8baed7058c1e126014f3fe58b3773f45c78cce7af5c26c09b6823896"}, - {file = "pyzmq-26.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:c0c8e8cadc81e44cc5088fcd53b9b3b4ce9344815f6c4a03aec653509296fae3"}, - {file = "pyzmq-26.4.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:5227cb8da4b6f68acfd48d20c588197fd67745c278827d5238c707daf579227b"}, - {file = "pyzmq-26.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1c07a7fa7f7ba86554a2b1bef198c9fed570c08ee062fd2fd6a4dcacd45f905"}, - {file = "pyzmq-26.4.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae775fa83f52f52de73183f7ef5395186f7105d5ed65b1ae65ba27cb1260de2b"}, - {file = "pyzmq-26.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c760d0226ebd52f1e6b644a9e839b5db1e107a23f2fcd46ec0569a4fdd4e63"}, - {file = "pyzmq-26.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ef8c6ecc1d520debc147173eaa3765d53f06cd8dbe7bd377064cdbc53ab456f5"}, - {file = "pyzmq-26.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3150ef4084e163dec29ae667b10d96aad309b668fac6810c9e8c27cf543d6e0b"}, - {file = "pyzmq-26.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4448c9e55bf8329fa1dcedd32f661bf611214fa70c8e02fee4347bc589d39a84"}, - {file = "pyzmq-26.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e07dde3647afb084d985310d067a3efa6efad0621ee10826f2cb2f9a31b89d2f"}, - {file = "pyzmq-26.4.0-cp312-cp312-win32.whl", hash = "sha256:ba034a32ecf9af72adfa5ee383ad0fd4f4e38cdb62b13624278ef768fe5b5b44"}, - {file = "pyzmq-26.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:056a97aab4064f526ecb32f4343917a4022a5d9efb6b9df990ff72e1879e40be"}, - {file = "pyzmq-26.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f23c750e485ce1eb639dbd576d27d168595908aa2d60b149e2d9e34c9df40e0"}, - {file = "pyzmq-26.4.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:c43fac689880f5174d6fc864857d1247fe5cfa22b09ed058a344ca92bf5301e3"}, - {file = "pyzmq-26.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:902aca7eba477657c5fb81c808318460328758e8367ecdd1964b6330c73cae43"}, - {file = "pyzmq-26.4.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5e48a830bfd152fe17fbdeaf99ac5271aa4122521bf0d275b6b24e52ef35eb6"}, - {file = "pyzmq-26.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31be2b6de98c824c06f5574331f805707c667dc8f60cb18580b7de078479891e"}, - {file = "pyzmq-26.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6332452034be001bbf3206ac59c0d2a7713de5f25bb38b06519fc6967b7cf771"}, - {file = "pyzmq-26.4.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:da8c0f5dd352136853e6a09b1b986ee5278dfddfebd30515e16eae425c872b30"}, - {file = "pyzmq-26.4.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:f4ccc1a0a2c9806dda2a2dd118a3b7b681e448f3bb354056cad44a65169f6d86"}, - {file = "pyzmq-26.4.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c0b5fceadbab461578daf8d1dcc918ebe7ddd2952f748cf30c7cf2de5d51101"}, - {file = "pyzmq-26.4.0-cp313-cp313-win32.whl", hash = "sha256:28e2b0ff5ba4b3dd11062d905682bad33385cfa3cc03e81abd7f0822263e6637"}, - {file = "pyzmq-26.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:23ecc9d241004c10e8b4f49d12ac064cd7000e1643343944a10df98e57bc544b"}, - {file = "pyzmq-26.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:1edb0385c7f025045d6e0f759d4d3afe43c17a3d898914ec6582e6f464203c08"}, - {file = "pyzmq-26.4.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:93a29e882b2ba1db86ba5dd5e88e18e0ac6b627026c5cfbec9983422011b82d4"}, - {file = "pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb45684f276f57110bb89e4300c00f1233ca631f08f5f42528a5c408a79efc4a"}, - {file = "pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f72073e75260cb301aad4258ad6150fa7f57c719b3f498cb91e31df16784d89b"}, - {file = "pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be37e24b13026cfedd233bcbbccd8c0bcd2fdd186216094d095f60076201538d"}, - {file = "pyzmq-26.4.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:237b283044934d26f1eeff4075f751b05d2f3ed42a257fc44386d00df6a270cf"}, - {file = "pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:b30f862f6768b17040929a68432c8a8be77780317f45a353cb17e423127d250c"}, - {file = "pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:c80fcd3504232f13617c6ab501124d373e4895424e65de8b72042333316f64a8"}, - {file = "pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:26a2a7451606b87f67cdeca2c2789d86f605da08b4bd616b1a9981605ca3a364"}, - {file = "pyzmq-26.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:98d948288ce893a2edc5ec3c438fe8de2daa5bbbd6e2e865ec5f966e237084ba"}, - {file = "pyzmq-26.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9f34f5c9e0203ece706a1003f1492a56c06c0632d86cb77bcfe77b56aacf27b"}, - {file = "pyzmq-26.4.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80c9b48aef586ff8b698359ce22f9508937c799cc1d2c9c2f7c95996f2300c94"}, - {file = "pyzmq-26.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3f2a5b74009fd50b53b26f65daff23e9853e79aa86e0aa08a53a7628d92d44a"}, - {file = "pyzmq-26.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:61c5f93d7622d84cb3092d7f6398ffc77654c346545313a3737e266fc11a3beb"}, - {file = "pyzmq-26.4.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4478b14cb54a805088299c25a79f27eaf530564a7a4f72bf432a040042b554eb"}, - {file = "pyzmq-26.4.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a28ac29c60e4ba84b5f58605ace8ad495414a724fe7aceb7cf06cd0598d04e1"}, - {file = "pyzmq-26.4.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43b03c1ceea27c6520124f4fb2ba9c647409b9abdf9a62388117148a90419494"}, - {file = "pyzmq-26.4.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7731abd23a782851426d4e37deb2057bf9410848a4459b5ede4fe89342e687a9"}, - {file = "pyzmq-26.4.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a222ad02fbe80166b0526c038776e8042cd4e5f0dec1489a006a1df47e9040e0"}, - {file = "pyzmq-26.4.0.tar.gz", hash = "sha256:4bd13f85f80962f91a651a7356fe0472791a5f7a92f227822b5acf44795c626d"}, -] - -[[package]] -name = "referencing" -version = "0.36.2" -requires_python = ">=3.9" -summary = "JSON Referencing + Python" -groups = ["dev"] -dependencies = [ - "attrs>=22.2.0", - "rpds-py>=0.7.0", - "typing-extensions>=4.4.0; python_version < \"3.13\"", -] -files = [ - {file = "referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"}, - {file = "referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa"}, -] - -[[package]] -name = "regex" -version = "2024.11.6" -requires_python = ">=3.8" -summary = "Alternative regular expression module, to replace re." -groups = ["dev"] -files = [ - {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, - {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, - {file = "regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e"}, - {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde"}, - {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e"}, - {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2"}, - {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf"}, - {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c"}, - {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86"}, - {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67"}, - {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d"}, - {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2"}, - {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008"}, - {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62"}, - {file = "regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e"}, - {file = "regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519"}, - {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638"}, - {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7"}, - {file = "regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20"}, - {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114"}, - {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3"}, - {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f"}, - {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"}, - {file = "regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55"}, - {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89"}, - {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d"}, - {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34"}, - {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d"}, - {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45"}, - {file = "regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9"}, - {file = "regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60"}, - {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a"}, - {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9"}, - {file = "regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2"}, - {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4"}, - {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577"}, - {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3"}, - {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e"}, - {file = "regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe"}, - {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e"}, - {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29"}, - {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39"}, - {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51"}, - {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"}, - {file = "regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"}, - {file = "regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"}, - {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84"}, - {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4"}, - {file = "regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0"}, - {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0"}, - {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7"}, - {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7"}, - {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c"}, - {file = "regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3"}, - {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07"}, - {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e"}, - {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6"}, - {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4"}, - {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d"}, - {file = "regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff"}, - {file = "regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a"}, - {file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"}, -] - -[[package]] -name = "requests" -version = "2.32.3" -requires_python = ">=3.8" -summary = "Python HTTP for Humans." -groups = ["dev", "docs"] -dependencies = [ - "certifi>=2017.4.17", - "charset-normalizer<4,>=2", - "idna<4,>=2.5", - "urllib3<3,>=1.21.1", -] -files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, -] - -[[package]] -name = "rfc3339-validator" -version = "0.1.4" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -summary = "A pure python RFC3339 validator" -groups = ["dev"] -dependencies = [ - "six", -] -files = [ - {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, - {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, -] - -[[package]] -name = "rfc3986-validator" -version = "0.1.1" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -summary = "Pure python rfc3986 validator" -groups = ["dev"] -files = [ - {file = "rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9"}, - {file = "rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055"}, -] - -[[package]] -name = "rich" -version = "13.9.4" -requires_python = ">=3.8.0" -summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -groups = ["dev"] -dependencies = [ - "markdown-it-py>=2.2.0", - "pygments<3.0.0,>=2.13.0", - "typing-extensions<5.0,>=4.0.0; python_version < \"3.11\"", -] -files = [ - {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, - {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, -] - -[[package]] -name = "rpds-py" -version = "0.24.0" -requires_python = ">=3.9" -summary = "Python bindings to Rust's persistent data structures (rpds)" -groups = ["dev"] -files = [ - {file = "rpds_py-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:006f4342fe729a368c6df36578d7a348c7c716be1da0a1a0f86e3021f8e98724"}, - {file = "rpds_py-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2d53747da70a4e4b17f559569d5f9506420966083a31c5fbd84e764461c4444b"}, - {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8acd55bd5b071156bae57b555f5d33697998752673b9de554dd82f5b5352727"}, - {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7e80d375134ddb04231a53800503752093dbb65dad8dabacce2c84cccc78e964"}, - {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60748789e028d2a46fc1c70750454f83c6bdd0d05db50f5ae83e2db500b34da5"}, - {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e1daf5bf6c2be39654beae83ee6b9a12347cb5aced9a29eecf12a2d25fff664"}, - {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b221c2457d92a1fb3c97bee9095c874144d196f47c038462ae6e4a14436f7bc"}, - {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:66420986c9afff67ef0c5d1e4cdc2d0e5262f53ad11e4f90e5e22448df485bf0"}, - {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:43dba99f00f1d37b2a0265a259592d05fcc8e7c19d140fe51c6e6f16faabeb1f"}, - {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a88c0d17d039333a41d9bf4616bd062f0bd7aa0edeb6cafe00a2fc2a804e944f"}, - {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc31e13ce212e14a539d430428cd365e74f8b2d534f8bc22dd4c9c55b277b875"}, - {file = "rpds_py-0.24.0-cp310-cp310-win32.whl", hash = "sha256:fc2c1e1b00f88317d9de6b2c2b39b012ebbfe35fe5e7bef980fd2a91f6100a07"}, - {file = "rpds_py-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0145295ca415668420ad142ee42189f78d27af806fcf1f32a18e51d47dd2052"}, - {file = "rpds_py-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2d3ee4615df36ab8eb16c2507b11e764dcc11fd350bbf4da16d09cda11fcedef"}, - {file = "rpds_py-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e13ae74a8a3a0c2f22f450f773e35f893484fcfacb00bb4344a7e0f4f48e1f97"}, - {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf86f72d705fc2ef776bb7dd9e5fbba79d7e1f3e258bf9377f8204ad0fc1c51e"}, - {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c43583ea8517ed2e780a345dd9960896afc1327e8cf3ac8239c167530397440d"}, - {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cd031e63bc5f05bdcda120646a0d32f6d729486d0067f09d79c8db5368f4586"}, - {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34d90ad8c045df9a4259c47d2e16a3f21fdb396665c94520dbfe8766e62187a4"}, - {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e838bf2bb0b91ee67bf2b889a1a841e5ecac06dd7a2b1ef4e6151e2ce155c7ae"}, - {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04ecf5c1ff4d589987b4d9882872f80ba13da7d42427234fce8f22efb43133bc"}, - {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:630d3d8ea77eabd6cbcd2ea712e1c5cecb5b558d39547ac988351195db433f6c"}, - {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ebcb786b9ff30b994d5969213a8430cbb984cdd7ea9fd6df06663194bd3c450c"}, - {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:174e46569968ddbbeb8a806d9922f17cd2b524aa753b468f35b97ff9c19cb718"}, - {file = "rpds_py-0.24.0-cp311-cp311-win32.whl", hash = "sha256:5ef877fa3bbfb40b388a5ae1cb00636a624690dcb9a29a65267054c9ea86d88a"}, - {file = "rpds_py-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:e274f62cbd274359eff63e5c7e7274c913e8e09620f6a57aae66744b3df046d6"}, - {file = "rpds_py-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d8551e733626afec514b5d15befabea0dd70a343a9f23322860c4f16a9430205"}, - {file = "rpds_py-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e374c0ce0ca82e5b67cd61fb964077d40ec177dd2c4eda67dba130de09085c7"}, - {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d69d003296df4840bd445a5d15fa5b6ff6ac40496f956a221c4d1f6f7b4bc4d9"}, - {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8212ff58ac6dfde49946bea57474a386cca3f7706fc72c25b772b9ca4af6b79e"}, - {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:528927e63a70b4d5f3f5ccc1fa988a35456eb5d15f804d276709c33fc2f19bda"}, - {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a824d2c7a703ba6daaca848f9c3d5cb93af0505be505de70e7e66829affd676e"}, - {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d51febb7a114293ffd56c6cf4736cb31cd68c0fddd6aa303ed09ea5a48e029"}, - {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fab5f4a2c64a8fb64fc13b3d139848817a64d467dd6ed60dcdd6b479e7febc9"}, - {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9be4f99bee42ac107870c61dfdb294d912bf81c3c6d45538aad7aecab468b6b7"}, - {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:564c96b6076a98215af52f55efa90d8419cc2ef45d99e314fddefe816bc24f91"}, - {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:75a810b7664c17f24bf2ffd7f92416c00ec84b49bb68e6a0d93e542406336b56"}, - {file = "rpds_py-0.24.0-cp312-cp312-win32.whl", hash = "sha256:f6016bd950be4dcd047b7475fdf55fb1e1f59fc7403f387be0e8123e4a576d30"}, - {file = "rpds_py-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:998c01b8e71cf051c28f5d6f1187abbdf5cf45fc0efce5da6c06447cba997034"}, - {file = "rpds_py-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2d8e4508e15fc05b31285c4b00ddf2e0eb94259c2dc896771966a163122a0c"}, - {file = "rpds_py-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f00c16e089282ad68a3820fd0c831c35d3194b7cdc31d6e469511d9bffc535c"}, - {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951cc481c0c395c4a08639a469d53b7d4afa252529a085418b82a6b43c45c240"}, - {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9ca89938dff18828a328af41ffdf3902405a19f4131c88e22e776a8e228c5a8"}, - {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0ef550042a8dbcd657dfb284a8ee00f0ba269d3f2286b0493b15a5694f9fe8"}, - {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b2356688e5d958c4d5cb964af865bea84db29971d3e563fb78e46e20fe1848b"}, - {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78884d155fd15d9f64f5d6124b486f3d3f7fd7cd71a78e9670a0f6f6ca06fb2d"}, - {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a4a535013aeeef13c5532f802708cecae8d66c282babb5cd916379b72110cf7"}, - {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:84e0566f15cf4d769dade9b366b7b87c959be472c92dffb70462dd0844d7cbad"}, - {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:823e74ab6fbaa028ec89615ff6acb409e90ff45580c45920d4dfdddb069f2120"}, - {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c61a2cb0085c8783906b2f8b1f16a7e65777823c7f4d0a6aaffe26dc0d358dd9"}, - {file = "rpds_py-0.24.0-cp313-cp313-win32.whl", hash = "sha256:60d9b630c8025b9458a9d114e3af579a2c54bd32df601c4581bd054e85258143"}, - {file = "rpds_py-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:6eea559077d29486c68218178ea946263b87f1c41ae7f996b1f30a983c476a5a"}, - {file = "rpds_py-0.24.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:d09dc82af2d3c17e7dd17120b202a79b578d79f2b5424bda209d9966efeed114"}, - {file = "rpds_py-0.24.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5fc13b44de6419d1e7a7e592a4885b323fbc2f46e1f22151e3a8ed3b8b920405"}, - {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c347a20d79cedc0a7bd51c4d4b7dbc613ca4e65a756b5c3e57ec84bd43505b47"}, - {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20f2712bd1cc26a3cc16c5a1bfee9ed1abc33d4cdf1aabd297fe0eb724df4272"}, - {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aad911555286884be1e427ef0dc0ba3929e6821cbeca2194b13dc415a462c7fd"}, - {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aeb3329c1721c43c58cae274d7d2ca85c1690d89485d9c63a006cb79a85771a"}, - {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a0f156e9509cee987283abd2296ec816225145a13ed0391df8f71bf1d789e2d"}, - {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa6800adc8204ce898c8a424303969b7aa6a5e4ad2789c13f8648739830323b7"}, - {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a18fc371e900a21d7392517c6f60fe859e802547309e94313cd8181ad9db004d"}, - {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9168764133fd919f8dcca2ead66de0105f4ef5659cbb4fa044f7014bed9a1797"}, - {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f6e3cec44ba05ee5cbdebe92d052f69b63ae792e7d05f1020ac5e964394080c"}, - {file = "rpds_py-0.24.0-cp313-cp313t-win32.whl", hash = "sha256:8ebc7e65ca4b111d928b669713865f021b7773350eeac4a31d3e70144297baba"}, - {file = "rpds_py-0.24.0-cp313-cp313t-win_amd64.whl", hash = "sha256:675269d407a257b8c00a6b58205b72eec8231656506c56fd429d924ca00bb350"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:619ca56a5468f933d940e1bf431c6f4e13bef8e688698b067ae68eb4f9b30e3a"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b28e5122829181de1898c2c97f81c0b3246d49f585f22743a1246420bb8d399"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e5ab32cf9eb3647450bc74eb201b27c185d3857276162c101c0f8c6374e098"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:208b3a70a98cf3710e97cabdc308a51cd4f28aa6e7bb11de3d56cd8b74bab98d"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbc4362e06f950c62cad3d4abf1191021b2ffaf0b31ac230fbf0526453eee75e"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebea2821cdb5f9fef44933617be76185b80150632736f3d76e54829ab4a3b4d1"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4df06c35465ef4d81799999bba810c68d29972bf1c31db61bfdb81dd9d5bb"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3aa13bdf38630da298f2e0d77aca967b200b8cc1473ea05248f6c5e9c9bdb44"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:041f00419e1da7a03c46042453598479f45be3d787eb837af382bfc169c0db33"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:d8754d872a5dfc3c5bf9c0e059e8107451364a30d9fd50f1f1a85c4fb9481164"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:896c41007931217a343eff197c34513c154267636c8056fb409eafd494c3dcdc"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:92558d37d872e808944c3c96d0423b8604879a3d1c86fdad508d7ed91ea547d5"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f9e0057a509e096e47c87f753136c9b10d7a91842d8042c2ee6866899a717c0d"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6e109a454412ab82979c5b1b3aee0604eca4bbf9a02693bb9df027af2bfa91a"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc1c892b1ec1f8cbd5da8de287577b455e388d9c328ad592eabbdcb6fc93bee5"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c39438c55983d48f4bb3487734d040e22dad200dab22c41e331cee145e7a50d"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d7e8ce990ae17dda686f7e82fd41a055c668e13ddcf058e7fb5e9da20b57793"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ea7f4174d2e4194289cb0c4e172d83e79a6404297ff95f2875cf9ac9bced8ba"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb2954155bb8f63bb19d56d80e5e5320b61d71084617ed89efedb861a684baea"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04f2b712a2206e13800a8136b07aaedc23af3facab84918e7aa89e4be0260032"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:eda5c1e2a715a4cbbca2d6d304988460942551e4e5e3b7457b50943cd741626d"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:9abc80fe8c1f87218db116016de575a7998ab1629078c90840e8d11ab423ee25"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6a727fd083009bc83eb83d6950f0c32b3c94c8b80a9b667c87f4bd1274ca30ba"}, - {file = "rpds_py-0.24.0.tar.gz", hash = "sha256:772cc1b2cd963e7e17e6cc55fe0371fb9c704d63e44cacec7b9b7f523b78919e"}, -] - -[[package]] -name = "rubicon-objc" -version = "0.5.0" -requires_python = ">=3.9" -summary = "A bridge between an Objective C runtime environment and Python." -groups = ["dev"] -marker = "platform_system == \"Darwin\"" -files = [ - {file = "rubicon_objc-0.5.0-py3-none-any.whl", hash = "sha256:a9c2a605120d6e5be327d3f42a71b60963125987e116f51846757b5e110854fa"}, - {file = "rubicon_objc-0.5.0.tar.gz", hash = "sha256:18f075649780d95df53d483642068c767d7d2cfbbf075ddef124a44b40b6d92e"}, -] - -[[package]] -name = "ruff" -version = "0.11.7" -requires_python = ">=3.7" -summary = "An extremely fast Python linter and code formatter, written in Rust." -groups = ["dev"] -files = [ - {file = "ruff-0.11.7-py3-none-linux_armv6l.whl", hash = "sha256:d29e909d9a8d02f928d72ab7837b5cbc450a5bdf578ab9ebee3263d0a525091c"}, - {file = "ruff-0.11.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dd1fb86b168ae349fb01dd497d83537b2c5541fe0626e70c786427dd8363aaee"}, - {file = "ruff-0.11.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d3d7d2e140a6fbbc09033bce65bd7ea29d6a0adeb90b8430262fbacd58c38ada"}, - {file = "ruff-0.11.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4809df77de390a1c2077d9b7945d82f44b95d19ceccf0c287c56e4dc9b91ca64"}, - {file = "ruff-0.11.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3a0c2e169e6b545f8e2dba185eabbd9db4f08880032e75aa0e285a6d3f48201"}, - {file = "ruff-0.11.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49b888200a320dd96a68e86736cf531d6afba03e4f6cf098401406a257fcf3d6"}, - {file = "ruff-0.11.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2b19cdb9cf7dae00d5ee2e7c013540cdc3b31c4f281f1dacb5a799d610e90db4"}, - {file = "ruff-0.11.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64e0ee994c9e326b43539d133a36a455dbaab477bc84fe7bfbd528abe2f05c1e"}, - {file = "ruff-0.11.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bad82052311479a5865f52c76ecee5d468a58ba44fb23ee15079f17dd4c8fd63"}, - {file = "ruff-0.11.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7940665e74e7b65d427b82bffc1e46710ec7f30d58b4b2d5016e3f0321436502"}, - {file = "ruff-0.11.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:169027e31c52c0e36c44ae9a9c7db35e505fee0b39f8d9fca7274a6305295a92"}, - {file = "ruff-0.11.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:305b93f9798aee582e91e34437810439acb28b5fc1fee6b8205c78c806845a94"}, - {file = "ruff-0.11.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a681db041ef55550c371f9cd52a3cf17a0da4c75d6bd691092dfc38170ebc4b6"}, - {file = "ruff-0.11.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:07f1496ad00a4a139f4de220b0c97da6d4c85e0e4aa9b2624167b7d4d44fd6b6"}, - {file = "ruff-0.11.7-py3-none-win32.whl", hash = "sha256:f25dfb853ad217e6e5f1924ae8a5b3f6709051a13e9dad18690de6c8ff299e26"}, - {file = "ruff-0.11.7-py3-none-win_amd64.whl", hash = "sha256:0a931d85959ceb77e92aea4bbedfded0a31534ce191252721128f77e5ae1f98a"}, - {file = "ruff-0.11.7-py3-none-win_arm64.whl", hash = "sha256:778c1e5d6f9e91034142dfd06110534ca13220bfaad5c3735f6cb844654f6177"}, - {file = "ruff-0.11.7.tar.gz", hash = "sha256:655089ad3224070736dc32844fde783454f8558e71f501cb207485fe4eee23d4"}, -] - -[[package]] -name = "s3transfer" -version = "0.12.0" -requires_python = ">=3.9" -summary = "An Amazon S3 Transfer Manager" -groups = ["dev"] -dependencies = [ - "botocore<2.0a.0,>=1.37.4", -] -files = [ - {file = "s3transfer-0.12.0-py3-none-any.whl", hash = "sha256:35b314d7d82865756edab59f7baebc6b477189e6ab4c53050e28c1de4d9cce18"}, - {file = "s3transfer-0.12.0.tar.gz", hash = "sha256:8ac58bc1989a3fdb7c7f3ee0918a66b160d038a147c7b5db1500930a607e9a1c"}, -] - -[[package]] -name = "safehttpx" -version = "0.1.6" -requires_python = ">3.9" -summary = "A small Python library created to help developers protect their applications from Server Side Request Forgery (SSRF) attacks." -groups = ["dev"] -dependencies = [ - "httpx", -] -files = [ - {file = "safehttpx-0.1.6-py3-none-any.whl", hash = "sha256:407cff0b410b071623087c63dd2080c3b44dc076888d8c5823c00d1e58cb381c"}, - {file = "safehttpx-0.1.6.tar.gz", hash = "sha256:b356bfc82cee3a24c395b94a2dbeabbed60aff1aa5fa3b5fe97c4f2456ebce42"}, -] - -[[package]] -name = "safetensors" -version = "0.5.3" -requires_python = ">=3.7" -summary = "" -groups = ["dev"] -files = [ - {file = "safetensors-0.5.3-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd20eb133db8ed15b40110b7c00c6df51655a2998132193de2f75f72d99c7073"}, - {file = "safetensors-0.5.3-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:21d01c14ff6c415c485616b8b0bf961c46b3b343ca59110d38d744e577f9cce7"}, - {file = "safetensors-0.5.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11bce6164887cd491ca75c2326a113ba934be596e22b28b1742ce27b1d076467"}, - {file = "safetensors-0.5.3-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4a243be3590bc3301c821da7a18d87224ef35cbd3e5f5727e4e0728b8172411e"}, - {file = "safetensors-0.5.3-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8bd84b12b1670a6f8e50f01e28156422a2bc07fb16fc4e98bded13039d688a0d"}, - {file = "safetensors-0.5.3-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:391ac8cab7c829452175f871fcaf414aa1e292b5448bd02620f675a7f3e7abb9"}, - {file = "safetensors-0.5.3-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cead1fa41fc54b1e61089fa57452e8834f798cb1dc7a09ba3524f1eb08e0317a"}, - {file = "safetensors-0.5.3-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1077f3e94182d72618357b04b5ced540ceb71c8a813d3319f1aba448e68a770d"}, - {file = "safetensors-0.5.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:799021e78287bac619c7b3f3606730a22da4cda27759ddf55d37c8db7511c74b"}, - {file = "safetensors-0.5.3-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df26da01aaac504334644e1b7642fa000bfec820e7cef83aeac4e355e03195ff"}, - {file = "safetensors-0.5.3-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:32c3ef2d7af8b9f52ff685ed0bc43913cdcde135089ae322ee576de93eae5135"}, - {file = "safetensors-0.5.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:37f1521be045e56fc2b54c606d4455573e717b2d887c579ee1dbba5f868ece04"}, - {file = "safetensors-0.5.3-cp38-abi3-win32.whl", hash = "sha256:cfc0ec0846dcf6763b0ed3d1846ff36008c6e7290683b61616c4b040f6a54ace"}, - {file = "safetensors-0.5.3-cp38-abi3-win_amd64.whl", hash = "sha256:836cbbc320b47e80acd40e44c8682db0e8ad7123209f69b093def21ec7cafd11"}, - {file = "safetensors-0.5.3.tar.gz", hash = "sha256:b6b0d6ecacec39a4fdd99cc19f4576f5219ce858e6fd8dbe7609df0b8dc56965"}, -] - -[[package]] -name = "scikit-image" -version = "0.25.2" -requires_python = ">=3.10" -summary = "Image processing in Python" -groups = ["dev"] -dependencies = [ - "imageio!=2.35.0,>=2.33", - "lazy-loader>=0.4", - "networkx>=3.0", - "numpy>=1.24", - "packaging>=21", - "pillow>=10.1", - "scipy>=1.11.4", - "tifffile>=2022.8.12", -] -files = [ - {file = "scikit_image-0.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d3278f586793176599df6a4cf48cb6beadae35c31e58dc01a98023af3dc31c78"}, - {file = "scikit_image-0.25.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:5c311069899ce757d7dbf1d03e32acb38bb06153236ae77fcd820fd62044c063"}, - {file = "scikit_image-0.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be455aa7039a6afa54e84f9e38293733a2622b8c2fb3362b822d459cc5605e99"}, - {file = "scikit_image-0.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c464b90e978d137330be433df4e76d92ad3c5f46a22f159520ce0fdbea8a09"}, - {file = "scikit_image-0.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:60516257c5a2d2f74387c502aa2f15a0ef3498fbeaa749f730ab18f0a40fd054"}, - {file = "scikit_image-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f4bac9196fb80d37567316581c6060763b0f4893d3aca34a9ede3825bc035b17"}, - {file = "scikit_image-0.25.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d989d64ff92e0c6c0f2018c7495a5b20e2451839299a018e0e5108b2680f71e0"}, - {file = "scikit_image-0.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2cfc96b27afe9a05bc92f8c6235321d3a66499995675b27415e0d0c76625173"}, - {file = "scikit_image-0.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24cc986e1f4187a12aa319f777b36008764e856e5013666a4a83f8df083c2641"}, - {file = "scikit_image-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:b4f6b61fc2db6340696afe3db6b26e0356911529f5f6aee8c322aa5157490c9b"}, - {file = "scikit_image-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8db8dd03663112783221bf01ccfc9512d1cc50ac9b5b0fe8f4023967564719fb"}, - {file = "scikit_image-0.25.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:483bd8cc10c3d8a7a37fae36dfa5b21e239bd4ee121d91cad1f81bba10cfb0ed"}, - {file = "scikit_image-0.25.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d1e80107bcf2bf1291acfc0bf0425dceb8890abe9f38d8e94e23497cbf7ee0d"}, - {file = "scikit_image-0.25.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a17e17eb8562660cc0d31bb55643a4da996a81944b82c54805c91b3fe66f4824"}, - {file = "scikit_image-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:bdd2b8c1de0849964dbc54037f36b4e9420157e67e45a8709a80d727f52c7da2"}, - {file = "scikit_image-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7efa888130f6c548ec0439b1a7ed7295bc10105458a421e9bf739b457730b6da"}, - {file = "scikit_image-0.25.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dd8011efe69c3641920614d550f5505f83658fe33581e49bed86feab43a180fc"}, - {file = "scikit_image-0.25.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28182a9d3e2ce3c2e251383bdda68f8d88d9fff1a3ebe1eb61206595c9773341"}, - {file = "scikit_image-0.25.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8abd3c805ce6944b941cfed0406d88faeb19bab3ed3d4b50187af55cf24d147"}, - {file = "scikit_image-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:64785a8acefee460ec49a354706db0b09d1f325674107d7fa3eadb663fb56d6f"}, - {file = "scikit_image-0.25.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330d061bd107d12f8d68f1d611ae27b3b813b8cdb0300a71d07b1379178dd4cd"}, - {file = "scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde"}, -] - -[[package]] -name = "scipy" -version = "1.15.2" -requires_python = ">=3.10" -summary = "Fundamental algorithms for scientific computing in Python" -groups = ["dev"] -dependencies = [ - "numpy<2.5,>=1.23.5", -] -files = [ - {file = "scipy-1.15.2-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a2ec871edaa863e8213ea5df811cd600734f6400b4af272e1c011e69401218e9"}, - {file = "scipy-1.15.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:6f223753c6ea76983af380787611ae1291e3ceb23917393079dcc746ba60cfb5"}, - {file = "scipy-1.15.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:ecf797d2d798cf7c838c6d98321061eb3e72a74710e6c40540f0e8087e3b499e"}, - {file = "scipy-1.15.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:9b18aa747da280664642997e65aab1dd19d0c3d17068a04b3fe34e2559196cb9"}, - {file = "scipy-1.15.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87994da02e73549dfecaed9e09a4f9d58a045a053865679aeb8d6d43747d4df3"}, - {file = "scipy-1.15.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69ea6e56d00977f355c0f84eba69877b6df084516c602d93a33812aa04d90a3d"}, - {file = "scipy-1.15.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:888307125ea0c4466287191e5606a2c910963405ce9671448ff9c81c53f85f58"}, - {file = "scipy-1.15.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9412f5e408b397ff5641080ed1e798623dbe1ec0d78e72c9eca8992976fa65aa"}, - {file = "scipy-1.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:b5e025e903b4f166ea03b109bb241355b9c42c279ea694d8864d033727205e65"}, - {file = "scipy-1.15.2-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:92233b2df6938147be6fa8824b8136f29a18f016ecde986666be5f4d686a91a4"}, - {file = "scipy-1.15.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:62ca1ff3eb513e09ed17a5736929429189adf16d2d740f44e53270cc800ecff1"}, - {file = "scipy-1.15.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4c6676490ad76d1c2894d77f976144b41bd1a4052107902238047fb6a473e971"}, - {file = "scipy-1.15.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a8bf5cb4a25046ac61d38f8d3c3426ec11ebc350246a4642f2f315fe95bda655"}, - {file = "scipy-1.15.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a8e34cf4c188b6dd004654f88586d78f95639e48a25dfae9c5e34a6dc34547e"}, - {file = "scipy-1.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28a0d2c2075946346e4408b211240764759e0fabaeb08d871639b5f3b1aca8a0"}, - {file = "scipy-1.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:42dabaaa798e987c425ed76062794e93a243be8f0f20fff6e7a89f4d61cb3d40"}, - {file = "scipy-1.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f5e296ec63c5da6ba6fa0343ea73fd51b8b3e1a300b0a8cae3ed4b1122c7462"}, - {file = "scipy-1.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:597a0c7008b21c035831c39927406c6181bcf8f60a73f36219b69d010aa04737"}, - {file = "scipy-1.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c4697a10da8f8765bb7c83e24a470da5797e37041edfd77fd95ba3811a47c4fd"}, - {file = "scipy-1.15.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:869269b767d5ee7ea6991ed7e22b3ca1f22de73ab9a49c44bad338b725603301"}, - {file = "scipy-1.15.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:bad78d580270a4d32470563ea86c6590b465cb98f83d760ff5b0990cb5518a93"}, - {file = "scipy-1.15.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:b09ae80010f52efddb15551025f9016c910296cf70adbf03ce2a8704f3a5ad20"}, - {file = "scipy-1.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a6fd6eac1ce74a9f77a7fc724080d507c5812d61e72bd5e4c489b042455865e"}, - {file = "scipy-1.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b871df1fe1a3ba85d90e22742b93584f8d2b8e6124f8372ab15c71b73e428b8"}, - {file = "scipy-1.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:03205d57a28e18dfd39f0377d5002725bf1f19a46f444108c29bdb246b6c8a11"}, - {file = "scipy-1.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:601881dfb761311045b03114c5fe718a12634e5608c3b403737ae463c9885d53"}, - {file = "scipy-1.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:e7c68b6a43259ba0aab737237876e5c2c549a031ddb7abc28c7b47f22e202ded"}, - {file = "scipy-1.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01edfac9f0798ad6b46d9c4c9ca0e0ad23dbf0b1eb70e96adb9fa7f525eff0bf"}, - {file = "scipy-1.15.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:08b57a9336b8e79b305a143c3655cc5bdbe6d5ece3378578888d2afbb51c4e37"}, - {file = "scipy-1.15.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:54c462098484e7466362a9f1672d20888f724911a74c22ae35b61f9c5919183d"}, - {file = "scipy-1.15.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:cf72ff559a53a6a6d77bd8eefd12a17995ffa44ad86c77a5df96f533d4e6c6bb"}, - {file = "scipy-1.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9de9d1416b3d9e7df9923ab23cd2fe714244af10b763975bea9e4f2e81cebd27"}, - {file = "scipy-1.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb530e4794fc8ea76a4a21ccb67dea33e5e0e60f07fc38a49e821e1eae3b71a0"}, - {file = "scipy-1.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5ea7ed46d437fc52350b028b1d44e002646e28f3e8ddc714011aaf87330f2f32"}, - {file = "scipy-1.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11e7ad32cf184b74380f43d3c0a706f49358b904fa7d5345f16ddf993609184d"}, - {file = "scipy-1.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:a5080a79dfb9b78b768cebf3c9dcbc7b665c5875793569f48bf0e2b1d7f68f6f"}, - {file = "scipy-1.15.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:447ce30cee6a9d5d1379087c9e474628dab3db4a67484be1b7dc3196bfb2fac9"}, - {file = "scipy-1.15.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:c90ebe8aaa4397eaefa8455a8182b164a6cc1d59ad53f79943f266d99f68687f"}, - {file = "scipy-1.15.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:def751dd08243934c884a3221156d63e15234a3155cf25978b0a668409d45eb6"}, - {file = "scipy-1.15.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:302093e7dfb120e55515936cb55618ee0b895f8bcaf18ff81eca086c17bd80af"}, - {file = "scipy-1.15.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd5b77413e1855351cdde594eca99c1f4a588c2d63711388b6a1f1c01f62274"}, - {file = "scipy-1.15.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d0194c37037707b2afa7a2f2a924cf7bac3dc292d51b6a925e5fcb89bc5c776"}, - {file = "scipy-1.15.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:bae43364d600fdc3ac327db99659dcb79e6e7ecd279a75fe1266669d9a652828"}, - {file = "scipy-1.15.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f031846580d9acccd0044efd1a90e6f4df3a6e12b4b6bd694a7bc03a89892b28"}, - {file = "scipy-1.15.2-cp313-cp313t-win_amd64.whl", hash = "sha256:fe8a9eb875d430d81755472c5ba75e84acc980e4a8f6204d402849234d3017db"}, - {file = "scipy-1.15.2.tar.gz", hash = "sha256:cd58a314d92838f7e6f755c8a2167ead4f27e1fd5c1251fd54289569ef3495ec"}, -] - -[[package]] -name = "seaborn" -version = "0.13.2" -requires_python = ">=3.8" -summary = "Statistical data visualization" -groups = ["dev"] -dependencies = [ - "matplotlib!=3.6.1,>=3.4", - "numpy!=1.24.0,>=1.20", - "pandas>=1.2", -] -files = [ - {file = "seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987"}, - {file = "seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7"}, -] - -[[package]] -name = "semantic-version" -version = "2.10.0" -requires_python = ">=2.7" -summary = "A library implementing the 'SemVer' scheme." -groups = ["dev"] -files = [ - {file = "semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177"}, - {file = "semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c"}, -] - -[[package]] -name = "send2trash" -version = "1.8.3" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -summary = "Send file to trash natively under Mac OS X, Windows and Linux" -groups = ["dev"] -files = [ - {file = "Send2Trash-1.8.3-py3-none-any.whl", hash = "sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9"}, - {file = "Send2Trash-1.8.3.tar.gz", hash = "sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf"}, -] - -[[package]] -name = "setuptools" -version = "79.0.1" -requires_python = ">=3.9" -summary = "Easily download, build, install, upgrade, and uninstall Python packages" -groups = ["dev"] -files = [ - {file = "setuptools-79.0.1-py3-none-any.whl", hash = "sha256:e147c0549f27767ba362f9da434eab9c5dc0045d5304feb602a0af001089fc51"}, - {file = "setuptools-79.0.1.tar.gz", hash = "sha256:128ce7b8f33c3079fd1b067ecbb4051a66e8526e7b65f6cec075dfc650ddfa88"}, -] - -[[package]] -name = "shapely" -version = "2.1.0" -requires_python = ">=3.10" -summary = "Manipulation and analysis of geometric objects" -groups = ["dev"] -dependencies = [ - "numpy>=1.21", -] -files = [ - {file = "shapely-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d3e5c5e3864d4dc431dd85a8e5137ebd39c8ac287b009d3fa80a07017b29c940"}, - {file = "shapely-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6eea89b16f5f3a064659126455d23fa3066bc3d6cd385c35214f06bf5871aa6"}, - {file = "shapely-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:183174ad0b21a81ee661f05e7c47aa92ebfae01814cd3cbe54adea7a4213f5f4"}, - {file = "shapely-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f239c1484af66bc14b81a76f2a8e0fada29d59010423253ff857d0ccefdaa93f"}, - {file = "shapely-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6220a466d1475141dad0cd8065d2549a5c2ed3fa4e2e02fb8ea65d494cfd5b07"}, - {file = "shapely-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4822d3ed3efb06145c34d29d5b56792f72b7d713300f603bfd5d825892c6f79f"}, - {file = "shapely-2.1.0-cp310-cp310-win32.whl", hash = "sha256:ea51ddf3d3c60866dca746081b56c75f34ff1b01acbd4d44269071a673c735b9"}, - {file = "shapely-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6f5e02e2cded9f4ec5709900a296c7f2cce5f8e9e9d80ba7d89ae2f4ed89d7b"}, - {file = "shapely-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8323031ef7c1bdda7a92d5ddbc7b6b62702e73ba37e9a8ccc8da99ec2c0b87c"}, - {file = "shapely-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4da7c6cd748d86ec6aace99ad17129d30954ccf5e73e9911cdb5f0fa9658b4f8"}, - {file = "shapely-2.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f0cdf85ff80831137067e7a237085a3ee72c225dba1b30beef87f7d396cf02b"}, - {file = "shapely-2.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f2be5d79aac39886f23000727cf02001aef3af8810176c29ee12cdc3ef3a50"}, - {file = "shapely-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:21a4515009f56d7a159cf5c2554264e82f56405b4721f9a422cb397237c5dca8"}, - {file = "shapely-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15cebc323cec2cb6b2eaa310fdfc621f6dbbfaf6bde336d13838fcea76c885a9"}, - {file = "shapely-2.1.0-cp311-cp311-win32.whl", hash = "sha256:cad51b7a5c8f82f5640472944a74f0f239123dde9a63042b3c5ea311739b7d20"}, - {file = "shapely-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:d4005309dde8658e287ad9c435c81877f6a95a9419b932fa7a1f34b120f270ae"}, - {file = "shapely-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53e7ee8bd8609cf12ee6dce01ea5affe676976cf7049315751d53d8db6d2b4b2"}, - {file = "shapely-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3cab20b665d26dbec0b380e15749bea720885a481fa7b1eedc88195d4a98cfa4"}, - {file = "shapely-2.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4a38b39a09340273c3c92b3b9a374272a12cc7e468aeeea22c1c46217a03e5c"}, - {file = "shapely-2.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:edaec656bdd9b71278b98e6f77c464b1c3b2daa9eace78012ff0f0b4b5b15b04"}, - {file = "shapely-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c8a732ddd9b25e7a54aa748e7df8fd704e23e5d5d35b7d376d80bffbfc376d04"}, - {file = "shapely-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9c93693ad8adfdc9138a5a2d42da02da94f728dd2e82d2f0f442f10e25027f5f"}, - {file = "shapely-2.1.0-cp312-cp312-win32.whl", hash = "sha256:d8ac6604eefe807e71a908524de23a37920133a1729fe3a4dfe0ed82c044cbf4"}, - {file = "shapely-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:f4f47e631aa4f9ec5576eac546eb3f38802e2f82aeb0552f9612cb9a14ece1db"}, - {file = "shapely-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b64423295b563f43a043eb786e7a03200ebe68698e36d2b4b1c39f31dfb50dfb"}, - {file = "shapely-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1b5578f45adc25b235b22d1ccb9a0348c8dc36f31983e57ea129a88f96f7b870"}, - {file = "shapely-2.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a7e83d383b27f02b684e50ab7f34e511c92e33b6ca164a6a9065705dd64bcb"}, - {file = "shapely-2.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:942031eb4d8f7b3b22f43ba42c09c7aa3d843aa10d5cc1619fe816e923b66e55"}, - {file = "shapely-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d2843c456a2e5627ee6271800f07277c0d2652fb287bf66464571a057dbc00b3"}, - {file = "shapely-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8c4b17469b7f39a5e6a7cfea79f38ae08a275427f41fe8b48c372e1449147908"}, - {file = "shapely-2.1.0-cp313-cp313-win32.whl", hash = "sha256:30e967abd08fce49513d4187c01b19f139084019f33bec0673e8dbeb557c45e4"}, - {file = "shapely-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:1dc8d4364483a14aba4c844b7bd16a6fa3728887e2c33dfa1afa34a3cf4d08a5"}, - {file = "shapely-2.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:673e073fea099d1c82f666fb7ab0a00a77eff2999130a69357ce11941260d855"}, - {file = "shapely-2.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6d1513f915a56de67659fe2047c1ad5ff0f8cbff3519d1e74fced69c9cb0e7da"}, - {file = "shapely-2.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d6a7043178890b9e028d80496ff4c79dc7629bff4d78a2f25323b661756bab8"}, - {file = "shapely-2.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb638378dc3d76f7e85b67d7e2bb1366811912430ac9247ac00c127c2b444cdc"}, - {file = "shapely-2.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:737124e87d91d616acf9a911f74ac55e05db02a43a6a7245b3d663817b876055"}, - {file = "shapely-2.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e6c229e7bb87aae5df82fa00b6718987a43ec168cc5affe095cca59d233f314"}, - {file = "shapely-2.1.0-cp313-cp313t-win32.whl", hash = "sha256:a9580bda119b1f42f955aa8e52382d5c73f7957e0203bc0c0c60084846f3db94"}, - {file = "shapely-2.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e8ff4e5cfd799ba5b6f37b5d5527dbd85b4a47c65b6d459a03d0962d2a9d4d10"}, - {file = "shapely-2.1.0.tar.gz", hash = "sha256:2cbe90e86fa8fc3ca8af6ffb00a77b246b918c7cf28677b7c21489b678f6b02e"}, -] - -[[package]] -name = "shellingham" -version = "1.5.4" -requires_python = ">=3.7" -summary = "Tool to Detect Surrounding Shell" -groups = ["dev"] -marker = "sys_platform != \"emscripten\"" -files = [ - {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, - {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, -] - -[[package]] -name = "six" -version = "1.17.0" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -summary = "Python 2 and 3 compatibility utilities" -groups = ["dev", "docs"] -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -requires_python = ">=3.7" -summary = "Sniff out which async library your code is running under" -groups = ["dev"] -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "soupsieve" -version = "2.7" -requires_python = ">=3.8" -summary = "A modern CSS selector implementation for Beautiful Soup." -groups = ["dev"] -files = [ - {file = "soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4"}, - {file = "soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a"}, -] - -[[package]] -name = "sse-starlette" -version = "2.3.3" -requires_python = ">=3.9" -summary = "SSE plugin for Starlette" -groups = ["dev"] -dependencies = [ - "anyio>=4.7.0", - "starlette>=0.41.3", -] -files = [ - {file = "sse_starlette-2.3.3-py3-none-any.whl", hash = "sha256:8b0a0ced04a329ff7341b01007580dd8cf71331cc21c0ccea677d500618da1e0"}, - {file = "sse_starlette-2.3.3.tar.gz", hash = "sha256:fdd47c254aad42907cfd5c5b83e2282be15be6c51197bf1a9b70b8e990522072"}, -] - -[[package]] -name = "stack-data" -version = "0.6.3" -summary = "Extract data from python stack frames and tracebacks for informative displays" -groups = ["dev"] -dependencies = [ - "asttokens>=2.1.0", - "executing>=1.2.0", - "pure-eval", -] -files = [ - {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, - {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, -] - -[[package]] -name = "starlette" -version = "0.46.2" -requires_python = ">=3.9" -summary = "The little ASGI library that shines." -groups = ["dev"] -dependencies = [ - "anyio<5,>=3.6.2", - "typing-extensions>=3.10.0; python_version < \"3.10\"", -] -files = [ - {file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"}, - {file = "starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5"}, -] - -[[package]] -name = "supervision" -version = "0.25.1" -requires_python = "<4.0,>=3.8" -summary = "A set of easy-to-use utils that will come in handy in any Computer Vision project" -groups = ["dev"] -dependencies = [ - "contourpy>=1.0.7; python_version >= \"3.8\" and python_version < \"3.13\"", - "contourpy>=1.3.0; python_version >= \"3.13\"", - "defusedxml<0.8.0,>=0.7.1", - "matplotlib<3.8.0,>=3.6.0; python_version == \"3.8\"", - "matplotlib>=3.6.0; python_version >= \"3.9\"", - "matplotlib>=3.7.3; python_version >= \"3.12\"", - "matplotlib>=3.9.2; python_version >= \"3.13\"", - "numpy>=1.21.2; python_version < \"3.13\"", - "numpy>=2.1.0; python_version >= \"3.13\"", - "opencv-python>=4.5.5.64", - "pillow>=9.4", - "pyyaml>=5.3", - "requests>=2.26.0", - "scipy<2.0.0,>=1.10.0; python_version >= \"3.9\"", - "scipy==1.10.0; python_version < \"3.9\"", - "scipy>=1.14.1; python_version >= \"3.13\"", - "tqdm>=4.62.3", -] -files = [ - {file = "supervision-0.25.1-py3-none-any.whl", hash = "sha256:ebc015c22983bc64563beda75f5f529e465e4020b318da07948ce03148307a72"}, - {file = "supervision-0.25.1.tar.gz", hash = "sha256:61781b4abe4fa6ff95c58af6aec7dd3451a78e7e6a797e9ea2787f93771dd031"}, -] - -[[package]] -name = "sympy" -version = "1.13.3" -requires_python = ">=3.8" -summary = "Computer algebra system (CAS) in Python" -groups = ["dev"] -dependencies = [ - "mpmath<1.4,>=1.1.0", -] -files = [ - {file = "sympy-1.13.3-py3-none-any.whl", hash = "sha256:54612cf55a62755ee71824ce692986f23c88ffa77207b30c1368eda4a7060f73"}, - {file = "sympy-1.13.3.tar.gz", hash = "sha256:b27fd2c6530e0ab39e275fc9b683895367e51d5da91baa8d3d64db2565fec4d9"}, -] - -[[package]] -name = "terminado" -version = "0.18.1" -requires_python = ">=3.8" -summary = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." -groups = ["dev"] -dependencies = [ - "ptyprocess; os_name != \"nt\"", - "pywinpty>=1.1.0; os_name == \"nt\"", - "tornado>=6.1.0", -] -files = [ - {file = "terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0"}, - {file = "terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e"}, -] - -[[package]] -name = "tifffile" -version = "2025.3.30" -requires_python = ">=3.10" -summary = "Read and write TIFF files" -groups = ["dev"] -dependencies = [ - "numpy", -] -files = [ - {file = "tifffile-2025.3.30-py3-none-any.whl", hash = "sha256:0ed6eee7b66771db2d1bfc42262a51b01887505d35539daef118f4ff8c0f629c"}, - {file = "tifffile-2025.3.30.tar.gz", hash = "sha256:3cdee47fe06cd75367c16bc3ff34523713156dae6cd498e3a392e5b39a51b789"}, -] - -[[package]] -name = "tinycss2" -version = "1.4.0" -requires_python = ">=3.8" -summary = "A tiny CSS parser" -groups = ["dev"] -dependencies = [ - "webencodings>=0.4", -] -files = [ - {file = "tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289"}, - {file = "tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7"}, -] - -[[package]] -name = "tokenizers" -version = "0.21.1" -requires_python = ">=3.9" -summary = "" -groups = ["dev"] -dependencies = [ - "huggingface-hub<1.0,>=0.16.4", -] -files = [ - {file = "tokenizers-0.21.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e78e413e9e668ad790a29456e677d9d3aa50a9ad311a40905d6861ba7692cf41"}, - {file = "tokenizers-0.21.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:cd51cd0a91ecc801633829fcd1fda9cf8682ed3477c6243b9a095539de4aecf3"}, - {file = "tokenizers-0.21.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28da6b72d4fb14ee200a1bd386ff74ade8992d7f725f2bde2c495a9a98cf4d9f"}, - {file = "tokenizers-0.21.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34d8cfde551c9916cb92014e040806122295a6800914bab5865deb85623931cf"}, - {file = "tokenizers-0.21.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaa852d23e125b73d283c98f007e06d4595732104b65402f46e8ef24b588d9f8"}, - {file = "tokenizers-0.21.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a21a15d5c8e603331b8a59548bbe113564136dc0f5ad8306dd5033459a226da0"}, - {file = "tokenizers-0.21.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdbd4c067c60a0ac7eca14b6bd18a5bebace54eb757c706b47ea93204f7a37c"}, - {file = "tokenizers-0.21.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd9a0061e403546f7377df940e866c3e678d7d4e9643d0461ea442b4f89e61a"}, - {file = "tokenizers-0.21.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:db9484aeb2e200c43b915a1a0150ea885e35f357a5a8fabf7373af333dcc8dbf"}, - {file = "tokenizers-0.21.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed248ab5279e601a30a4d67bdb897ecbe955a50f1e7bb62bd99f07dd11c2f5b6"}, - {file = "tokenizers-0.21.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:9ac78b12e541d4ce67b4dfd970e44c060a2147b9b2a21f509566d556a509c67d"}, - {file = "tokenizers-0.21.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e5a69c1a4496b81a5ee5d2c1f3f7fbdf95e90a0196101b0ee89ed9956b8a168f"}, - {file = "tokenizers-0.21.1-cp39-abi3-win32.whl", hash = "sha256:1039a3a5734944e09de1d48761ade94e00d0fa760c0e0551151d4dd851ba63e3"}, - {file = "tokenizers-0.21.1-cp39-abi3-win_amd64.whl", hash = "sha256:0f0dcbcc9f6e13e675a66d7a5f2f225a736745ce484c1a4e07476a89ccdad382"}, - {file = "tokenizers-0.21.1.tar.gz", hash = "sha256:a1bb04dc5b448985f86ecd4b05407f5a8d97cb2c0532199b2a302a604a0165ab"}, -] - -[[package]] -name = "tomli" -version = "2.2.1" -requires_python = ">=3.8" -summary = "A lil' TOML parser" -groups = ["dev", "test"] -marker = "python_version < \"3.11\"" -files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, -] - -[[package]] -name = "tomlkit" -version = "0.13.2" -requires_python = ">=3.8" -summary = "Style preserving TOML library" -groups = ["dev"] -files = [ - {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, - {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, -] - -[[package]] -name = "torch" -version = "2.7.0" -requires_python = ">=3.9.0" -summary = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" -groups = ["dev"] -dependencies = [ - "filelock", - "fsspec", - "jinja2", - "networkx", - "nvidia-cublas-cu12==12.6.4.1; platform_system == \"Linux\" and platform_machine == \"x86_64\"", - "nvidia-cuda-cupti-cu12==12.6.80; platform_system == \"Linux\" and platform_machine == \"x86_64\"", - "nvidia-cuda-nvrtc-cu12==12.6.77; platform_system == \"Linux\" and platform_machine == \"x86_64\"", - "nvidia-cuda-runtime-cu12==12.6.77; platform_system == \"Linux\" and platform_machine == \"x86_64\"", - "nvidia-cudnn-cu12==9.5.1.17; platform_system == \"Linux\" and platform_machine == \"x86_64\"", - "nvidia-cufft-cu12==11.3.0.4; platform_system == \"Linux\" and platform_machine == \"x86_64\"", - "nvidia-cufile-cu12==1.11.1.6; platform_system == \"Linux\" and platform_machine == \"x86_64\"", - "nvidia-curand-cu12==10.3.7.77; platform_system == \"Linux\" and platform_machine == \"x86_64\"", - "nvidia-cusolver-cu12==11.7.1.2; platform_system == \"Linux\" and platform_machine == \"x86_64\"", - "nvidia-cusparse-cu12==12.5.4.2; platform_system == \"Linux\" and platform_machine == \"x86_64\"", - "nvidia-cusparselt-cu12==0.6.3; platform_system == \"Linux\" and platform_machine == \"x86_64\"", - "nvidia-nccl-cu12==2.26.2; platform_system == \"Linux\" and platform_machine == \"x86_64\"", - "nvidia-nvjitlink-cu12==12.6.85; platform_system == \"Linux\" and platform_machine == \"x86_64\"", - "nvidia-nvtx-cu12==12.6.77; platform_system == \"Linux\" and platform_machine == \"x86_64\"", - "setuptools; python_version >= \"3.12\"", - "sympy>=1.13.3", - "triton==3.3.0; platform_system == \"Linux\" and platform_machine == \"x86_64\"", - "typing-extensions>=4.10.0", -] -files = [ - {file = "torch-2.7.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c9afea41b11e1a1ab1b258a5c31afbd646d6319042bfe4f231b408034b51128b"}, - {file = "torch-2.7.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0b9960183b6e5b71239a3e6c883d8852c304e691c0b2955f7045e8a6d05b9183"}, - {file = "torch-2.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:2ad79d0d8c2a20a37c5df6052ec67c2078a2c4e9a96dd3a8b55daaff6d28ea29"}, - {file = "torch-2.7.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:34e0168ed6de99121612d72224e59b2a58a83dae64999990eada7260c5dd582d"}, - {file = "torch-2.7.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2b7813e904757b125faf1a9a3154e1d50381d539ced34da1992f52440567c156"}, - {file = "torch-2.7.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fd5cfbb4c3bbadd57ad1b27d56a28008f8d8753733411a140fcfb84d7f933a25"}, - {file = "torch-2.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:58df8d5c2eeb81305760282b5069ea4442791a6bbf0c74d9069b7b3304ff8a37"}, - {file = "torch-2.7.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:0a8d43caa342b9986101ec5feb5bbf1d86570b5caa01e9cb426378311258fdde"}, - {file = "torch-2.7.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:36a6368c7ace41ad1c0f69f18056020b6a5ca47bedaca9a2f3b578f5a104c26c"}, - {file = "torch-2.7.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:15aab3e31c16feb12ae0a88dba3434a458874636f360c567caa6a91f6bfba481"}, - {file = "torch-2.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:f56d4b2510934e072bab3ab8987e00e60e1262fb238176168f5e0c43a1320c6d"}, - {file = "torch-2.7.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:30b7688a87239a7de83f269333651d8e582afffce6f591fff08c046f7787296e"}, - {file = "torch-2.7.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:868ccdc11798535b5727509480cd1d86d74220cfdc42842c4617338c1109a205"}, - {file = "torch-2.7.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9b52347118116cf3dff2ab5a3c3dd97c719eb924ac658ca2a7335652076df708"}, - {file = "torch-2.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:434cf3b378340efc87c758f250e884f34460624c0523fe5c9b518d205c91dd1b"}, - {file = "torch-2.7.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:edad98dddd82220465b106506bb91ee5ce32bd075cddbcf2b443dfaa2cbd83bf"}, - {file = "torch-2.7.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:2a885fc25afefb6e6eb18a7d1e8bfa01cc153e92271d980a49243b250d5ab6d9"}, - {file = "torch-2.7.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:176300ff5bc11a5f5b0784e40bde9e10a35c4ae9609beed96b4aeb46a27f5fae"}, - {file = "torch-2.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d0ca446a93f474985d81dc866fcc8dccefb9460a29a456f79d99c29a78a66993"}, - {file = "torch-2.7.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:27f5007bdf45f7bb7af7f11d1828d5c2487e030690afb3d89a651fd7036a390e"}, -] - -[[package]] -name = "torchvision" -version = "0.22.0" -requires_python = ">=3.9" -summary = "image and video datasets and models for torch deep learning" -groups = ["dev"] -dependencies = [ - "numpy", - "pillow!=8.3.*,>=5.3.0", - "torch==2.7.0", -] -files = [ - {file = "torchvision-0.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:72256f1d7ff510b16c9fb4dd488584d0693f40c792f286a9620674438a81ccca"}, - {file = "torchvision-0.22.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:810ea4af3bc63cf39e834f91f4218ff5999271caaffe2456247df905002bd6c0"}, - {file = "torchvision-0.22.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6fbca169c690fa2b9b8c39c0ad76d5b8992296d0d03df01e11df97ce12b4e0ac"}, - {file = "torchvision-0.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:8c869df2e8e00f7b1d80a34439e6d4609b50fe3141032f50b38341ec2b59404e"}, - {file = "torchvision-0.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:191ea28321fc262d8aa1a7fe79c41ff2848864bf382f9f6ea45c41dde8313792"}, - {file = "torchvision-0.22.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6c5620e10ffe388eb6f4744962106ed7cf1508d26e6fdfa0c10522d3249aea24"}, - {file = "torchvision-0.22.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ce292701c77c64dd3935e3e31c722c3b8b176a75f76dc09b804342efc1db5494"}, - {file = "torchvision-0.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:e4017b5685dbab4250df58084f07d95e677b2f3ed6c2e507a1afb8eb23b580ca"}, - {file = "torchvision-0.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:31c3165418fe21c3d81fe3459e51077c2f948801b8933ed18169f54652796a0f"}, - {file = "torchvision-0.22.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8f116bc82e0c076e70ba7776e611ed392b9666aa443662e687808b08993d26af"}, - {file = "torchvision-0.22.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ce4dc334ebd508de2c534817c9388e928bc2500cf981906ae8d6e2ca3bf4727a"}, - {file = "torchvision-0.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:24b8c9255c209ca419cc7174906da2791c8b557b75c23496663ec7d73b55bebf"}, - {file = "torchvision-0.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ece17995857dd328485c9c027c0b20ffc52db232e30c84ff6c95ab77201112c5"}, - {file = "torchvision-0.22.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:471c6dd75bb984c6ebe4f60322894a290bf3d4b195e769d80754f3689cd7f238"}, - {file = "torchvision-0.22.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:2b839ac0610a38f56bef115ee5b9eaca5f9c2da3c3569a68cc62dbcc179c157f"}, - {file = "torchvision-0.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:4ada1c08b2f761443cd65b7c7b4aec9e2fc28f75b0d4e1b1ebc9d3953ebccc4d"}, - {file = "torchvision-0.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cdc96daa4658b47ce9384154c86ed1e70cba9d972a19f5de6e33f8f94a626790"}, - {file = "torchvision-0.22.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:753d3c84eeadd5979a33b3b73a25ecd0aa4af44d6b45ed2c70d44f5e0ac68312"}, - {file = "torchvision-0.22.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:b30e3ed29e4a61f7499bca50f57d8ebd23dfc52b14608efa17a534a55ee59a03"}, - {file = "torchvision-0.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e5d680162694fac4c8a374954e261ddfb4eb0ce103287b0f693e4e9c579ef957"}, -] - -[[package]] -name = "tornado" -version = "6.4.2" -requires_python = ">=3.8" -summary = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." -groups = ["dev"] -files = [ - {file = "tornado-6.4.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1"}, - {file = "tornado-6.4.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803"}, - {file = "tornado-6.4.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec"}, - {file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946"}, - {file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf"}, - {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634"}, - {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73"}, - {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c"}, - {file = "tornado-6.4.2-cp38-abi3-win32.whl", hash = "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482"}, - {file = "tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38"}, - {file = "tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b"}, -] - -[[package]] -name = "tqdm" -version = "4.67.1" -requires_python = ">=3.7" -summary = "Fast, Extensible Progress Meter" -groups = ["dev"] -dependencies = [ - "colorama; platform_system == \"Windows\"", -] -files = [ - {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, - {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, -] - -[[package]] -name = "traitlets" -version = "5.14.3" -requires_python = ">=3.8" -summary = "Traitlets Python configuration system" -groups = ["dev"] -files = [ - {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, - {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, -] - -[[package]] -name = "transformers" -version = "4.51.3" -requires_python = ">=3.9.0" -summary = "State-of-the-art Machine Learning for JAX, PyTorch and TensorFlow" -groups = ["dev"] -dependencies = [ - "filelock", - "huggingface-hub<1.0,>=0.30.0", - "numpy>=1.17", - "packaging>=20.0", - "pyyaml>=5.1", - "regex!=2019.12.17", - "requests", - "safetensors>=0.4.3", - "tokenizers<0.22,>=0.21", - "tqdm>=4.27", -] -files = [ - {file = "transformers-4.51.3-py3-none-any.whl", hash = "sha256:fd3279633ceb2b777013234bbf0b4f5c2d23c4626b05497691f00cfda55e8a83"}, - {file = "transformers-4.51.3.tar.gz", hash = "sha256:e292fcab3990c6defe6328f0f7d2004283ca81a7a07b2de9a46d67fd81ea1409"}, -] - -[[package]] -name = "triton" -version = "3.3.0" -summary = "A language and compiler for custom Deep Learning operations" -groups = ["dev"] -marker = "platform_system == \"Linux\" and platform_machine == \"x86_64\"" -dependencies = [ - "setuptools>=40.8.0", -] -files = [ - {file = "triton-3.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fad99beafc860501d7fcc1fb7045d9496cbe2c882b1674640304949165a916e7"}, - {file = "triton-3.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3161a2bf073d6b22c4e2f33f951f3e5e3001462b2570e6df9cd57565bdec2984"}, - {file = "triton-3.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b68c778f6c4218403a6bd01be7484f6dc9e20fe2083d22dd8aef33e3b87a10a3"}, - {file = "triton-3.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47bc87ad66fa4ef17968299acacecaab71ce40a238890acc6ad197c3abe2b8f1"}, - {file = "triton-3.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce4700fc14032af1e049005ae94ba908e71cd6c2df682239aed08e49bc71b742"}, -] - -[[package]] -name = "typer" -version = "0.15.2" -requires_python = ">=3.7" -summary = "Typer, build great CLIs. Easy to code. Based on Python type hints." -groups = ["dev"] -marker = "sys_platform != \"emscripten\"" -dependencies = [ - "click>=8.0.0", - "rich>=10.11.0", - "shellingham>=1.3.0", - "typing-extensions>=3.7.4.3", -] -files = [ - {file = "typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc"}, - {file = "typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5"}, -] - -[[package]] -name = "types-python-dateutil" -version = "2.9.0.20241206" -requires_python = ">=3.8" -summary = "Typing stubs for python-dateutil" -groups = ["dev"] -files = [ - {file = "types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53"}, - {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"}, -] - -[[package]] -name = "types-requests" -version = "2.32.0.20250328" -requires_python = ">=3.9" -summary = "Typing stubs for requests" -groups = ["dev"] -dependencies = [ - "urllib3>=2", -] -files = [ - {file = "types_requests-2.32.0.20250328-py3-none-any.whl", hash = "sha256:72ff80f84b15eb3aa7a8e2625fffb6a93f2ad5a0c20215fc1dcfa61117bcb2a2"}, - {file = "types_requests-2.32.0.20250328.tar.gz", hash = "sha256:c9e67228ea103bd811c96984fac36ed2ae8da87a36a633964a21f199d60baf32"}, -] - -[[package]] -name = "typing-extensions" -version = "4.13.2" -requires_python = ">=3.8" -summary = "Backported and Experimental Type Hints for Python 3.8+" -groups = ["dev", "test"] -files = [ - {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, - {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, -] - -[[package]] -name = "typing-inspection" -version = "0.4.0" -requires_python = ">=3.9" -summary = "Runtime typing introspection tools" -groups = ["dev"] -dependencies = [ - "typing-extensions>=4.12.0", -] -files = [ - {file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"}, - {file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"}, -] - -[[package]] -name = "tzdata" -version = "2025.2" -requires_python = ">=2" -summary = "Provider of IANA time zone data" -groups = ["dev"] -files = [ - {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, - {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, -] - -[[package]] -name = "ultralytics" -version = "8.3.116" -requires_python = ">=3.8" -summary = "Ultralytics YOLO 🚀 for SOTA object detection, multi-object tracking, instance segmentation, pose estimation and image classification." -groups = ["dev"] -dependencies = [ - "matplotlib>=3.3.0", - "numpy<=2.1.1,>=1.23.0", - "opencv-python>=4.6.0", - "pandas>=1.1.4", - "pillow>=7.1.2", - "psutil", - "py-cpuinfo", - "pyyaml>=5.3.1", - "requests>=2.23.0", - "scipy>=1.4.1", - "seaborn>=0.11.0", - "torch!=2.4.0,>=1.8.0; sys_platform == \"win32\"", - "torch>=1.8.0", - "torchvision>=0.9.0", - "tqdm>=4.64.0", - "ultralytics-thop>=2.0.0", -] -files = [ - {file = "ultralytics-8.3.116-py3-none-any.whl", hash = "sha256:2c9b2a7fd3a9e7cd2e27de0e179dd52a3dc9528a7908b1dd80e5709c1a9dfbb4"}, - {file = "ultralytics-8.3.116.tar.gz", hash = "sha256:33458953c367dcb8b3df29312d3e48f4ff7454229319332e0e21af93623178f7"}, -] - -[[package]] -name = "ultralytics-thop" -version = "2.0.14" -requires_python = ">=3.8" -summary = "Ultralytics THOP package for fast computation of PyTorch model FLOPs and parameters." -groups = ["dev"] -dependencies = [ - "numpy", - "torch", -] -files = [ - {file = "ultralytics_thop-2.0.14-py3-none-any.whl", hash = "sha256:720b421e2459179fee21ec8f730d242a20774cd4b0a00a58d02351a39ec3881c"}, - {file = "ultralytics_thop-2.0.14.tar.gz", hash = "sha256:38ebfdbd3cd8dafdc3d26ec3a7d4f604fbeed5e69a74e61a48060b39736c945c"}, -] - -[[package]] -name = "uri-template" -version = "1.3.0" -requires_python = ">=3.7" -summary = "RFC 6570 URI Template Processor" -groups = ["dev"] -files = [ - {file = "uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7"}, - {file = "uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363"}, -] - -[[package]] -name = "urllib3" -version = "2.4.0" -requires_python = ">=3.9" -summary = "HTTP library with thread-safe connection pooling, file post, and more." -groups = ["dev", "docs"] -files = [ - {file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, - {file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, -] - -[[package]] -name = "uvicorn" -version = "0.34.2" -requires_python = ">=3.9" -summary = "The lightning-fast ASGI server." -groups = ["dev"] -dependencies = [ - "click>=7.0", - "h11>=0.8", - "typing-extensions>=4.0; python_version < \"3.11\"", -] -files = [ - {file = "uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403"}, - {file = "uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328"}, -] - -[[package]] -name = "uvicorn" -version = "0.34.2" -extras = ["standard"] -requires_python = ">=3.9" -summary = "The lightning-fast ASGI server." -groups = ["dev"] -dependencies = [ - "colorama>=0.4; sys_platform == \"win32\"", - "httptools>=0.6.3", - "python-dotenv>=0.13", - "pyyaml>=5.1", - "uvicorn==0.34.2", - "uvloop!=0.15.0,!=0.15.1,>=0.14.0; (sys_platform != \"cygwin\" and sys_platform != \"win32\") and platform_python_implementation != \"PyPy\"", - "watchfiles>=0.13", - "websockets>=10.4", -] -files = [ - {file = "uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403"}, - {file = "uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328"}, -] - -[[package]] -name = "uvloop" -version = "0.21.0" -requires_python = ">=3.8.0" -summary = "Fast implementation of asyncio event loop on top of libuv" -groups = ["dev"] -marker = "(sys_platform != \"cygwin\" and sys_platform != \"win32\") and platform_python_implementation != \"PyPy\"" -files = [ - {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"}, - {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"}, - {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26"}, - {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb"}, - {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f"}, - {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c"}, - {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8"}, - {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0"}, - {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e"}, - {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb"}, - {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6"}, - {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d"}, - {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c"}, - {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2"}, - {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d"}, - {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc"}, - {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb"}, - {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f"}, - {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281"}, - {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af"}, - {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6"}, - {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816"}, - {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc"}, - {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553"}, - {file = "uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3"}, -] - -[[package]] -name = "watchdog" -version = "6.0.0" -requires_python = ">=3.9" -summary = "Filesystem events monitoring" -groups = ["docs"] -files = [ - {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, - {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, - {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, - {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, - {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, - {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, - {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, - {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, - {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, - {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, - {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, - {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, - {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, - {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, - {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, - {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, - {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, - {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, - {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, -] - -[[package]] -name = "watchfiles" -version = "1.0.5" -requires_python = ">=3.9" -summary = "Simple, modern and high performance file watching and code reload in python." -groups = ["dev"] -dependencies = [ - "anyio>=3.0.0", -] -files = [ - {file = "watchfiles-1.0.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5c40fe7dd9e5f81e0847b1ea64e1f5dd79dd61afbedb57759df06767ac719b40"}, - {file = "watchfiles-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c0db396e6003d99bb2d7232c957b5f0b5634bbd1b24e381a5afcc880f7373fb"}, - {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b551d4fb482fc57d852b4541f911ba28957d051c8776e79c3b4a51eb5e2a1b11"}, - {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:830aa432ba5c491d52a15b51526c29e4a4b92bf4f92253787f9726fe01519487"}, - {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a16512051a822a416b0d477d5f8c0e67b67c1a20d9acecb0aafa3aa4d6e7d256"}, - {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe0cbc787770e52a96c6fda6726ace75be7f840cb327e1b08d7d54eadc3bc85"}, - {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d363152c5e16b29d66cbde8fa614f9e313e6f94a8204eaab268db52231fe5358"}, - {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee32c9a9bee4d0b7bd7cbeb53cb185cf0b622ac761efaa2eba84006c3b3a614"}, - {file = "watchfiles-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29c7fd632ccaf5517c16a5188e36f6612d6472ccf55382db6c7fe3fcccb7f59f"}, - {file = "watchfiles-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e637810586e6fe380c8bc1b3910accd7f1d3a9a7262c8a78d4c8fb3ba6a2b3d"}, - {file = "watchfiles-1.0.5-cp310-cp310-win32.whl", hash = "sha256:cd47d063fbeabd4c6cae1d4bcaa38f0902f8dc5ed168072874ea11d0c7afc1ff"}, - {file = "watchfiles-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:86c0df05b47a79d80351cd179893f2f9c1b1cae49d96e8b3290c7f4bd0ca0a92"}, - {file = "watchfiles-1.0.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:237f9be419e977a0f8f6b2e7b0475ababe78ff1ab06822df95d914a945eac827"}, - {file = "watchfiles-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0da39ff917af8b27a4bdc5a97ac577552a38aac0d260a859c1517ea3dc1a7c4"}, - {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cfcb3952350e95603f232a7a15f6c5f86c5375e46f0bd4ae70d43e3e063c13d"}, - {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:68b2dddba7a4e6151384e252a5632efcaa9bc5d1c4b567f3cb621306b2ca9f63"}, - {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95cf944fcfc394c5f9de794ce581914900f82ff1f855326f25ebcf24d5397418"}, - {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf6cd9f83d7c023b1aba15d13f705ca7b7d38675c121f3cc4a6e25bd0857ee9"}, - {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:852de68acd6212cd6d33edf21e6f9e56e5d98c6add46f48244bd479d97c967c6"}, - {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5730f3aa35e646103b53389d5bc77edfbf578ab6dab2e005142b5b80a35ef25"}, - {file = "watchfiles-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:18b3bd29954bc4abeeb4e9d9cf0b30227f0f206c86657674f544cb032296acd5"}, - {file = "watchfiles-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ba5552a1b07c8edbf197055bc9d518b8f0d98a1c6a73a293bc0726dce068ed01"}, - {file = "watchfiles-1.0.5-cp311-cp311-win32.whl", hash = "sha256:2f1fefb2e90e89959447bc0420fddd1e76f625784340d64a2f7d5983ef9ad246"}, - {file = "watchfiles-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b6e76ceb1dd18c8e29c73f47d41866972e891fc4cc7ba014f487def72c1cf096"}, - {file = "watchfiles-1.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:266710eb6fddc1f5e51843c70e3bebfb0f5e77cf4f27129278c70554104d19ed"}, - {file = "watchfiles-1.0.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5eb568c2aa6018e26da9e6c86f3ec3fd958cee7f0311b35c2630fa4217d17f2"}, - {file = "watchfiles-1.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a04059f4923ce4e856b4b4e5e783a70f49d9663d22a4c3b3298165996d1377f"}, - {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e380c89983ce6e6fe2dd1e1921b9952fb4e6da882931abd1824c092ed495dec"}, - {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fe43139b2c0fdc4a14d4f8d5b5d967f7a2777fd3d38ecf5b1ec669b0d7e43c21"}, - {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee0822ce1b8a14fe5a066f93edd20aada932acfe348bede8aa2149f1a4489512"}, - {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a0dbcb1c2d8f2ab6e0a81c6699b236932bd264d4cef1ac475858d16c403de74d"}, - {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2014a2b18ad3ca53b1f6c23f8cd94a18ce930c1837bd891262c182640eb40a6"}, - {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f6ae86d5cb647bf58f9f655fcf577f713915a5d69057a0371bc257e2553234"}, - {file = "watchfiles-1.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1a7bac2bde1d661fb31f4d4e8e539e178774b76db3c2c17c4bb3e960a5de07a2"}, - {file = "watchfiles-1.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ab626da2fc1ac277bbf752446470b367f84b50295264d2d313e28dc4405d663"}, - {file = "watchfiles-1.0.5-cp312-cp312-win32.whl", hash = "sha256:9f4571a783914feda92018ef3901dab8caf5b029325b5fe4558c074582815249"}, - {file = "watchfiles-1.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:360a398c3a19672cf93527f7e8d8b60d8275119c5d900f2e184d32483117a705"}, - {file = "watchfiles-1.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:1a2902ede862969077b97523987c38db28abbe09fb19866e711485d9fbf0d417"}, - {file = "watchfiles-1.0.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0b289572c33a0deae62daa57e44a25b99b783e5f7aed81b314232b3d3c81a11d"}, - {file = "watchfiles-1.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a056c2f692d65bf1e99c41045e3bdcaea3cb9e6b5a53dcaf60a5f3bd95fc9763"}, - {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9dca99744991fc9850d18015c4f0438865414e50069670f5f7eee08340d8b40"}, - {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:894342d61d355446d02cd3988a7326af344143eb33a2fd5d38482a92072d9563"}, - {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab44e1580924d1ffd7b3938e02716d5ad190441965138b4aa1d1f31ea0877f04"}, - {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6f9367b132078b2ceb8d066ff6c93a970a18c3029cea37bfd7b2d3dd2e5db8f"}, - {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2e55a9b162e06e3f862fb61e399fe9f05d908d019d87bf5b496a04ef18a970a"}, - {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0125f91f70e0732a9f8ee01e49515c35d38ba48db507a50c5bdcad9503af5827"}, - {file = "watchfiles-1.0.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:13bb21f8ba3248386337c9fa51c528868e6c34a707f729ab041c846d52a0c69a"}, - {file = "watchfiles-1.0.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:839ebd0df4a18c5b3c1b890145b5a3f5f64063c2a0d02b13c76d78fe5de34936"}, - {file = "watchfiles-1.0.5-cp313-cp313-win32.whl", hash = "sha256:4a8ec1e4e16e2d5bafc9ba82f7aaecfeec990ca7cd27e84fb6f191804ed2fcfc"}, - {file = "watchfiles-1.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:f436601594f15bf406518af922a89dcaab416568edb6f65c4e5bbbad1ea45c11"}, - {file = "watchfiles-1.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f59b870db1f1ae5a9ac28245707d955c8721dd6565e7f411024fa374b5362d1d"}, - {file = "watchfiles-1.0.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9475b0093767e1475095f2aeb1d219fb9664081d403d1dff81342df8cd707034"}, - {file = "watchfiles-1.0.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc533aa50664ebd6c628b2f30591956519462f5d27f951ed03d6c82b2dfd9965"}, - {file = "watchfiles-1.0.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed1cd825158dcaae36acce7b2db33dcbfd12b30c34317a88b8ed80f0541cc57"}, - {file = "watchfiles-1.0.5.tar.gz", hash = "sha256:b7529b5dcc114679d43827d8c35a07c493ad6f083633d573d81c660abc5979e9"}, -] - -[[package]] -name = "wcwidth" -version = "0.2.13" -summary = "Measures the displayed width of unicode strings in a terminal" -groups = ["dev"] -dependencies = [ - "backports-functools-lru-cache>=1.2.1; python_version < \"3.2\"", -] -files = [ - {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, - {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, -] - -[[package]] -name = "webcolors" -version = "24.11.1" -requires_python = ">=3.9" -summary = "A library for working with the color formats defined by HTML and CSS." -groups = ["dev"] -files = [ - {file = "webcolors-24.11.1-py3-none-any.whl", hash = "sha256:515291393b4cdf0eb19c155749a096f779f7d909f7cceea072791cb9095b92e9"}, - {file = "webcolors-24.11.1.tar.gz", hash = "sha256:ecb3d768f32202af770477b8b65f318fa4f566c22948673a977b00d589dd80f6"}, -] - -[[package]] -name = "webencodings" -version = "0.5.1" -summary = "Character encoding aliases for legacy web content" -groups = ["dev"] -files = [ - {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, - {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, -] - -[[package]] -name = "websocket-client" -version = "1.8.0" -requires_python = ">=3.8" -summary = "WebSocket client for Python with low level API options" -groups = ["dev"] -files = [ - {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, - {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, -] - -[[package]] -name = "websockets" -version = "15.0.1" -requires_python = ">=3.9" -summary = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -groups = ["dev"] -files = [ - {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, - {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, - {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, - {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, - {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, - {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"}, - {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, - {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, - {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, - {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, - {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, - {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, - {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, - {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, - {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, - {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, - {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"}, - {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, - {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, - {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, - {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, - {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, - {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, - {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, - {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, - {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, - {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, - {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"}, - {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, - {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, - {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, - {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, - {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, - {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, - {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, - {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, - {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, - {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, - {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"}, - {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, - {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, - {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, - {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, - {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, - {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, - {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, -] - -[[package]] -name = "widgetsnbextension" -version = "4.0.14" -requires_python = ">=3.7" -summary = "Jupyter interactive widgets for Jupyter Notebook" -groups = ["dev"] -files = [ - {file = "widgetsnbextension-4.0.14-py3-none-any.whl", hash = "sha256:4875a9eaf72fbf5079dc372a51a9f268fc38d46f767cbf85c43a36da5cb9b575"}, - {file = "widgetsnbextension-4.0.14.tar.gz", hash = "sha256:a3629b04e3edb893212df862038c7232f62973373869db5084aed739b437b5af"}, -] - -[[package]] -name = "yarl" -version = "1.20.0" -requires_python = ">=3.9" -summary = "Yet another URL library" -groups = ["dev", "test"] -dependencies = [ - "idna>=2.0", - "multidict>=4.0", - "propcache>=0.2.1", -] -files = [ - {file = "yarl-1.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f1f6670b9ae3daedb325fa55fbe31c22c8228f6e0b513772c2e1c623caa6ab22"}, - {file = "yarl-1.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85a231fa250dfa3308f3c7896cc007a47bc76e9e8e8595c20b7426cac4884c62"}, - {file = "yarl-1.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a06701b647c9939d7019acdfa7ebbfbb78ba6aa05985bb195ad716ea759a569"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7595498d085becc8fb9203aa314b136ab0516c7abd97e7d74f7bb4eb95042abe"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af5607159085dcdb055d5678fc2d34949bd75ae6ea6b4381e784bbab1c3aa195"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:95b50910e496567434cb77a577493c26bce0f31c8a305135f3bda6a2483b8e10"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b594113a301ad537766b4e16a5a6750fcbb1497dcc1bc8a4daae889e6402a634"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:083ce0393ea173cd37834eb84df15b6853b555d20c52703e21fbababa8c129d2"}, - {file = "yarl-1.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f1a350a652bbbe12f666109fbddfdf049b3ff43696d18c9ab1531fbba1c977a"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fb0caeac4a164aadce342f1597297ec0ce261ec4532bbc5a9ca8da5622f53867"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d88cc43e923f324203f6ec14434fa33b85c06d18d59c167a0637164863b8e995"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e52d6ed9ea8fd3abf4031325dc714aed5afcbfa19ee4a89898d663c9976eb487"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ce360ae48a5e9961d0c730cf891d40698a82804e85f6e74658fb175207a77cb2"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:06d06c9d5b5bc3eb56542ceeba6658d31f54cf401e8468512447834856fb0e61"}, - {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c27d98f4e5c4060582f44e58309c1e55134880558f1add7a87c1bc36ecfade19"}, - {file = "yarl-1.20.0-cp310-cp310-win32.whl", hash = "sha256:f4d3fa9b9f013f7050326e165c3279e22850d02ae544ace285674cb6174b5d6d"}, - {file = "yarl-1.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc906b636239631d42eb8a07df8359905da02704a868983265603887ed68c076"}, - {file = "yarl-1.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fdb5204d17cb32b2de2d1e21c7461cabfacf17f3645e4b9039f210c5d3378bf3"}, - {file = "yarl-1.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eaddd7804d8e77d67c28d154ae5fab203163bd0998769569861258e525039d2a"}, - {file = "yarl-1.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:634b7ba6b4a85cf67e9df7c13a7fb2e44fa37b5d34501038d174a63eaac25ee2"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d409e321e4addf7d97ee84162538c7258e53792eb7c6defd0c33647d754172e"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ea52f7328a36960ba3231c6677380fa67811b414798a6e071c7085c57b6d20a9"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8703517b924463994c344dcdf99a2d5ce9eca2b6882bb640aa555fb5efc706a"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:077989b09ffd2f48fb2d8f6a86c5fef02f63ffe6b1dd4824c76de7bb01e4f2e2"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0acfaf1da020253f3533526e8b7dd212838fdc4109959a2c53cafc6db611bff2"}, - {file = "yarl-1.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4230ac0b97ec5eeb91d96b324d66060a43fd0d2a9b603e3327ed65f084e41f8"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a6a1e6ae21cdd84011c24c78d7a126425148b24d437b5702328e4ba640a8902"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:86de313371ec04dd2531f30bc41a5a1a96f25a02823558ee0f2af0beaa7ca791"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dd59c9dd58ae16eaa0f48c3d0cbe6be8ab4dc7247c3ff7db678edecbaf59327f"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a0bc5e05f457b7c1994cc29e83b58f540b76234ba6b9648a4971ddc7f6aa52da"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c9471ca18e6aeb0e03276b5e9b27b14a54c052d370a9c0c04a68cefbd1455eb4"}, - {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40ed574b4df723583a26c04b298b283ff171bcc387bc34c2683235e2487a65a5"}, - {file = "yarl-1.20.0-cp311-cp311-win32.whl", hash = "sha256:db243357c6c2bf3cd7e17080034ade668d54ce304d820c2a58514a4e51d0cfd6"}, - {file = "yarl-1.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:8c12cd754d9dbd14204c328915e23b0c361b88f3cffd124129955e60a4fbfcfb"}, - {file = "yarl-1.20.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f"}, - {file = "yarl-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e"}, - {file = "yarl-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018"}, - {file = "yarl-1.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1"}, - {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b"}, - {file = "yarl-1.20.0-cp312-cp312-win32.whl", hash = "sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64"}, - {file = "yarl-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c"}, - {file = "yarl-1.20.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2137810a20b933b1b1b7e5cf06a64c3ed3b4747b0e5d79c9447c00db0e2f752f"}, - {file = "yarl-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:447c5eadd750db8389804030d15f43d30435ed47af1313303ed82a62388176d3"}, - {file = "yarl-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42fbe577272c203528d402eec8bf4b2d14fd49ecfec92272334270b850e9cd7d"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18e321617de4ab170226cd15006a565d0fa0d908f11f724a2c9142d6b2812ab0"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4345f58719825bba29895011e8e3b545e6e00257abb984f9f27fe923afca2501"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d9b980d7234614bc4674468ab173ed77d678349c860c3af83b1fffb6a837ddc"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af4baa8a445977831cbaa91a9a84cc09debb10bc8391f128da2f7bd070fc351d"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123393db7420e71d6ce40d24885a9e65eb1edefc7a5228db2d62bcab3386a5c0"}, - {file = "yarl-1.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab47acc9332f3de1b39e9b702d9c916af7f02656b2a86a474d9db4e53ef8fd7a"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4a34c52ed158f89876cba9c600b2c964dfc1ca52ba7b3ab6deb722d1d8be6df2"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:04d8cfb12714158abf2618f792c77bc5c3d8c5f37353e79509608be4f18705c9"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7dc63ad0d541c38b6ae2255aaa794434293964677d5c1ec5d0116b0e308031f5"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d02b591a64e4e6ca18c5e3d925f11b559c763b950184a64cf47d74d7e41877"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:95fc9876f917cac7f757df80a5dda9de59d423568460fe75d128c813b9af558e"}, - {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb769ae5760cd1c6a712135ee7915f9d43f11d9ef769cb3f75a23e398a92d384"}, - {file = "yarl-1.20.0-cp313-cp313-win32.whl", hash = "sha256:70e0c580a0292c7414a1cead1e076c9786f685c1fc4757573d2967689b370e62"}, - {file = "yarl-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:4c43030e4b0af775a85be1fa0433119b1565673266a70bf87ef68a9d5ba3174c"}, - {file = "yarl-1.20.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b6c4c3d0d6a0ae9b281e492b1465c72de433b782e6b5001c8e7249e085b69051"}, - {file = "yarl-1.20.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8681700f4e4df891eafa4f69a439a6e7d480d64e52bf460918f58e443bd3da7d"}, - {file = "yarl-1.20.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:84aeb556cb06c00652dbf87c17838eb6d92cfd317799a8092cee0e570ee11229"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f166eafa78810ddb383e930d62e623d288fb04ec566d1b4790099ae0f31485f1"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5d3d6d14754aefc7a458261027a562f024d4f6b8a798adb472277f675857b1eb"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a8f64df8ed5d04c51260dbae3cc82e5649834eebea9eadfd829837b8093eb00"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d9949eaf05b4d30e93e4034a7790634bbb41b8be2d07edd26754f2e38e491de"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c366b254082d21cc4f08f522ac201d0d83a8b8447ab562732931d31d80eb2a5"}, - {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91bc450c80a2e9685b10e34e41aef3d44ddf99b3a498717938926d05ca493f6a"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c2aa4387de4bc3a5fe158080757748d16567119bef215bec643716b4fbf53f9"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d2cbca6760a541189cf87ee54ff891e1d9ea6406079c66341008f7ef6ab61145"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:798a5074e656f06b9fad1a162be5a32da45237ce19d07884d0b67a0aa9d5fdda"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f106e75c454288472dbe615accef8248c686958c2e7dd3b8d8ee2669770d020f"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3b60a86551669c23dc5445010534d2c5d8a4e012163218fc9114e857c0586fdd"}, - {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e429857e341d5e8e15806118e0294f8073ba9c4580637e59ab7b238afca836f"}, - {file = "yarl-1.20.0-cp313-cp313t-win32.whl", hash = "sha256:65a4053580fe88a63e8e4056b427224cd01edfb5f951498bfefca4052f0ce0ac"}, - {file = "yarl-1.20.0-cp313-cp313t-win_amd64.whl", hash = "sha256:53b2da3a6ca0a541c1ae799c349788d480e5144cac47dba0266c7cb6c76151fe"}, - {file = "yarl-1.20.0-py3-none-any.whl", hash = "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124"}, - {file = "yarl-1.20.0.tar.gz", hash = "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307"}, -] diff --git a/pyproject.toml b/pyproject.toml index 5be2eca1..36fa43c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "CUA (Computer Use Agent) mono-repo" license = { text = "MIT" } name = "cua-workspace" readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.11" version = "0.1.0" [project.urls] @@ -53,13 +53,13 @@ respect-source-order = true [tool.black] line-length = 100 -target-version = ["py310"] +target-version = ["py311"] [tool.ruff] fix = true line-length = 100 select = ["B", "E", "F", "I"] -target-version = "py310" +target-version = "py311" [tool.ruff.format] docstring-code-format = true @@ -68,7 +68,7 @@ docstring-code-format = true check_untyped_defs = true disallow_untyped_defs = true ignore_missing_imports = true -python_version = "3.10" +python_version = "3.11" show_error_codes = true strict = true warn_return_any = true diff --git a/scripts/playground.sh b/scripts/playground.sh index 332e35a1..4cdb1ffa 100755 --- a/scripts/playground.sh +++ b/scripts/playground.sh @@ -2,83 +2,201 @@ set -e -echo "🚀 Setting up CUA playground environment..." +echo "🚀 Launching C/ua Computer-Use Agent UI..." -# Check for Apple Silicon Mac -if [[ $(uname -s) != "Darwin" || $(uname -m) != "arm64" ]]; then - echo "❌ This script requires an Apple Silicon Mac (M1/M2/M3/M4)." - exit 1 -fi - -# Check for macOS 15 (Sequoia) or newer -OSVERSION=$(sw_vers -productVersion) -if [[ $(echo "$OSVERSION 15.0" | tr " " "\n" | sort -V | head -n 1) != "15.0" ]]; then - echo "❌ This script requires macOS 15 (Sequoia) or newer. You have $OSVERSION." - exit 1 -fi +# Save the original working directory +ORIGINAL_DIR="$(pwd)" -# Create a temporary directory for our work -TMP_DIR=$(mktemp -d) -cd "$TMP_DIR" +# Directories used by the script +DEMO_DIR="$HOME/.cua-demo" +VENV_DIR="$DEMO_DIR/venv" # Function to clean up on exit cleanup() { cd ~ - rm -rf "$TMP_DIR" + rm -rf "$TMP_DIR" 2>/dev/null || true } + +# Create a temporary directory for our work +TMP_DIR=$(mktemp -d) +cd "$TMP_DIR" trap cleanup EXIT -# Install Lume if not already installed -if ! command -v lume &> /dev/null; then - echo "📦 Installing Lume CLI..." - curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/lume/scripts/install.sh | bash +# Ask user to choose between local macOS VMs or C/ua Cloud Containers +echo "" +echo "Choose your C/ua setup:" +echo "1) ☁️ C/ua Cloud Containers (works on any system)" +echo "2) 🖥️ Local macOS VMs (requires Apple Silicon Mac + macOS 15+)" +echo "" +read -p "Enter your choice (1 or 2): " CHOICE + +if [[ "$CHOICE" == "1" ]]; then + # C/ua Cloud Container setup + echo "" + echo "☁️ Setting up C/ua Cloud Containers..." + echo "" - # Add lume to PATH for this session if it's not already there - if ! command -v lume &> /dev/null; then - export PATH="$PATH:$HOME/.lume/bin" + # Check if existing .env.local already has CUA_API_KEY (check current dir and demo dir) + # Look for .env.local in the original working directory (before cd to temp dir) + CURRENT_ENV_FILE="$ORIGINAL_DIR/.env.local" + DEMO_ENV_FILE="$DEMO_DIR/.env.local" + + CUA_API_KEY="" + + # First check current directory + if [[ -f "$CURRENT_ENV_FILE" ]] && grep -q "CUA_API_KEY=" "$CURRENT_ENV_FILE"; then + EXISTING_CUA_KEY=$(grep "CUA_API_KEY=" "$CURRENT_ENV_FILE" | cut -d'=' -f2- | tr -d '"' | tr -d "'" | xargs) + if [[ -n "$EXISTING_CUA_KEY" && "$EXISTING_CUA_KEY" != "your_cua_api_key_here" && "$EXISTING_CUA_KEY" != "" ]]; then + CUA_API_KEY="$EXISTING_CUA_KEY" + fi fi -fi - -# Pull the macOS CUA image if not already present -if ! lume ls | grep -q "macos-sequoia-cua"; then - # Check available disk space - IMAGE_SIZE_GB=30 - AVAILABLE_SPACE_KB=$(df -k $HOME | tail -1 | awk '{print $4}') - AVAILABLE_SPACE_GB=$(($AVAILABLE_SPACE_KB / 1024 / 1024)) - echo "📊 The macOS CUA image will use approximately ${IMAGE_SIZE_GB}GB of disk space." - echo " You currently have ${AVAILABLE_SPACE_GB}GB available on your system." + # Then check demo directory if not found in current dir + if [[ -z "$CUA_API_KEY" ]] && [[ -f "$DEMO_ENV_FILE" ]] && grep -q "CUA_API_KEY=" "$DEMO_ENV_FILE"; then + EXISTING_CUA_KEY=$(grep "CUA_API_KEY=" "$DEMO_ENV_FILE" | cut -d'=' -f2- | tr -d '"' | tr -d "'" | xargs) + if [[ -n "$EXISTING_CUA_KEY" && "$EXISTING_CUA_KEY" != "your_cua_api_key_here" && "$EXISTING_CUA_KEY" != "" ]]; then + CUA_API_KEY="$EXISTING_CUA_KEY" + fi + fi - # Prompt for confirmation - read -p " Continue? [y]/n: " CONTINUE - CONTINUE=${CONTINUE:-y} + # If no valid API key found, prompt for one + if [[ -z "$CUA_API_KEY" ]]; then + echo "To use C/ua Cloud Containers, you need to:" + echo "1. Sign up at https://trycua.com" + echo "2. Create a Cloud Container" + echo "3. Generate an Api Key" + echo "" + read -p "Enter your C/ua Api Key: " CUA_API_KEY + + if [[ -z "$CUA_API_KEY" ]]; then + echo "❌ C/ua Api Key is required for Cloud Containers." + exit 1 + fi + fi - if [[ $CONTINUE =~ ^[Yy]$ ]]; then - echo "📥 Pulling macOS CUA image (this may take a while)..." - lume pull macos-sequoia-cua:latest - else - echo "❌ Installation cancelled." + USE_CLOUD=true + +elif [[ "$CHOICE" == "2" ]]; then + # Local macOS VM setup + echo "" + echo "🖥️ Setting up local macOS VMs..." + + # Check for Apple Silicon Mac + if [[ $(uname -s) != "Darwin" || $(uname -m) != "arm64" ]]; then + echo "❌ Local macOS VMs require an Apple Silicon Mac (M1/M2/M3/M4)." + echo "💡 Consider using C/ua Cloud Containers instead (option 1)." + exit 1 + fi + + # Check for macOS 15 (Sequoia) or newer + OSVERSION=$(sw_vers -productVersion) + if [[ $(echo "$OSVERSION 15.0" | tr " " "\n" | sort -V | head -n 1) != "15.0" ]]; then + echo "❌ Local macOS VMs require macOS 15 (Sequoia) or newer. You have $OSVERSION." + echo "💡 Consider using C/ua Cloud Containers instead (option 1)." exit 1 fi + + USE_CLOUD=false + +else + echo "❌ Invalid choice. Please run the script again and choose 1 or 2." + exit 1 +fi + +# Install Lume if not already installed (only for local VMs) +if [[ "$USE_CLOUD" == "false" ]]; then + if ! command -v lume &> /dev/null; then + echo "📦 Installing Lume CLI..." + curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/lume/scripts/install.sh | bash + + # Add lume to PATH for this session if it's not already there + if ! command -v lume &> /dev/null; then + export PATH="$PATH:$HOME/.local/bin" + fi + fi + + # Pull the macOS CUA image if not already present + if ! lume ls | grep -q "macos-sequoia-cua"; then + # Check available disk space + IMAGE_SIZE_GB=30 + AVAILABLE_SPACE_KB=$(df -k $HOME | tail -1 | awk '{print $4}') + AVAILABLE_SPACE_GB=$(($AVAILABLE_SPACE_KB / 1024 / 1024)) + + echo "📊 The macOS CUA image will use approximately ${IMAGE_SIZE_GB}GB of disk space." + echo " You currently have ${AVAILABLE_SPACE_GB}GB available on your system." + + # Prompt for confirmation + read -p " Continue? [y]/n: " CONTINUE + CONTINUE=${CONTINUE:-y} + + if [[ $CONTINUE =~ ^[Yy]$ ]]; then + echo "📥 Pulling macOS CUA image (this may take a while)..." + lume pull macos-sequoia-cua:latest + else + echo "❌ Installation cancelled." + exit 1 + fi + fi fi # Create a Python virtual environment echo "🐍 Setting up Python environment..." -PYTHON_CMD="python3" -# Check if Python 3.11+ is available -PYTHON_VERSION=$($PYTHON_CMD --version 2>&1 | cut -d" " -f2) -PYTHON_MAJOR=$(echo $PYTHON_VERSION | cut -d. -f1) -PYTHON_MINOR=$(echo $PYTHON_VERSION | cut -d. -f2) +# Try different Python commands in order of preference +PYTHON_CMD="" +for cmd in python3.11 python3 python; do + if command -v $cmd &> /dev/null; then + # Check this Python version + PYTHON_VERSION=$($cmd --version 2>&1 | cut -d" " -f2) + PYTHON_MAJOR=$(echo $PYTHON_VERSION | cut -d. -f1) + PYTHON_MINOR=$(echo $PYTHON_VERSION | cut -d. -f2) + + if [ "$PYTHON_MAJOR" -eq 3 ] && [ "$PYTHON_MINOR" -eq 11 ]; then + PYTHON_CMD=$cmd + echo "✅ Found suitable Python: $cmd (version $PYTHON_VERSION)" + break + elif [ "$PYTHON_MAJOR" -eq 3 ] && [ "$PYTHON_MINOR" -gt 11 ]; then + PYTHON_CMD=$cmd + PYTHON_TOO_NEW=true + echo "⚠️ Found $cmd (version $PYTHON_VERSION) but only Python 3.11.x is supported." + break + else + echo "⚠️ Found $cmd (version $PYTHON_VERSION) but it's too old, trying next..." + fi + fi +done -if [ "$PYTHON_MAJOR" -lt 3 ] || ([ "$PYTHON_MAJOR" -eq 3 ] && [ "$PYTHON_MINOR" -lt 11 ]); then - echo "❌ Python 3.11+ is required. You have $PYTHON_VERSION." - echo "Please install Python 3.11+ and try again." - exit 1 +# If no suitable Python was found, or if Python is too new, offer to exit or continue +if [ -z "$PYTHON_CMD" ] || [ "$PYTHON_TOO_NEW" = true ]; then + OS_TYPE=$(uname -s) + if [ "$PYTHON_TOO_NEW" = true ]; then + echo -e "\n❌ Python version $PYTHON_VERSION detected. Only Python 3.11.x is supported. Newer versions (e.g., 3.12+) are not yet supported." + else + if [[ "$OS_TYPE" == "Darwin" ]]; then + echo -e "\n❌ python3.11 not found. To continue, we recommend running this:\n\n $ brew install python@3.11\n" + elif [[ "$OS_TYPE" == "MINGW"* || "$OS_TYPE" == "CYGWIN"* || "$OS_TYPE" == "MSYS"* ]]; then + echo -e "\n❌ python3.11 not found. Please install Python 3.11 from https://www.python.org/downloads/\n" + else + echo -e "\n❌ python3.11 not found. Please install Python 3.11 from your package manager or https://www.python.org/downloads/\n" + fi + fi + while true; do + echo "Would you like to exit so you can install Python 3.11, or continue anyway? (e = exit, c = continue): " + read -n 1 -r PYTHON_CONT_CHOICE + echo + if [[ "$PYTHON_CONT_CHOICE" =~ ^[Ee]$ ]]; then + echo "Exiting so you can install Python 3.11." + exit 1 + elif [[ "$PYTHON_CONT_CHOICE" =~ ^[Cc]$ ]]; then + echo "⚠️ Continuing without Python 3.11. Some features may not work as expected." + break + else + echo "Please enter 'e' to exit or 'c' to continue." + fi + done fi # Create a virtual environment -VENV_DIR="$HOME/.cua-venv" if [ ! -d "$VENV_DIR" ]; then $PYTHON_CMD -m venv "$VENV_DIR" fi @@ -87,66 +205,148 @@ fi source "$VENV_DIR/bin/activate" # Install required packages -echo "📦 Updating CUA packages..." -pip install -U pip +echo "📦 Updating C/ua packages..." +pip install -U pip setuptools wheel Cmake pip install -U cua-computer "cua-agent[all]" -# Temporary fix for mlx-vlm, see https://github.com/Blaizzy/mlx-vlm/pull/349 -pip install git+https://github.com/ddupont808/mlx-vlm.git@stable/fix/qwen2-position-id +# Install mlx-vlm on Apple Silicon Macs +if [[ $(uname -m) == 'arm64' ]]; then + echo "Installing mlx-vlm for Apple Silicon Macs..." + pip install git+https://github.com/Blaizzy/mlx-vlm.git + # pip install git+https://github.com/ddupont808/mlx-vlm.git@stable/fix/qwen2-position-id +fi # Create a simple demo script -DEMO_DIR="$HOME/.cua-demo" mkdir -p "$DEMO_DIR" -cat > "$DEMO_DIR/run_demo.py" << 'EOF' +# Create .env.local file with API keys (only if it doesn't exist) +if [[ ! -f "$DEMO_DIR/.env.local" ]]; then + cat > "$DEMO_DIR/.env.local" << EOF +# Uncomment and add your API keys here +# OPENAI_API_KEY=your_openai_api_key_here +# ANTHROPIC_API_KEY=your_anthropic_api_key_here +CUA_API_KEY=your_cua_api_key_here +EOF + echo "📝 Created .env.local file with API key placeholders" +else + echo "📝 Found existing .env.local file - keeping your current settings" +fi + +if [[ "$USE_CLOUD" == "true" ]]; then + # Add CUA API key to .env.local if not already present + if ! grep -q "CUA_API_KEY" "$DEMO_DIR/.env.local"; then + echo "CUA_API_KEY=$CUA_API_KEY" >> "$DEMO_DIR/.env.local" + echo "🔑 Added CUA_API_KEY to .env.local" + elif grep -q "CUA_API_KEY=your_cua_api_key_here" "$DEMO_DIR/.env.local"; then + # Update placeholder with actual key + sed -i.bak "s/CUA_API_KEY=your_cua_api_key_here/CUA_API_KEY=$CUA_API_KEY/" "$DEMO_DIR/.env.local" + echo "🔑 Updated CUA_API_KEY in .env.local" + fi +fi + +# Create a convenience script to run the demo +cat > "$DEMO_DIR/start_ui.sh" << EOF +#!/bin/bash +source "$VENV_DIR/bin/activate" +cd "$DEMO_DIR" +python run_demo.py +EOF +chmod +x "$DEMO_DIR/start_ui.sh" + +echo "✅ Setup complete!" + +if [[ "$USE_CLOUD" == "true" ]]; then + # Create run_demo.py for cloud containers + cat > "$DEMO_DIR/run_demo.py" << 'EOF' import asyncio import os +from pathlib import Path +from dotenv import load_dotenv from computer import Computer from agent import ComputerAgent, LLM, AgentLoop, LLMProvider from agent.ui.gradio.app import create_gradio_ui -# Try to load API keys from environment -api_key = os.environ.get("OPENAI_API_KEY", "") -if not api_key: - print("\n⚠️ No OpenAI API key found. You'll need to provide one in the UI.") +# Load environment variables from .env.local +load_dotenv(Path(__file__).parent / ".env.local") + +# Check for required API keys +cua_api_key = os.environ.get("CUA_API_KEY", "") +if not cua_api_key: + print("\n❌ CUA_API_KEY not found in .env.local file.") + print("Please add your CUA API key to the .env.local file.") + exit(1) + +openai_key = os.environ.get("OPENAI_API_KEY", "") +anthropic_key = os.environ.get("ANTHROPIC_API_KEY", "") + +if not openai_key and not anthropic_key: + print("\n⚠️ No OpenAI or Anthropic API keys found in .env.local.") + print("Please add at least one API key to use AI agents.") + +print("🚀 Starting CUA playground with Cloud Containers...") +print("📝 Edit .env.local to update your API keys") # Launch the Gradio UI and open it in the browser app = create_gradio_ui() app.launch(share=False, inbrowser=True) EOF +else + # Create run_demo.py for local macOS VMs + cat > "$DEMO_DIR/run_demo.py" << 'EOF' +import asyncio +import os +from pathlib import Path +from dotenv import load_dotenv +from computer import Computer +from agent import ComputerAgent, LLM, AgentLoop, LLMProvider +from agent.ui.gradio.app import create_gradio_ui -# Create a convenience script to run the demo -cat > "$DEMO_DIR/start_demo.sh" << EOF -#!/bin/bash -source "$VENV_DIR/bin/activate" -cd "$DEMO_DIR" -python run_demo.py +# Load environment variables from .env.local +load_dotenv(Path(__file__).parent / ".env.local") + +# Try to load API keys from environment +openai_key = os.environ.get("OPENAI_API_KEY", "") +anthropic_key = os.environ.get("ANTHROPIC_API_KEY", "") + +if not openai_key and not anthropic_key: + print("\n⚠️ No OpenAI or Anthropic API keys found in .env.local.") + print("Please add at least one API key to use AI agents.") + +print("🚀 Starting CUA playground with local macOS VMs...") +print("📝 Edit .env.local to update your API keys") + +# Launch the Gradio UI and open it in the browser +app = create_gradio_ui() +app.launch(share=False, inbrowser=True) EOF -chmod +x "$DEMO_DIR/start_demo.sh" +fi -echo "✅ Setup complete!" -echo "🖥️ You can start the CUA playground by running: $DEMO_DIR/start_demo.sh" - -# Check if the VM is running -echo "🔍 Checking if the macOS CUA VM is running..." -VM_RUNNING=$(lume ls | grep "macos-sequoia-cua" | grep "running" || echo "") - -if [ -z "$VM_RUNNING" ]; then - echo "🚀 Starting the macOS CUA VM in the background..." - lume run macos-sequoia-cua:latest & - # Wait a moment for the VM to initialize - sleep 5 - echo "✅ VM started successfully." -else - echo "✅ macOS CUA VM is already running." +echo "☁️ CUA Cloud Container setup complete!" +echo "📝 Edit $DEMO_DIR/.env.local to update your API keys" +echo "🖥️ Start the playground by running: $DEMO_DIR/start_ui.sh" + +# Check if the VM is running (only for local setup) +if [[ "$USE_CLOUD" == "false" ]]; then + echo "🔍 Checking if the macOS CUA VM is running..." + VM_RUNNING=$(lume ls | grep "macos-sequoia-cua" | grep "running" || echo "") + + if [ -z "$VM_RUNNING" ]; then + echo "🚀 Starting the macOS CUA VM in the background..." + lume run macos-sequoia-cua:latest & + # Wait a moment for the VM to initialize + sleep 5 + echo "✅ VM started successfully." + else + echo "✅ macOS CUA VM is already running." + fi fi # Ask if the user wants to start the demo now echo -read -p "Would you like to start the CUA playground now? (y/n) " -n 1 -r +read -p "Would you like to start the C/ua Computer-Use Agent UI now? (y/n) " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then - echo "🚀 Starting the CUA playground..." + echo "🚀 Starting the C/ua Computer-Use Agent UI..." echo "" - "$DEMO_DIR/start_demo.sh" + "$DEMO_DIR/start_ui.sh" fi diff --git a/scripts/run-docker-dev.sh b/scripts/run-docker-dev.sh index bb6ef188..d1301d09 100755 --- a/scripts/run-docker-dev.sh +++ b/scripts/run-docker-dev.sh @@ -24,6 +24,24 @@ IMAGE_NAME="cua-dev-image" CONTAINER_NAME="cua-dev-container" PLATFORM="linux/arm64" +# Detect platform based on architecture +arch=$(uname -m) + +if [[ $arch == x86_64* ]]; then + PLATFORM="linux/amd64" + print_info "X64 Architecture detected, using platform: ${PLATFORM}" +elif [[ $arch == i*86 ]]; then + PLATFORM="linux/386" + print_info "X32 Architecture detected, using platform: ${PLATFORM}" +elif [[ $arch == arm* ]] || [[ $arch == aarch64 ]]; then + PLATFORM="linux/arm64" + print_info "ARM Architecture detected, using platform: ${PLATFORM}" +else + # Fallback to amd64 for unknown architectures + PLATFORM="linux/amd64" + print_info "Unknown architecture ($arch), defaulting to platform: ${PLATFORM}" +fi + # Environment variables PYTHONPATH="/app/libs/core:/app/libs/computer:/app/libs/agent:/app/libs/som:/app/libs/pylume:/app/libs/computer-server" @@ -47,7 +65,7 @@ case "$1" in if [ "$2" == "--interactive" ]; then print_info "Running the development Docker container with interactive shell..." print_info "Mounting source code from host" - print_info "Connecting to host.docker.internal:3000" + print_info "Connecting to host.docker.internal:7777" docker run -it --rm \ --platform=${PLATFORM} \ @@ -56,6 +74,7 @@ case "$1" in -e PYTHONPATH=${PYTHONPATH} \ -e DISPLAY=${DISPLAY:-:0} \ -e PYLUME_HOST="host.docker.internal" \ + -p 7860:7860 \ ${IMAGE_NAME} bash else # Run the specified example @@ -64,7 +83,7 @@ case "$1" in exit 1 fi print_info "Running example: $2" - print_info "Connecting to host.docker.internal:3000" + print_info "Connecting to host.docker.internal:7777" docker run -it --rm \ --platform=${PLATFORM} \ @@ -73,6 +92,7 @@ case "$1" in -e PYTHONPATH=${PYTHONPATH} \ -e DISPLAY=${DISPLAY:-:0} \ -e PYLUME_HOST="host.docker.internal" \ + -p 7860:7860 \ ${IMAGE_NAME} python "/app/examples/$2" fi ;; diff --git a/tests/files.py b/tests/files.py new file mode 100644 index 00000000..388b7656 --- /dev/null +++ b/tests/files.py @@ -0,0 +1,141 @@ +""" +File System Interface Tests +Tests for the file system methods of the Computer interface (macOS). +Required environment variables: +- CUA_API_KEY: API key for C/ua cloud provider +- CUA_CONTAINER_NAME: Name of the container to use +""" + +import os +import asyncio +import pytest +from pathlib import Path +import sys +import traceback + +# Load environment variables from .env file +project_root = Path(__file__).parent.parent +env_file = project_root / ".env" +print(f"Loading environment from: {env_file}") +from dotenv import load_dotenv + +load_dotenv(env_file) + +# Add paths to sys.path if needed +pythonpath = os.environ.get("PYTHONPATH", "") +for path in pythonpath.split(":"): + if path and path not in sys.path: + sys.path.insert(0, path) # Insert at beginning to prioritize + print(f"Added to sys.path: {path}") + +from computer.computer import Computer + +@pytest.fixture(scope="session") +async def computer(): + """Shared Computer instance for all test cases.""" + # # Create a remote Linux computer with C/ua + # computer = Computer( + # os_type="linux", + # api_key=os.getenv("CUA_API_KEY"), + # name=str(os.getenv("CUA_CONTAINER_NAME")), + # provider_type=VMProviderType.CLOUD, + # ) + + # Create a local macOS computer with C/ua + # computer = Computer() + + # Connect to host computer + computer = Computer(use_host_computer_server=True) + + try: + await computer.run() + yield computer + finally: + await computer.disconnect() + +@pytest.mark.asyncio(loop_scope="session") +async def test_file_exists(computer): + tmp_path = "test_file_exists.txt" + # Ensure file does not exist + if await computer.interface.file_exists(tmp_path): + await computer.interface.delete_file(tmp_path) + exists = await computer.interface.file_exists(tmp_path) + assert exists is False, f"File {tmp_path} should not exist" + # Create file and check again + await computer.interface.write_text(tmp_path, "hello") + exists = await computer.interface.file_exists(tmp_path) + assert exists is True, f"File {tmp_path} should exist" + await computer.interface.delete_file(tmp_path) + + +@pytest.mark.asyncio(loop_scope="session") +async def test_directory_exists(computer): + tmp_dir = "test_directory_exists" + if await computer.interface.directory_exists(tmp_dir): + # Remove all files in directory before removing directory + files = await computer.interface.list_dir(tmp_dir) + for fname in files: + await computer.interface.delete_file(f"{tmp_dir}/{fname}") + # Remove the directory itself + await computer.interface.delete_dir(tmp_dir) + exists = await computer.interface.directory_exists(tmp_dir) + assert exists is False, f"Directory {tmp_dir} should not exist" + await computer.interface.create_dir(tmp_dir) + exists = await computer.interface.directory_exists(tmp_dir) + assert exists is True, f"Directory {tmp_dir} should exist" + # Cleanup: remove files and directory + files = await computer.interface.list_dir(tmp_dir) + for fname in files: + await computer.interface.delete_file(f"{tmp_dir}/{fname}") + await computer.interface.delete_dir(tmp_dir) + + +@pytest.mark.asyncio(loop_scope="session") +async def test_list_dir(computer): + tmp_dir = "test_list_dir" + if not await computer.interface.directory_exists(tmp_dir): + await computer.interface.create_dir(tmp_dir) + files = ["foo.txt", "bar.txt"] + for fname in files: + await computer.interface.write_text(f"{tmp_dir}/{fname}", "hi") + result = await computer.interface.list_dir(tmp_dir) + assert set(result) >= set(files), f"Directory {tmp_dir} should contain files {files}" + for fname in files: + await computer.interface.delete_file(f"{tmp_dir}/{fname}") + await computer.interface.delete_dir(tmp_dir) + + +@pytest.mark.asyncio(loop_scope="session") +async def test_read_write_text(computer): + tmp_path = "test_rw_text.txt" + content = "sample text" + await computer.interface.write_text(tmp_path, content) + read = await computer.interface.read_text(tmp_path) + assert read == content, "File content should match" + await computer.interface.delete_file(tmp_path) + + +@pytest.mark.asyncio(loop_scope="session") +async def test_delete_file(computer): + tmp_path = "test_delete_file.txt" + await computer.interface.write_text(tmp_path, "bye") + exists = await computer.interface.file_exists(tmp_path) + assert exists is True, "File should exist" + await computer.interface.delete_file(tmp_path) + exists = await computer.interface.file_exists(tmp_path) + assert exists is False, "File should not exist" + + +@pytest.mark.asyncio(loop_scope="session") +async def test_create_dir(computer): + tmp_dir = "test_create_dir" + if await computer.interface.directory_exists(tmp_dir): + await computer.interface.delete_dir(tmp_dir) + await computer.interface.create_dir(tmp_dir) + exists = await computer.interface.directory_exists(tmp_dir) + assert exists is True, "Directory should exist" + await computer.interface.delete_dir(tmp_dir) + +if __name__ == "__main__": + # Run tests directly + pytest.main([__file__, "-v"]) diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 00000000..998cbeaf --- /dev/null +++ b/tests/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +asyncio_mode = auto +markers = + asyncio: asyncio mark \ No newline at end of file diff --git a/tests/venv.py b/tests/venv.py new file mode 100644 index 00000000..7097c2fd --- /dev/null +++ b/tests/venv.py @@ -0,0 +1,205 @@ +""" +Virtual Environment Testing Module +This module tests the ability to execute python code in a virtual environment within C/ua Containers. + +Required environment variables: +- CUA_API_KEY: API key for C/ua cloud provider +- CUA_CONTAINER_NAME: Name of the container to use +""" + +import os +import asyncio +import pytest +from pathlib import Path +import sys +import traceback + +# Load environment variables from .env file +project_root = Path(__file__).parent.parent +env_file = project_root / ".env" +print(f"Loading environment from: {env_file}") +from dotenv import load_dotenv + +load_dotenv(env_file) + +# Add paths to sys.path if needed +pythonpath = os.environ.get("PYTHONPATH", "") +for path in pythonpath.split(":"): + if path and path not in sys.path: + sys.path.insert(0, path) # Insert at beginning to prioritize + print(f"Added to sys.path: {path}") + +from computer.computer import Computer +from computer.providers.base import VMProviderType +from computer.helpers import sandboxed, set_default_computer + + +@pytest.fixture(scope="session") +async def computer(): + """Shared Computer instance for all test cases.""" + # # Create a remote Linux computer with C/ua + # computer = Computer( + # os_type="linux", + # api_key=os.getenv("CUA_API_KEY"), + # name=str(os.getenv("CUA_CONTAINER_NAME")), + # provider_type=VMProviderType.CLOUD, + # ) + + # Create a local macOS computer with C/ua + computer = Computer() + + try: + await computer.run() + yield computer + finally: + await computer.disconnect() + + +# Sample test cases +@pytest.mark.asyncio(loop_scope="session") +async def test_venv_install(computer): + """Test virtual environment creation and package installation.""" + # Create a test virtual environment and install requests + stdout, _ = await computer.venv_install("test_env", ["requests"]) + + # Check that installation was successful (no major errors) + assert "Successfully installed" in stdout or "Requirement already satisfied" in stdout + +@pytest.mark.asyncio(loop_scope="session") +async def test_venv_cmd(computer): + """Test executing shell commands in virtual environment.""" + # Test Python version check + stdout, _ = await computer.venv_cmd("test_env", "python --version") + + assert "Python" in stdout + +@pytest.mark.asyncio(loop_scope="session") +async def test_venv_exec(computer): + """Test executing Python functions in virtual environment.""" + def test_function(message="Hello World"): + import sys + return f"Python {sys.version_info.major}.{sys.version_info.minor}: {message}" + + result = await computer.venv_exec("test_env", test_function, message="Test successful!") + + assert "Python" in result + assert "Test successful!" in result + +@pytest.mark.asyncio(loop_scope="session") +async def test_venv_exec_with_package(computer): + """Test executing Python functions that use installed packages.""" + def test_requests(): + import requests + return f"requests version: {requests.__version__}" + + result = await computer.venv_exec("test_env", test_requests) + + assert "requests version:" in result + +@pytest.mark.asyncio(loop_scope="session") +async def test_venv_exec_error_handling(computer): + """Test error handling in venv_exec.""" + def test_error(): + raise ValueError("This is a test error") + + with pytest.raises(ValueError, match="This is a test error"): + await computer.venv_exec("test_env", test_error) + +@pytest.mark.asyncio(loop_scope="session") +async def test_venv_exec_with_args_kwargs(computer): + """Test executing Python functions with args and kwargs that return an object.""" + def create_data_object(name, age, *hobbies, **metadata): + return { + "name": name, + "age": age, + "hobbies": list(hobbies), + "metadata": metadata, + "status": "active" + } + + args = ["Alice", 25, "reading", "coding"] + kwargs = {"location": "New York", "department": "Engineering"} + + result = await computer.venv_exec( + "test_env", + create_data_object, + *args, + **kwargs + ) + + assert result["name"] == "Alice" + assert result["age"] == 25 + assert result["hobbies"] == ["reading", "coding"] + assert result["metadata"]["location"] == "New York" + assert result["status"] == "active" + +@pytest.mark.asyncio(loop_scope="session") +async def test_venv_exec_stdout_capture(computer, capfd): + """Test capturing stdout from Python functions executed in virtual environment.""" + def hello_world_function(): + print("Hello World!") + return "Function completed" + + # Execute the function in the virtual environment + result = await computer.venv_exec("test_env", hello_world_function) + + # Capture stdout and stderr + out, _ = capfd.readouterr() + + # Assert the stdout contains our expected output + assert out == "Hello World!\n\n" + assert result == "Function completed" + +@pytest.mark.asyncio(loop_scope="session") +async def test_remote_decorator(computer): + """Test the remote decorator from computer.helpers module.""" + # Set the computer as default for the remote decorator + set_default_computer(computer) + + # Define a function with the remote decorator + @sandboxed("test_env") + def get_package_version(): + import sys + import platform + return { + "python_version": sys.version, + "platform": platform.platform(), + "success": True + } + + # Call the decorated function + result = await get_package_version() + + # Verify the function executed in the virtual environment + assert "python_version" in result + assert "platform" in result + assert result["success"] == True + +@pytest.mark.asyncio(loop_scope="session") +async def test_remote_decorator_with_custom_computer(computer): + """Test the remote decorator with explicitly specified computer instance.""" + # Define a function with the remote decorator that explicitly specifies the computer + @sandboxed("test_env", computer=computer) + def get_system_info(): + import os + import sys + return { + "python_version": sys.version, + "environment_vars": dict(os.environ), + "working_directory": os.getcwd() + } + + # Call the decorated function + result = await get_system_info() + + # Verify the function executed in the virtual environment + assert "python_version" in result + assert "environment_vars" in result + assert "working_directory" in result + # The virtual environment should have a different working directory + # than the current test process + assert result["working_directory"] != os.getcwd() + +if __name__ == "__main__": + # Run tests directly + pytest.main([__file__, "-v"])