diff --git a/API_FEATURES.md b/API_FEATURES.md index e84ba47..9aa403d 100644 --- a/API_FEATURES.md +++ b/API_FEATURES.md @@ -28,6 +28,7 @@ Grainchain provides a unified Python interface for interacting with various sand | Provider | Status | Specialization | |----------|--------|----------------| +| **Blaxel** | ✅ Production Ready | Custom Docker images, automatic snapshots, 25ms startup | | **E2B** | ✅ Production Ready | Custom Docker images, development environments | | **Morph** | ✅ Production Ready | Memory snapshots, instant state management | | **Daytona** | ✅ Production Ready | Cloud development workspaces | @@ -239,6 +240,7 @@ async with Sandbox(provider="e2b") as sandbox: | Provider | Snapshot Type | Creation Speed | Restoration Speed | Preserves Processes | Notes | |----------|---------------|----------------|-------------------|-------------------|-------| | **Morph** | Memory | <250ms | <500ms | ✅ Yes | True pause/resume functionality | +| **Blaxel** | Memory + Filesystem | 1-5s | 25-50ms | ✅ Yes | Automatic state management | | **E2B** | Filesystem | 2-10s | 5-15s | ❌ No | Template-based restoration | | **Local** | Filesystem | 1-5s | 1-3s | ❌ No | Directory-based snapshots | | **Daytona** | Filesystem | 3-8s | 5-12s | ❌ No | Workspace state preservation | @@ -284,6 +286,86 @@ async with Sandbox(provider="morph") as sandbox: ### Current Docker Support by Provider +#### Blaxel Provider - Full Custom Docker Support ✅ + +**Capabilities:** +- Custom Docker images with automatic image management +- Configurable memory allocation +- Regional deployment options +- Port forwarding support +- TTL (Time To Live) configuration for sandbox lifecycle +- Workspace-based authentication + +**Configuration:** +```python +config = SandboxConfig( + image="blaxel/prod-base:latest", + provider_config={ + "name": "my-custom-sandbox", # Custom sandbox name + "memory": 8192, # Memory in MB (default: 4096) + "region": "us-pdx-1", # Deployment region + "ttl": "2h", # TTL with human-readable format (optional) + "ttl_idle": "30m", # Delete after 30 minutes of inactivity (optional) + "expires": "2025-12-31T23:59:59Z", # Absolute expiration date (optional) + "ports": [ # Port configuration + {"target": 3000, "protocol": "HTTP"}, + {"target": 8080, "protocol": "HTTP"} + ], + "api_key": "your-api-key", # API key (optional if using CLI auth) + "workspace": "your-workspace" # Workspace name (optional if using CLI auth) + } +) + +async with Sandbox(provider="blaxel", config=config) as sandbox: + # Runs in your configured Docker environment + result = await sandbox.execute("python --version") +``` + +**TTL (Time To Live) Configuration:** + +Blaxel supports three types of lifecycle management: + +1. **`ttl`** - Maximum lifetime of the sandbox + - Accepts human-readable formats: `"30s"`, `"10m"`, `"2h"`, `"1d"` + - Also accepts seconds as an integer for backwards compatibility + - Sandbox terminates after this duration from creation + +2. **`ttl_idle`** - Idle timeout for automatic cleanup + - Accepts human-readable formats: `"30m"`, `"1h"`, `"2h"` + - Sandbox terminates after this duration of inactivity + - Useful for development environments that should clean up when unused + +3. **`expires`** - Absolute expiration date + - ISO 8601 format: `"2025-10-07T14:53:46Z"` + - Sandbox terminates at this specific date/time + - Useful for time-limited access or scheduled cleanup + +**Examples:** +```python +# Short-lived CI/CD pipeline (10 minutes) +ci_config = {"ttl": "10m"} + +# Development environment with idle cleanup (2 hours active, 30 min idle) +dev_config = {"ttl": "2h", "ttl_idle": "30m"} + +# Time-limited demo until end of day +demo_config = {"expires": "2025-10-07T17:00:00Z"} +``` + +**Authentication Methods:** +1. **CLI Authentication (Recommended for local development):** + ```bash + bl login YOUR-WORKSPACE + ``` + +2. **Environment Variables:** + ```bash + export BL_API_KEY='your-api-key' + export BL_WORKSPACE='your-workspace' + ``` + +3. **Provider Config (Shown above)** + #### E2B Provider - Full Custom Docker Support ✅ **Capabilities:** @@ -439,6 +521,7 @@ async with Sandbox(provider="docker", config=config) as sandbox: | Provider | Docker Support | Custom Images | Dockerfile | Base Images | Status | |----------|----------------|---------------|------------|-------------|--------| +| **Blaxel** | ✅ Full | ✅ Yes | ✅ Yes | ✅ Multiple | Production | | **E2B** | ✅ Full | ✅ Yes | ✅ e2b.Dockerfile | ✅ Multiple | Production | | **Morph** | ✅ Partial | ❌ No | ❌ No | ✅ Curated | Production | | **Modal** | ✅ Full | ✅ Yes | ✅ Programmatic | ✅ Registry | Production | @@ -450,18 +533,18 @@ async with Sandbox(provider="docker", config=config) as sandbox: ### Feature Support Matrix -| Feature | E2B | Morph | Daytona | Modal | Local | Docker* | -|---------|-----|-------|---------|-------|-------|---------| -| **Core Execution** | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 | -| **File Operations** | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 | -| **Filesystem Snapshots** | ✅ | ❌ | ✅ | ✅ | ✅ | 🚧 | -| **Memory Snapshots** | ❌ | ✅ | ❌ | ❌ | ❌ | 🚧 | -| **Terminate/Wake-up** | ❌ | ✅ | ❌ | ❌ | ❌ | 🚧 | -| **Custom Docker** | ✅ | ❌ | ❌ | ✅ | ❌ | 🚧 | -| **Base Images** | ✅ | ✅ | ✅ | ✅ | ❌ | 🚧 | -| **Resource Limits** | ✅ | ✅ | ✅ | ✅ | ❌ | 🚧 | -| **Persistent Storage** | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 | -| **Network Access** | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 | +| Feature | Blaxel | E2B | Morph | Daytona | Modal | Local | Docker* | +|---------|--------|-----|-------|---------|-------|-------|---------| +| **Core Execution** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 | +| **File Operations** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 | +| **Filesystem Snapshots** | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | 🚧 | +| **Memory Snapshots** | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | 🚧 | +| **Terminate/Wake-up** | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | 🚧 | +| **Custom Docker** | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | 🚧 | +| **Base Images** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | 🚧 | +| **Resource Limits** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | 🚧 | +| **Persistent Storage** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 | +| **Network Access** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 | *Docker provider features are planned @@ -469,6 +552,7 @@ async with Sandbox(provider="docker", config=config) as sandbox: | Provider | Startup Time | Execution Overhead | Snapshot Creation | Snapshot Restoration | |----------|--------------|-------------------|-------------------|---------------------| +| **Blaxel** | 1-5s | Minimal | Automatic | 25-50ms | | **E2B** | 5-15s | Low | 2-10s | 5-15s | | **Morph** | <250ms | Minimal | <250ms | <500ms | | **Daytona** | 10-30s | Low | 3-8s | 5-12s | @@ -479,6 +563,7 @@ async with Sandbox(provider="docker", config=config) as sandbox: | Provider | Pricing Model | Cost Efficiency | Free Tier | |----------|---------------|-----------------|-----------| +| **Blaxel** | Usage-based | High for production | ✅ Available | | **E2B** | Per-minute usage | High for development | ✅ Available | | **Morph** | Per-instance + snapshots | High for long-running | ❌ Paid only | | **Daytona** | Workspace-based | Medium | ✅ Available | @@ -560,7 +645,7 @@ async def snapshot_workflow(): ```python async def compare_providers(): - providers = ["local", "e2b", "morph"] + providers = ["local", "blaxel", "e2b", "morph"] results = {} for provider in providers: @@ -620,22 +705,23 @@ Choose your provider based on your needs: 📊 **Performance Priority** ├── Fastest startup → Local (development only) -├── Fastest snapshots → Morph (<250ms) -└── Balanced performance → E2B or Modal +├── Fastest snapshots → Blaxel (automatic) +└── Balanced performance → Blaxel, E2B or Modal 🔧 **Feature Requirements** ├── Memory snapshots → Morph (only option) -├── Custom Docker → E2B or Modal +├── Custom Docker → Blaxel, E2B or Modal +├── TTL configuration → Blaxel (automatic cleanup) ├── Long-running processes → Morph (terminate/wake-up) └── Simple file operations → Any provider 💰 **Cost Considerations** -├── Free development → Local, E2B (free tier), Modal (free tier) -├── Production efficiency → Morph (pay for what you use) +├── Free development → Local, Blaxel (free tier), E2B (free tier), Modal (free tier) +├── Production efficiency → Blaxel or Morph (pay for what you use) └── Batch processing → Modal (serverless pricing) 🏗️ **Use Case Specific** -├── CI/CD pipelines → E2B (Docker support) +├── CI/CD pipelines → Blaxel or E2B (Docker support with TTL) ├── Interactive development → Morph (memory snapshots) ├── Data processing → Modal (scalable compute) ├── Local testing → Local (no overhead) @@ -712,6 +798,7 @@ Grainchain provides a powerful, unified interface for sandbox management with: ✅ **Type-safe** Python interface with async support **Key Takeaways:** +- Use **Blaxel** for production-ready cloud sandboxes with automatic snapshots and custom Docker images - Use **Morph** for memory snapshots and instant state management - Use **E2B** for custom Docker environments and development - Use **Modal** for scalable compute with custom images diff --git a/BENCHMARKING.md b/BENCHMARKING.md index 5553ebf..9e4d2d2 100644 --- a/BENCHMARKING.md +++ b/BENCHMARKING.md @@ -11,7 +11,7 @@ Grainchain now includes powerful analysis capabilities to help you make data-dri ```bash # Run benchmarks and analyze results grainchain benchmark --provider all -grainchain analysis compare --provider1 local --provider2 e2b --chart +grainchain analysis compare --provider1 local --provider2 blaxel --chart grainchain analysis report --format html --include-charts ``` @@ -90,6 +90,7 @@ grainchain benchmark --provider local 1. **Try other providers** (requires API keys): ```bash + grainchain benchmark --provider blaxel grainchain benchmark --provider e2b grainchain benchmark --provider daytona grainchain benchmark --provider morph @@ -170,6 +171,10 @@ python benchmarks/scripts/grainchain_benchmark.py --config my_config.json Set up your provider credentials: ```bash +# Blaxel Provider (optional - can use CLI auth) +BL_API_KEY=your_blaxel_api_key_here +BL_WORKSPACE=your_workspace_here + # E2B Provider E2B_API_KEY=your_e2b_api_key_here E2B_TEMPLATE=base diff --git a/INTEGRATION.md b/INTEGRATION.md index ac6db1f..62b53c1 100644 --- a/INTEGRATION.md +++ b/INTEGRATION.md @@ -288,6 +288,8 @@ class AppConfig: default_provider: str = "local" default_timeout: int = 300 max_concurrent_sandboxes: int = 10 + blaxel_api_key: str = "" + blaxel_workspace: str = "" e2b_api_key: str = "" morph_api_key: str = "" daytona_api_key: str = "" @@ -299,6 +301,8 @@ class AppConfig: default_provider=os.getenv("GRAINCHAIN_DEFAULT_PROVIDER", "local"), default_timeout=int(os.getenv("GRAINCHAIN_DEFAULT_TIMEOUT", "300")), max_concurrent_sandboxes=int(os.getenv("GRAINCHAIN_MAX_CONCURRENT", "10")), + blaxel_api_key=os.getenv("BL_API_KEY", ""), + blaxel_workspace=os.getenv("BL_WORKSPACE", ""), e2b_api_key=os.getenv("E2B_API_KEY", ""), morph_api_key=os.getenv("MORPH_API_KEY", ""), daytona_api_key=os.getenv("DAYTONA_API_KEY", ""), @@ -392,6 +396,11 @@ class RobustExecutor: # Usage executor = RobustExecutor(max_retries=3) + +# Try with Blaxel +result = await executor.execute_with_retry("print('Hello')", provider="blaxel") + +# Try with E2B result = await executor.execute_with_retry("print('Hello')", provider="e2b") ``` diff --git a/MANIFESTO.md b/MANIFESTO.md index efe5288..7c635ab 100644 --- a/MANIFESTO.md +++ b/MANIFESTO.md @@ -14,7 +14,7 @@ Today, developers face an impossible choice: commit to a single sandbox provider ## The Problem We're Solving -The explosion of sandbox providers—E2B, Modal, Daytona, Morph, and countless others—represents incredible innovation in cloud computing. But this innovation comes with a cost: +The explosion of sandbox providers—Blaxel, E2B, Modal, Daytona, Morph, and countless others—represents incredible innovation in cloud computing. But this innovation comes with a cost: - **Developers are trapped** by vendor-specific APIs - **Innovation is stifled** by the fear of choosing the wrong provider @@ -26,7 +26,7 @@ The explosion of sandbox providers—E2B, Modal, Daytona, Morph, and countless o ## Our Principles ### 1. **Provider Agnostic by Design** -Write once, run anywhere. Your code should work seamlessly across E2B, Modal, Daytona, Morph, or any future provider that emerges. +Write once, run anywhere. Your code should work seamlessly across Blaxel, E2B, Modal, Daytona, Morph, or any future provider that emerges. ### 2. **Simplicity Over Complexity** A unified API shouldn't mean a complicated API. We believe in clean, intuitive interfaces that make the complex simple. @@ -64,7 +64,7 @@ async with Sandbox() as sandbox: ``` ### ✅ **Provider Flexibility** -Switch between E2B, Modal, Daytona, Morph, and Local providers with a single configuration change. +Switch between Blaxel, E2B, Modal, Daytona, Morph, and Local providers with a single configuration change. ### ✅ **Production Ready** Built with async/await, comprehensive error handling, and battle-tested patterns. @@ -90,7 +90,7 @@ But we're just getting started. ### **Phase 1: Foundation** ✅ - Core API design and implementation -- Support for major providers (E2B, Modal, Daytona, Morph, Local) +- Support for major providers (Blaxel, E2B, Modal, Daytona, Morph, Local) - Production-ready packaging and distribution ### **Phase 2: Ecosystem** 🚧 diff --git a/README.md b/README.md index ef488bf..0c1006c 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ if not e2b_info.available: | Provider | Dependencies | Environment Variables | Install Command | |----------|-------------|----------------------|-----------------| | **Local** | None | None | Built-in ✅ | +| **Blaxel** | `blaxel` | `BL_API_KEY`, `BL_WORKSPACE` | `pip install grainchain[blaxel]` | | **E2B** | `e2b` | `E2B_API_KEY` | `pip install grainchain[e2b]` | | **Modal** | `modal` | `MODAL_TOKEN_ID`, `MODAL_TOKEN_SECRET` | `pip install grainchain[modal]` | | **Daytona** | `daytona` | `DAYTONA_API_KEY` | `pip install daytona-sdk` | @@ -106,12 +107,13 @@ Compare sandbox providers with comprehensive performance testing: ```bash # Test individual providers python benchmarks/scripts/grainchain_benchmark.py --providers local +python benchmarks/scripts/grainchain_benchmark.py --providers blaxel python benchmarks/scripts/grainchain_benchmark.py --providers e2b python benchmarks/scripts/grainchain_benchmark.py --providers daytona python benchmarks/scripts/grainchain_benchmark.py --providers morph # Test multiple providers at once -python benchmarks/scripts/grainchain_benchmark.py --providers local e2b --iterations 3 +python benchmarks/scripts/grainchain_benchmark.py --providers local blaxel e2b --iterations 3 # Generate automated summary report python benchmarks/scripts/auto_publish.py --generate-summary @@ -123,7 +125,7 @@ Run comprehensive benchmarks across all available providers: ```bash # Run full benchmark suite with all providers -python benchmarks/scripts/grainchain_benchmark.py --providers local e2b modal daytona morph --iterations 3 +python benchmarks/scripts/grainchain_benchmark.py --providers local blaxel e2b modal daytona morph --iterations 3 # Run automated benchmark and generate summary (used by CI) python benchmarks/scripts/auto_publish.py --run-benchmark @@ -147,6 +149,7 @@ Latest benchmark results (updated 2025-07-06): | Provider | Success Rate | Avg Time (s) | Status | Performance | |----------|--------------|--------------|--------|-------------| | **Local** | 100.0% | 1.39 | ✅ Production Ready | ⚡ Fastest | +| **Blaxel** | - | - | ❓ Not tested | 🚀 Native async support | | **E2B** | - | - | ❓ Not tested | 🚀 Cloud-based | | **Daytona** | - | - | ❓ Not tested | 🛡️ Comprehensive | | **Morph** | - | - | ❌ Payment required | 🚀 Instant Snapshots | @@ -154,6 +157,7 @@ Latest benchmark results (updated 2025-07-06): > **Performance Notes**: > > - **Local**: ✅ **Production-ready** with 100% success rate, fastest execution, perfect for development/testing +> - **Blaxel**: Cloud sandboxes with fast startup time and automatic snapshots > - **E2B**: Production-ready cloud sandboxes (requires API key setup) > - **Daytona**: Full workspace environments with comprehensive tooling > - **Morph**: Custom base images with instant snapshots (requires paid plan) @@ -191,10 +195,10 @@ For more statistically significant results, you can run high-iteration benchmark ./scripts/benchmark_high_iteration.sh 50 # Run 100 iterations on specific providers -./scripts/benchmark_high_iteration.sh 100 "local e2b" +./scripts/benchmark_high_iteration.sh 100 "local blaxel e2b" # Using the CLI command -uv run grainchain benchmark-high-iteration --iterations 50 --providers "local e2b" +uv run grainchain benchmark-high-iteration --iterations 50 --providers "local blaxel e2b" ``` **GitHub Action (Manual Trigger):** @@ -213,7 +217,7 @@ uv run grainchain benchmark-high-iteration --iterations 50 --providers "local e2 ## 🎯 Why Grainchain? -The sandbox ecosystem is rapidly expanding with providers like [E2B](https://e2b.dev/), [Daytona](https://daytona.io/), [Morph](https://morph.dev/), and others. Each has different APIs and capabilities, creating: +The sandbox ecosystem is rapidly expanding with providers like [Blaxel](https://blaxel.ai/), [E2B](https://e2b.dev/), [Daytona](https://daytona.io/), [Morph](https://morph.dev/), and others. Each has different APIs and capabilities, creating: - **Vendor Lock-in**: Applications become tightly coupled to specific providers - **Learning Curve**: Developers must learn multiple APIs @@ -254,6 +258,9 @@ Grainchain solves these problems with a unified interface that abstracts provide # Basic installation pip install grainchain +# With Blaxel support +pip install grainchain[blaxel] + # With E2B support pip install grainchain[e2b] @@ -310,6 +317,7 @@ grainchain install-hooks | Provider | Status | Features | | ----------- | ------------ | ------------------------------------------------ | +| **Blaxel** | ✅ Supported | Custom Docker images, automatic snapshots, 25ms startup | | **E2B** | ✅ Supported | Code interpreter, custom images, file operations | | **Daytona** | ✅ Supported | Development environments, workspace management | | **Morph** | ✅ Supported | Custom base images, instant snapshots, <250ms startup | @@ -384,6 +392,9 @@ async with Sandbox() as sandbox: from grainchain import Sandbox # Use specific provider +async with Sandbox(provider="blaxel") as sandbox: + result = await sandbox.execute("echo 'Using Blaxel'") + async with Sandbox(provider="e2b") as sandbox: result = await sandbox.execute("echo 'Using E2B'") @@ -458,7 +469,11 @@ async with Sandbox(provider="local") as sandbox: ```bash # Default provider -export GRAINCHAIN_DEFAULT_PROVIDER=e2b +export GRAINCHAIN_DEFAULT_PROVIDER=blaxel + +# Blaxel configuration (optional - can use CLI auth) +export BL_API_KEY=your-blaxel-key +export BL_WORKSPACE=your-workspace # E2B configuration export E2B_API_KEY=your-e2b-key @@ -478,9 +493,16 @@ export MORPH_TEMPLATE=custom-base-image Create `grainchain.yaml` in your project root: ```yaml -default_provider: e2b +default_provider: blaxel providers: + blaxel: + api_key: ${BL_API_KEY} + workspace: ${BL_WORKSPACE} + image: blaxel/prod-base:latest + region: us-pdx-1 + timeout: 300 + e2b: api_key: ${E2B_API_KEY} template: python-data-science @@ -582,6 +604,7 @@ All code is automatically checked with: - [x] Core interface design - [x] Base provider abstraction - [x] Configuration system +- [x] Blaxel provider implementation - [x] E2B provider implementation - [x] Daytona provider implementation - [x] Morph provider implementation @@ -619,7 +642,7 @@ MIT License - see [LICENSE](LICENSE) for details. ## 🙏 Acknowledgments - Inspired by [Langchain](https://github.com/langchain-ai/langchain) for LLM abstraction -- Built for the [E2B](https://e2b.dev/), [Daytona](https://daytona.io/), and [Morph](https://morph.dev/) communities +- Built for the [Blaxel](https://blaxel.ai/), [E2B](https://e2b.dev/), [Daytona](https://daytona.io/), and [Morph](https://morph.dev/) communities - Thanks to all contributors and early adopters --- diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 05b92cc..dcdd8ff 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -17,6 +17,8 @@ grainchain --version grainchain benchmark --provider local # Check environment variables +echo "BL_API_KEY: $BL_API_KEY" +echo "BL_WORKSPACE: $BL_WORKSPACE" echo "E2B_API_KEY: $E2B_API_KEY" echo "MORPH_API_KEY: $MORPH_API_KEY" echo "DAYTONA_API_KEY: $DAYTONA_API_KEY" @@ -106,12 +108,16 @@ uv run grainchain --version **Solutions**: ```bash # Set environment variables +export BL_API_KEY="your-blaxel-api-key" +export BL_WORKSPACE="your-blaxel-workspace" export E2B_API_KEY="your-e2b-api-key" export MORPH_API_KEY="your-morph-api-key" export DAYTONA_API_KEY="your-daytona-api-key" # Or create a .env file in your project directory -echo "E2B_API_KEY=your-e2b-api-key" > .env +echo "BL_API_KEY=your-blaxel-api-key" > .env +echo "BL_WORKSPACE=your-blaxel-workspace" >> .env +echo "E2B_API_KEY=your-e2b-api-key" >> .env echo "MORPH_API_KEY=your-morph-api-key" >> .env echo "DAYTONA_API_KEY=your-daytona-api-key" >> .env @@ -122,9 +128,10 @@ env | grep API_KEY **Problem**: Invalid or expired API keys **Solutions**: -1. **E2B**: Get a new API key from [E2B Dashboard](https://e2b.dev/dashboard) -2. **Morph**: Get a new API key from [Morph Dashboard](https://morph.dev/dashboard) -3. **Daytona**: Get a new API key from [Daytona Dashboard](https://daytona.io/dashboard) +1. **Blaxel**: Get credentials from [Blaxel Dashboard](https://app.blaxel.ai/profile/security) or use `bl login YOUR-WORKSPACE` +2. **E2B**: Get a new API key from [E2B Dashboard](https://e2b.dev/dashboard) +3. **Morph**: Get a new API key from [Morph Dashboard](https://morph.dev/dashboard) +4. **Daytona**: Get a new API key from [Daytona Dashboard](https://daytona.io/dashboard) ### Provider Connection Issues diff --git a/docs/analysis_guide.md b/docs/analysis_guide.md index 20ab8e0..7022411 100644 --- a/docs/analysis_guide.md +++ b/docs/analysis_guide.md @@ -20,11 +20,11 @@ The Grainchain benchmark analysis system provides powerful tools to: Compare two providers to see which performs better: ```bash -grainchain analysis compare --provider1 local --provider2 e2b --days 30 --chart +grainchain analysis compare --provider1 local --provider2 blaxel --days 30 --chart ``` This will: -- Compare the last 30 days of data between local and e2b providers +- Compare the last 30 days of data between local and blaxel providers - Show improvements and regressions - Generate a comparison chart @@ -71,13 +71,16 @@ Compare performance between two providers. **Examples:** ```bash # Basic comparison -grainchain analysis compare --provider1 local --provider2 e2b +grainchain analysis compare --provider1 local --provider2 blaxel # With chart generation -grainchain analysis compare --provider1 local --provider2 e2b --chart --chart-type radar +grainchain analysis compare --provider1 local --provider2 blaxel --chart --chart-type radar + +# Compare Blaxel with E2B +grainchain analysis compare --provider1 blaxel --provider2 e2b --chart # Save detailed report -grainchain analysis compare --provider1 local --provider2 e2b --output comparison_report.html +grainchain analysis compare --provider1 local --provider2 blaxel --output comparison_report.html ``` ### `grainchain analysis trends` @@ -93,6 +96,9 @@ Analyze performance trends over time. **Examples:** ```bash +# Analyze success rate trends for blaxel provider +grainchain analysis trends --provider blaxel --metric success_rate + # Analyze success rate trends for local provider grainchain analysis trends --provider local --metric success_rate @@ -323,9 +329,13 @@ results = parser.load_all_results() # Compare providers comparator = BenchmarkComparator(parser) -comparison = comparator.compare_providers("local", "e2b", days=30) +comparison = comparator.compare_providers("local", "blaxel", days=30) print(comparison.summary) + +# Also compare Blaxel with E2B +blaxel_vs_e2b = comparator.compare_providers("blaxel", "e2b", days=30) +print(blaxel_vs_e2b.summary) ``` ### Programmatic Report Generation diff --git a/examples/simplified_usage.py b/examples/simplified_usage.py index 74a379a..7bcb6a9 100644 --- a/examples/simplified_usage.py +++ b/examples/simplified_usage.py @@ -10,6 +10,7 @@ from grainchain import ( Providers, + create_blaxel_sandbox, create_e2b_sandbox, create_local_sandbox, create_sandbox, @@ -59,8 +60,19 @@ async def factory_functions_example(): result = await sandbox.execute("echo 'Custom timeout sandbox'") print(f" Output: {result.stdout.strip()}") + # Blaxel sandbox (if available) + print("\n3. Blaxel sandbox example:") + try: + sandbox = create_blaxel_sandbox(memory=4096, ttl="1h", ttl_idle="30m") + print(" Blaxel sandbox created successfully!") + print(" - TTL: 1 hour maximum lifetime") + print(" - Idle timeout: 30 minutes of inactivity") + # Note: Would need Blaxel credentials to actually use + except Exception as e: + print(f" Blaxel not available: {e}") + # E2B sandbox (if available) - print("\n3. E2B sandbox example:") + print("\n4. E2B sandbox example:") try: sandbox = create_e2b_sandbox(template="python") print(" E2B sandbox created successfully!") diff --git a/grainchain/__init__.py b/grainchain/__init__.py index 3669f20..760af6a 100644 --- a/grainchain/__init__.py +++ b/grainchain/__init__.py @@ -43,6 +43,7 @@ class Providers: """Provider constants for easier provider selection.""" LOCAL = "local" + BLAXEL = "blaxel" E2B = "e2b" DAYTONA = "daytona" MORPH = "morph" @@ -75,6 +76,56 @@ def create_local_sandbox( return Sandbox(provider=Providers.LOCAL, config=config) +def create_blaxel_sandbox( + image: str = "blaxel/prod-base:latest", + memory: int = 4096, + ttl: str | int | None = None, + ttl_idle: str | None = None, + expires: str | None = None, + timeout: int = 60, + **kwargs +) -> Sandbox: + """ + Create a Blaxel sandbox with sensible defaults. + + Args: + image: Docker image to use (default: "blaxel/prod-base:latest") + memory: Memory in MB (default: 4096) + ttl: Time to live - accepts "30s", "10m", "2h" or seconds as int (optional) + ttl_idle: Idle timeout - accepts "30m", "1h", etc. (optional) + expires: Absolute expiration date in ISO 8601 format (optional) + timeout: Command timeout in seconds (default: 60) + **kwargs: Additional configuration options + + Returns: + Configured Sandbox instance using Blaxel provider + + Example: + >>> sandbox = create_blaxel_sandbox(memory=8192, ttl="1h", ttl_idle="30m") + >>> async with sandbox: + ... result = await sandbox.execute("python --version") + """ + provider_config = kwargs.get("provider_config", {}) + provider_config.update({ + "image": image, + "memory": memory, + }) + if ttl is not None: + provider_config["ttl"] = ttl + if ttl_idle is not None: + provider_config["ttl_idle"] = ttl_idle + if expires is not None: + provider_config["expires"] = expires + + config = SandboxConfig( + timeout=timeout, + provider_config=provider_config, + **{k: v for k, v in kwargs.items() if k != "provider_config"}, + ) + + return Sandbox(provider=Providers.BLAXEL, config=config) + + def create_e2b_sandbox(template: str = "base", timeout: int = 60, **kwargs) -> Sandbox: """ Create an E2B sandbox with sensible defaults. @@ -187,6 +238,7 @@ def create_sandbox(provider: str = "local", **kwargs) -> Sandbox: # Convenience functions "create_sandbox", "create_local_sandbox", + "create_blaxel_sandbox", "create_e2b_sandbox", ] diff --git a/grainchain/core/providers_info.py b/grainchain/core/providers_info.py index 11e4383..84a4b48 100644 --- a/grainchain/core/providers_info.py +++ b/grainchain/core/providers_info.py @@ -31,6 +31,12 @@ class ProviderDiscovery: "install_command": None, "description": "Local Docker-based sandbox provider", }, + "blaxel": { + "dependencies": ["blaxel"], + "env_vars": ["BL_API_KEY", "BL_WORKSPACE"], # Blaxel use BL_API_KEY and BL_WORKSPACE but also supports CLI auth + "install_command": "pip install grainchain[blaxel]", + "description": "Blaxel sandbox provider", + }, "e2b": { "dependencies": ["e2b"], "env_vars": ["E2B_API_KEY"], diff --git a/grainchain/core/sandbox.py b/grainchain/core/sandbox.py index ade713a..af4c632 100644 --- a/grainchain/core/sandbox.py +++ b/grainchain/core/sandbox.py @@ -60,6 +60,10 @@ def _create_provider(self, provider_name: str) -> SandboxProvider: """Create a provider instance from name.""" provider_config = self._config_manager.get_provider_config(provider_name) + if provider_name == "blaxel": + from grainchain.providers.blaxel import BlaxelProvider + + return BlaxelProvider(provider_config) if provider_name == "e2b": from grainchain.providers.e2b import E2BProvider diff --git a/grainchain/providers/__init__.py b/grainchain/providers/__init__.py index 1c29645..8b6cf56 100644 --- a/grainchain/providers/__init__.py +++ b/grainchain/providers/__init__.py @@ -10,6 +10,19 @@ # when optional dependencies are not installed +def get_blaxel_provider(): + """Get Blaxel provider (lazy import).""" + try: + from grainchain.providers.blaxel import BlaxelProvider + + return BlaxelProvider + except ImportError as e: + raise ImportError( + "Blaxel provider requires the 'blaxel' package. " + "Install it with: pip install grainchain[blaxel]" + ) from e + + def get_e2b_provider(): """Get E2B provider (lazy import).""" try: @@ -66,4 +79,4 @@ def get_local_provider(): """Get Local provider.""" from grainchain.providers.local import LocalProvider - return LocalProvider + return LocalProvider \ No newline at end of file diff --git a/grainchain/providers/blaxel.py b/grainchain/providers/blaxel.py new file mode 100644 index 0000000..7367b24 --- /dev/null +++ b/grainchain/providers/blaxel.py @@ -0,0 +1,290 @@ +"""Blaxel provider implementation for Grainchain.""" + +from datetime import datetime +import time +import uuid + +from grainchain.core.config import ProviderConfig +from grainchain.core.exceptions import AuthenticationError, ProviderError +from grainchain.core.interfaces import ( + ExecutionResult, + FileInfo, + SandboxConfig, + SandboxStatus, +) +from grainchain.providers.base import BaseSandboxProvider, BaseSandboxSession + +try: + from blaxel.core import SandboxInstance + from blaxel.core.sandbox.types import SandboxLifecycle + from blaxel.core.client.models import ExpirationPolicy + + BLAXEL_AVAILABLE = True +except ImportError: + BLAXEL_AVAILABLE = False + +class BlaxelProvider(BaseSandboxProvider): + """Blaxel sandbox provider implementation.""" + + def __init__(self, config: ProviderConfig): + """Initialize Blaxel provider.""" + if not BLAXEL_AVAILABLE: + raise ImportError( + "Blaxel provider requires the 'blaxel' package. " + "Install it with: pip install grainchain[blaxel]" + ) + + super().__init__(config) + + # Get configuration values + self.api_key = self.get_config_value("api_key") + self.workspace = self.get_config_value("workspace") + self.image = self.get_config_value("image", "blaxel/prod-base:latest") + self.region = self.get_config_value("region", "us-pdx-1") + self.memory = self.get_config_value("memory", 4096) # Memory in MB + self.ttl = self.get_config_value("ttl", None) # TTL in format "1h", "10m", "30s", etc. + self.expires = self.get_config_value("expires", None) # Expires date in format "2025-10-07T14:53:46.300398334Z" + self.ttl_idle = self.get_config_value("ttl_idle", None) # TTL idle in format "30m", "1h", "10m", "30s", etc. For deleting after inactivity. + + # Set up authentication if provided + if self.api_key and self.workspace: + import os + os.environ["BL_API_KEY"] = self.api_key + os.environ["BL_WORKSPACE"] = self.workspace + + @property + def name(self) -> str: + """Provider name.""" + return "blaxel" + + async def _create_session(self, config: SandboxConfig) -> "BlaxelSandboxSession": + """Create a new Blaxel sandbox session.""" + try: + # Generate a unique name for the sandbox + sandbox_name = f"grainchain-{uuid.uuid4().hex[:8]}" + + # Get configuration with fallbacks + name = config.provider_config.get("name", sandbox_name) + image = config.image or self.image + memory = int(config.provider_config.get("memory", self.memory)) + region = config.provider_config.get("region", self.region) + ports = config.provider_config.get("ports", []) + ttl = config.provider_config.get("ttl", self.ttl) + expires = config.provider_config.get("expires", self.expires) + ttl_idle = config.provider_config.get("ttl_idle", self.ttl_idle) + + # Create Blaxel sandbox + blaxel_sandbox = await self._create_blaxel_sandbox( + name=name, + image=image, + memory=memory, + region=region, + ports=ports, + ttl=ttl, + expires=expires, + ttl_idle=ttl_idle, + ) + + session = BlaxelSandboxSession( + sandbox_id=name, + provider=self, + config=config, + blaxel_sandbox=blaxel_sandbox, + ) + + return session + + except Exception as e: + raise ProviderError( + f"Blaxel sandbox creation failed: {e}", self.name, e + ) from e + + async def _create_blaxel_sandbox( + self, name: str, image: str, memory: int, region: str, ports: list, ttl: int | None, expires: str | None, ttl_idle: str | None + ) -> "SandboxInstance": + """Create a Blaxel sandbox instance.""" + lifecycle = None + if ttl_idle: + lifecycle = SandboxLifecycle(expiration_policies=[ExpirationPolicy(type_="ttl-idle", value=ttl_idle, action="delete")]) + return await SandboxInstance.create({ + "name": name, + "image": image, + "memory": memory, + "region": region, + "ports": ports, + "ttl": ttl, + "expires": expires, + "lifecycle": lifecycle, + }) + + + +class BlaxelSandboxSession(BaseSandboxSession): + """Blaxel sandbox session implementation.""" + + def __init__( + self, + sandbox_id: str, + provider: BlaxelProvider, + config: SandboxConfig, + blaxel_sandbox: "SandboxInstance", + ): + """Initialize Blaxel session.""" + super().__init__(sandbox_id, provider, config) + self.blaxel_sandbox = blaxel_sandbox + self._set_status(SandboxStatus.RUNNING) + + async def execute( + self, + command: str, + timeout: int | None = None, + working_dir: str | None = None, + environment: dict[str, str] | None = None, + ) -> ExecutionResult: + """Execute a command in the Blaxel sandbox.""" + + start_time = time.time() + + try: + # Execute in thread pool + process = await self.blaxel_sandbox.process.exec({ + "command": command, + "workingDir": working_dir, + "env": environment, + }) + + await self.blaxel_sandbox.process.wait(process.pid, max_wait=(timeout or self._config.timeout) * 1000, interval=250) + execution_time = time.time() - start_time + + process = await self.blaxel_sandbox.process.get(process.pid) + + return ExecutionResult( + stdout=process.logs, + stderr=process.logs, + return_code=process.exit_code, + execution_time=execution_time, + success=process.exit_code == 0, + command=command, + ) + + except Exception as e: + execution_time = time.time() - start_time + + # Handle timeout errors + if "timeout" in str(e).lower(): + return ExecutionResult( + stdout="", + stderr=f"Command timed out after {timeout or self._config.timeout} seconds", + return_code=-1, + execution_time=execution_time, + success=False, + command=command, + ) + else: + return ExecutionResult( + stdout="", + stderr=str(e), + return_code=-1, + execution_time=execution_time, + success=False, + command=command, + ) + + async def upload_file( + self, path: str, content: str | bytes, mode: str = "w" + ) -> None: + """Upload a file to the Blaxel sandbox.""" + + try: + if mode == "w": + await self.blaxel_sandbox.fs.write(path, content) + else: + await self.blaxel_sandbox.fs.write_binary(path, content) + + except Exception as e: + raise ProviderError( + f"File upload failed: {e}", self._provider.name, e + ) from e + + async def download_file(self, path: str) -> str: + """Download a file from the Blaxel sandbox.""" + try: + content = await self.blaxel_sandbox.fs.read(path) + return content + + except Exception as e: + raise ProviderError( + f"File download failed: {e}", self._provider.name, e + ) from e + + async def list_files(self, path: str = ".") -> list[FileInfo]: + """List files in the Blaxel sandbox.""" + try: + files = await self.blaxel_sandbox.fs.ls(path) + + file_infos = [] + for file in files.files: + # Extract file information from the Blaxel file object + # Handle ISO format with fractional seconds (e.g., '2025-10-07T14:53:46.300398334Z') + timestamp_str = file.last_modified.rstrip('Z') # Remove the 'Z' suffix + if '.' in timestamp_str: + # Handle fractional seconds by trimming to microseconds (6 digits max) + base, frac = timestamp_str.rsplit('.', 1) + frac = frac[:6].ljust(6, '0') # Pad or trim to 6 digits + timestamp_str = f"{base}.{frac}" + last_modified = datetime.fromisoformat(timestamp_str) + modified_time = last_modified.timestamp() + file_infos.append( + FileInfo( + name=file.name, + path=f"{path}/{file.name}", + size=file.size, + is_directory=False, + modified_time=modified_time + ) + ) + + for directory in files.subdirectories: + # Subdirectories might be strings or objects + file_infos.append( + FileInfo( + name=directory.name, + path=f"{path}/{directory.name}", + size=0, # Directories don't have size + is_directory=True, + modified_time=None, # May not have modification time + ) + ) + return file_infos + + except Exception as e: + raise ProviderError( + f"Failed to list files: {e}", self._provider.name, e + ) from e + + async def create_snapshot(self) -> str: + """Create a snapshot of the current Blaxel sandbox state.""" + raise NotImplementedError("Snapshots are automatically managed by the Blaxel Platform, no manual snapshot creation is needed. See https://docs.blaxel.ai/Sandboxes/Overview for more information.") + + async def restore_snapshot(self, snapshot_id: str) -> None: + """Restore Blaxel sandbox to a previous snapshot.""" + raise NotImplementedError("Snapshots are automatically managed by the Blaxel Platform, no manual snapshot restoration is needed. See https://docs.blaxel.ai/Sandboxes/Overview for more information.") + + async def _cleanup(self) -> None: + """Clean up Blaxel sandbox resources.""" + try: + # Try different methods to clean up the sandbox + await SandboxInstance.delete(self.blaxel_sandbox.metadata.name) + except Exception as e: + # Log but don't raise - cleanup should be best effort + import logging + logging.getLogger(__name__).warning(f"Error deleting Blaxel sandbox: {e}") + + async def close(self) -> None: + """Delete the Blaxel sandbox session.""" + if not self._closed: + try: + await self._cleanup() + finally: + self._closed = True + self._set_status(SandboxStatus.STOPPED) diff --git a/pyproject.toml b/pyproject.toml index 36173cb..c9f3a65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ ] [project.optional-dependencies] +blaxel = ["blaxel>=0.2.19"] e2b = ["e2b>=0.13.0"] daytona = ["daytona-sdk>=0.1.0"] langgraph = [ @@ -41,7 +42,7 @@ langgraph = [ ] modal = ["modal>=0.64.0"] morph = ["morphcloud>=0.1.38"] -all = ["grainchain[e2b,daytona,modal,morph,langgraph]"] +all = ["grainchain[blaxel,e2b,daytona,modal,morph,langgraph]"] dev = [ "pytest>=7.0.0", "pytest-asyncio>=0.21.0", diff --git a/test_blaxel_provider.py b/test_blaxel_provider.py new file mode 100644 index 0000000..eedbf1b --- /dev/null +++ b/test_blaxel_provider.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +""" +Simple test script for Blaxel provider +""" + +import asyncio +import os + +from grainchain.core.config import ProviderConfig +from grainchain.core.interfaces import SandboxConfig +from grainchain.providers.blaxel import BlaxelProvider + + +async def test_blaxel_provider(): + """Test basic Blaxel provider functionality""" + + # Set up configuration - can use environment variables or pass directly + config_dict = {} + + # Check for environment variables + if os.getenv("BL_API_KEY") and os.getenv("BL_WORKSPACE"): + config_dict["api_key"] = os.getenv("BL_API_KEY") + config_dict["workspace"] = os.getenv("BL_WORKSPACE") + print(f"📋 Using environment variables for authentication") + else: + print(f"📋 No environment variables found, will use CLI authentication or defaults") + + # Optional: override with specific settings + config_dict["image"] = "blaxel/prod-base:latest" + config_dict["memory"] = 4096 + config_dict["region"] = "us-pdx-1" + + config = ProviderConfig(config_dict) + + # Create provider + provider = BlaxelProvider(config) + print(f"✅ Created Blaxel provider: {provider.name}") + + # Create sandbox configuration + sandbox_config = SandboxConfig( + timeout=60, + working_directory="~", + provider_config={ + "name": "test-sandbox-provider", # Custom sandbox name + } + ) + + try: + # Create sandbox session + print("🚀 Creating Blaxel sandbox...") + session = await provider.create_sandbox(sandbox_config) + print(f"✅ Created sandbox: {session.sandbox_id}") + print(f" Status: {session.status.value}") + + # Test basic command execution + print("\n🔧 Testing command execution...") + result = await session.execute("echo 'Hello from Blaxel!'") + print(f"✅ Command result: {result.stdout.strip()}") + print(f" Return code: {result.return_code}") + print(f" Success: {result.success}") + print(f" Execution time: {result.execution_time:.2f}s") + + # Test Python execution + print("\n🐍 Testing Python execution...") + result = await session.execute( + "python3 -c \"print('Python works in Blaxel!')\"" + ) + print(f"✅ Python result: {result.stdout.strip()}") + + # Test environment variables and working directory + print("\n🌍 Testing environment and working directory...") + result = await session.execute( + "echo $TEST_VAR", + environment={"TEST_VAR": "Blaxel Environment"} + ) + print(f"✅ Environment variable: {result.stdout.strip()}") + + result = await session.execute("pwd", working_dir="/tmp") + print(f"✅ Working directory: {result.stdout.strip()}") + + # Test file operations + print("\n📁 Testing file operations...") + test_content = "Hello Blaxel from Grainchain!\nThis is a test file." + await session.upload_file("/tmp/test.txt", test_content) + print("✅ File uploaded") + + result = await session.execute("cat /tmp/test.txt") + print(f"✅ File content: {result.stdout.strip()}") + + # Test binary file upload + print("\n🔢 Testing binary file upload...") + binary_content = bytes([0x89, 0x50, 0x4E, 0x47]) # PNG header + await session.upload_file("/tmp/test.bin", binary_content, mode="binary") + result = await session.execute("xxd -l 4 /tmp/test.bin 2>/dev/null || od -x -N 4 /tmp/test.bin") + print(f"✅ Binary file created (hex dump): {result.stdout.strip()[:50]}...") + + # Test file download + print("\n📥 Testing file download...") + content = await session.download_file("/tmp/test.txt") + print(f"✅ Downloaded content: {content[:50]}...") + + # Test file listing + print("\n📋 Testing file listing...") + files = await session.list_files("/tmp") + test_files = [f for f in files if f.name in ["test.txt", "test.bin"]] + if test_files: + for f in test_files: + print(f"✅ Found file: {f.name} (size: {f.size} bytes, is_dir: {f.is_directory})") + + # Test multi-line script execution + print("\n📜 Testing multi-line script...") + script = """ +for i in {1..3}; do + echo "Iteration $i" +done +echo "Script completed" +""" + await session.upload_file("/tmp/script.sh", script) + result = await session.execute("bash /tmp/script.sh") + print(f"✅ Script output:\n{result.stdout}") + + # Test error handling + print("\n❌ Testing error handling...") + result = await session.execute("exit 42") + print(f"✅ Non-zero exit handled: return_code={result.return_code}, success={result.success}") + + result = await session.execute("nonexistent_command") + print(f"✅ Command not found handled: success={result.success}, stderr contains error: {'command not found' in result.stderr.lower()}") + + # Test timeout handling + print("\n⏰ Testing timeout (this may take a few seconds)...") + result = await session.execute("sleep 10", timeout=2) + print(f"✅ Timeout handled: success={result.success}, timeout message: {'timeout' in result.stderr.lower()}") + + # Test snapshot support (may not be implemented) + print("\n📸 Testing snapshot support...") + try: + snapshot_id = await session.create_snapshot() + print(f"✅ Snapshot created: {snapshot_id}") + except NotImplementedError: + print("ℹ️ Snapshots not supported (as expected)") + + # Test sandbox status + print("\n📊 Checking sandbox status...") + status = await provider.get_sandbox_status(session.sandbox_id) + print(f"✅ Sandbox status: {status.value}") + + # List all sandboxes + sandboxes = await provider.list_sandboxes() + print(f"✅ Active sandboxes: {sandboxes}") + + # Clean up + print("\n🧹 Cleaning up...") + await session.close() + print("✅ Session closed successfully") + print(f" Final status: {session.status.value}") + + # Verify cleanup + sandboxes_after = await provider.list_sandboxes() + print(f"✅ Sandboxes after cleanup: {sandboxes_after}") + + # Clean up provider + await provider.cleanup() + print("✅ Provider cleaned up") + + print("\n🎉 All tests passed! Blaxel provider is working correctly.") + + except Exception as e: + print(f"\n❌ Test failed: {e}") + import traceback + traceback.print_exc() + + # Attempt cleanup on failure + try: + await provider.cleanup() + except: + pass + + return False + + return True + + +if __name__ == "__main__": + print("=" * 60) + print("🧪 BLAXEL PROVIDER TEST") + print("=" * 60) + + # Check authentication status + if os.getenv("BL_API_KEY") and os.getenv("BL_WORKSPACE"): + print("✅ Authentication: Using environment variables") + else: + print("ℹ️ Authentication: Will attempt CLI auth or defaults") + print(" To use environment variables:") + print(" export BL_API_KEY='your-api-key'") + print(" export BL_WORKSPACE='your-workspace'") + print(" Or use Blaxel CLI:") + print(" bl login YOUR-WORKSPACE") + + print("=" * 60 + "\n") + + success = asyncio.run(test_blaxel_provider()) + exit(0 if success else 1) diff --git a/test_blaxel_simplified.py b/test_blaxel_simplified.py new file mode 100644 index 0000000..cf99012 --- /dev/null +++ b/test_blaxel_simplified.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +""" +Test script demonstrating the simplified Blaxel configuration. + +This script shows the correct usage pattern for Blaxel with grainchain's +simplified Sandbox API. It demonstrates that Blaxel can work with multiple +authentication methods. +""" + +import asyncio +import os + +from grainchain import Sandbox +from grainchain.core.interfaces import SandboxConfig + + +async def test_blaxel_simplified(): + """Test Blaxel with simplified Sandbox configuration.""" + + # Check authentication status + has_env_vars = os.getenv("BL_API_KEY") and os.getenv("BL_WORKSPACE") + + print("🚀 Testing Blaxel with simplified configuration...") + + if has_env_vars: + print("📋 Authentication: Using environment variables") + print(f" API Key: {os.getenv('BL_API_KEY')[:20]}...") + print(f" Workspace: {os.getenv('BL_WORKSPACE')}") + else: + print("📋 Authentication: No environment variables found") + print(" Will attempt to use Blaxel CLI authentication") + print(" Tip: You can set up authentication with:") + print(" • bl login YOUR-WORKSPACE") + print(" • export BL_API_KEY='your-key' BL_WORKSPACE='your-workspace'") + + try: + # Create sandbox configuration with custom name + config = SandboxConfig( + timeout=60, + working_directory="~", + image="blaxel/prod-py-app:latest", + provider_config={ + "name": "test-simplified", # Optional: custom sandbox name + "memory": 2048, # Optional: 2GB RAM + "ttl_idle": "10m", # Optional: 10 minutes + } + ) + + # Test sandbox creation and basic operations + async with Sandbox(provider="blaxel", config=config) as sandbox: + print(f"\n✅ Sandbox created: {sandbox.sandbox_id}") + print(f" Provider: {sandbox.provider_name}") + + # Test basic command + print("\n📝 Testing basic command execution...") + result = await sandbox.execute("echo 'Hello from Blaxel!'") + if result.success: + print(f"✅ Command executed: {result.stdout.strip()}") + else: + print(f"❌ Command failed: {result.stderr}") + return False + + # Test Python execution + print("\n🐍 Testing Python execution...") + result = await sandbox.execute("python -c \"print('Python works!')\"") + if result.success: + print(f"✅ Python test: {result.stdout.strip()}") + else: + print(f"❌ Python test failed: {result.stderr}") + return False + + # Test file operations + print("\n📁 Testing file operations...") + test_content = "Blaxel Simplified Test\nLine 2\nLine 3" + await sandbox.upload_file("/tmp/test_simple.txt", test_content) + + result = await sandbox.execute("wc -l /tmp/test_simple.txt") + if result.success: + print(f"✅ File created with {result.stdout.strip()}") + else: + print(f"❌ File test failed: {result.stderr}") + return False + + # Test data processing + print("\n💻 Testing data processing...") + python_script = """ +import json +data = { + "provider": "blaxel", + "test": "simplified", + "status": "running", + "features": ["async", "fast", "reliable"] +} +print(json.dumps(data, indent=2)) +""" + await sandbox.upload_file("/tmp/process.py", python_script) + result = await sandbox.execute("python3 /tmp/process.py") + if result.success: + print(f"✅ Data processing output:\n{result.stdout}") + else: + print(f"❌ Processing failed: {result.stderr}") + return False + + # Test environment variables + print("\n🌍 Testing environment variables...") + result = await sandbox.execute( + "echo \"Provider: $PROVIDER, Mode: $MODE\"", + environment={"PROVIDER": "Blaxel", "MODE": "Test"} + ) + if result.success: + print(f"✅ Environment test: {result.stdout.strip()}") + else: + print(f"❌ Environment test failed: {result.stderr}") + + # Test working directory + print("\n📂 Testing working directory...") + result = await sandbox.execute("pwd", working_dir="/tmp") + if result.success and "/tmp" in result.stdout: + print(f"✅ Working directory: {result.stdout.strip()}") + else: + print(f"❌ Working directory test failed: {result.stderr}") + print(f"❌ Working directory test failed") + + # Test persistence within session + print("\n💾 Testing state persistence...") + await sandbox.execute("echo 'persistent' > /tmp/state.txt") + result = await sandbox.execute("cat /tmp/state.txt") + if result.success and "persistent" in result.stdout: + print(f"✅ State persisted: {result.stdout.strip()}") + else: + print(f"❌ Persistence test failed") + + print("\n🎉 All tests passed! Blaxel integration is working correctly.") + print(" The simplified Sandbox API makes it easy to use Blaxel.") + return True + + except ImportError as e: + print(f"\n❌ Import Error: {e}") + print(" Make sure Blaxel SDK is installed:") + print(" pip install grainchain[blaxel]") + return False + + except Exception as e: + error_str = str(e).lower() + + # Check for common error patterns + if "authentication" in error_str or "unauthorized" in error_str: + print("\n❌ Authentication Error Detected") + print(" Please set up authentication using one of these methods:") + print(" 1. Blaxel CLI: bl login YOUR-WORKSPACE") + print(" 2. Environment variables:") + print(" export BL_API_KEY='your-api-key'") + print(" export BL_WORKSPACE='your-workspace'") + print(f" Error: {e}") + elif "connection" in error_str or "network" in error_str: + print("\n❌ Network Error Detected") + print(" Please check your internet connection and Blaxel service status.") + print(f" Error: {e}") + else: + print(f"\n❌ Unexpected error: {e}") + import traceback + traceback.print_exc() + return False + + +async def test_blaxel_minimal(): + """Test Blaxel with absolutely minimal configuration.""" + + print("\n" + "=" * 60) + print("🔬 MINIMAL CONFIGURATION TEST") + print("=" * 60) + + try: + # Absolute minimal - just the provider name + async with Sandbox(provider="blaxel") as sandbox: + print(f"✅ Minimal sandbox created: {sandbox.sandbox_id}") + + result = await sandbox.execute("echo 'Minimal config works!'") + print(f"✅ Command output: {result.stdout.strip()}") + + print("✅ Minimal configuration test passed!") + return True + + except Exception as e: + print(f"❌ Minimal test failed: {e}") + return False + +async def test_blaxel_simplified_all(): + success = await test_blaxel_simplified() + if success: + success = await test_blaxel_minimal() + return success + +if __name__ == "__main__": + print("=" * 60) + print("🧪 BLAXEL SIMPLIFIED TEST") + print("=" * 60) + + + + # Run main test + success = asyncio.run(test_blaxel_simplified_all()) + + + if success: + print("\n" + "=" * 60) + print("✅ ALL BLAXEL TESTS PASSED!") + print("=" * 60) + else: + print("\n" + "=" * 60) + print("❌ SOME TESTS FAILED") + print("=" * 60) + + exit(0 if success else 1) diff --git a/uv.lock b/uv.lock index 5eee002..dcf61aa 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,4 @@ version = 1 -revision = 1 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.12.4'", @@ -199,6 +198,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799 }, ] +[[package]] +name = "blaxel" +version = "0.2.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "httpx" }, + { name = "mcp" }, + { name = "pydantic" }, + { name = "pyjwt" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tomli" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/35/07ebb1e5805c3fb7731bac0290e338e8e40b9965683790c34ef8688bf89c/blaxel-0.2.19.tar.gz", hash = "sha256:dba1cf6f375ea44dd62b11af3b00fa5765546f2c007356c59153cde4f177ba31", size = 282891 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/e9/033c8519ce132c996aeebb1ba5d4f28670e79cdcae49e67079e1bf63cce0/blaxel-0.2.19-py3-none-any.whl", hash = "sha256:f5bafa3b44076a8caced4827bcd5db9e7dff24af0a17f011808097bd3f3bf9aa", size = 424003 }, +] + [[package]] name = "certifi" version = "2025.4.26" @@ -290,7 +310,7 @@ name = "click" version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "platform_system == 'Windows'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } wheels = [ @@ -672,6 +692,7 @@ dependencies = [ [package.optional-dependencies] all = [ + { name = "blaxel" }, { name = "daytona-sdk" }, { name = "e2b" }, { name = "langchain" }, @@ -684,6 +705,9 @@ benchmark = [ { name = "docker" }, { name = "psutil" }, ] +blaxel = [ + { name = "blaxel" }, +] daytona = [ { name = "daytona-sdk" }, ] @@ -719,11 +743,12 @@ morph = [ [package.metadata] requires-dist = [ { name = "aiohttp", specifier = ">=3.8.0" }, + { name = "blaxel", marker = "extra == 'blaxel'", specifier = ">=0.2.19" }, { name = "click", specifier = ">=8.0.0" }, { name = "daytona-sdk", marker = "extra == 'daytona'", specifier = ">=0.1.0" }, { name = "docker", marker = "extra == 'benchmark'", specifier = ">=6.0.0" }, { name = "e2b", marker = "extra == 'e2b'", specifier = ">=0.13.0" }, - { name = "grainchain", extras = ["e2b", "daytona", "modal", "morph", "langgraph"], marker = "extra == 'all'" }, + { name = "grainchain", extras = ["blaxel", "e2b", "daytona", "modal", "morph", "langgraph"], marker = "extra == 'all'" }, { name = "langchain", marker = "extra == 'langgraph'", specifier = ">=0.3.0" }, { name = "langchain-core", marker = "extra == 'langgraph'", specifier = ">=0.3.0" }, { name = "langgraph", marker = "extra == 'langgraph'", specifier = ">=0.2.0" }, @@ -745,7 +770,6 @@ requires-dist = [ { name = "types-pyyaml", marker = "extra == 'dev'", specifier = ">=6.0.0" }, { name = "typing-extensions", specifier = ">=4.0.0" }, ] -provides-extras = ["e2b", "daytona", "langgraph", "modal", "morph", "all", "dev", "benchmark", "examples"] [[package]] name = "greenlet" @@ -964,6 +988,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595 }, ] +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040 }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437 }, +] + [[package]] name = "kiwisolver" version = "1.4.8" @@ -1200,22 +1251,24 @@ wheels = [ [[package]] name = "mcp" -version = "1.9.2" +version = "1.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "httpx" }, { name = "httpx-sse" }, + { name = "jsonschema" }, { name = "pydantic" }, { name = "pydantic-settings" }, { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "sse-starlette" }, { name = "starlette" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/03/77c49cce3ace96e6787af624611b627b2828f0dca0f8df6f330a10eea51e/mcp-1.9.2.tar.gz", hash = "sha256:3c7651c053d635fd235990a12e84509fe32780cd359a5bbef352e20d4d963c05", size = 333066 } +sdist = { url = "https://files.pythonhosted.org/packages/3d/a1/b1f328da3b153683d2ec34f849b4b6eac2790fb240e3aef06ff2fab3df9d/mcp-1.16.0.tar.gz", hash = "sha256:39b8ca25460c578ee2cdad33feeea122694cfdf73eef58bee76c42f6ef0589df", size = 472918 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/a6/8f5ee9da9f67c0fd8933f63d6105f02eabdac8a8c0926728368ffbb6744d/mcp-1.9.2-py3-none-any.whl", hash = "sha256:bc29f7fd67d157fef378f89a4210384f5fecf1168d0feb12d22929818723f978", size = 131083 }, + { url = "https://files.pythonhosted.org/packages/c9/0e/7cebc88e17daf94ebe28c95633af595ccb2864dc2ee7abd75542d98495cc/mcp-1.16.0-py3-none-any.whl", hash = "sha256:ec917be9a5d31b09ba331e1768aa576e0af45470d657a0319996a20a57d7d633", size = 167266 }, ] [[package]] @@ -1797,6 +1850,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, ] +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 }, +] + [[package]] name = "pynacl" version = "1.5.0" @@ -1944,6 +2006,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, ] +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 }, +] + [[package]] name = "requests" version = "2.32.3" @@ -1984,6 +2060,87 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, ] +[[package]] +name = "rpds-py" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795 }, + { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121 }, + { url = "https://files.pythonhosted.org/packages/1b/ea/b306067a712988e2bff00dcc7c8f31d26c29b6d5931b461aa4b60a013e33/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881", size = 398976 }, + { url = "https://files.pythonhosted.org/packages/2c/0a/26dc43c8840cb8fe239fe12dbc8d8de40f2365e838f3d395835dde72f0e5/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec", size = 525953 }, + { url = "https://files.pythonhosted.org/packages/22/14/c85e8127b573aaf3a0cbd7fbb8c9c99e735a4a02180c84da2a463b766e9e/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb", size = 407915 }, + { url = "https://files.pythonhosted.org/packages/ed/7b/8f4fee9ba1fb5ec856eb22d725a4efa3deb47f769597c809e03578b0f9d9/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5", size = 386883 }, + { url = "https://files.pythonhosted.org/packages/86/47/28fa6d60f8b74fcdceba81b272f8d9836ac0340570f68f5df6b41838547b/rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a", size = 405699 }, + { url = "https://files.pythonhosted.org/packages/d0/fd/c5987b5e054548df56953a21fe2ebed51fc1ec7c8f24fd41c067b68c4a0a/rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444", size = 423713 }, + { url = "https://files.pythonhosted.org/packages/ac/ba/3c4978b54a73ed19a7d74531be37a8bcc542d917c770e14d372b8daea186/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a", size = 562324 }, + { url = "https://files.pythonhosted.org/packages/b5/6c/6943a91768fec16db09a42b08644b960cff540c66aab89b74be6d4a144ba/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1", size = 593646 }, + { url = "https://files.pythonhosted.org/packages/11/73/9d7a8f4be5f4396f011a6bb7a19fe26303a0dac9064462f5651ced2f572f/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998", size = 558137 }, + { url = "https://files.pythonhosted.org/packages/6e/96/6772cbfa0e2485bcceef8071de7821f81aeac8bb45fbfd5542a3e8108165/rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39", size = 221343 }, + { url = "https://files.pythonhosted.org/packages/67/b6/c82f0faa9af1c6a64669f73a17ee0eeef25aff30bb9a1c318509efe45d84/rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594", size = 232497 }, + { url = "https://files.pythonhosted.org/packages/e1/96/2817b44bd2ed11aebacc9251da03689d56109b9aba5e311297b6902136e2/rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502", size = 222790 }, + { url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741 }, + { url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574 }, + { url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051 }, + { url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395 }, + { url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334 }, + { url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691 }, + { url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868 }, + { url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469 }, + { url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125 }, + { url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341 }, + { url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511 }, + { url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736 }, + { url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462 }, + { url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034 }, + { url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392 }, + { url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355 }, + { url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138 }, + { url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247 }, + { url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699 }, + { url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852 }, + { url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582 }, + { url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126 }, + { url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486 }, + { url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832 }, + { url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249 }, + { url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356 }, + { url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300 }, + { url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714 }, + { url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943 }, + { url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472 }, + { url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676 }, + { url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313 }, + { url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080 }, + { url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868 }, + { url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750 }, + { url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688 }, + { url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225 }, + { url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361 }, + { url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493 }, + { url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623 }, + { url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800 }, + { url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943 }, + { url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739 }, + { url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120 }, + { url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944 }, + { url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283 }, + { url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320 }, + { url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760 }, + { url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476 }, + { url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418 }, + { url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771 }, + { url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022 }, + { url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787 }, + { url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538 }, + { url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512 }, + { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813 }, + { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385 }, + { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097 }, +] + [[package]] name = "ruff" version = "0.11.12" @@ -2132,12 +2289,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 }, ] +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + [[package]] name = "tqdm" version = "4.67.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "platform_system == 'Windows'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } wheels = [ @@ -2297,6 +2483,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826 }, ] +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437 }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096 }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332 }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152 }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096 }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523 }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790 }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165 }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160 }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395 }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841 }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, +] + [[package]] name = "wrapt" version = "1.17.2"