diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 045fc53..4bfa50c 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -30,11 +30,5 @@ jobs: - name: Install dependencies run: make setup - - name: Check dependency sync - run: | - echo "::group::Dependency sync check" - uv run python scripts/sync_example_deps.py --check - echo "::endgroup::" - - name: Run quality checks run: make ci-quality-github diff --git a/.github/workflows/test-examples.yml b/.github/workflows/test-examples.yml index 24d2af9..92a5d5e 100644 --- a/.github/workflows/test-examples.yml +++ b/.github/workflows/test-examples.yml @@ -38,4 +38,4 @@ jobs: run: make venv-info - name: Run quality checks - run: make quality-check-strict + run: make quality-check diff --git a/01_getting_started/01_hello_world/.env.example b/01_getting_started/01_hello_world/.env.example deleted file mode 100644 index 91af5f2..0000000 --- a/01_getting_started/01_hello_world/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -# RUNPOD_API_KEY=your_api_key_here -# FLASH_HOST=localhost -# FLASH_PORT=8888 -# LOG_LEVEL=INFO diff --git a/01_getting_started/01_hello_world/.gitignore b/01_getting_started/01_hello_world/.gitignore index 5da3c4c..65aaf17 100644 --- a/01_getting_started/01_hello_world/.gitignore +++ b/01_getting_started/01_hello_world/.gitignore @@ -42,3 +42,4 @@ uv.lock # OS .DS_Store Thumbs.db +.flash/ diff --git a/01_getting_started/01_hello_world/README.md b/01_getting_started/01_hello_world/README.md index 3fa6e7d..1764184 100644 --- a/01_getting_started/01_hello_world/README.md +++ b/01_getting_started/01_hello_world/README.md @@ -26,21 +26,19 @@ Get your API key from [Runpod Settings](https://www.runpod.io/console/user/setti flash run ``` -Server starts at **http://localhost:8000** +Server starts at **http://localhost:8888** ### 4. Test the API -```bash -# Health check -curl http://localhost:8000/ping +Visit **http://localhost:8888/docs** for interactive API documentation. QB endpoints are auto-generated by `flash run` based on your `@remote` functions. -# GPU worker -curl -X POST http://localhost:8000/gpu/hello \ +```bash +curl -X POST http://localhost:8888/gpu_worker/run_sync \ -H "Content-Type: application/json" \ -d '{"message": "Hello GPU!"}' ``` -Visit **http://localhost:8000/docs** for interactive API documentation. +Visit **http://localhost:8888/docs** for interactive API documentation. ### Full CLI Documentation @@ -67,7 +65,9 @@ The worker demonstrates: ## API Endpoints -### POST /gpu/hello +QB (queue-based) endpoints are auto-generated from `@remote` functions. Visit `/docs` for the full API schema. + +### `gpu_hello` Executes a simple GPU worker and returns system/GPU information. @@ -100,9 +100,7 @@ Executes a simple GPU worker and returns system/GPU information. ``` 01_hello_world/ -├── main.py # FastAPI application ├── gpu_worker.py # GPU worker with @remote decorator -├── mothership.py # Mothership endpoint configuration ├── pyproject.toml # Project metadata ├── requirements.txt # Dependencies ├── .env.example # Environment variables template diff --git a/01_getting_started/01_hello_world/__init__.py b/01_getting_started/01_hello_world/__init__.py deleted file mode 100644 index 72afaa4..0000000 --- a/01_getting_started/01_hello_world/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Hello world example.""" diff --git a/01_getting_started/01_hello_world/gpu_worker.py b/01_getting_started/01_hello_world/gpu_worker.py index fe8b0cd..6fbd837 100644 --- a/01_getting_started/01_hello_world/gpu_worker.py +++ b/01_getting_started/01_hello_world/gpu_worker.py @@ -1,46 +1,20 @@ -## Hello world: GPU serverless workers -# In this part of the example code, we provision a GPU-based worker and have it -# execute code. We can run the worker directly, or have it handle API requests -# to the router function. It's registered to a subrouter in the __init__.py -# file in this folder, and subsequently imported by main.py and attached to the -# FastAPI app there. +# GPU serverless worker -- detects available GPU hardware. +# Run with: flash run +# Test directly: python gpu_worker.py +from runpod_flash import GpuGroup, LiveServerless, remote -# Scaling behavior is controlled by configuration passed to the -# `LiveServerless` class. -from fastapi import APIRouter -from pydantic import BaseModel -from runpod_flash import ( - GpuGroup, - LiveServerless, - remote, -) - -# Here, we'll define several variables that change the -# default behavior of our serverless endpoint. `workersMin` sets our endpoint -# to scale to 0 active containers; `workersMax` will allow our endpoint to run -# up to 3 workers in parallel as the endpoint receives more work. We also set -# an idle timeout of 5 minutes so that any active worker stays alive for 5 -# minutes after completing a request. gpu_config = LiveServerless( name="01_01_gpu_worker", - gpus=[GpuGroup.ANY], # Run on any GPU + gpus=[GpuGroup.ANY], workersMin=0, workersMax=3, idleTimeout=5, ) -# Decorating our function with `remote` will package up the function code and -# deploy it on the infrastructure according to the passed input config. The -# results from the worker will be returned to your terminal. In this example -# the function will return a greeting to the input string passed in the `name` -# key. The code itself will run on a GPU worker, and information about the GPU -# the worker has access to will be included in the response. @remote(resource_config=gpu_config) -async def gpu_hello( - input_data: dict, -) -> dict: - """Simple GPU worker example with GPU detection.""" +async def gpu_hello(input_data: dict) -> dict: + """Simple GPU worker that returns GPU hardware info.""" import platform from datetime import datetime @@ -51,10 +25,7 @@ async def gpu_hello( gpu_count = torch.cuda.device_count() gpu_memory = torch.cuda.get_device_properties(0).total_memory / (1024**3) - message = input_data.get( - "message", - "Hello from GPU worker!", - ) + message = input_data.get("message", "Hello from GPU worker!") return { "status": "success", @@ -64,10 +35,7 @@ async def gpu_hello( "available": gpu_available, "name": gpu_name, "count": gpu_count, - "memory_gb": round( - gpu_memory, - 2, - ), + "memory_gb": round(gpu_memory, 2), }, "timestamp": datetime.now().isoformat(), "platform": platform.system(), @@ -75,29 +43,6 @@ async def gpu_hello( } -# We define a subrouter for our gpu worker so that our main router in `main.py` -# can attach it for routing gpu-specific requests. -gpu_router = APIRouter() - - -class MessageRequest(BaseModel): - """Request model for GPU worker.""" - - message: str = "Hello from GPU!" - - -@gpu_router.post("/hello") -async def hello( - request: MessageRequest, -): - """Simple GPU worker endpoint.""" - result = await gpu_hello({"message": request.message}) - return result - - -# This code is packaged up as a "worker" that will handle requests sent to the -# endpoint at /gpu/hello, but you can also trigger it directly by running -# python -m workers.gpu.endpoint if __name__ == "__main__": import asyncio diff --git a/01_getting_started/01_hello_world/main.py b/01_getting_started/01_hello_world/main.py deleted file mode 100644 index e0e51c2..0000000 --- a/01_getting_started/01_hello_world/main.py +++ /dev/null @@ -1,71 +0,0 @@ -## Example 1: Hello world -# This is an example of a simple Flash application. -# It consists of an API router (this file) that routes requests to a local -# endpoint on your machine to worker code. Worker code is executed on Runpod -# infrastructure on serverless workers with GPUs. - -# Subrouters and associated worker function code are defined in the ./workers/ -# dir and attached to the main router in this file. By default, running -# `flash run` will start the local API server on your machine serving requests -# from port 8888. - -# We'll define a resource configuration for a GPU worker on a Runpod serverless -# endpoint. The GPU worker will return information about the infrastructure -# it executes on. - -import logging -import os - -from fastapi import FastAPI -from gpu_worker import gpu_router - -logger = logging.getLogger(__name__) - -# We define a simple FastAPI app to serve requests from localhost. -app = FastAPI( - title="Flash Application", - description="Distributed GPU computing with Runpod Flash", - version="0.1.0", -) - -# Attach gpu worker subrouters - this will route any requests to our -# app with the prefix /gpu to the gpu subrouter. To see the subrouter in action, -# start the app and execute the following command in another terminal window: -# curl -X POST http://localhost:8888/gpu/hello -d '{"input": "hello"}' -H "Content-Type: application/json" -app.include_router( - gpu_router, - prefix="/gpu", - tags=["GPU Workers"], -) - - -# The homepage for our main endpoint will just return a plaintext json object -# containing the endpoints defined in this app. -@app.get("/") -def home(): - return { - "message": "Flash Application", - "docs": "/docs", - "endpoints": { - "gpu_hello": "/gpu/hello", - }, - } - - -@app.get("/ping") -def ping(): - return {"status": "healthy"} - - -if __name__ == "__main__": - import uvicorn - - host = os.getenv("FLASH_HOST", "localhost") - port = int(os.getenv("FLASH_PORT", 8888)) - logger.info(f"Starting Flash server on {host}:{port}") - - uvicorn.run( - app, - host=host, - port=port, - ) diff --git a/01_getting_started/01_hello_world/mothership.py b/01_getting_started/01_hello_world/mothership.py deleted file mode 100644 index e417f98..0000000 --- a/01_getting_started/01_hello_world/mothership.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -Mothership Endpoint Configuration - -The mothership endpoint serves your FastAPI application routes. -It is automatically deployed as a CPU-optimized load-balanced endpoint. - -To customize this configuration: -- Modify worker scaling: change workersMin and workersMax values -- Use GPU load balancer: import LiveLoadBalancer instead of CpuLiveLoadBalancer -- Change endpoint name: update the 'name' parameter - -To disable mothership deployment: -- Delete this file, or -- Comment out the 'mothership' variable below -""" - -from runpod_flash import CpuLiveLoadBalancer - -# Mothership endpoint configuration -# This serves your FastAPI app routes from main.py -mothership = CpuLiveLoadBalancer( - name="01_01_hello_world-mothership", - # workersMin=1, - # workersMax=3, -) - -# Examples of customization: - -# Increase scaling for high traffic -# mothership = CpuLiveLoadBalancer( -# name="mothership-01_01_hello_world", -# workersMin=2, -# workersMax=10, -# ) - -# Use GPU-based load balancer instead of CPU -# (requires importing LiveLoadBalancer) -# from runpod_flash import LiveLoadBalancer -# mothership = LiveLoadBalancer( -# name="mothership-01_01_hello_world", -# gpus=[GpuGroup.ANY], -# ) - -# Custom endpoint name -# mothership = CpuLiveLoadBalancer( -# name="my-api-gateway", -# workersMin=1, -# workersMax=3, -# ) - -# To disable mothership: -# - Delete this entire file, or -# - Comment out the 'mothership' variable above diff --git a/01_getting_started/01_hello_world/requirements.txt b/01_getting_started/01_hello_world/requirements.txt deleted file mode 100644 index d252e70..0000000 --- a/01_getting_started/01_hello_world/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -runpod-flash \ No newline at end of file diff --git a/01_getting_started/02_cpu_worker/.env.example b/01_getting_started/02_cpu_worker/.env.example deleted file mode 100644 index 91af5f2..0000000 --- a/01_getting_started/02_cpu_worker/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -# RUNPOD_API_KEY=your_api_key_here -# FLASH_HOST=localhost -# FLASH_PORT=8888 -# LOG_LEVEL=INFO diff --git a/01_getting_started/02_cpu_worker/README.md b/01_getting_started/02_cpu_worker/README.md index 68857b5..28741f4 100644 --- a/01_getting_started/02_cpu_worker/README.md +++ b/01_getting_started/02_cpu_worker/README.md @@ -26,22 +26,18 @@ Get your API key from [Runpod Settings](https://www.runpod.io/console/user/setti flash run ``` -Server starts at **http://localhost:8000** +Server starts at **http://localhost:8888** ### 4. Test the API -```bash -# Health check -curl http://localhost:8000/ping +Visit **http://localhost:8888/docs** for interactive API documentation. QB endpoints are auto-generated by `flash run` based on your `@remote` functions. -# CPU worker -curl -X POST http://localhost:8000/cpu/hello \ +```bash +curl -X POST http://localhost:8888/cpu_worker/run_sync \ -H "Content-Type: application/json" \ -d '{"name": "Flash User"}' ``` -Visit **http://localhost:8000/docs** for interactive API documentation. - ### Full CLI Documentation For complete CLI usage including deployment, environment management, and troubleshooting: @@ -67,7 +63,9 @@ The worker demonstrates: ## API Endpoints -### POST /cpu/hello +QB (queue-based) endpoints are auto-generated from `@remote` functions. Visit `/docs` for the full API schema. + +### `cpu_hello` Executes a simple CPU worker and returns a greeting with system information. @@ -94,9 +92,7 @@ Executes a simple CPU worker and returns a greeting with system information. ``` 02_cpu_worker/ -├── main.py # FastAPI application ├── cpu_worker.py # CPU worker with @remote decorator -├── mothership.py # Mothership endpoint configuration ├── pyproject.toml # Project metadata ├── requirements.txt # Dependencies ├── .env.example # Environment variables template diff --git a/01_getting_started/02_cpu_worker/__init__.py b/01_getting_started/02_cpu_worker/__init__.py deleted file mode 100644 index f927b6b..0000000 --- a/01_getting_started/02_cpu_worker/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""CPU worker example.""" diff --git a/01_getting_started/02_cpu_worker/cpu_worker.py b/01_getting_started/02_cpu_worker/cpu_worker.py index 8c9f281..429d1dc 100644 --- a/01_getting_started/02_cpu_worker/cpu_worker.py +++ b/01_getting_started/02_cpu_worker/cpu_worker.py @@ -1,33 +1,20 @@ -## Hello world: CPU serverless workers -# In this part of the example code, we provision a CPU-based worker (no GPUs) and have it execute code. -# We can run the worker directly, or have it handle API requests to the router function. -# It's registered to a subrouter in the __init__.py file in this folder, and subsequently -# imported by main.py and attached to the FastAPI app there. -from fastapi import APIRouter -from pydantic import BaseModel +# CPU serverless worker -- lightweight processing without GPU. +# Run with: flash run +# Test directly: python cpu_worker.py from runpod_flash import CpuInstanceType, CpuLiveServerless, remote -# Scaling behavior is controlled by configuration passed to the `CpuLiveServerless` class. -# Here, we'll define several variables that change the default behavior of our serverless endpoint. -# `workersMin` sets our endpoint to scale to 0 active containers; `workersMax` will allow our endpoint -# to run up to 5 workers in parallel as the endpoint receives more work. -# We also set an idle timeout of 5 minutes so that any active worker stays alive for 5 minutes after completing a request. cpu_config = CpuLiveServerless( name="01_02_cpu_worker", instanceIds=[CpuInstanceType.CPU3C_1_2], - workersMin=0, # Scale to zero + workersMin=0, workersMax=3, - idleTimeout=5, # Leave workers alive for 5 minutes after they serve a request + idleTimeout=5, ) -# Decorating our function with `remote` will package up the function code and deploy it on the infrastructure -# according to the passed input config. -# In this example the function will return a greeting to the input string passed in the `name` key. -# The results are displayed in your terminal, but the work was performed by CPU workers on runpod infra. @remote(resource_config=cpu_config) async def cpu_hello(input_data: dict) -> dict: - """Simple CPU worker example.""" + """Simple CPU worker that returns a greeting.""" import platform from datetime import datetime @@ -43,24 +30,6 @@ async def cpu_hello(input_data: dict) -> dict: } -cpu_router = APIRouter() - - -class MessageRequest(BaseModel): - """Request model for CPU worker.""" - - name: str = "Flash expert" - - -@cpu_router.post("/hello") -async def hello(request: MessageRequest): - """Simple CPU worker endpoint.""" - result = await cpu_hello({"message": request.name}) - return result - - -# This code is packaged up as a "worker" that will handle requests sent to the endpoint at -# /cpu/hello, but you can also trigger it locally by running python -m workers.cpu.endpoint. if __name__ == "__main__": import asyncio diff --git a/01_getting_started/02_cpu_worker/main.py b/01_getting_started/02_cpu_worker/main.py deleted file mode 100644 index 1ea67d6..0000000 --- a/01_getting_started/02_cpu_worker/main.py +++ /dev/null @@ -1,60 +0,0 @@ -## Example 2: Hello world (with CPU workers) -# This is an example of a simple Flash application. -# It consists of an API router (this file) that routes requests to a local endpoint on your machine to -# worker code. Worker code is executed on Runpod infrastructure on low-cost, cpu-based serverless workers. - -# Subrouters and associated worker function code are defined in the ./workers/ dir -# and attached to the main router in this file. By default, running `flash run` -# will start the local API server on your machine serving requests from port 8888. - -# There are two worker resource configurations for this app: a gpu and a cpu config. -# A CPU-only configured worker allows you to execute code that doesn't require GPUs on -# runpod infrastructure for a lower cost. It will return a simple greeting when you invoke the code directly -# or send a request to its corresponding endpoint. The GPU worker will return information about the infrastructure -# it executes on. - -import logging -import os - -from cpu_worker import cpu_router -from fastapi import FastAPI - -logger = logging.getLogger(__name__) - -# We define a simple FastAPI app to serve requests from localhost. -app = FastAPI( - title="Flash Application", - description="Distributed GPU and CPU computing with Runpod Flash", - version="0.1.0", -) - -# Attach gpu and cpu worker subrouters - this will route any requests to our app -# with the prefix /cpu to our CPU subrouter. Try out the following command -# in a separate terminal window after starting your app: -# curl -X POST http://localhost:8888/cpu/hello -d '{"input": "hello"}' -H "Content-Type: application/json" -app.include_router(cpu_router, prefix="/cpu", tags=["CPU Workers"]) - - -# The homepage for our main endpoint will just return a plaintext json object containing the endpoints defined in this app. -@app.get("/") -def home(): - return { - "message": "Flash Application", - "docs": "/docs", - "endpoints": {"cpu_hello": "/cpu/hello"}, - } - - -@app.get("/ping") -def ping(): - return {"status": "healthy"} - - -if __name__ == "__main__": - import uvicorn - - host = os.getenv("FLASH_HOST", "localhost") - port = int(os.getenv("FLASH_PORT", 8888)) - logger.info(f"Starting Flash server on {host}:{port}") - - uvicorn.run(app, host=host, port=port) diff --git a/01_getting_started/02_cpu_worker/mothership.py b/01_getting_started/02_cpu_worker/mothership.py deleted file mode 100644 index 106109a..0000000 --- a/01_getting_started/02_cpu_worker/mothership.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -Mothership Endpoint Configuration - -The mothership endpoint serves your FastAPI application routes. -It is automatically deployed as a CPU-optimized load-balanced endpoint. - -To customize this configuration: -- Modify worker scaling: change workersMin and workersMax values -- Use GPU load balancer: import LiveLoadBalancer instead of CpuLiveLoadBalancer -- Change endpoint name: update the 'name' parameter - -To disable mothership deployment: -- Delete this file, or -- Comment out the 'mothership' variable below -""" - -from runpod_flash import CpuLiveLoadBalancer - -# Mothership endpoint configuration -# This serves your FastAPI app routes from main.py -mothership = CpuLiveLoadBalancer( - name="01_02_cpu_worker-mothership", - # workersMin=1, - # workersMax=3, -) - -# Examples of customization: - -# Increase scaling for high traffic -# mothership = CpuLiveLoadBalancer( -# name="mothership-01_02_cpu_worker", -# workersMin=2, -# workersMax=10, -# ) - -# Use GPU-based load balancer instead of CPU -# (requires importing LiveLoadBalancer) -# from runpod_flash import LiveLoadBalancer -# mothership = LiveLoadBalancer( -# name="mothership-01_02_cpu_worker", -# gpus=[GpuGroup.ANY], -# ) - -# Custom endpoint name -# mothership = CpuLiveLoadBalancer( -# name="my-api-gateway", -# workersMin=1, -# workersMax=3, -# ) - -# To disable mothership: -# - Delete this entire file, or -# - Comment out the 'mothership' variable above diff --git a/01_getting_started/02_cpu_worker/requirements.txt b/01_getting_started/02_cpu_worker/requirements.txt deleted file mode 100644 index d252e70..0000000 --- a/01_getting_started/02_cpu_worker/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -runpod-flash \ No newline at end of file diff --git a/01_getting_started/03_mixed_workers/.env.example b/01_getting_started/03_mixed_workers/.env.example deleted file mode 100644 index 91af5f2..0000000 --- a/01_getting_started/03_mixed_workers/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -# RUNPOD_API_KEY=your_api_key_here -# FLASH_HOST=localhost -# FLASH_PORT=8888 -# LOG_LEVEL=INFO diff --git a/01_getting_started/03_mixed_workers/README.md b/01_getting_started/03_mixed_workers/README.md index 370fa5b..dba3533 100644 --- a/01_getting_started/03_mixed_workers/README.md +++ b/01_getting_started/03_mixed_workers/README.md @@ -8,26 +8,25 @@ Learn the production pattern of combining CPU and GPU workers for cost-effective - **Cost optimization** - Using GPU only when necessary - **Pipeline orchestration** - Coordinating multiple worker types - **Production patterns** - Real-world ML service architecture -- **Input validation** - Pydantic validation at each pipeline stage ## Architecture ``` User Request - ↓ + | CPU Worker (Preprocessing) -- Input validation -- Text cleaning +- Text normalization +- Text cleaning - Tokenization - ↓ + | GPU Worker (Inference) - ML model inference - Classification - ↓ + | CPU Worker (Postprocessing) - Result formatting - Aggregation - ↓ + | Response ``` @@ -76,11 +75,11 @@ curl -X POST http://localhost:8888/classify \ -d '{"text": "This product is amazing! I love it!"}' # Individual stages -curl -X POST http://localhost:8888/cpu/preprocess \ +curl -X POST http://localhost:8888/cpu_worker/run_sync \ -H "Content-Type: application/json" \ -d '{"text": "Test message"}' -curl -X POST http://localhost:8888/gpu/inference \ +curl -X POST http://localhost:8888/gpu_worker/run_sync \ -H "Content-Type: application/json" \ -d '{"cleaned_text": "Test message", "word_count": 2}' ``` @@ -96,7 +95,7 @@ For complete CLI usage including deployment, environment management, and trouble ## Why This Pattern? -### GPU-Only Pipeline (❌ Expensive) +### GPU-Only Pipeline (Expensive) ``` Every operation on GPU = $0.0015/sec - Preprocessing: $0.0015/sec @@ -105,7 +104,7 @@ Every operation on GPU = $0.0015/sec Total: $0.0045/sec ``` -### Mixed Pipeline (✅ Cost-Effective) +### Mixed Pipeline (Cost-Effective) ``` Use right tool for each job - Preprocessing: $0.0002/sec (CPU) @@ -116,236 +115,18 @@ Total: $0.0019/sec ## When to Use This Pattern -✅ **Use mixed workers when:** +**Use mixed workers when:** - ML inference is only part of your workflow - You have preprocessing/postprocessing logic - You want to minimize GPU costs - You need input validation before expensive operations - You process data after inference -❌ **Don't use when:** +**Don't use when:** - Your entire workflow needs GPU (image processing, video, etc.) - Overhead of orchestration > cost savings - Very simple operations (single worker is fine) -## Input Validation - -This example demonstrates production-grade input validation using Pydantic at each pipeline stage. - -### Why Validation in Pipelines? - -**Key principle: Fail fast before expensive operations** - -- Catch errors **before** GPU inference (saves money) -- Validate at API layer (prevents bad data reaching workers) -- Provide clear error messages (better developer experience) -- Type-safe data structures (prevent runtime errors) - -### Validation at Each Stage - -**1. Pipeline Endpoint (/classify)** - -```python -class ClassifyRequest(BaseModel): - text: str - - @field_validator("text") - @classmethod - def validate_text(cls, v): - if not v or not v.strip(): - raise ValueError("Text cannot be empty") - if len(v) < 3: - raise ValueError("Text too short (minimum 3 characters)") - if len(v) > 10000: - raise ValueError("Text too long (maximum 10,000 characters)") - return v -``` - -**2. Preprocessing Endpoint (/cpu/preprocess)** - -```python -class PreprocessRequest(BaseModel): - text: str # Same validation as ClassifyRequest -``` - -Validates: -- Text is not empty or whitespace -- Minimum 3 characters -- Maximum 10,000 characters - -**3. Inference Endpoint (/gpu/inference)** - -```python -class InferenceRequest(BaseModel): - cleaned_text: str - word_count: int = Field(ge=0) # >= 0 - - @field_validator("cleaned_text") - @classmethod - def validate_text_not_empty(cls, v): - if not v or not v.strip(): - raise ValueError("Cleaned text cannot be empty") - return v -``` - -Validates: -- Cleaned text is not empty -- Word count is non-negative - -**4. Postprocessing Endpoint (/cpu/postprocess)** - -```python -class Prediction(BaseModel): - label: str - confidence: float - - @field_validator("confidence") - @classmethod - def validate_confidence(cls, v): - if not 0.0 <= v <= 1.0: - raise ValueError(f"Confidence must be between 0.0 and 1.0") - return v - -class PostprocessRequest(BaseModel): - predictions: list[Prediction] # Validates structure! - original_text: str - metadata: dict -``` - -Validates: -- Predictions is a list of `Prediction` objects -- Each prediction has `label` (str) and `confidence` (float 0.0-1.0) -- Rejects invalid structures like `["string"]` - -### Remote Serialization Pattern - -**Problem:** Pydantic models can't be directly serialized for remote workers. - -**Solution:** Convert models to dicts using `.model_dump()`: - -```python -# ❌ BAD - Pydantic models cause serialization errors -result = await postprocess_results({ - "predictions": request.predictions, # List[Prediction] objects -}) - -# ✅ GOOD - Convert to dicts first -result = await postprocess_results({ - "predictions": [pred.model_dump() for pred in request.predictions], -}) -``` - -This pattern: -1. Validates at API layer (type safety + error messages) -2. Converts to plain dicts before remote call (avoids serialization issues) -3. Remote worker receives clean, validated data - -### Testing Validation - -**Valid requests:** - -```bash -# Pipeline endpoint -curl -X POST http://localhost:8888/classify \ - -H "Content-Type: application/json" \ - -d '{"text": "This product is amazing!"}' - -# Postprocessing endpoint -curl -X POST http://localhost:8888/cpu/postprocess \ - -H "Content-Type: application/json" \ - -d '{ - "predictions": [ - {"label": "positive", "confidence": 0.9}, - {"label": "negative", "confidence": 0.1} - ], - "original_text": "This is great!", - "metadata": {"word_count": 3} - }' -``` - -**Invalid requests (rejected with 422):** - -```bash -# Too short -curl -X POST http://localhost:8888/classify \ - -H "Content-Type: application/json" \ - -d '{"text": "Hi"}' -# Error: Text too short (minimum 3 characters) - -# Invalid predictions structure -curl -X POST http://localhost:8888/cpu/postprocess \ - -H "Content-Type: application/json" \ - -d '{ - "predictions": ["string"], - "original_text": "Test", - "metadata": {} - }' -# Error: Input should be a valid dictionary or instance of Prediction - -# Confidence out of range -curl -X POST http://localhost:8888/cpu/postprocess \ - -H "Content-Type: application/json" \ - -d '{ - "predictions": [{"label": "test", "confidence": 1.5}], - "original_text": "Test", - "metadata": {} - }' -# Error: Confidence must be between 0.0 and 1.0 -``` - -### Validation Best Practices - -**1. Validate early** - At API layer, not in worker functions - -```python -# ✅ GOOD - Validation in Pydantic model -class Request(BaseModel): - text: str - - @field_validator("text") - @classmethod - def validate_text(cls, v): - if len(v) < 3: - raise ValueError("Too short") - return v - -# ❌ BAD - Validation in worker function -@remote(resource_config=config) -async def process(data: dict) -> dict: - if len(data["text"]) < 3: - return {"error": "Too short"} # Wastes remote call -``` - -**2. Save costs** - Reject bad requests before GPU operations - -```python -# Pipeline validates input → rejects early → GPU never called → money saved -``` - -**3. Clear errors** - Help API consumers fix issues - -```python -# ✅ GOOD -raise ValueError("Text too long (maximum 10,000 characters). Got 15000 characters.") - -# ❌ BAD -raise ValueError("Invalid text") -``` - -**4. Type safety** - Use structured models, not raw dicts - -```python -# ✅ GOOD - Type-safe, validated -predictions: list[Prediction] - -# ❌ BAD - No validation, error-prone -predictions: list -``` - -### Resources - -See also: [04_dependencies](../04_dependencies/) for more validation patterns - ## Worker Configurations ### CPU Preprocessing Worker @@ -392,28 +173,32 @@ postprocess_config = CpuLiveServerless( ## Pipeline Orchestration -The `/classify` endpoint orchestrates all workers: +The `/classify` load-balanced endpoint orchestrates all workers: ```python -@app.post("/classify") -async def classify_text(request: ClassifyRequest): - # Stage 1: CPU Preprocessing - preprocess_result = await preprocess_text({"text": request.text}) - - # Stage 2: GPU Inference - gpu_result = await gpu_inference({ - "cleaned_text": preprocess_result["cleaned_text"], - "word_count": preprocess_result["word_count"], - }) - - # Stage 3: CPU Postprocessing - final_result = await postprocess_results({ - "predictions": gpu_result["predictions"], - "original_text": request.text, - "metadata": metadata, - }) - - return final_result +@remote(resource_config=pipeline_config, method="POST", path="/classify") +async def classify(text: str) -> dict: + """Complete ML pipeline: CPU preprocess -> GPU inference -> CPU postprocess.""" + preprocess_result = await preprocess_text({"text": text}) + + gpu_result = await gpu_inference( + { + "cleaned_text": preprocess_result["cleaned_text"], + "word_count": preprocess_result["word_count"], + } + ) + + return await postprocess_results( + { + "predictions": gpu_result["predictions"], + "original_text": text, + "metadata": { + "word_count": preprocess_result["word_count"], + "sentence_count": preprocess_result["sentence_count"], + "model": gpu_result["model_info"], + }, + } + ) ``` ## Cost Analysis @@ -427,15 +212,15 @@ Assumptions: **Mixed Pipeline:** ``` -Preprocessing: 0.05 × $0.0002 × 10,000 = $0.10/day -Inference: 0.2 × $0.0015 × 10,000 = $3.00/day -Postprocessing: 0.03 × $0.0002 × 10,000 = $0.06/day +Preprocessing: 0.05 x $0.0002 x 10,000 = $0.10/day +Inference: 0.2 x $0.0015 x 10,000 = $3.00/day +Postprocessing: 0.03 x $0.0002 x 10,000 = $0.06/day Total: $3.16/day = $94.80/month ``` **GPU-Only Pipeline:** ``` -All stages: 0.28 × $0.0015 × 10,000 = $4.20/day +All stages: 0.28 x $0.0015 x 10,000 = $4.20/day Total: $126/month ``` @@ -445,31 +230,7 @@ For higher volumes, savings multiply significantly. ## Production Best Practices -### 1. Input Validation - -Always validate input at API layer **before** pipeline execution: - -```python -class ClassifyRequest(BaseModel): - text: str - - @field_validator("text") - @classmethod - def validate_text(cls, v): - # Validation happens here - fails fast before GPU - if not v or len(v) < 3 or len(v) > 10000: - raise ValueError("Invalid text") - return v - -@app.post("/classify") -async def classify(request: ClassifyRequest): - # Input already validated by Pydantic - # GPU won't be called for invalid requests - result = await run_pipeline(request.text) - return result -``` - -### 2. Error Handling +### 1. Error Handling ```python try: @@ -488,18 +249,18 @@ except Exception as e: raise HTTPException(status_code=500, detail=str(e)) ``` -### 3. Timeouts +### 2. Timeouts Set appropriate timeouts for each stage: ```python # CPU stages: short timeouts preprocess_config.executionTimeout = 30 # seconds -# GPU stage: longer timeout +# GPU stage: longer timeout gpu_config.executionTimeout = 120 # seconds ``` -### 4. Monitoring +### 3. Monitoring Track costs per stage: ```python @@ -519,51 +280,6 @@ flash deploy new production flash deploy send production ``` -## Advanced Patterns - -### Conditional GPU Usage - -Only use GPU when necessary: - -```python -@app.post("/classify-smart") -async def smart_classify(request: ClassifyRequest): - # Preprocess - preprocess_result = await preprocess_text({"text": request.text}) - - # Check if GPU is needed - if preprocess_result["word_count"] < 5: - # Simple rule-based classification (no GPU) - return {"classification": "neutral", "method": "rules"} - - # Use GPU for complex cases - gpu_result = await gpu_inference(preprocess_result) - return await postprocess_results(gpu_result) -``` - -### Batch Processing - -Process multiple inputs together: - -```python -@app.post("/classify-batch") -async def batch_classify(requests: list[ClassifyRequest]): - # Preprocess all on CPU - preprocessed = await asyncio.gather(*[ - preprocess_text({"text": r.text}) for r in requests - ]) - - # Batch GPU inference - gpu_results = await gpu_batch_inference(preprocessed) - - # Postprocess all on CPU - final_results = await asyncio.gather(*[ - postprocess_results(r) for r in gpu_results - ]) - - return final_results -``` - ## Troubleshooting ### Pipeline Failing at GPU Stage diff --git a/01_getting_started/03_mixed_workers/__init__.py b/01_getting_started/03_mixed_workers/__init__.py deleted file mode 100644 index 0e7f92c..0000000 --- a/01_getting_started/03_mixed_workers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Mixed workers example.""" diff --git a/01_getting_started/03_mixed_workers/workers/cpu/endpoint.py b/01_getting_started/03_mixed_workers/cpu_worker.py similarity index 72% rename from 01_getting_started/03_mixed_workers/workers/cpu/endpoint.py rename to 01_getting_started/03_mixed_workers/cpu_worker.py index 1137e2a..4a5a621 100644 --- a/01_getting_started/03_mixed_workers/workers/cpu/endpoint.py +++ b/01_getting_started/03_mixed_workers/cpu_worker.py @@ -1,36 +1,28 @@ +# CPU workers for text preprocessing and postprocessing. +# Part of the mixed CPU/GPU pipeline example. +# Run with: flash run +# Test directly: python cpu_worker.py from runpod_flash import CpuInstanceType, CpuLiveServerless, remote -# Preprocessing CPU worker (fast, cheap) cpu_config = CpuLiveServerless( name="01_03_mixed_workers_cpu", - instanceIds=[CpuInstanceType.CPU3G_2_8], # Small instance - 2 vCPU, 8GB + instanceIds=[CpuInstanceType.CPU3G_2_8], idleTimeout=3, ) @remote(resource_config=cpu_config) async def preprocess_text(input_data: dict) -> dict: - """ - Preprocessing on CPU: cleaning and tokenization. - - Why CPU: - - No ML operations - - Fast text operations - - 85% cheaper than GPU - - Note: Input validation handled by Pydantic at API layer - """ + """Preprocess text: cleaning and tokenization (cheap CPU work).""" import re from datetime import datetime text = input_data.get("text", "") - # Text cleaning cleaned_text = text.strip() - cleaned_text = re.sub(r"\s+", " ", cleaned_text) # Remove extra whitespace - cleaned_text = re.sub(r"[^\w\s.,!?-]", "", cleaned_text) # Remove special chars + cleaned_text = re.sub(r"\s+", " ", cleaned_text) + cleaned_text = re.sub(r"[^\w\s.,!?-]", "", cleaned_text) - # Simple tokenization (word count, sentence count) words = cleaned_text.split() sentences = len(re.split(r"[.!?]+", cleaned_text)) @@ -48,21 +40,13 @@ async def preprocess_text(input_data: dict) -> dict: @remote(resource_config=cpu_config) async def postprocess_results(input_data: dict) -> dict: - """ - Postprocessing on CPU: formatting, aggregation, logging. - - Why CPU: - - No ML operations - - Simple data formatting - - 85% cheaper than GPU - """ + """Postprocess GPU results: formatting and aggregation (cheap CPU work).""" from datetime import datetime predictions = input_data.get("predictions", []) original_text = input_data.get("original_text", "") metadata = input_data.get("metadata", {}) - # Find top prediction if predictions: top_prediction = max(predictions, key=lambda x: x["confidence"]) confidence_level = ( @@ -76,10 +60,11 @@ async def postprocess_results(input_data: dict) -> dict: top_prediction = None confidence_level = "none" - # Format response - formatted_result = { + return { "status": "success", - "text_preview": original_text[:100] + "..." if len(original_text) > 100 else original_text, + "text_preview": original_text[:100] + "..." + if len(original_text) > 100 + else original_text, "classification": { "label": top_prediction["label"] if top_prediction else "unknown", "confidence": top_prediction["confidence"] if top_prediction else 0, @@ -94,17 +79,10 @@ async def postprocess_results(input_data: dict) -> dict: "worker_type": "CPU Postprocessing", } - return formatted_result - -# Test locally if __name__ == "__main__": import asyncio - from dotenv import find_dotenv, load_dotenv - - load_dotenv(find_dotenv()) # Find and load root .env file - async def test_workers(): print("\n=== Testing Preprocessing ===") preprocess_result = await preprocess_text( diff --git a/01_getting_started/03_mixed_workers/workers/gpu/endpoint.py b/01_getting_started/03_mixed_workers/gpu_worker.py similarity index 75% rename from 01_getting_started/03_mixed_workers/workers/gpu/endpoint.py rename to 01_getting_started/03_mixed_workers/gpu_worker.py index 2c2ff89..064adfb 100644 --- a/01_getting_started/03_mixed_workers/workers/gpu/endpoint.py +++ b/01_getting_started/03_mixed_workers/gpu_worker.py @@ -1,9 +1,12 @@ +# GPU worker for ML inference (sentiment classification). +# Part of the mixed CPU/GPU pipeline example. +# Run with: flash run +# Test directly: python gpu_worker.py from runpod_flash import GpuGroup, LiveServerless, remote -# GPU worker for ML inference (expensive, powerful) gpu_config = LiveServerless( name="01_03_mixed_inference", - gpus=[GpuGroup.ADA_24], # RTX 4090 - 24GB + gpus=[GpuGroup.ADA_24], workersMin=0, workersMax=3, idleTimeout=5, @@ -12,14 +15,7 @@ @remote(resource_config=gpu_config, dependencies=["torch"]) async def gpu_inference(input_data: dict) -> dict: - """ - GPU inference: classification with mock model. - - Why GPU: - - ML model inference - - Matrix operations - - Worth the cost for compute-intensive tasks - """ + """GPU inference: mock sentiment classification.""" import random from datetime import datetime @@ -28,7 +24,6 @@ async def gpu_inference(input_data: dict) -> dict: cleaned_text = input_data.get("cleaned_text", "") word_count = input_data.get("word_count", 0) - # GPU info gpu_available = torch.cuda.is_available() if gpu_available: gpu_name = torch.cuda.get_device_name(0) @@ -37,21 +32,22 @@ async def gpu_inference(input_data: dict) -> dict: gpu_name = "No GPU (running locally)" gpu_memory = 0 - # Mock model inference (in real scenario, load actual model) - # Simulating sentiment analysis classification predictions = [] - - # Positive sentiment indicators - positive_words = ["good", "great", "excellent", "love", "best", "happy", "wonderful"] + positive_words = [ + "good", + "great", + "excellent", + "love", + "best", + "happy", + "wonderful", + ] negative_words = ["bad", "terrible", "worst", "hate", "poor", "awful", "horrible"] - text_lower = cleaned_text.lower() - # Simple sentiment scoring positive_score = sum(1 for word in positive_words if word in text_lower) negative_score = sum(1 for word in negative_words if word in text_lower) - # Create predictions based on word presence if positive_score > negative_score: predictions = [ {"label": "positive", "confidence": 0.75 + random.uniform(0, 0.24)}, @@ -71,12 +67,10 @@ async def gpu_inference(input_data: dict) -> dict: {"label": "negative", "confidence": random.uniform(0.10, 0.30)}, ] - # Normalize confidences to sum to 1.0 total = sum(p["confidence"] for p in predictions) for p in predictions: p["confidence"] = round(p["confidence"] / total, 4) - # Simulate GPU tensor operations (in real scenario, this would be model forward pass) if gpu_available: dummy_tensor = torch.randn(100, 100, device="cuda") _ = torch.matmul(dummy_tensor, dummy_tensor) @@ -91,7 +85,9 @@ async def gpu_inference(input_data: dict) -> dict: "gpu_memory_gb": round(gpu_memory, 2) if gpu_memory else 0, }, "input_stats": { - "cleaned_text": cleaned_text[:100] + "..." if len(cleaned_text) > 100 else cleaned_text, + "cleaned_text": cleaned_text[:100] + "..." + if len(cleaned_text) > 100 + else cleaned_text, "word_count": word_count, }, "timestamp": datetime.now().isoformat(), @@ -99,14 +95,9 @@ async def gpu_inference(input_data: dict) -> dict: } -# Test locally if __name__ == "__main__": import asyncio - from dotenv import find_dotenv, load_dotenv - - load_dotenv(find_dotenv()) # Find and load root .env file - test_payload = { "cleaned_text": "This is a great product! I love it.", "word_count": 8, diff --git a/01_getting_started/03_mixed_workers/main.py b/01_getting_started/03_mixed_workers/main.py deleted file mode 100644 index 8c29aa8..0000000 --- a/01_getting_started/03_mixed_workers/main.py +++ /dev/null @@ -1,127 +0,0 @@ -import logging -import os - -from fastapi import FastAPI, HTTPException -from pydantic import BaseModel, field_validator -from workers.cpu import cpu_router -from workers.cpu.endpoint import postprocess_results, preprocess_text -from workers.gpu import gpu_router -from workers.gpu.endpoint import gpu_inference - -logger = logging.getLogger(__name__) - - -app = FastAPI( - title="Mixed GPU/CPU Flash Application", - description="Cost-effective ML pipeline combining CPU preprocessing with GPU inference", - version="0.1.0", -) - -# Include individual worker routers -app.include_router(gpu_router, prefix="/gpu", tags=["GPU Workers"]) -app.include_router(cpu_router, prefix="/cpu", tags=["CPU Workers"]) - - -class ClassifyRequest(BaseModel): - """Request model for complete classification pipeline.""" - - text: str - - @field_validator("text") - @classmethod - def validate_text(cls, v): - if not v or not v.strip(): - raise ValueError("Text cannot be empty") - if len(v) < 3: - raise ValueError( - 'Text too short (minimum 3 characters). Example: {"text": "Hello world"}' - ) - if len(v) > 10000: - raise ValueError(f"Text too long (maximum 10,000 characters). Got {len(v)} characters.") - return v - - -@app.post("/classify") -async def classify_text(request: ClassifyRequest): - """ - Complete ML pipeline: CPU → GPU → CPU - - Pipeline stages: - 1. CPU Preprocessing: Validate and clean text (fast, cheap) - 2. GPU Inference: Run ML model (expensive, necessary) - 3. CPU Postprocessing: Format results (fast, cheap) - - This architecture minimizes GPU usage and maximizes cost-effectiveness. - """ - try: - # Stage 1: Preprocess on CPU (fast, cheap) - # Note: Input validation already done by Pydantic at API layer - preprocess_result = await preprocess_text({"text": request.text}) - - # Stage 2: GPU inference (expensive, necessary) - gpu_result = await gpu_inference( - { - "cleaned_text": preprocess_result["cleaned_text"], - "word_count": preprocess_result["word_count"], - } - ) - - # Stage 3: Postprocess on CPU (fast, cheap) - final_result = await postprocess_results( - { - "predictions": gpu_result["predictions"], - "original_text": request.text, - "metadata": { - "word_count": preprocess_result["word_count"], - "sentence_count": preprocess_result["sentence_count"], - "model": gpu_result["model_info"], - }, - } - ) - - return final_result - - except Exception as e: - logger.error(f"Pipeline error: {e}") - raise HTTPException(status_code=500, detail=f"Processing error: {e!s}") from e - - -@app.get("/", tags=["Info"]) -def home(): - return { - "message": "Mixed GPU/CPU Flash Application", - "description": "Cost-effective ML pipeline combining CPU and GPU workers", - "docs": "/docs", - "pipeline": { - "classify": "POST /classify - Complete pipeline (CPU → GPU → CPU)", - "cpu_preprocess": "POST /cpu/preprocess - CPU text preprocessing", - "gpu_inference": "POST /gpu/inference - GPU ML inference", - "cpu_postprocess": "POST /cpu/postprocess - CPU result formatting", - }, - "architecture": { - "pattern": "CPU (cheap) → GPU (expensive) → CPU (cheap)", - "cost_savings": "85% vs GPU-only pipeline", - }, - } - - -@app.get("/health", tags=["Info"]) -def health(): - return { - "status": "healthy", - "workers": { - "cpu_preprocess": "ready", - "gpu_inference": "ready", - "cpu_postprocess": "ready", - }, - } - - -if __name__ == "__main__": - import uvicorn - - host = os.getenv("FLASH_HOST", "localhost") - port = int(os.getenv("FLASH_PORT", 8888)) - logger.info(f"Starting Mixed GPU/CPU Flash server on {host}:{port}") - - uvicorn.run(app, host=host, port=port) diff --git a/01_getting_started/03_mixed_workers/mothership.py b/01_getting_started/03_mixed_workers/mothership.py deleted file mode 100644 index 513d841..0000000 --- a/01_getting_started/03_mixed_workers/mothership.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -Mothership Endpoint Configuration - -The mothership endpoint serves your FastAPI application routes. -It is automatically deployed as a CPU-optimized load-balanced endpoint. - -To customize this configuration: -- Modify worker scaling: change workersMin and workersMax values -- Use GPU load balancer: import LiveLoadBalancer instead of CpuLiveLoadBalancer -- Change endpoint name: update the 'name' parameter - -To disable mothership deployment: -- Delete this file, or -- Comment out the 'mothership' variable below -""" - -from runpod_flash import CpuLiveLoadBalancer - -# Mothership endpoint configuration -# This serves your FastAPI app routes from main.py -mothership = CpuLiveLoadBalancer( - name="01_03_mixed_workers-mothership", - # workersMin=1, - # workersMax=3, -) - -# Examples of customization: - -# Increase scaling for high traffic -# mothership = CpuLiveLoadBalancer( -# name="mothership-01_03_mixed_workers", -# workersMin=2, -# workersMax=10, -# ) - -# Use GPU-based load balancer instead of CPU -# (requires importing LiveLoadBalancer) -# from runpod_flash import LiveLoadBalancer -# mothership = LiveLoadBalancer( -# name="mothership-01_03_mixed_workers", -# gpus=[GpuGroup.ANY], -# ) - -# Custom endpoint name -# mothership = CpuLiveLoadBalancer( -# name="my-api-gateway", -# workersMin=1, -# workersMax=3, -# ) - -# To disable mothership: -# - Delete this entire file, or -# - Comment out the 'mothership' variable above diff --git a/01_getting_started/03_mixed_workers/pipeline.py b/01_getting_started/03_mixed_workers/pipeline.py new file mode 100644 index 0000000..62b6869 --- /dev/null +++ b/01_getting_started/03_mixed_workers/pipeline.py @@ -0,0 +1,36 @@ +# Classification pipeline: CPU preprocess -> GPU inference -> CPU postprocess. +# Demonstrates cross-worker orchestration via a load-balanced endpoint. +# Run with: flash run +from cpu_worker import postprocess_results, preprocess_text +from gpu_worker import gpu_inference +from runpod_flash import CpuLiveLoadBalancer, remote + +pipeline_config = CpuLiveLoadBalancer( + name="01_03_classify_pipeline", + workersMin=1, +) + + +@remote(resource_config=pipeline_config, method="POST", path="/classify") +async def classify(text: str) -> dict: + """Complete ML pipeline: CPU preprocess -> GPU inference -> CPU postprocess.""" + preprocess_result = await preprocess_text({"text": text}) + + gpu_result = await gpu_inference( + { + "cleaned_text": preprocess_result["cleaned_text"], + "word_count": preprocess_result["word_count"], + } + ) + + return await postprocess_results( + { + "predictions": gpu_result["predictions"], + "original_text": text, + "metadata": { + "word_count": preprocess_result["word_count"], + "sentence_count": preprocess_result["sentence_count"], + "model": gpu_result["model_info"], + }, + } + ) diff --git a/01_getting_started/03_mixed_workers/requirements.txt b/01_getting_started/03_mixed_workers/requirements.txt deleted file mode 100644 index d252e70..0000000 --- a/01_getting_started/03_mixed_workers/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -runpod-flash \ No newline at end of file diff --git a/01_getting_started/03_mixed_workers/workers/__init__.py b/01_getting_started/03_mixed_workers/workers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/01_getting_started/03_mixed_workers/workers/cpu/__init__.py b/01_getting_started/03_mixed_workers/workers/cpu/__init__.py deleted file mode 100644 index a522189..0000000 --- a/01_getting_started/03_mixed_workers/workers/cpu/__init__.py +++ /dev/null @@ -1,85 +0,0 @@ -from fastapi import APIRouter -from pydantic import BaseModel, field_validator - -from .endpoint import postprocess_results, preprocess_text - -cpu_router = APIRouter() - - -class PreprocessRequest(BaseModel): - """Request model for text preprocessing.""" - - text: str - - @field_validator("text") - @classmethod - def validate_text(cls, v): - if not v or not v.strip(): - raise ValueError("Text cannot be empty") - if len(v) < 3: - raise ValueError( - 'Text too short (minimum 3 characters). Example: {"text": "Hello world"}' - ) - if len(v) > 10000: - raise ValueError(f"Text too long (maximum 10,000 characters). Got {len(v)} characters.") - return v - - -@cpu_router.post("/preprocess") -async def preprocess_endpoint(request: PreprocessRequest): - """ - Preprocess text before GPU inference. - - Fast, cheap CPU operation for validation and cleaning. - """ - result = await preprocess_text({"text": request.text}) - return result - - -class Prediction(BaseModel): - """Individual prediction with label and confidence.""" - - label: str - confidence: float - - @field_validator("confidence") - @classmethod - def validate_confidence(cls, v): - if not 0.0 <= v <= 1.0: - raise ValueError(f"Confidence must be between 0.0 and 1.0, got {v}") - return v - - -class PostprocessRequest(BaseModel): - """Request model for result postprocessing.""" - - predictions: list[Prediction] - original_text: str - metadata: dict - - @field_validator("predictions") - @classmethod - def validate_predictions_not_empty(cls, v): - if not v: - raise ValueError( - 'Predictions cannot be empty. Example: [{"label": "positive", "confidence": 0.9}]' - ) - return v - - -@cpu_router.post("/postprocess") -async def postprocess_endpoint(request: PostprocessRequest): - """ - Postprocess GPU inference results. - - Fast, cheap CPU operation for formatting and aggregation. - """ - # Convert Pydantic models to dicts for remote serialization - result = await postprocess_results( - { - "predictions": [pred.model_dump() for pred in request.predictions], - "original_text": request.original_text, - "metadata": request.metadata, - } - ) - return result diff --git a/01_getting_started/03_mixed_workers/workers/gpu/__init__.py b/01_getting_started/03_mixed_workers/workers/gpu/__init__.py deleted file mode 100644 index 01fe478..0000000 --- a/01_getting_started/03_mixed_workers/workers/gpu/__init__.py +++ /dev/null @@ -1,36 +0,0 @@ -from fastapi import APIRouter -from pydantic import BaseModel, Field, field_validator - -from .endpoint import gpu_inference - -gpu_router = APIRouter() - - -class InferenceRequest(BaseModel): - """Request model for GPU inference.""" - - cleaned_text: str - word_count: int = Field(ge=0) - - @field_validator("cleaned_text") - @classmethod - def validate_text_not_empty(cls, v): - if not v or not v.strip(): - raise ValueError("Cleaned text cannot be empty") - return v - - -@gpu_router.post("/inference") -async def inference_endpoint(request: InferenceRequest): - """ - Run ML inference on GPU. - - Expensive GPU operation - only use after CPU preprocessing. - """ - result = await gpu_inference( - { - "cleaned_text": request.cleaned_text, - "word_count": request.word_count, - } - ) - return result diff --git a/01_getting_started/04_dependencies/.env.example b/01_getting_started/04_dependencies/.env.example deleted file mode 100644 index 91af5f2..0000000 --- a/01_getting_started/04_dependencies/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -# RUNPOD_API_KEY=your_api_key_here -# FLASH_HOST=localhost -# FLASH_PORT=8888 -# LOG_LEVEL=INFO diff --git a/01_getting_started/04_dependencies/README.md b/01_getting_started/04_dependencies/README.md index 93c30b9..eaa5a8f 100644 --- a/01_getting_started/04_dependencies/README.md +++ b/01_getting_started/04_dependencies/README.md @@ -8,7 +8,6 @@ Learn how to manage Python packages and system dependencies in Flash workers. - **System dependencies** - Installing apt packages (ffmpeg, libgl1, etc.) - **Version pinning** - Reproducible builds with exact versions - **Dependency optimization** - Minimizing cold start time -- **Input validation** - Using Pydantic field validators for data quality ## Quick Start @@ -75,10 +74,10 @@ Install apt packages: async def process_video(data: dict) -> dict: import cv2 import subprocess - + # FFmpeg available subprocess.run(["ffmpeg", "-version"]) - + # OpenCV works (needs libgl1) cap = cv2.VideoCapture("video.mp4") ``` @@ -97,293 +96,6 @@ async def simple_function(data: dict) -> dict: return {"result": "processed"} ``` -## Input Validation with Pydantic - -Flash uses FastAPI and Pydantic for request validation. Validate inputs at the API layer before they reach your worker functions. - -### Why Validate? - -- **Prevent errors** - Catch invalid data before processing -- **Better error messages** - Clear feedback to API consumers -- **Type safety** - Enforce data structure and types -- **Documentation** - Pydantic models auto-generate API docs - -### Basic Validation - -Define request models with type hints: - -```python -from pydantic import BaseModel - -class DataRequest(BaseModel): - """Request model with automatic validation.""" - data: list[list[int]] # List of lists of integers - threshold: float = 0.5 # Optional with default -``` - -FastAPI automatically: -- Validates types (returns 422 if invalid) -- Generates OpenAPI docs -- Provides helpful error messages - -### Field Validators - -Use `@field_validator` for custom validation logic: - -```python -from pydantic import BaseModel, field_validator - -class DataRequest(BaseModel): - data: list[list[int]] - - @field_validator("data") - @classmethod - def validate_two_columns(cls, v): - if not v: - raise ValueError("Data cannot be empty") - - # Require at least 2 rows for statistics - if len(v) < 2: - raise ValueError( - f"Need at least 2 rows to compute statistics, got {len(v)}. " - f'Example: {{"data": [[1, 2], [3, 4]]}}' - ) - - # Check each row has exactly 2 columns - for i, row in enumerate(v): - if len(row) != 2: - raise ValueError( - f"Row {i} has {len(row)} columns, expected exactly 2. " - f'Example: {{"data": [[1, 2], [3, 4]]}}' - ) - - return v -``` - -This example (from `workers/cpu/__init__.py:14-30`) validates: -1. Data is not empty -2. At least 2 rows (prevents NaN in statistics) -3. Each row has exactly 2 columns - -### Validation in FastAPI Router - -Connect request models to endpoints: - -```python -from fastapi import APIRouter -from pydantic import BaseModel, field_validator - -router = APIRouter() - -class DataRequest(BaseModel): - data: list[list[int]] - - @field_validator("data") - @classmethod - def validate_structure(cls, v): - # Custom validation logic - return v - -@router.post("/data") -async def process_endpoint(request: DataRequest): - """FastAPI validates request automatically.""" - result = await process_data({"data": request.data}) - return result -``` - -### Testing Validation - -Valid requests: -```bash -curl -X POST http://localhost:8888/cpu/data \ - -H "Content-Type: application/json" \ - -d '{"data": [[1, 2], [3, 4], [5, 6]]}' -``` - -Invalid requests return 422 with helpful errors: - -```bash -# Too few rows -curl -X POST http://localhost:8888/cpu/data \ - -H "Content-Type: application/json" \ - -d '{"data": [[1, 2]]}' - -# Response: -{ - "detail": [ - { - "type": "value_error", - "msg": "Value error, Need at least 2 rows to compute statistics, got 1. Example: {\"data\": [[1, 2], [3, 4]]}" - } - ] -} -``` - -```bash -# Wrong column count -curl -X POST http://localhost:8888/cpu/data \ - -H "Content-Type: application/json" \ - -d '{"data": [[1, 2, 3], [4, 5, 6]]}' - -# Response: -{ - "detail": [ - { - "type": "value_error", - "msg": "Value error, Row 0 has 3 columns, expected exactly 2. Example: {\"data\": [[1, 2], [3, 4]]}" - } - ] -} -``` - -### Validation Best Practices - -**1. Validate early** - At the API layer, not in worker functions - -```python -# ✅ GOOD - Validation in Pydantic model -class DataRequest(BaseModel): - data: list[list[int]] - - @field_validator("data") - @classmethod - def validate_data(cls, v): - # Validation logic here - return v - -@router.post("/process") -async def endpoint(request: DataRequest): - result = await worker(request.data) # Already validated - return result - -# ❌ BAD - Validation in worker function -@remote(resource_config=config) -async def worker(input_data: dict): - data = input_data["data"] - if not data or not all(len(row) == 2 for row in data): - return {"status": "error", "error": "Invalid data"} - # Process data... -``` - -**2. Provide helpful error messages** - -```python -# ✅ GOOD - Clear, actionable message -raise ValueError( - f"Row {i} has {len(row)} columns, expected exactly 2. " - f'Example: {{"data": [[1, 2], [3, 4]]}}' -) - -# ❌ BAD - Vague message -raise ValueError("Invalid data format") -``` - -**3. Validate constraints, not just types** - -```python -from pydantic import BaseModel, field_validator, Field - -class ImageRequest(BaseModel): - width: int = Field(gt=0, le=4096) # 1-4096 - height: int = Field(gt=0, le=4096) # 1-4096 - quality: int = Field(ge=1, le=100) # 1-100 - - @field_validator("width", "height") - @classmethod - def validate_dimensions(cls, v): - if v % 8 != 0: - raise ValueError(f"Dimension must be divisible by 8, got {v}") - return v -``` - -**4. Use multiple validators for complex validation** - -```python -class DataRequest(BaseModel): - data: list[list[float]] - normalize: bool = False - - @field_validator("data") - @classmethod - def validate_not_empty(cls, v): - if not v: - raise ValueError("Data cannot be empty") - return v - - @field_validator("data") - @classmethod - def validate_dimensions(cls, v): - # Check all rows have same length - if len(set(len(row) for row in v)) > 1: - raise ValueError("All rows must have same length") - return v -``` - -### Common Validation Patterns - -**Range validation:** -```python -from pydantic import Field - -class Request(BaseModel): - temperature: float = Field(ge=0.0, le=1.0) # 0.0 to 1.0 - max_tokens: int = Field(gt=0, le=4096) # 1 to 4096 -``` - -**String validation:** -```python -from pydantic import field_validator -import re - -class TextRequest(BaseModel): - text: str - - @field_validator("text") - @classmethod - def validate_text(cls, v): - if len(v) < 10: - raise ValueError("Text must be at least 10 characters") - if len(v) > 10000: - raise ValueError("Text must not exceed 10,000 characters") - return v -``` - -**List validation:** -```python -class BatchRequest(BaseModel): - items: list[str] - - @field_validator("items") - @classmethod - def validate_batch_size(cls, v): - if len(v) == 0: - raise ValueError("Batch cannot be empty") - if len(v) > 100: - raise ValueError("Batch size cannot exceed 100 items") - return v -``` - -**Enum validation:** -```python -from enum import Enum -from pydantic import BaseModel - -class OutputFormat(str, Enum): - JSON = "json" - CSV = "csv" - PARQUET = "parquet" - -class ExportRequest(BaseModel): - data: list[dict] - format: OutputFormat # Only accepts "json", "csv", or "parquet" -``` - -### Resources - -- [Pydantic Documentation](https://docs.pydantic.dev/) -- [FastAPI Request Validation](https://fastapi.tiangolo.com/tutorial/body/) -- [Field Validators Guide](https://docs.pydantic.dev/latest/concepts/validators/) - ## Version Constraints ### Exact Version (==) @@ -485,14 +197,14 @@ system_dependencies=["ffmpeg", "libgl1", "wget"] ### 1. Pin Versions for Production ```python -# ✅ GOOD - Reproducible +# Good - Reproducible dependencies=[ "torch==2.1.0", "transformers==4.35.2", "numpy==1.26.2", ] -# ❌ BAD - Unpredictable +# Bad - Unpredictable dependencies=[ "torch", # Version changes over time "transformers", @@ -503,7 +215,7 @@ dependencies=[ ### 2. Minimize Dependencies ```python -# ✅ GOOD - Only what's needed +# Good - Only what's needed @remote( dependencies=["requests"] # Just one package ) @@ -511,7 +223,7 @@ async def fetch_data(url: str): import requests return requests.get(url).json() -# ❌ BAD - Unnecessary bloat +# Bad - Unnecessary bloat @remote( dependencies=[ "requests", @@ -529,8 +241,8 @@ async def fetch_data(url: str): ```bash # Test locally first -python -m workers.gpu.endpoint -python -m workers.cpu.endpoint +python gpu_worker.py +python cpu_worker.py ``` ### 4. Document Dependencies diff --git a/01_getting_started/04_dependencies/__init__.py b/01_getting_started/04_dependencies/__init__.py deleted file mode 100644 index f8afef5..0000000 --- a/01_getting_started/04_dependencies/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Dependencies example.""" diff --git a/01_getting_started/04_dependencies/workers/cpu/endpoint.py b/01_getting_started/04_dependencies/cpu_worker.py similarity index 96% rename from 01_getting_started/04_dependencies/workers/cpu/endpoint.py rename to 01_getting_started/04_dependencies/cpu_worker.py index 980b104..9abac4e 100644 --- a/01_getting_started/04_dependencies/workers/cpu/endpoint.py +++ b/01_getting_started/04_dependencies/cpu_worker.py @@ -1,3 +1,6 @@ +# CPU workers demonstrating data science and zero-dependency patterns. +# Run with: flash run +# Test directly: python cpu_worker.py from runpod_flash import CpuInstanceType, CpuLiveServerless, remote # Worker with data science dependencies @@ -119,10 +122,6 @@ async def minimal_process(input_data: dict) -> dict: if __name__ == "__main__": import asyncio - from dotenv import find_dotenv, load_dotenv - - load_dotenv(find_dotenv()) # Find and load root .env file - async def test(): print("\n=== Testing Data Dependencies ===") data_result = await process_data({"data": [[1, 2], [3, 4], [5, 6]]}) diff --git a/01_getting_started/04_dependencies/workers/gpu/endpoint.py b/01_getting_started/04_dependencies/gpu_worker.py similarity index 96% rename from 01_getting_started/04_dependencies/workers/gpu/endpoint.py rename to 01_getting_started/04_dependencies/gpu_worker.py index 624acf3..64970bf 100644 --- a/01_getting_started/04_dependencies/workers/gpu/endpoint.py +++ b/01_getting_started/04_dependencies/gpu_worker.py @@ -1,3 +1,6 @@ +# GPU workers demonstrating Python and system dependency management. +# Run with: flash run +# Test directly: python gpu_worker.py from runpod_flash import GpuGroup, LiveServerless, remote # Worker with ML dependencies (versioned) @@ -109,10 +112,6 @@ async def process_with_system_deps(input_data: dict) -> dict: if __name__ == "__main__": import asyncio - from dotenv import find_dotenv, load_dotenv - - load_dotenv(find_dotenv()) # Find and load root .env file - async def test(): print("\n=== Testing ML Dependencies ===") ml_result = await process_with_ml_libs({}) diff --git a/01_getting_started/04_dependencies/main.py b/01_getting_started/04_dependencies/main.py deleted file mode 100644 index 11d07d0..0000000 --- a/01_getting_started/04_dependencies/main.py +++ /dev/null @@ -1,62 +0,0 @@ -import logging -import os - -from fastapi import FastAPI -from workers.cpu import cpu_router -from workers.gpu import gpu_router - -logger = logging.getLogger(__name__) - - -app = FastAPI( - title="Dependency Management Examples", - description="Learn how to manage Python and system dependencies in Flash workers", - version="0.1.0", -) - -app.include_router(gpu_router, prefix="/gpu", tags=["GPU Workers"]) -app.include_router(cpu_router, prefix="/cpu", tags=["CPU Workers"]) - - -@app.get("/", tags=["Info"]) -def home(): - return { - "message": "Flash Dependency Management Examples", - "description": "Examples of Python and system dependency management", - "docs": "/docs", - "examples": { - "ml_deps": "POST /gpu/ml-deps - ML dependencies (torch, pillow, numpy)", - "system_deps": "POST /gpu/system-deps - System dependencies (ffmpeg, libgl1)", - "data_deps": "POST /cpu/data - Data science dependencies (pandas, scipy)", - "minimal": "POST /cpu/minimal - No dependencies (fastest)", - }, - "concepts": [ - "Version pinning (torch==2.1.0)", - "Version constraints (>=, <, ~=)", - "System packages via apt", - "Minimal dependencies for fast cold start", - ], - } - - -@app.get("/health", tags=["Info"]) -def health(): - return { - "status": "healthy", - "workers": { - "ml_deps": "GPU worker with torch, pillow, numpy", - "system_deps": "GPU worker with ffmpeg, libgl1", - "data_deps": "CPU worker with pandas, scipy", - "minimal": "CPU worker with no dependencies", - }, - } - - -if __name__ == "__main__": - import uvicorn - - host = os.getenv("FLASH_HOST", "localhost") - port = int(os.getenv("FLASH_PORT", 8888)) - logger.info(f"Starting Dependency Management server on {host}:{port}") - - uvicorn.run(app, host=host, port=port) diff --git a/01_getting_started/04_dependencies/mothership.py b/01_getting_started/04_dependencies/mothership.py deleted file mode 100644 index dcc91aa..0000000 --- a/01_getting_started/04_dependencies/mothership.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -Mothership Endpoint Configuration - -The mothership endpoint serves your FastAPI application routes. -It is automatically deployed as a CPU-optimized load-balanced endpoint. - -To customize this configuration: -- Modify worker scaling: change workersMin and workersMax values -- Use GPU load balancer: import LiveLoadBalancer instead of CpuLiveLoadBalancer -- Change endpoint name: update the 'name' parameter - -To disable mothership deployment: -- Delete this file, or -- Comment out the 'mothership' variable below -""" - -from runpod_flash import CpuLiveLoadBalancer - -# Mothership endpoint configuration -# This serves your FastAPI app routes from main.py -mothership = CpuLiveLoadBalancer( - name="01_04_dependencies-mothership", - # workersMin=1, - # workersMax=3, -) - -# Examples of customization: - -# Increase scaling for high traffic -# mothership = CpuLiveLoadBalancer( -# name="mothership-01_04_dependencies", -# workersMin=2, -# workersMax=10, -# ) - -# Use GPU-based load balancer instead of CPU -# (requires importing LiveLoadBalancer) -# from runpod_flash import LiveLoadBalancer -# mothership = LiveLoadBalancer( -# name="mothership-01_04_dependencies", -# gpus=[GpuGroup.ANY], -# ) - -# Custom endpoint name -# mothership = CpuLiveLoadBalancer( -# name="my-api-gateway", -# workersMin=1, -# workersMax=3, -# ) - -# To disable mothership: -# - Delete this entire file, or -# - Comment out the 'mothership' variable above diff --git a/01_getting_started/04_dependencies/requirements.txt b/01_getting_started/04_dependencies/requirements.txt deleted file mode 100644 index d252e70..0000000 --- a/01_getting_started/04_dependencies/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -runpod-flash \ No newline at end of file diff --git a/01_getting_started/04_dependencies/workers/__init__.py b/01_getting_started/04_dependencies/workers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/01_getting_started/04_dependencies/workers/cpu/__init__.py b/01_getting_started/04_dependencies/workers/cpu/__init__.py deleted file mode 100644 index 180925f..0000000 --- a/01_getting_started/04_dependencies/workers/cpu/__init__.py +++ /dev/null @@ -1,50 +0,0 @@ -from fastapi import APIRouter -from pydantic import BaseModel, field_validator - -from .endpoint import minimal_process, process_data - -cpu_router = APIRouter() - - -class DataRequest(BaseModel): - """Request model for data processing.""" - - data: list[list[int]] - - @field_validator("data") - @classmethod - def validate_two_columns(cls, v): - if not v: - raise ValueError("Data cannot be empty") - if len(v) < 2: - raise ValueError( - f"Need at least 2 rows to compute statistics, got {len(v)}. " - f'Example: {{"data": [[1, 2], [3, 4]]}}' - ) - for i, row in enumerate(v): - if len(row) != 2: - raise ValueError( - f"Row {i} has {len(row)} columns, expected exactly 2. " - f'Example: {{"data": [[1, 2], [3, 4]]}}' - ) - return v - - -class TextRequest(BaseModel): - """Request model for text processing.""" - - text: str - - -@cpu_router.post("/data") -async def data_endpoint(request: DataRequest): - """Test worker with data science dependencies (pandas, numpy, scipy).""" - result = await process_data({"data": request.data}) - return result - - -@cpu_router.post("/minimal") -async def minimal_endpoint(request: TextRequest): - """Test worker with NO dependencies (fastest cold start).""" - result = await minimal_process({"text": request.text}) - return result diff --git a/01_getting_started/04_dependencies/workers/gpu/__init__.py b/01_getting_started/04_dependencies/workers/gpu/__init__.py deleted file mode 100644 index c47de1e..0000000 --- a/01_getting_started/04_dependencies/workers/gpu/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -from fastapi import APIRouter - -from .endpoint import process_with_ml_libs, process_with_system_deps - -gpu_router = APIRouter() - - -@gpu_router.post("/ml-deps") -async def ml_deps_endpoint(): - """Test worker with ML dependencies (torch, pillow, numpy).""" - result = await process_with_ml_libs({}) - return result - - -@gpu_router.post("/system-deps") -async def system_deps_endpoint(): - """Test worker with system dependencies (ffmpeg, libgl1).""" - result = await process_with_system_deps({}) - return result diff --git a/01_getting_started/README.md b/01_getting_started/README.md index 881e275..9729a00 100644 --- a/01_getting_started/README.md +++ b/01_getting_started/README.md @@ -12,12 +12,11 @@ The simplest Flash application with GPU workers - Creating GPU workers - Using the `@remote` decorator - Running Flash applications locally -- Testing endpoints with FastAPI docs +- Testing endpoints with Swagger docs **Concepts:** - `LiveServerless` configuration for GPU workers - Worker auto-scaling (min/max workers) -- FastAPI router integration ### [02_cpu_worker](./02_cpu_worker/) CPU-only worker example for non-GPU workloads. @@ -35,14 +34,11 @@ Mixed GPU/CPU workers for cost-effective ML pipelines. - Mixed worker architecture (CPU preprocessing → GPU inference → CPU postprocessing) - Cost optimization (85% savings vs GPU-only pipeline) - Pipeline orchestration patterns -- Pydantic input validation at each stage - Fail-fast validation before expensive GPU operations -- Remote serialization with .model_dump() **Concepts:** - `CpuLiveServerless` for preprocessing and postprocessing -- Pipeline orchestration with FastAPI -- Validation patterns for production APIs +- Pipeline orchestration with load-balanced endpoints ### [04_dependencies](./04_dependencies/) Managing Python packages and system dependencies. @@ -50,7 +46,6 @@ Managing Python packages and system dependencies. **What you'll learn:** - Python dependency versioning and constraints - System package installation (ffmpeg, libgl1) -- Input validation with Pydantic field validators - Version constraints (==, >=, <, ~=) - Minimizing cold start time - Best practices for reproducible builds @@ -59,7 +54,6 @@ Managing Python packages and system dependencies. - `dependencies` parameter for Python packages - `system_dependencies` parameter for apt packages - Version pinning for reproducibility -- Pydantic `@field_validator` for request validation - Dependency optimization strategies ## Learning Path diff --git a/02_ml_inference/01_text_to_speech/.env.example b/02_ml_inference/01_text_to_speech/.env.example deleted file mode 100644 index 8360712..0000000 --- a/02_ml_inference/01_text_to_speech/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -# FLASH_HOST=localhost -# FLASH_PORT=8888 -# LOG_LEVEL=INFO -# RUNPOD_API_KEY=your_api_key_here diff --git a/02_ml_inference/01_text_to_speech/README.md b/02_ml_inference/01_text_to_speech/README.md index b5b565b..c669660 100644 --- a/02_ml_inference/01_text_to_speech/README.md +++ b/02_ml_inference/01_text_to_speech/README.md @@ -39,39 +39,29 @@ First run provisions the endpoint (~1 min). Server starts at http://localhost:88 ### Test the Endpoint -**Get WAV file directly:** -```bash -curl -X POST http://localhost:8888/gpu/tts/audio \ - -H "Content-Type: application/json" \ - -d '{"text": "Hello world!", "speaker": "Ryan", "language": "English"}' \ - --output speech.wav -``` +Visit http://localhost:8888/docs for interactive API documentation. QB endpoints are auto-generated by `flash run` based on your `@remote` functions. -**Get JSON with base64 audio:** +**Generate speech (JSON with base64 audio):** ```bash -curl -X POST http://localhost:8888/gpu/tts \ +curl -X POST http://localhost:8888/gpu_worker/run_sync \ -H "Content-Type: application/json" \ -d '{"text": "Hello world!", "speaker": "Ryan", "language": "English"}' ``` -**With emotion/style control:** +**List available voices:** ```bash -curl -X POST http://localhost:8888/gpu/tts/audio \ +curl -X POST http://localhost:8888/gpu_worker/run_sync \ -H "Content-Type: application/json" \ - -d '{"text": "I have great news!", "speaker": "Ryan", "language": "English", "instruct": "Speak with excitement and joy"}' \ - --output excited.wav + -d '{}' ``` -**List available voices:** -```bash -curl http://localhost:8888/gpu/voices -``` +Check `/docs` for the exact auto-generated endpoint paths and schemas. -Visit http://localhost:8888/docs for interactive API documentation. +## API Functions -## API Endpoints +QB (queue-based) endpoints are auto-generated from `@remote` functions. Visit `/docs` for the full API schema. -### POST /gpu/tts +### `generate_speech` Generate speech and return JSON with base64-encoded WAV audio. @@ -96,13 +86,7 @@ Generate speech and return JSON with base64-encoded WAV audio. } ``` -### POST /gpu/tts/audio - -Generate speech and return raw WAV file directly. - -Same request body as `/gpu/tts`. Returns `audio/wav` content type. - -### GET /gpu/voices +### `get_voices` List available voices and supported languages. @@ -153,7 +137,7 @@ flash deploy send production - **Cold start delay**: First request after idle takes 20-30s to load the model. Use `flash run --auto-provision` during development. - **Out of memory**: The model requires 24GB+ VRAM. Ensure `GpuGroup.ADA_24` or higher is configured. -- **Invalid speaker/language**: Check `/gpu/voices` for valid options. +- **Invalid speaker/language**: Use `get_voices` to check valid options. ## References diff --git a/02_ml_inference/01_text_to_speech/__init__.py b/02_ml_inference/01_text_to_speech/__init__.py deleted file mode 100644 index 1a418c3..0000000 --- a/02_ml_inference/01_text_to_speech/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Text-to-speech example using Qwen3-TTS.""" diff --git a/02_ml_inference/01_text_to_speech/workers/gpu/endpoint.py b/02_ml_inference/01_text_to_speech/gpu_worker.py similarity index 97% rename from 02_ml_inference/01_text_to_speech/workers/gpu/endpoint.py rename to 02_ml_inference/01_text_to_speech/gpu_worker.py index 83bd818..87eaff9 100644 --- a/02_ml_inference/01_text_to_speech/workers/gpu/endpoint.py +++ b/02_ml_inference/01_text_to_speech/gpu_worker.py @@ -1,3 +1,7 @@ +# Qwen3-TTS text-to-speech GPU worker. +# Run with: flash run +# Test directly: python gpu_worker.py + from runpod_flash import GpuGroup, LiveServerless, remote # GPU config for Qwen3-TTS - needs 24GB+ VRAM for 1.7B model @@ -162,7 +166,7 @@ async def get_voices(input_data: dict) -> dict: } -# Test locally with: python -m workers.gpu.endpoint +# Test locally with: python gpu_worker.py if __name__ == "__main__": import asyncio diff --git a/02_ml_inference/01_text_to_speech/main.py b/02_ml_inference/01_text_to_speech/main.py deleted file mode 100644 index 2852d8e..0000000 --- a/02_ml_inference/01_text_to_speech/main.py +++ /dev/null @@ -1,44 +0,0 @@ -import logging -import os - -from fastapi import FastAPI -from workers.gpu import gpu_router - -logger = logging.getLogger(__name__) - - -app = FastAPI( - title="Qwen3-TTS API", - description="Text-to-Speech API using Qwen3-TTS on RunPod serverless GPUs", - version="1.0.0", -) - -app.include_router(gpu_router, prefix="/gpu", tags=["TTS"]) - - -@app.get("/") -def home(): - return { - "message": "Qwen3-TTS API", - "docs": "/docs", - "endpoints": { - "tts": "/gpu/tts", - "tts_audio": "/gpu/tts/audio", - "voices": "/gpu/voices", - }, - } - - -@app.get("/ping") -def ping(): - return {"status": "healthy"} - - -if __name__ == "__main__": - import uvicorn - - host = os.getenv("FLASH_HOST", "localhost") - port = int(os.getenv("FLASH_PORT", 8888)) - logger.info(f"Starting Flash server on {host}:{port}") - - uvicorn.run(app, host=host, port=port) diff --git a/02_ml_inference/01_text_to_speech/mothership.py b/02_ml_inference/01_text_to_speech/mothership.py deleted file mode 100644 index 8b2390d..0000000 --- a/02_ml_inference/01_text_to_speech/mothership.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -Mothership Endpoint Configuration - -The mothership endpoint serves your FastAPI application routes. -It is automatically deployed as a CPU-optimized load-balanced endpoint. - -To customize this configuration: -- Modify worker scaling: change workersMin and workersMax values -- Use GPU load balancer: import LiveLoadBalancer instead of CpuLiveLoadBalancer -- Change endpoint name: update the 'name' parameter - -To disable mothership deployment: -- Delete this file, or -- Comment out the 'mothership' variable below - -Documentation: https://docs.runpod.io/flash/mothership -""" - -from runpod_flash import CpuLiveLoadBalancer - -# Mothership endpoint configuration -# This serves your FastAPI app routes from main.py -mothership = CpuLiveLoadBalancer( - name="02_01_text_to_speech-mothership", - workersMin=1, - workersMax=3, -) - -# Examples of customization: - -# Increase scaling for high traffic -# mothership = CpuLiveLoadBalancer( -# name="mothership", -# workersMin=2, -# workersMax=10, -# ) - -# Use GPU-based load balancer instead of CPU -# (requires importing LiveLoadBalancer) -# from runpod_flash import LiveLoadBalancer -# mothership = LiveLoadBalancer( -# name="mothership", -# gpus=[GpuGroup.ANY], -# ) - -# Custom endpoint name -# mothership = CpuLiveLoadBalancer( -# name="my-api-gateway", -# workersMin=1, -# workersMax=3, -# ) - -# To disable mothership: -# - Delete this entire file, or -# - Comment out the 'mothership' variable above diff --git a/02_ml_inference/01_text_to_speech/pyproject.toml b/02_ml_inference/01_text_to_speech/pyproject.toml index 4396186..332c836 100644 --- a/02_ml_inference/01_text_to_speech/pyproject.toml +++ b/02_ml_inference/01_text_to_speech/pyproject.toml @@ -5,6 +5,4 @@ description = "Text-to-Speech using Qwen3-TTS on RunPod serverless GPUs" requires-python = ">=3.10" dependencies = [ "runpod-flash", - "fastapi>=0.104.0", - "uvicorn>=0.24.0", ] diff --git a/02_ml_inference/01_text_to_speech/requirements.txt b/02_ml_inference/01_text_to_speech/requirements.txt deleted file mode 100644 index a73ed1a..0000000 --- a/02_ml_inference/01_text_to_speech/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -runpod-flash diff --git a/02_ml_inference/01_text_to_speech/workers/__init__.py b/02_ml_inference/01_text_to_speech/workers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/02_ml_inference/01_text_to_speech/workers/gpu/__init__.py b/02_ml_inference/01_text_to_speech/workers/gpu/__init__.py deleted file mode 100644 index 6c8f26d..0000000 --- a/02_ml_inference/01_text_to_speech/workers/gpu/__init__.py +++ /dev/null @@ -1,81 +0,0 @@ -import base64 - -from fastapi import APIRouter -from fastapi.responses import Response -from pydantic import BaseModel, Field - -from .endpoint import generate_speech, get_voices - -gpu_router = APIRouter() - - -class TTSRequest(BaseModel): - """Request model for text-to-speech generation.""" - - text: str = Field(..., description="The text to synthesize into speech") - speaker: str = Field( - default="Ryan", - description="Voice to use. Options: Vivian, Serena, Uncle_Fu, Dylan, Eric, Ryan, Aiden, Ono_Anna, Sohee", - ) - language: str = Field( - default="Auto", - description="Language. Options: Chinese, English, Japanese, Korean, German, French, Russian, Portuguese, Spanish, Italian, Auto", - ) - instruct: str | None = Field( - default=None, - description="Optional emotion/style instruction (e.g., 'Speak happily', 'Say it slowly and calmly')", - ) - - -@gpu_router.post("/tts") -async def text_to_speech(request: TTSRequest) -> dict: - """ - Generate speech from text using Qwen3-TTS. - - Returns JSON with base64-encoded WAV audio. - """ - payload = { - "text": request.text, - "speaker": request.speaker, - "language": request.language, - } - if request.instruct: - payload["instruct"] = request.instruct - - result = await generate_speech(payload) - return result - - -@gpu_router.post("/tts/audio") -async def text_to_speech_audio(request: TTSRequest) -> Response: - """ - Generate speech from text using Qwen3-TTS. - - Returns raw WAV audio file directly. - """ - payload = { - "text": request.text, - "speaker": request.speaker, - "language": request.language, - } - if request.instruct: - payload["instruct"] = request.instruct - - result = await generate_speech(payload) - - if result["status"] == "error": - return result - - audio_bytes = base64.b64decode(result["audio_base64"]) - return Response( - content=audio_bytes, - media_type="audio/wav", - headers={"Content-Disposition": "attachment; filename=speech.wav"}, - ) - - -@gpu_router.get("/voices") -async def list_voices() -> dict: - """Get available voices and languages.""" - result = await get_voices({}) - return result diff --git a/03_advanced_workers/05_load_balancer/.env.example b/03_advanced_workers/05_load_balancer/.env.example deleted file mode 100644 index 8360712..0000000 --- a/03_advanced_workers/05_load_balancer/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -# FLASH_HOST=localhost -# FLASH_PORT=8888 -# LOG_LEVEL=INFO -# RUNPOD_API_KEY=your_api_key_here diff --git a/03_advanced_workers/05_load_balancer/README.md b/03_advanced_workers/05_load_balancer/README.md index 845a6a3..8004c92 100644 --- a/03_advanced_workers/05_load_balancer/README.md +++ b/03_advanced_workers/05_load_balancer/README.md @@ -81,15 +81,6 @@ curl -X POST http://localhost:8888/05_load_balancer/cpu/transform \ -d '{"text": "hello", "operation": "uppercase"}' ``` -### 5. Run Standalone (for this example only) - -```bash -cd 03_advanced_workers/05_load_balancer -python main.py -``` - -This runs the example on **http://localhost:8000** (default standalone port) without the example name prefix. The unified app uses port 8888, but standalone mode defaults to 8000 unless FLASH_PORT is set. - ## How Load-Balancer Endpoints Work ### Defining Routes with @remote Decorator @@ -160,17 +151,11 @@ The following paths are reserved and cannot be used: ``` 05_load_balancer/ -├── main.py # FastAPI application -├── workers/ -│ ├── gpu/ # GPU load-balancer endpoints -│ │ ├── __init__.py # Router with endpoints -│ │ └── endpoint.py # @remote functions -│ └── cpu/ # CPU load-balancer endpoints -│ ├── __init__.py # Router with endpoints -│ └── endpoint.py # @remote functions -├── .env.example # Environment template -├── requirements.txt # Dependencies -└── README.md # This file +├── gpu_lb.py # GPU load-balancer endpoints with @remote +├── cpu_lb.py # CPU load-balancer endpoints with @remote +├── .env.example # Environment template +├── requirements.txt # Dependencies +└── README.md # This file ``` ## GPU Service Endpoints @@ -334,10 +319,10 @@ async def process(data: dict) -> dict: ```bash # Test GPU worker -python -m workers.gpu.endpoint +python gpu_lb.py # Test CPU worker -python -m workers.cpu.endpoint +python cpu_lb.py ``` ## Deployment @@ -427,7 +412,7 @@ RUNPOD_API_KEY=your_api_key_here # Optional FLASH_HOST=localhost # Server host (default: localhost) -FLASH_PORT=8000 # Server port (default: 8000) +FLASH_PORT=8888 # Server port (default: 8888) LOG_LEVEL=INFO # Logging level (default: INFO) ``` @@ -485,14 +470,6 @@ For current pricing, see [RunPod Pricing](https://www.runpod.io/pricing). - Adjust `workersMin` to keep workers warm if consistent low latency is critical - Consider using `idleTimeout` to reduce cold starts -### Endpoint discovery not working - -**Problem**: Example doesn't load in unified app with `flash run` -- Ensure routers are named `gpu_router` and `cpu_router` -- Verify routers are properly exported in `__init__.py` files -- Check that `main.py` includes routers with `app.include_router()` -- Run `flash run` from the repository root, not the example directory - ## Next Steps 1. Explore the endpoints via Swagger UI (`/docs`) diff --git a/03_advanced_workers/05_load_balancer/workers/cpu/endpoint.py b/03_advanced_workers/05_load_balancer/cpu_lb.py similarity index 96% rename from 03_advanced_workers/05_load_balancer/workers/cpu/endpoint.py rename to 03_advanced_workers/05_load_balancer/cpu_lb.py index 2dd86a5..55949c4 100644 --- a/03_advanced_workers/05_load_balancer/workers/cpu/endpoint.py +++ b/03_advanced_workers/05_load_balancer/cpu_lb.py @@ -1,3 +1,6 @@ +# CPU load-balanced endpoints with custom HTTP routes. +# Run with: flash run +# Test directly: python cpu_lb.py from runpod_flash import CpuLiveLoadBalancer, remote cpu_config = CpuLiveLoadBalancer( @@ -95,7 +98,6 @@ async def transform_data(text: str, operation: str = "uppercase") -> dict: } -# Test locally with: python -m workers.cpu.endpoint if __name__ == "__main__": import asyncio diff --git a/03_advanced_workers/05_load_balancer/workers/gpu/endpoint.py b/03_advanced_workers/05_load_balancer/gpu_lb.py similarity index 82% rename from 03_advanced_workers/05_load_balancer/workers/gpu/endpoint.py rename to 03_advanced_workers/05_load_balancer/gpu_lb.py index f8c8d5c..28001c6 100644 --- a/03_advanced_workers/05_load_balancer/workers/gpu/endpoint.py +++ b/03_advanced_workers/05_load_balancer/gpu_lb.py @@ -1,3 +1,6 @@ +# GPU load-balanced endpoints with custom HTTP routes. +# Run with: flash run +# Test directly: python gpu_lb.py from runpod_flash import LiveLoadBalancer, remote gpu_config = LiveLoadBalancer( @@ -13,11 +16,11 @@ async def gpu_health() -> dict: @remote(gpu_config, method="POST", path="/compute") -async def compute_intensive(request: dict) -> dict: +async def compute_intensive(numbers: list[float]) -> dict: """Perform compute-intensive operation on GPU. Args: - request: Request dict with numbers to process + numbers: List of numbers to process Returns: Computation results @@ -25,14 +28,13 @@ async def compute_intensive(request: dict) -> dict: import time from datetime import datetime, timezone - numbers = request.get("numbers", []) start_time = time.time() # Simulate GPU-intensive computation result = sum(x**2 for x in numbers) - mean = sum(numbers) / len(numbers) - max_val = max(numbers) - min_val = min(numbers) + mean = sum(numbers) / len(numbers) if numbers else 0 + max_val = max(numbers) if numbers else None + min_val = min(numbers) if numbers else None compute_time = (time.time() - start_time) * 1000 @@ -68,7 +70,6 @@ async def gpu_info() -> dict: return info -# Test locally with: python -m workers.gpu.endpoint if __name__ == "__main__": import asyncio @@ -80,8 +81,7 @@ async def test(): print(f" {result}\n") print("2. Compute intensive:") - request_data = {"numbers": [1, 2, 3, 4, 5]} - result = await compute_intensive(request_data) + result = await compute_intensive([1.0, 2.0, 3.0, 4.0, 5.0]) print(f" Sum of squares: {result['sum_of_squares']}") print(f" Mean: {result['mean']}\n") diff --git a/03_advanced_workers/05_load_balancer/main.py b/03_advanced_workers/05_load_balancer/main.py deleted file mode 100644 index 7115e75..0000000 --- a/03_advanced_workers/05_load_balancer/main.py +++ /dev/null @@ -1,55 +0,0 @@ -import logging -import os - -from fastapi import FastAPI -from workers.cpu import cpu_router -from workers.gpu import gpu_router - -logger = logging.getLogger(__name__) - - -app = FastAPI( - title="Load Balancer Example", - description="Demonstrates load-balancer endpoints with custom HTTP routes using Flash", - version="0.1.0", -) - -# Include routers for unified app discovery -app.include_router(gpu_router, prefix="/gpu", tags=["GPU Compute Service"]) -app.include_router(cpu_router, prefix="/cpu", tags=["CPU Data Service"]) - - -@app.get("/") -def home(): - return { - "message": "Load Balancer Example API", - "description": "Demonstrates load-balancer endpoints with custom HTTP routes", - "note": "Load-balancer endpoints are defined with @remote(resource, method=..., path=...)", - "docs": "/docs", - "gpu_endpoints": { - "health": "GET /gpu/health", - "compute": "POST /gpu/compute", - "info": "GET /gpu/info", - }, - "cpu_endpoints": { - "health": "GET /cpu/health", - "validate": "POST /cpu/validate", - "transform": "POST /cpu/transform", - }, - } - - -@app.get("/ping") -def ping(): - """Health check endpoint for load balancer.""" - return {"status": "healthy"} - - -if __name__ == "__main__": - import uvicorn - - host = os.getenv("FLASH_HOST", "localhost") - port = int(os.getenv("FLASH_PORT", 8000)) - logger.info(f"Starting Load Balancer Example on {host}:{port}") - - uvicorn.run(app, host=host, port=port) diff --git a/03_advanced_workers/05_load_balancer/mothership.py b/03_advanced_workers/05_load_balancer/mothership.py deleted file mode 100644 index 3e620be..0000000 --- a/03_advanced_workers/05_load_balancer/mothership.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -Mothership Endpoint Configuration - -The mothership endpoint serves your FastAPI application routes. -It is automatically deployed as a CPU-optimized load-balanced endpoint. - -To customize this configuration: -- Modify worker scaling: change workersMin and workersMax values -- Use GPU load balancer: import LiveLoadBalancer instead of CpuLiveLoadBalancer -- Change endpoint name: update the 'name' parameter - -To disable mothership deployment: -- Delete this file, or -- Comment out the 'mothership' variable below -""" - -from runpod_flash import CpuLiveLoadBalancer - -# Mothership endpoint configuration -# This serves your FastAPI app routes from main.py -mothership = CpuLiveLoadBalancer( - name="03_05_load_balancer-mothership", - # workersMin=1, - # workersMax=3, -) - -# Examples of customization: - -# Increase scaling for high traffic -# mothership = CpuLiveLoadBalancer( -# name="mothership-03_05_load_balancer", -# workersMin=2, -# workersMax=10, -# ) - -# Use GPU-based load balancer instead of CPU -# (requires importing LiveLoadBalancer) -# from runpod_flash import LiveLoadBalancer -# mothership = LiveLoadBalancer( -# name="mothership-03_05_load_balancer", -# gpus=[GpuGroup.ANY], -# ) - -# Custom endpoint name -# mothership = CpuLiveLoadBalancer( -# name="my-api-gateway", -# workersMin=1, -# workersMax=3, -# ) - -# To disable mothership: -# - Delete this entire file, or -# - Comment out the 'mothership' variable above diff --git a/03_advanced_workers/05_load_balancer/requirements.txt b/03_advanced_workers/05_load_balancer/requirements.txt deleted file mode 100644 index d252e70..0000000 --- a/03_advanced_workers/05_load_balancer/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -runpod-flash \ No newline at end of file diff --git a/03_advanced_workers/05_load_balancer/workers/__init__.py b/03_advanced_workers/05_load_balancer/workers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/03_advanced_workers/05_load_balancer/workers/cpu/__init__.py b/03_advanced_workers/05_load_balancer/workers/cpu/__init__.py deleted file mode 100644 index e4d1cb5..0000000 --- a/03_advanced_workers/05_load_balancer/workers/cpu/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -"""CPU Load-Balancer Endpoints - -Load-balancer endpoints use the @remote decorator with method and path parameters. -These decorators automatically create HTTP routes that are registered with the -Load Balancer runtime. - -For the unified app discovery, we export a router that documents the endpoints. -The actual HTTP handling is managed by the runpod-flash-lb framework. -""" - -from fastapi import APIRouter -from pydantic import BaseModel - -from .endpoint import cpu_health, transform_data, validate_data - - -# Pydantic models for request validation -class ValidateRequest(BaseModel): - """Request model for text validation.""" - - text: str - - -class TransformRequest(BaseModel): - """Request model for text transformation.""" - - text: str - operation: str = "uppercase" - - -# Export for unified app discovery -cpu_router = APIRouter() - - -@cpu_router.get("/health") -async def get_cpu_health(): - """CPU service health check.""" - return await cpu_health() - - -@cpu_router.post("/validate") -async def post_validate(request: ValidateRequest): - """Validate and analyze text data.""" - return await validate_data(request.text) - - -@cpu_router.post("/transform") -async def post_transform(request: TransformRequest): - """Transform text data with specified operation.""" - return await transform_data(request.text, request.operation) - - -__all__ = ["cpu_router"] diff --git a/03_advanced_workers/05_load_balancer/workers/gpu/__init__.py b/03_advanced_workers/05_load_balancer/workers/gpu/__init__.py deleted file mode 100644 index 1fe8c10..0000000 --- a/03_advanced_workers/05_load_balancer/workers/gpu/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -"""GPU Load-Balancer Endpoints - -Load-balancer endpoints use the @remote decorator with method and path parameters. -These decorators automatically create HTTP routes that are registered with the -Load Balancer runtime. - -For the unified app discovery, we export a router that documents the endpoints. -The actual HTTP handling is managed by the runpod-flash-lb framework. -""" - -from fastapi import APIRouter -from pydantic import BaseModel, field_validator - -from .endpoint import compute_intensive, gpu_health, gpu_info - - -class ComputeRequest(BaseModel): - """Request model for compute-intensive operations.""" - - numbers: list[int] - - @field_validator("numbers") - @classmethod - def validate_numbers(cls, v: list[int]) -> list[int]: - """Validate that numbers list is not empty.""" - if not v: - raise ValueError("numbers list cannot be empty") - return v - - -# Export for unified app discovery -gpu_router = APIRouter() - - -@gpu_router.get("/health") -async def get_gpu_health(): - """GPU service health check.""" - return await gpu_health() - - -@gpu_router.post("/compute") -async def post_gpu_compute(request: ComputeRequest): - """Perform compute-intensive operation on GPU.""" - return await compute_intensive(request.model_dump()) - - -@gpu_router.get("/info") -async def get_gpu_info(): - """Get GPU device information.""" - return await gpu_info() - - -__all__ = ["gpu_router"] diff --git a/05_data_workflows/01_network_volumes/README.md b/05_data_workflows/01_network_volumes/README.md index 511171d..d0b28bf 100644 --- a/05_data_workflows/01_network_volumes/README.md +++ b/05_data_workflows/01_network_volumes/README.md @@ -34,26 +34,25 @@ Server starts at `http://localhost:8888` ### 4. Test the API +**Generate an image (GPU worker):** ```bash -# Health check -curl http://localhost:8888/ping - -# Generate an image on the GPU worker -curl -X POST http://localhost:8888/gpu/generate \ +curl -X POST http://localhost:8888/gpu_worker/run_sync \ -H "Content-Type: application/json" \ - -d '{"prompt": "a lighthouse on a cliff at sunrise"}' + -d '{"prompt": "a sunset over mountains"}' +``` -# List generated images from the CPU worker -curl http://localhost:8888/cpu/image +**List generated images (CPU worker):** +```bash +curl http://localhost:8888/images +``` -# Fetch a single image by filename -curl http://localhost:8888/cpu/image/ --output image.png +**Get a specific image (CPU worker):** +```bash +curl http://localhost:8888/images/sd_generated_20240101_120000.png ``` Visit `http://localhost:8888/docs` for interactive API documentation. -Browse the image gallery at `http://localhost:8888/cpu/`. - ## What You'll Learn - How to attach a shared `NetworkVolume` to GPU and CPU workers @@ -69,26 +68,21 @@ Browse the image gallery at `http://localhost:8888/cpu/`. ``` 01_network_volumes/ -├── main.py -├── workers/ -│ ├── __init__.py # Network volume definition -│ ├── gpu/ -│ │ ├── __init__.py # GPU router -│ │ └── endpoint.py # Stable Diffusion worker -│ └── cpu/ -│ ├── __init__.py # CPU router -│ └── endpoint.py # List and serve images +├── gpu_worker.py # Stable Diffusion worker with @remote +├── cpu_worker.py # List and serve images with @remote ├── requirements.txt └── README.md ``` ## API Endpoints -### POST /gpu/generate +### POST /gpu_worker/run_sync + +GPU worker (QB, class-based `@remote`). Generates an image and saves it to the shared volume. **Request**: ```json -{ "prompt": "string" } +{ "prompt": "a sunset over mountains" } ``` **Response**: @@ -107,20 +101,22 @@ Browse the image gallery at `http://localhost:8888/cpu/`. } ``` -### GET /cpu/image +### GET /images + +CPU worker (LB, explicit path). Lists all generated images on the shared volume. **Response**: ```json -{ "status": "success", "images": ["file.png"] } +{ "status": "success", "images": ["sd_generated_20240101_120000.png"] } ``` -### GET /cpu/image/{file_id} +### GET /images/{file_name} -Returns the PNG file as `image/png`. +CPU worker (LB, explicit path). Returns metadata and base64-encoded content for a single image. ## Notes -- The network volume is defined in `workers/__init__.py` and attached to both workers. +- The network volume is defined inline in each worker file and attached to both workers. - Stable Diffusion weights are cached in the volume so cold starts are faster after the first run. ## Deployment diff --git a/05_data_workflows/01_network_volumes/cpu_worker.py b/05_data_workflows/01_network_volumes/cpu_worker.py new file mode 100644 index 0000000..d947c48 --- /dev/null +++ b/05_data_workflows/01_network_volumes/cpu_worker.py @@ -0,0 +1,60 @@ +# CPU worker with network volume for listing and serving generated images. +# Run with: flash run +# Test directly: python cpu_worker.py +from runpod_flash import CpuLiveLoadBalancer, NetworkVolume, remote + +volume = NetworkVolume( + name="flash-05-volume", + size=50, +) + +cpu_config = CpuLiveLoadBalancer( + name="05_01_cpu_worker", + workersMin=1, + networkVolume=volume, +) + + +@remote(resource_config=cpu_config, path="/images", method="GET") +async def list_images_in_volume() -> dict: + """List generated images from the shared volume.""" + import os + + image_dir = "/runpod-volume/generated_images" + if not os.path.isdir(image_dir): + return {"status": "success", "images": []} + + return { + "status": "success", + "images": os.listdir(image_dir), + } + + +@remote(resource_config=cpu_config, path="/images/{file_name}", method="GET") +async def get_image_from_volume(file_name: str) -> dict: + """Get image metadata from the shared volume.""" + import base64 + from pathlib import Path + + image_path = Path(f"/runpod-volume/generated_images/{file_name}") + + if not image_path.is_file(): + return {"status": "error", "error": "file not found"} + + image_bytes = image_path.read_bytes() + return { + "status": "success", + "filename": image_path.name, + "size_bytes": len(image_bytes), + "image_base64": base64.b64encode(image_bytes).decode("utf-8"), + } + + +if __name__ == "__main__": + import asyncio + + print( + "Checking cpu worker by listing image files in /runpod-volume/generated_images/" + ) + result = asyncio.run(list_images_in_volume()) + print(f"Result: {result}") diff --git a/05_data_workflows/01_network_volumes/workers/gpu/endpoint.py b/05_data_workflows/01_network_volumes/gpu_worker.py similarity index 71% rename from 05_data_workflows/01_network_volumes/workers/gpu/endpoint.py rename to 05_data_workflows/01_network_volumes/gpu_worker.py index e63c480..c948228 100644 --- a/05_data_workflows/01_network_volumes/workers/gpu/endpoint.py +++ b/05_data_workflows/01_network_volumes/gpu_worker.py @@ -1,17 +1,21 @@ -## GPU worker with network volumes -# In this example, a GPU worker runs Stable Diffusion and writes outputs to the shared volume. +# GPU worker with network volume for Stable Diffusion image generation. +# Run with: flash run +# Test directly: python gpu_worker.py import logging -from runpod_flash import GpuGroup, LiveServerless, remote - -from .. import volume +from runpod_flash import GpuGroup, LiveServerless, NetworkVolume, remote logger = logging.getLogger(__name__) MODEL_PATH = "/runpod-volume/models" +volume = NetworkVolume( + name="flash-05-volume", + size=50, +) + gpu_config = LiveServerless( - name="gpu_worker", + name="05_01_gpu_worker", gpus=[GpuGroup.ANY], workersMin=0, workersMax=3, @@ -39,22 +43,22 @@ def __init__(self): self.pipe = StableDiffusionPipeline.from_pretrained( "runwayml/stable-diffusion-v1-5", torch_dtype=torch.float16, - safety_checker=None, # Disable to save memory + safety_checker=None, use_safetensors=True, requires_safety_checker=False, - low_cpu_mem_usage=True, # Additional memory optimization + low_cpu_mem_usage=True, ) - # Move to GPU and enable small memory optimizations. self.pipe = self.pipe.to("cuda") - self.pipe.enable_attention_slicing() # Additional memory saving + self.pipe.enable_attention_slicing() - # Clean up any leftover memory gc.collect() torch.cuda.empty_cache() self.logger.info("Compact Stable Diffusion initialized successfully!") - self.logger.info(f"Model weights stored in {model_path}: {os.listdir(model_path)}") + self.logger.info( + f"Model weights stored in {model_path}: {os.listdir(model_path)}" + ) async def generate_image(self, prompt: str) -> dict: """Generate a single image from prompt.""" @@ -68,22 +72,19 @@ async def generate_image(self, prompt: str) -> dict: height=512, ).images[0] - # Save output into the shared volume for the CPU worker to read. import datetime import os output_dir = "/runpod-volume/generated_images" os.makedirs(output_dir, exist_ok=True) - # Save image locally with timestamp. timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") image_filename = f"sd_generated_{timestamp}.png" image_path = os.path.join(output_dir, image_filename) image.save(image_path) self.logger.info(f"Image saved to: {image_path}") - # Create response data - response_data = { + return { "prompt": prompt, "image_path": image_path, "timestamp": timestamp, @@ -95,14 +96,14 @@ async def generate_image(self, prompt: str) -> dict: }, "message": "Image generated and saved locally!", } - return response_data -# Test locally with: python -m workers.gpu.endpoint if __name__ == "__main__": import asyncio test_payload = {"message": "Testing GPU worker"} logger.info(f"Testing GPU worker with payload: {test_payload}") sd = SimpleSD() - asyncio.run(sd.generate_image("make an image of a cute labrador retriever surfing a wave")) + asyncio.run( + sd.generate_image("make an image of a cute labrador retriever surfing a wave") + ) diff --git a/05_data_workflows/01_network_volumes/main.py b/05_data_workflows/01_network_volumes/main.py deleted file mode 100644 index a00d80b..0000000 --- a/05_data_workflows/01_network_volumes/main.py +++ /dev/null @@ -1,64 +0,0 @@ -import logging -import os -from contextlib import asynccontextmanager - -from fastapi import FastAPI -from workers.cpu import cpu_router -from workers.gpu import gpu_router -from workers.gpu.endpoint import SimpleSD - -logger = logging.getLogger(__name__) - - -@asynccontextmanager -async def lifespan(app: FastAPI): - # Load the GPU model once and share it across requests. - logger.info("Loading SimpleSD model...") - app.state.sd_model = SimpleSD() - logger.info("Model loaded successfully") - - yield - - # Clean up resources on shutdown. - logger.info("Cleaning up resources...") - if hasattr(app.state, "sd_model"): - del app.state.sd_model - logger.info("Cleanup complete") - - -app = FastAPI( - lifespan=lifespan, - title="Flash Application", - description="Distributed GPU and CPU computing with Runpod Flash", - version="0.1.0", -) - -# Wire GPU and CPU routers into the app. -app.include_router(gpu_router, prefix="/gpu", tags=["GPU Workers"]) -app.include_router(cpu_router, prefix="/cpu", tags=["CPU Workers"]) - - -@app.get("/") -def home(): - return { - "message": "Flash Application", - "docs": "/docs", - } - - -@app.get("/ping") -def ping(): - return {"status": "healthy"} - - -if __name__ == "__main__": - import uvicorn - - port = int(os.getenv("PORT", 8888)) - logger.info(f"Starting Flash server on port {port}") - logger.info( - "Try generating an image with a prompt by sending a POST request to http://localhost:8888/generate" - ) - logger.info("List generated images by querying /images") - - uvicorn.run(app, host="0.0.0.0", port=port) diff --git a/05_data_workflows/01_network_volumes/requirements.txt b/05_data_workflows/01_network_volumes/requirements.txt deleted file mode 100644 index d252e70..0000000 --- a/05_data_workflows/01_network_volumes/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -runpod-flash \ No newline at end of file diff --git a/05_data_workflows/01_network_volumes/workers/__init__.py b/05_data_workflows/01_network_volumes/workers/__init__.py deleted file mode 100644 index ff2a525..0000000 --- a/05_data_workflows/01_network_volumes/workers/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from runpod_flash import NetworkVolume - -# Shared volume used by both GPU and CPU workers. -volume = NetworkVolume( - name="flash-05-volume", - size=50, # in GB -) diff --git a/05_data_workflows/01_network_volumes/workers/cpu/__init__.py b/05_data_workflows/01_network_volumes/workers/cpu/__init__.py deleted file mode 100644 index fd2e234..0000000 --- a/05_data_workflows/01_network_volumes/workers/cpu/__init__.py +++ /dev/null @@ -1,52 +0,0 @@ -from fastapi import APIRouter -from fastapi.responses import HTMLResponse - -from .endpoint import get_image_from_volume, list_images_in_volume - -cpu_router = APIRouter() - - -@cpu_router.get("/", response_class=HTMLResponse) -async def browse_images() -> HTMLResponse: - result = await list_images_in_volume() - images = result.get("images", []) - - items = "\n".join(f'
  • {image}
  • ' for image in images) - body = items or "
  • No images found yet.
  • " - - html = f""" - - - - - Network Volume Images - - - -

    Generated Images

    - - -""" - - return HTMLResponse(content=html) - - -@cpu_router.get("/image") -async def list_images(): - # Simple index of files produced by the GPU worker. - result = await list_images_in_volume() - return result - - -@cpu_router.get("/image/{file_id}") -async def get_image(file_id: str): - # Serve a single image file from the shared volume. - result = await get_image_from_volume(file_id) - return result diff --git a/05_data_workflows/01_network_volumes/workers/cpu/endpoint.py b/05_data_workflows/01_network_volumes/workers/cpu/endpoint.py deleted file mode 100644 index 46f6064..0000000 --- a/05_data_workflows/01_network_volumes/workers/cpu/endpoint.py +++ /dev/null @@ -1,54 +0,0 @@ -from fastapi.exceptions import HTTPException -from fastapi.responses import Response -from runpod_flash import CpuLiveServerless, remote - -from .. import volume - -cpu_config = CpuLiveServerless( - name="cpu_worker", - workersMin=0, - workersMax=2, - networkVolume=volume, -) - - -@remote(resource_config=cpu_config) -async def list_images_in_volume() -> dict: - import os - - """List generated images from the shared volume.""" - image_dir = "/runpod-volume/generated_images" - images = os.listdir(image_dir) - - return { - "status": "success", - "images": images, - } - - -@remote(resource_config=cpu_config, dependencies=["fastapi"]) -async def get_image_from_volume(file_id: str) -> "Response": - from pathlib import Path - - from fastapi.responses import Response - - image_path = Path(f"/runpod-volume/generated_images/{file_id}") - - if not image_path.is_file(): - raise HTTPException(status_code=404, detail="file not found") - - image_bytes = image_path.read_bytes() - return Response( - content=image_bytes, - media_type="image/png", - headers={"Content-Disposition": f'inline; filename="{image_path.name}"'}, - ) - - -# Test locally with: python -m workers.cpu.endpoint -if __name__ == "__main__": - import asyncio - - print("checking cpu worker by listing image files in /runpod-volume/generated_images/") - result = asyncio.run(list_images_in_volume()) - print(f"Result: {result}") diff --git a/05_data_workflows/01_network_volumes/workers/gpu/__init__.py b/05_data_workflows/01_network_volumes/workers/gpu/__init__.py deleted file mode 100644 index 79a3ee5..0000000 --- a/05_data_workflows/01_network_volumes/workers/gpu/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -from fastapi import APIRouter, Request -from pydantic import BaseModel - -gpu_router = APIRouter() - - -class GenerateRequest(BaseModel): - prompt: str - - -@gpu_router.post("/generate") -async def generate(request: GenerateRequest, req: Request): - """Simple GPU worker endpoint.""" - # Use the shared model loaded in app startup. - sd_model = req.app.state.sd_model - result = await sd_model.generate_image(request.prompt) - return result diff --git a/CLAUDE.md b/CLAUDE.md index c8f25ce..a767730 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,560 +1,114 @@ # Flash Examples: AI Coding Assistant Guidelines -This document provides instructions for AI coding assistants (Claude Code, Cursor, GitHub Copilot, etc.) working on the flash-examples repository. +Instructions for AI coding assistants working on the flash-examples repository. ## Project Overview -The flash-examples repository contains production-ready examples demonstrating Flash framework capabilities. Examples are organized by category and automatically discovered by a unified FastAPI application through pattern matching. +Production-ready examples demonstrating Flash framework capabilities. Each example is a flat directory of standalone worker files auto-discovered by `flash run`. -**Key Architecture**: The root `main.py` scans category directories (`01_getting_started/`, `02_ml_inference/`, etc.) and dynamically loads all examples via APIRouter exports, creating a single unified app with auto-generated documentation. +## Architecture: Flat-File Pattern -**Important**: For full contribution guidelines, see [CONTRIBUTING.md](./CONTRIBUTING.md). +No FastAPI boilerplate, no routers, no `main.py`, no `mothership.py`. Each worker is a self-contained file with a `@remote` decorator. -## Critical: Use flash init for New Examples ⚠️ - -### The Rule -**Always use `flash init` to create new examples. Never copy-paste or duplicate existing example directories.** - -### Why -- `flash init` generates the correct, current project structure -- Copy-pasting perpetuates outdated patterns and structure issues -- Ensures consistency across all examples -- Provides clean boilerplate aligned with Flash's latest conventions - -### Workflow - -When creating a new example: - -```bash -# 1. Navigate to the appropriate category -cd 03_advanced_workers - -# 2. Use flash init to create clean project structure -flash init my_new_example -cd my_new_example - -# 3. Review existing examples in this category to understand patterns -# (don't copy code - understand the patterns) - -# 4. Implement your example with fresh code - -# 5. Run make consolidate-deps to update root dependencies -cd ../.. -make consolidate-deps +``` +example_name/ +├── README.md +├── gpu_worker.py # @remote decorated functions +├── cpu_worker.py # Optional CPU worker +└── pipeline.py # Optional cross-worker orchestration ``` -### Study Without Copying - -- Review existing examples to understand patterns and best practices -- Study how `@remote` decorator is configured -- Examine APIRouter setup and error handling -- Look at input validation patterns -- **But**: Generate your own implementation, don't duplicate code - -## Flash-Specific Patterns - -### Remote Worker Decorator +### Worker File Pattern ```python -from runpod_flash import remote, LiveServerless, GpuGroup +from runpod_flash import GpuGroup, LiveServerless, remote -# Configure resource requirements -# Naming convention: {category}_{example}_{worker_type} gpu_config = LiveServerless( - name="01_01_getting_started_gpu", + name="01_01_gpu_worker", gpus=[GpuGroup.ANY], workersMin=0, workersMax=3, - idleTimeout=300, + idleTimeout=5, ) @remote(resource_config=gpu_config) -async def process_image(input_data: dict) -> dict: - """Process an image with GPU support.""" - image_url = input_data.get("image_url") +async def my_function(input_data: dict) -> dict: + """All imports inside the function body.""" + import torch # implementation - return {"status": "success", "result": "..."} + return {"status": "success"} ``` -Key points: -- Imports are from `runpod_flash`, not `flash` -- Create a config object (`LiveServerless` or `CpuLiveServerless`) for GPU/CPU workers -- Pass config via `resource_config` parameter to `@remote` decorator -- Use `async/await` for all worker functions -- Return serializable data (dict, list, str, etc.) -- Include comprehensive docstrings - -### APIRouter Export Pattern - -**IMPORTANT**: Routers must follow the naming pattern `{worker_type}_router` (e.g., `gpu_router`, `cpu_router`). The unified app discovery system explicitly looks for this pattern. - -**Single-file workers** (e.g., `gpu_worker.py`): -```python -from fastapi import APIRouter -from pydantic import BaseModel +Key rules: +- Imports from `runpod_flash`, not `flash` +- Config object at module level (`LiveServerless`, `CpuLiveLoadBalancer`, `CpuLiveServerless`) +- All runtime imports inside the `@remote` function body +- Return serializable data (dict, list, str) +- Worker naming convention: `{category}_{example}_{worker_type}` (e.g., `01_01_gpu_worker`) -class MyRequest(BaseModel): - input_data: str +### Resource Types -class MyResponse(BaseModel): - status: str - result: str +| Type | Import | Use Case | +|------|--------|----------| +| `LiveServerless` | `from runpod_flash import LiveServerless, GpuGroup` | GPU workers | +| `CpuLiveLoadBalancer` | `from runpod_flash import CpuLiveLoadBalancer` | CPU workers, pipelines | +| `CpuLiveServerless` | `from runpod_flash import CpuLiveServerless` | CPU serverless | -# Router must be named gpu_router (for gpu_worker.py) -gpu_router = APIRouter() +### Cross-Worker Orchestration -@gpu_router.post("/endpoint") -async def my_endpoint(request: MyRequest) -> MyResponse: - """Handle endpoint request.""" - # implementation - return MyResponse(status="success", result="...") -``` +Pipeline files import functions from other workers and chain them: -**Directory-based workers** (e.g., `workers/gpu/__init__.py`): ```python -from fastapi import APIRouter -from .endpoint import my_endpoint -from pydantic import BaseModel - -class MyRequest(BaseModel): - input_data: str - -# Router must be named gpu_router (for workers/gpu/ directory) -gpu_router = APIRouter() - -@gpu_router.post("/endpoint") -async def handle_endpoint(request: MyRequest): - """Handle endpoint request.""" - return await my_endpoint(request) -``` - -The unified app auto-discovery looks for: -- **Single-file**: `{worker_type}_router` in `{worker_type}_worker.py` (e.g., `gpu_router` in `gpu_worker.py`) -- **Directory**: `{worker_type}_router` in `workers/{worker_type}/__init__.py` (e.g., `gpu_router` in `workers/gpu/__init__.py`) - -### Mothership Endpoint Configuration - -Every example includes a `mothership.py` file that configures the mothership endpoint - the load-balanced endpoint serving your FastAPI application routes. +from cpu_worker import preprocess_text +from gpu_worker import gpu_inference +from runpod_flash import CpuLiveLoadBalancer, remote -```python -from runpod_flash import CpuLiveLoadBalancer +pipeline_config = CpuLiveLoadBalancer(name="pipeline", workersMin=1) -# Mothership endpoint configuration -mothership = CpuLiveLoadBalancer( - name="01_01_hello_world-mothership", - workersMin=1, - workersMax=3, -) +@remote(resource_config=pipeline_config, method="POST", path="/classify") +async def classify(text: str) -> dict: + result = await preprocess_text({"text": text}) + return await gpu_inference(result) ``` -**To customize:** -- Change `workersMin` and `workersMax` for scaling behavior -- Use `LiveLoadBalancer` instead of `CpuLiveLoadBalancer` for GPU -- Update the `name` parameter with appropriate endpoint name - -**To disable mothership deployment:** -- Delete the `mothership.py` file, or -- Comment out the `mothership` variable - -See the template `mothership.py` in your example for examples of customization. +## Creating New Examples -### Input Validation with Pydantic +Always use `flash init`: -Always use Pydantic for request validation: - -```python -from pydantic import BaseModel, Field, EmailStr - -class ProcessRequest(BaseModel): - image_url: str = Field(..., description="URL of image to process") - quality: int = Field(default=90, ge=1, le=100) - email: EmailStr - -class ProcessResponse(BaseModel): - status: str - result: dict - processing_time: float -``` - -Benefits: -- Automatic request validation -- Type safety -- OpenAPI documentation auto-generation -- Clear error messages - -### Error Handling - -```python -from fastapi import HTTPException, status - -@router.post("/process") -async def process(request: ProcessRequest) -> ProcessResponse: - try: - result = await execute_processing(request) - return ProcessResponse(status="success", result=result) - except ValueError as e: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Invalid input: {str(e)}" - ) - except Exception as e: - logger.error(f"Processing failed", error=str(e)) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Processing failed. Check server logs." - ) -``` - -Guidelines: -- Catch specific exceptions, not bare `except` -- Provide actionable error messages -- Use appropriate HTTP status codes -- Log errors with context -- Never expose internal implementation details - -## Dependency Management - -### Adding Dependencies - -1. Add to your example's `pyproject.toml`: -```toml -[project] -dependencies = [ - "torch>=2.0.0", - "pillow>=10.0.0", -] -``` - -2. Pin specific versions for reproducibility -3. Document why each dependency is needed in your example's README - -### Consolidating Dependencies - -**Critical**: After adding dependencies, run: ```bash -cd /path/to/flash-examples -make consolidate-deps -``` - -This: -- Extracts all example dependencies to the root `pyproject.toml` -- Updates `uv.lock` with pinned versions -- Ensures all examples can be run in the unified app environment - -**Never** directly edit the root `pyproject.toml` - it's auto-generated by `make consolidate-deps`. - -## Code Standards - -### Type Hints (Mandatory) - -Every function must have type hints: - -```python -# ✅ GOOD -def calculate_features(image: np.ndarray, threshold: float) -> dict[str, float]: - """Extract image features.""" - pass - -# ❌ BAD -def calculate_features(image, threshold): - """Extract image features.""" - pass -``` - -### Early Returns / Guard Clauses - -```python -# ✅ GOOD - Handle edge cases first -async def process_image(url: str) -> dict: - if not url: - raise ValueError("URL is required") - - if not url.startswith(("http://", "https://")): - raise ValueError("Invalid URL scheme") - - # Main logic follows - image = await download_image(url) - return extract_features(image) - -# ❌ BAD - Nested conditions -async def process_image(url: str) -> dict: - if url: - if url.startswith(("http://", "https://")): - image = await download_image(url) - return extract_features(image) - else: - raise ValueError("Invalid URL scheme") - else: - raise ValueError("URL is required") -``` - -### Logging (Not Print Statements) - -```python -import logging - -logger = logging.getLogger(__name__) - -# ✅ GOOD -logger.info("Processing started", extra={"image_url": url, "user_id": user_id}) -logger.error("Download failed", exc_info=True, extra={"url": url}) - -# ❌ BAD -print(f"Processing {url}") -print("ERROR: Download failed") -``` - -### No Hardcoded Credentials - -```python -# ✅ GOOD - Use environment variables -import os -api_key = os.getenv("API_KEY") - -# ❌ BAD - Hardcoded -api_key = "sk-1234567890abcdef" +cd 01_getting_started +flash init my_new_example ``` -## Testing Requirements +Never copy-paste existing examples. -### Must Pass `flash run` +## Development Workflow -Your example must run successfully in the unified app: ```bash -flash run -# Visit http://localhost:8888/docs to test all endpoints -``` - -### Test in Unified Context - -When testing locally, ensure your routes work with the unified app's auto-discovery: -- Your routes are prefixed with your example directory name -- Example: `/03_advanced_workers/my_example/endpoint` -- Test via the Swagger UI at `/docs` - -### Async Testing with pytest-asyncio - -```python -import pytest - -@pytest.mark.asyncio -async def test_process_image(): - # Arrange - url = "https://example.com/image.jpg" - - # Act - result = await process_image(url) - - # Assert - assert result["status"] == "success" - assert "features" in result +flash run # Start local dev server (localhost:8888) +# Visit http://localhost:8888/docs for interactive API docs +python gpu_worker.py # Test a single worker directly ``` -### VS Code Debugging - -Examples include `.vscode/launch.json` for debugging endpoints directly in VS Code. - -## Documentation Standards - -### README.md Structure - -Create a comprehensive `README.md` with these sections: - -```markdown -# Example Name - -Brief description of what this example demonstrates. - -## Overview - -Longer explanation of the use case and key learnings. - -## What You'll Learn - -- Key concept 1 -- Key concept 2 -- Key concept 3 - -## Architecture - -Diagram or description of how this example works. - -## Quick Start - -### Prerequisites -- Python 3.10+ -- Flash SDK - -### Installation +## Quality Gates ```bash -cd path/to/this/example -flash run -``` - -### Test the Endpoint - -```bash -curl -X POST http://localhost:8888/endpoint \ - -H "Content-Type: application/json" \ - -d '{"input": "value"}' -``` - -## API Endpoints - -### POST /endpoint - -**Description**: What this endpoint does. - -**Request**: -```json -{ - "input": "string" -} -``` - -**Response**: -```json -{ - "status": "success", - "result": "..." -} +make quality-check # Required before every commit +make lint # Ruff linter +make format-check # Ruff format check ``` -**Error Handling**: -- 400 Bad Request: When input validation fails -- 500 Internal Server Error: When processing fails - -## Deployment - -Instructions for deploying to Runpod or other platforms. - -## Cost Estimates - -Approximate monthly costs for running this example in production. - -## Common Issues - -Troubleshooting section with solutions. - -## References - -- Flash documentation -- External libraries documentation - - -### Code Comments - -- Comment complex logic only -- Prefer clear code over clever code -- Document non-obvious design decisions -- Docstrings for all functions and classes - -## Project Structure - -Your example directory should follow one of these patterns: - -### Pattern 1: Single-file Workers - -Use for simple examples with a single worker type and minimal logic. - -``` -example_name/ -├── README.md -├── main.py # FastAPI app for local development -├── mothership.py # Mothership endpoint configuration -├── gpu_worker.py # Exports router (GPU worker) -├── cpu_worker.py # Exports router (CPU worker) [optional] -├── pyproject.toml # Dependencies and metadata -├── requirements.txt # Pin dependencies for reproducibility -├── .env.example # Environment variables template -├── .gitignore # Git ignore patterns -├── .flashignore # Flash build ignore patterns -└── .python-version # Python version (optional) -``` - -**Example**: `01_getting_started/01_hello_world/`, `01_getting_started/02_cpu_worker/` - -### Pattern 2: Directory-based Workers - -Use for complex examples with multiple worker types, shared utilities, or extensive logic. - -``` -example_name/ -├── README.md -├── main.py # FastAPI app for local development -├── mothership.py # Mothership endpoint configuration -├── pyproject.toml # Dependencies and metadata -├── requirements.txt # Pin dependencies for reproducibility -├── .env.example # Environment variables template -├── .gitignore # Git ignore patterns -├── .flashignore # Flash build ignore patterns -├── .python-version # Python version (optional) -└── workers/ - ├── __init__.py - ├── gpu/ - │ ├── __init__.py # Exports gpu_router - │ └── endpoint.py # @remote functions - └── cpu/ - ├── __init__.py # Exports cpu_router - └── endpoint.py # @remote functions -``` - -**Example**: `01_getting_started/03_mixed_workers/`, `03_advanced_workers/05_load_balancer/` - -### Files That Should NOT Be Committed - -These files are auto-generated and should be ignored: - -- `flash_manifest.json` - Generated by flash build -- `uv.lock` - Generated by uv sync (included in root uv.lock) - -These are already added to `.gitignore` patterns. - -## Pre-Commit Checklist - -Before submitting an example: - -- [ ] Used `flash init` to create the structure -- [ ] `mothership.py` is present (or intentionally deleted if not deploying mothership) -- [ ] `.gitignore` includes Flash auto-generated files (flash_manifest.json, uv.lock) -- [ ] All functions have type hints -- [ ] All endpoints have input validation (Pydantic models) -- [ ] Error handling is implemented -- [ ] No hardcoded credentials or secrets -- [ ] Using logging, not print statements -- [ ] **@remote functions only access local scope** - - All imports are inside the function - - All helper functions defined inside the function - - No references to module-level variables or functions -- [ ] `flash run` executes without errors -- [ ] All endpoints tested via `/docs` Swagger UI -- [ ] README.md is comprehensive and accurate -- [ ] Dependencies added to pyproject.toml -- [ ] `make consolidate-deps` has been run -- [ ] No auto-generated files are tracked by git (flash_manifest.json, uv.lock) -- [ ] Code follows the standards in this guide - -## Common Mistakes to Avoid +## Code Standards -1. **Accessing external scope in @remote functions (CRITICAL)**: Only local variables, parameters, and internal imports work - - ❌ Don't call functions or access variables defined outside the function - - ❌ Don't use module-level imports - - ✅ Import and define everything inside the function -2. **Copying existing examples**: Always use `flash init` -3. **Missing type hints**: Every parameter and return value -4. **Hardcoded values**: Use environment variables -5. **No error handling**: All external operations need try/except -6. **Print instead of logging**: Use the logging module -7. **Bare except clauses**: Always catch specific exceptions -8. **No input validation**: Use Pydantic models -9. **Not running consolidate-deps**: Critical for shared environment -10. **Routes not exporting router**: Unified app won't discover them -11. **Mutable default arguments**: Use None and initialize in function +- Type hints on all function signatures +- Early returns / guard clauses over nested conditions +- No hardcoded credentials (use `os.getenv()`) +- No `print()` in production code (logging module or skip for examples) +- Catch specific exceptions, never bare `except:` -## Getting Help +## Common Mistakes -1. Check existing examples in the same category -2. Review CONTRIBUTING.md for additional guidelines -3. See Flash documentation at https://docs.runpod.io -4. Check repository issues and discussions +1. **Accessing external scope in @remote functions** -- only local variables, parameters, and internal imports work +2. **Module-level imports of heavy libraries** -- import torch, numpy, etc. inside the function body +3. **Missing `if __name__ == "__main__"` test block** -- each worker should be independently testable +4. **Mutable default arguments** -- use `None` and initialize in function body diff --git a/CLI-REFERENCE.md b/CLI-REFERENCE.md index 6116cd9..9990e0b 100644 --- a/CLI-REFERENCE.md +++ b/CLI-REFERENCE.md @@ -101,8 +101,6 @@ flash init my-api --force ### What It Creates -- `main.py` - FastAPI application entry point -- `mothership.py` - Mothership endpoint configuration - `gpu_worker.py` - GPU worker template with `@remote` decorator - `pyproject.toml` - Project dependencies - `.env.example` - Environment variable template @@ -177,8 +175,8 @@ flash run ### What It Does -1. Loads `main.py` FastAPI application -2. Discovers all `@remote` decorated functions +1. Discovers all `.py` files (excluding `.venv/`, `.flash/`, `.runpod/`, `__init__.py`) and finds `@remote` decorated functions via AST parsing +2. Generates a FastAPI application with routes for each discovered function 3. Starts uvicorn development server with hot reload 4. Provides interactive API documentation at `/docs` 5. Optionally provisions remote resources if `--auto-provision` is enabled diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 60612b3..b75869c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,9 +44,9 @@ All examples must meet these standards: - [ ] All endpoints return correct responses - [ ] Error handling is implemented - [ ] Environment variables are documented -- [ ] Dependencies are pinned in pyproject.toml -- [ ] Dependencies consolidated to root with `make consolidate-deps` -- [ ] Example loads in unified app (`flash run` from root) +- [ ] Dependencies are declared in pyproject.toml +- [ ] Runtime deps declared in `@remote(dependencies=[...])` +- [ ] Example discovered by `flash run` from project root ### 2. Code Quality - [ ] Clear, readable code @@ -130,26 +130,17 @@ flash run # Test all endpoints ``` -### 5. Consolidate Dependencies to Root +### 5. Verify Discovery -**Important**: The unified app loads all examples dynamically, so all example dependencies must be available in the root environment. - -After adding your example with its dependencies, consolidate them to the root: +`flash run` auto-discovers all `.py` files containing `@remote` functions. Verify your example loads: ```bash # From the repository root -make consolidate-deps # Consolidates your example's deps to root pyproject.toml -uv sync # Installs the new dependencies +flash run +# Check http://localhost:8888/docs for your new endpoints ``` -The consolidation script: -- Automatically scans your example's `pyproject.toml` -- Merges all dependencies into the root configuration -- Lets pip/uv handle version resolution - -If pip/uv reports version conflicts, resolve them manually in the root `pyproject.toml`. - -**Verification**: Run `flash run` from the repository root to ensure your example loads without import errors. +Runtime dependencies (torch, transformers, etc.) are declared in `@remote(dependencies=[...])` and installed on the remote worker, not locally. ### 6. Create Pull Request @@ -170,69 +161,28 @@ Create a PR with: ### Standard Structure +Each example is a flat directory with self-contained worker files (named `*_worker.py` by convention): + ``` your_example/ ├── README.md # Required: comprehensive documentation -├── main.py # Required: FastAPI application -├── workers/ # Required: remote worker functions -│ ├── gpu/ -│ │ ├── __init__.py -│ │ └── endpoint.py -│ └── cpu/ -│ ├── __init__.py -│ └── endpoint.py -├── requirements.txt # Required: pinned dependencies -├── pyproject.toml # Required: project metadata -├── .env.example # Required: environment template -├── .gitignore # Required: ignore .env, .venv, etc. -├── .flashignore # Optional: files to exclude from deployment -├── tests/ # Recommended: test files -│ ├── test_endpoints.py +├── gpu_worker.py # Required: GPU worker with @remote decorator +├── cpu_worker.py # Optional: CPU worker with @remote decorator +├── pyproject.toml # Required: project metadata and dependencies +├── .flashignore # Optional: files to exclude from deployment +├── tests/ # Recommended: test files +│ ├── test_workers.py │ └── conftest.py -└── assets/ # Optional: images, diagrams, etc. +└── assets/ # Optional: images, diagrams, etc. └── architecture.png ``` -### Minimal main.py - -```python -from fastapi import FastAPI -from workers.gpu import gpu_router -from workers.cpu import cpu_router - -app = FastAPI( - title="Your Example", - description="Brief description", - version="0.1.0", -) - -app.include_router(gpu_router, prefix="/gpu", tags=["GPU Workers"]) -app.include_router(cpu_router, prefix="/cpu", tags=["CPU Workers"]) - -@app.get("/") -def home(): - return { - "message": "Your Example API", - "docs": "/docs", - } - -@app.get("/health") -def health(): - return {"status": "healthy"} - -if __name__ == "__main__": - import uvicorn - import os - - host = os.getenv("FLASH_HOST", "localhost") - port = int(os.getenv("FLASH_PORT", 8888)) - uvicorn.run(app, host=host, port=port) -``` +`flash run` discovers all `.py` files with `@remote` functions automatically -- no `main.py`, no `workers/` directories, no router wiring. -### Minimal Worker (endpoint.py) +### Minimal Worker (`gpu_worker.py`) ```python -from runpod_flash import remote, LiveServerless, GpuGroup +from runpod_flash import remote, LiveServerless config = LiveServerless(name="your_worker") @@ -377,8 +327,8 @@ Two debug configurations are available: - **Python Debugger: Current File** - Debug any Python file - **Flash Worker: Debug Endpoint** - Debug worker endpoint files with async support -To debug an endpoint: -1. Open any `endpoint.py` file (e.g., `01_getting_started/01_hello_world/workers/gpu/endpoint.py`) +To debug a worker: +1. Open any `*_worker.py` file (e.g., `01_getting_started/01_hello_world/hello_worker.py`) 2. Set breakpoints in your worker functions 3. Press F5 or select "Debug: Start Debugging" 4. Choose the appropriate debug configuration @@ -391,9 +341,9 @@ The `.env` file is automatically loaded, so your `RUNPOD_API_KEY` is available d Add tests for your worker functions: ```python -# tests/test_endpoints.py +# tests/test_workers.py import pytest -from workers.gpu.endpoint import your_function +from your_example.gpu_worker import your_function @pytest.mark.asyncio async def test_your_function(): @@ -457,67 +407,51 @@ RUNPOD_API_KEY = "hardcoded_key" # Never do this! ### Error Handling ```python -# Good -from fastapi import HTTPException +# Good - handle errors within @remote functions +from runpod_flash import remote, LiveServerless + +config = LiveServerless(name="processor") -@router.post("/process") -async def process(data: dict): +@remote(resource_config=config) +async def process(data: dict) -> dict: try: - result = await worker_function(data) - return result + result = do_work(data) + return {"status": "success", "result": result} except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) - except Exception as e: - logger.error(f"Unexpected error: {e}") - raise HTTPException(status_code=500, detail="Internal server error") + return {"status": "error", "detail": str(e)} # Bad -@router.post("/process") -async def process(data: dict): - result = await worker_function(data) # No error handling +@remote(resource_config=config) +async def process(data: dict) -> dict: + result = do_work(data) # No error handling return result ``` ## Dependencies -### Requirements.txt - -Pin all dependencies: - -```txt -runpod_flash==1.2.3 -fastapi==0.104.1 -uvicorn[standard]==0.24.0 -torch==2.1.0 -transformers==4.35.0 -``` - -Generate with: -```bash -pip freeze > requirements.txt -``` - ### pyproject.toml -Include project metadata: +Each example declares only its local development dependencies in `pyproject.toml`. Runtime deps needed on the GPU (torch, transformers, etc.) go in `@remote(dependencies=[...])`: ```toml [project] name = "your-example" version = "0.1.0" description = "Brief description" -requires-python = ">=3.12" +requires-python = ">=3.10" dependencies = [ "runpod-flash", ] ``` +The root `requirements.txt` is a generated lockfile -- do not add per-example `requirements.txt` files. + ## Security Best Practices 1. **Never commit secrets** - Use `.env` files (gitignored) - - Provide `.env.example` templates - - Document all required environment variables + - The root `.env.example` documents required variables + - Do not add per-example `.env.example` files 2. **Validate inputs** - Use Pydantic models diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 934aecd..4c14d40 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -451,33 +451,32 @@ Then review and test changes before committing. ### Creating New Examples -**1. Create directory structure:** +**1. Create directory and worker files:** ```bash -mkdir -p 01_getting_started/05_new_example/workers/{gpu,cpu} +mkdir 01_getting_started/05_new_example cd 01_getting_started/05_new_example ``` **2. Create files:** ```bash -touch README.md main.py .env.example -touch workers/gpu/__init__.py workers/gpu/endpoint.py -touch workers/cpu/__init__.py workers/cpu/endpoint.py +touch README.md gpu_worker.py pyproject.toml ``` -**3. Generate dependencies:** +Each worker file (named `*_worker.py` by convention) is self-contained with `@remote` decorated functions. `flash run` discovers all `.py` files with `@remote` functions automatically -- no `main.py`, no `workers/` directories needed. + +**3. Declare dependencies:** + +Add a `pyproject.toml` with `runpod-flash` as the only local dependency. Runtime deps (torch, etc.) go in `@remote(dependencies=[...])`. + +**4. Verify discovery:** ```bash -# Create pyproject.toml with dependencies -# Then generate requirements.txt cd ../../ # Back to root -make requirements.txt -cp requirements.txt 01_getting_started/05_new_example/ +flash run # Discovers all .py files with @remote functions ``` -**4. Follow example structure** (see README.md) - ### Cleaning Up **Remove build artifacts:** diff --git a/Makefile b/Makefile index 57cc80d..a702ca6 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help venv-info setup dev verify-setup consolidate-deps check-deps sync-deps update-deps clean clean-venv lint lint-fix format format-check typecheck quality-check quality-check-strict ci-quality-github +.PHONY: help venv-info setup dev verify-setup clean clean-venv lint lint-fix format format-check quality-check ci-quality-github # ============================================================================ # Environment Manager Auto-Detection @@ -234,61 +234,6 @@ endif dev: setup # Alias for 'make setup' (backward compatibility) -# ============================================================================ -# Dependency File Generation -# ============================================================================ - -requirements.txt: # Generate requirements.txt from pyproject.toml - @echo "Generating requirements.txt from pyproject.toml..." -ifeq ($(PKG_MANAGER),uv) - uv pip compile pyproject.toml -o requirements.txt -else ifdef HAS_UV - uv pip compile pyproject.toml -o requirements.txt -else - @echo "runpod-flash" > requirements.txt - @echo "✓ Basic requirements.txt created (install 'uv' for dependency resolution)" -endif - -environment.yml: # Generate conda environment.yml from pyproject.toml - @echo "Generating environment.yml for conda..." - @echo "name: flash-examples" > environment.yml - @echo "channels:" >> environment.yml - @echo " - conda-forge" >> environment.yml - @echo " - defaults" >> environment.yml - @echo "dependencies:" >> environment.yml - @echo " - python>=3.11" >> environment.yml - @echo " - pip" >> environment.yml - @echo " - pip:" >> environment.yml - @echo " - runpod-flash" >> environment.yml - @echo "✓ environment.yml created" - -consolidate-deps: # Consolidate example dependencies to root pyproject.toml - @echo "Consolidating example dependencies..." - $(PYTHON) scripts/sync_example_deps.py - -check-deps: # Verify example dependencies are consolidated (CI mode) - @echo "Verifying dependencies are consolidated..." - $(PYTHON) scripts/sync_example_deps.py --check - -sync-deps: requirements.txt environment.yml # Generate all dependency files - @echo "✓ All dependency files synced" - -update-deps: # Update dependencies to latest versions - @echo "Updating dependencies with $(PKG_MANAGER)..." -ifeq ($(PKG_MANAGER),uv) - uv sync --upgrade - uv pip compile --upgrade pyproject.toml -o requirements.txt -else ifeq ($(PKG_MANAGER),poetry) - poetry update -else ifeq ($(PKG_MANAGER),pipenv) - pipenv update -else ifeq ($(PKG_MANAGER),conda) - conda run -p ./.venv pip install --upgrade runpod-flash -else ifeq ($(PKG_MANAGER),pip) - .venv/bin/pip install --upgrade runpod-flash -endif - @echo "✓ Dependencies updated" - # ============================================================================ # Cleanup # ============================================================================ @@ -309,7 +254,7 @@ clean-venv: # Remove virtual environment directory fi # ============================================================================ -# Code Quality - Linting +# Code Quality # ============================================================================ lint: # Check code with ruff @@ -320,10 +265,6 @@ lint-fix: # Fix code issues with ruff @echo "Fixing code issues with ruff..." $(PYTHON_RUN) ruff check . --fix -# ============================================================================ -# Code Quality - Formatting -# ============================================================================ - format: # Format code with ruff @echo "Formatting code with ruff..." $(PYTHON_RUN) ruff format . @@ -332,28 +273,9 @@ format-check: # Check code formatting @echo "Checking code formatting..." $(PYTHON_RUN) ruff format --check . -# ============================================================================ -# Code Quality - Type Checking -# ============================================================================ - -typecheck: # Check types with mypy - @echo "Running mypy type checker..." - @$(PYTHON_RUN) mypy . || { [ $$? -eq 2 ] && echo "No Python files found for type checking"; } - -# ============================================================================ -# Quality Gates (used in CI) -# ============================================================================ - quality-check: format-check lint # Essential quality gate for CI @echo "✓ Quality checks passed" -quality-check-strict: format-check lint typecheck check-deps # Strict quality gate with type checking - @echo "✓ Strict quality checks passed" - -# ============================================================================ -# GitHub Actions Specific -# ============================================================================ - ci-quality-github: # Quality checks with GitHub Actions formatting @echo "::group::Code formatting check" $(PYTHON_RUN) ruff format --check . diff --git a/main.py b/main.py deleted file mode 100644 index 50d5fc3..0000000 --- a/main.py +++ /dev/null @@ -1,761 +0,0 @@ -""" -Unified Flash Examples Application - -This file automatically discovers and consolidates all Flash examples into one FastAPI application. -Run `flash run` from the root directory to access all examples from a single server. - -The discovery system automatically finds: -1. Single-file worker patterns (e.g., gpu_worker.py, cpu_worker.py) -2. Directory-based worker patterns (e.g., workers/gpu/__init__.py) -""" - -import importlib.util -import logging -import os -import sys -from pathlib import Path -from typing import Any -from urllib.parse import quote - -from fastapi import APIRouter, FastAPI -from fastapi.responses import HTMLResponse - -logger = logging.getLogger(__name__) - -BASE_DIR = Path(__file__).parent -EXAMPLES_DIRS = [ - BASE_DIR / "01_getting_started", - BASE_DIR / "02_ml_inference", - BASE_DIR / "03_advanced_workers", - BASE_DIR / "04_scaling_performance", - BASE_DIR / "05_data_workflows", - BASE_DIR / "06_real_world", -] - -# Route discovery constants -SKIP_HEALTH_CHECK_PATHS = ("/", "/health") -SKIP_OPENAPI_PATHS = ("/openapi.json", "/docs", "/docs/oauth2-redirect", "/redoc") -SKIP_WORKER_TYPES = ("gpu", "cpu", "workers") - -app = FastAPI( - title="Runpod Flash Examples - Unified Demo", - description="All Flash examples automatically discovered and unified in one FastAPI application", - version="1.0.0", -) - - -def load_module_from_path(module_name: str, file_path: Path) -> Any: - """Dynamically load a Python module from a file path.""" - spec = importlib.util.spec_from_file_location(module_name, file_path) - if spec is None or spec.loader is None: - return None - - module = importlib.util.module_from_spec(spec) - sys.modules[module_name] = module - spec.loader.exec_module(module) - return module - - -def discover_single_file_routers( - example_path: Path, example_name: str, example_tag: str = "" -) -> list[dict[str, Any]]: - """ - Discover routers from single-file worker patterns. - - Looks for files like gpu_worker.py, cpu_worker.py that export APIRouters. - """ - routers: list[dict[str, Any]] = [] - worker_files = list(example_path.glob("*_worker.py")) - - for worker_file in worker_files: - worker_type = worker_file.stem.replace("_worker", "") # e.g., 'gpu' or 'cpu' - module_name = f"{example_name}_{worker_type}_worker" - - try: - module = load_module_from_path(module_name, worker_file) - if module is None: - continue - - # Look for router (common naming: gpu_router, cpu_router, etc.) - router_name = f"{worker_type}_router" - if hasattr(module, router_name): - router = getattr(module, router_name) - if isinstance(router, APIRouter): - # Extract route information for deep linking - routes = [] - for route in router.routes: - if ( - hasattr(route, "endpoint") - and hasattr(route, "path") - and hasattr(route, "name") - ): - routes.append( - { - "path": str(route.path), - "name": str(route.name), - } - ) - - routers.append( - { - "router": router, - "prefix": f"/{example_name}/{worker_type}", - "tags": [example_tag], - "worker_type": worker_type, - "routes": routes, - } - ) - logger.info(f"Loaded {example_name}/{worker_type} from {worker_file.name}") - except Exception as e: - logger.warning(f"Could not load {worker_file}: {e}") - - return routers - - -def discover_directory_routers( - example_path: Path, example_name: str, example_tag: str = "" -) -> list[dict[str, Any]]: - """ - Discover routers from directory-based worker patterns. - - Looks for workers/gpu/__init__.py, workers/cpu/__init__.py that export APIRouters. - """ - routers: list[dict[str, Any]] = [] - workers_dir = example_path / "workers" - - if not workers_dir.exists() or not workers_dir.is_dir(): - return routers - - # Add workers directory to path for imports - workers_dir_str = str(workers_dir.parent) - if workers_dir_str not in sys.path: - sys.path.insert(0, workers_dir_str) - - # Look for worker type directories (gpu, cpu, etc.) - for worker_dir in workers_dir.iterdir(): - if not worker_dir.is_dir() or worker_dir.name.startswith("_"): - continue - - init_file = worker_dir / "__init__.py" - if not init_file.exists(): - continue - - worker_type = worker_dir.name - module_name = f"{example_name}_workers_{worker_type}" - - try: - module = load_module_from_path(module_name, init_file) - if module is None: - continue - - # Look for router (common naming: gpu_router, cpu_router, etc.) - router_name = f"{worker_type}_router" - if hasattr(module, router_name): - router = getattr(module, router_name) - if isinstance(router, APIRouter): - # Extract route information for deep linking - routes = [] - for route in router.routes: - if ( - hasattr(route, "endpoint") - and hasattr(route, "path") - and hasattr(route, "name") - ): - routes.append( - { - "path": str(route.path), - "name": str(route.name), - } - ) - - routers.append( - { - "router": router, - "prefix": f"/{example_name}/{worker_type}", - "tags": [example_tag], - "worker_type": worker_type, - "routes": routes, - } - ) - logger.info(f"Loaded {example_name}/{worker_type} from workers/{worker_type}") - except Exception as e: - logger.warning(f"Could not load {worker_dir}: {e}") - - return routers - - -def discover_main_app_routes( - example_path: Path, example_name: str, example_tag: str = "" -) -> list[dict[str, Any]]: - """ - Discover context routes from main.py FastAPI app. - - Extracts only direct routes from the app's router (not included routers). - Skips health check, info endpoints, and auto-generated OpenAPI routes. - Returns routes as a single router grouped with the example (no separate prefix). - """ - routers: list[dict[str, Any]] = [] - main_file = example_path / "main.py" - - if not main_file.exists(): - return routers - - module_name = f"{example_name}_main_context" - - try: - module = load_module_from_path(module_name, main_file) - if module is None: - return routers - - # Look for FastAPI app instance - if not hasattr(module, "app"): - return routers - - main_app = module.app - if not hasattr(main_app, "routes"): - return routers - - from fastapi.routing import Mount - - # We want to extract routes that were directly added to the app (via @app.post, etc.) - # not routes from included routers. We'll check the app's router directly. - routes_list = [] - context_router = APIRouter() - - # Look at direct routes from the main app's router - if hasattr(main_app, "router") and hasattr(main_app.router, "routes"): - for route in main_app.router.routes: - # Skip Mount routes (these are included routers like /gpu, /cpu) - if isinstance(route, Mount): - continue - - # Skip health check and info endpoints - if hasattr(route, "path") and route.path in SKIP_HEALTH_CHECK_PATHS: - continue - - # Skip auto-generated OpenAPI documentation routes - if hasattr(route, "path") and route.path in SKIP_OPENAPI_PATHS: - continue - - # Skip routes from included routers (routes with /gpu/, /cpu/, etc.) - if hasattr(route, "path"): - path = str(route.path) - # Skip routes that start with common worker types or look like they're from included routers - if any(f"/{wt}/" in path for wt in SKIP_WORKER_TYPES): - continue - - # Include everything else - if hasattr(route, "endpoint") and hasattr(route, "path") and hasattr(route, "name"): - routes_list.append( - { - "path": str(route.path), - "name": str(route.name), - } - ) - context_router.routes.append(route) - - if context_router.routes: - routers.append( - { - "router": context_router, - "prefix": f"/{example_name}", - "tags": [example_tag], - "worker_type": "api", - "routes": routes_list, - } - ) - logger.info( - f"Loaded {example_name} context routes from main.py ({len(routes_list)} routes)" - ) - - except Exception as e: - logger.debug(f"Could not load context routes from {main_file}: {e}") - - return routers - - -def discover_example_routers( - example_path: Path, category_name: str = "", example_name: str = "" -) -> list[dict[str, Any]]: - """ - Discover all routers from an example directory. - - Tries single-file, directory-based, and main.py app patterns. - Groups all routes under a single example tag: "Category > Example" - Includes context routes from main.py (like pipelines) grouped with the example. - """ - if not example_name: - example_name = example_path.name - - # Build example tag: "01 Getting Started > 01 Hello World" - example_tag = ( - f"{category_name} > {example_name.replace('_', ' ').title()}" - if category_name - else example_name.replace("_", " ").title() - ) - - routers: list[dict[str, Any]] = [] - - # Try single-file pattern - routers.extend(discover_single_file_routers(example_path, example_name, example_tag)) - - # Try directory-based pattern - routers.extend(discover_directory_routers(example_path, example_name, example_tag)) - - # Extract context routes (main.py app routes like pipelines, classifiers) - # These are grouped with the example, not given special tagging - routers.extend(discover_main_app_routes(example_path, example_name, example_tag)) - - return routers - - -def register_all_examples() -> dict[str, dict[str, Any]]: - """ - Discover and register all examples. - - Returns a dictionary of category -> examples metadata for the home endpoint. - """ - examples_by_category: dict[str, dict[str, Any]] = {} - - for examples_dir in EXAMPLES_DIRS: - if not examples_dir.exists() or not examples_dir.is_dir(): - continue - - category_name = examples_dir.name # e.g., "01_getting_started" - category_display = category_name.replace("_", " ").title() # "01 Getting Started" - - if category_name not in examples_by_category: - examples_by_category[category_name] = { - "display_name": category_display, - "examples": {}, - } - - # Iterate through example directories - for example_path in sorted(examples_dir.iterdir()): - if not example_path.is_dir() or example_path.name.startswith("."): - continue - - example_name = example_path.name - routers = discover_example_routers(example_path, category_display, example_name) - - if routers: - # Register routers with FastAPI - for router_info in routers: - app.include_router( - router_info["router"], - prefix=router_info["prefix"], - tags=router_info["tags"], - ) - - # Build metadata - examples_by_category[category_name]["examples"][example_name] = { - "description": f"Example: {example_name.replace('_', ' ').title()}", - "category_display": category_display, - "endpoints": { - router_info["worker_type"]: { - "prefix": router_info["prefix"], - "routes": router_info.get("routes", []), - "tag": router_info["tags"][0] if router_info["tags"] else "", - } - for router_info in routers - }, - } - - return examples_by_category - - -# Discover and register all examples -examples_metadata = register_all_examples() - - -def extract_operation_ids_from_app() -> dict[str, dict[str, list[str]]]: - """ - Extract actual operation IDs from the FastAPI app's routes. - - Returns a mapping of example_name -> worker_type -> [operation_ids] - """ - operation_ids_map: dict[str, dict[str, list[str]]] = {} - - for route in app.routes: - if hasattr(route, "tags") and hasattr(route, "unique_id"): - tags = route.tags or [] - operation_id = str(route.unique_id) - - # Parse tag to get example_name and worker_type - # Tag format: "01 Hello World - GPU" - for tag in tags: - if " - " in tag: - parts = tag.split(" - ") - if len(parts) == 2: - display_name = parts[0].strip() - worker_type = parts[1].strip().lower() - - # Convert display name back to example_name - # "01 Hello World" -> "01_hello_world" - example_name = display_name.lower().replace(" ", "_") - - if example_name not in operation_ids_map: - operation_ids_map[example_name] = {} - if worker_type not in operation_ids_map[example_name]: - operation_ids_map[example_name][worker_type] = [] - - operation_ids_map[example_name][worker_type].append(operation_id) - - return operation_ids_map - - -@app.get("/", response_class=HTMLResponse) -def home(): - """Branded home page with links to all automatically discovered examples.""" - - # Extract actual operation IDs from the app - operation_ids_map = extract_operation_ids_from_app() - - # Build examples list HTML grouped by category - categories_html = "" - total_examples = 0 - total_endpoints = 0 - - for category_data in examples_metadata.values(): - category_display = category_data["display_name"] - examples = category_data["examples"] - - if not examples: - continue - - total_examples += len(examples) - - # Build example cards for this category - examples_cards = "" - for example_name, metadata in examples.items(): - display_name = example_name.replace("_", " ").title() - endpoints_html = "" - - total_endpoints += len(metadata["endpoints"]) - - for worker_type, endpoint_info in metadata["endpoints"].items(): - # Use the actual tag from the router registration - tag_name = endpoint_info.get("tag", "") - if not tag_name: - # Fallback to old format if tag is not available - tag_name = f"{display_name} - {worker_type.upper()}" - - encoded_tag = quote(tag_name) - - # Get the actual operation ID from the app - operation_ids = operation_ids_map.get(example_name, {}).get(worker_type.lower(), []) - if operation_ids: - # Use the first operation ID for this worker type - operation_id = operation_ids[0] - doc_link = f"/docs#/{encoded_tag}/{operation_id}" - else: - # Fallback to tag-only link - doc_link = f"/docs#/{encoded_tag}" - - endpoints_html += f'{worker_type.upper()}' - - examples_cards += f""" -
    -

    {display_name}

    -
    {endpoints_html}
    -
    - """ - - # Add category section - categories_html += f""" -
    -

    {category_display}

    -
    - {examples_cards} -
    -
    - """ - - return f""" - - - - - - Runpod Flash Examples - - - - - - -
    -
    - -

    Flash Examples

    -

    - Explore production-ready examples for Runpod Flash. All examples automatically discovered and unified in one FastAPI application. -

    - -
    -
    -
    {total_examples}
    -
    Examples
    -
    -
    -
    {total_endpoints}
    -
    Endpoints
    -
    -
    - - -
    - -
    - {categories_html} -
    - - -
    - - - """ - - -@app.get("/health", tags=["Info"]) -def health(): - """Health check endpoint.""" - examples_loaded = {} - total_examples = 0 - - for category_data in examples_metadata.values(): - for example_name in category_data["examples"]: - examples_loaded[example_name] = True - total_examples += 1 - - return { - "status": "healthy", - "examples_loaded": examples_loaded, - "total_examples": total_examples, - } - - -if __name__ == "__main__": - import uvicorn - - host = os.getenv("FLASH_HOST", "localhost") - port = int(os.getenv("FLASH_PORT", 8888)) - logger.info(f"Starting unified Flash examples server on {host}:{port}") - logger.info(f"Discovered {len(examples_metadata)} examples") - - uvicorn.run(app, host=host, port=port) diff --git a/pyproject.toml b/pyproject.toml index 7ab7030..d0e6fa7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,120 +5,17 @@ description = "A collection of example applications showcasing Runpod Flash - a readme = "README.md" requires-python = ">=3.10" dependencies = [ - "fastapi>=0.104.0", - "numpy>=2.0.2", - "pillow>=10.0.0", - "python-multipart>=0.0.6", "runpod-flash", - "structlog>=23.0.0", - "uvicorn>=0.24.0", ] [dependency-groups] dev = [ "ruff>=0.8.0", - "mypy>=1.13.0", - "pytest>=8.3.0", - "pytest-asyncio>=0.25.0", - "pytest-cov>=6.0.0", - "tomli>=2.0.0", - "tomli-w>=1.0.0", - "packaging>=21.0.0", ] [build-system] -requires = [ - "setuptools>=61.0", -] +requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [tool.setuptools] py-modules = [] - -[tool.ruff] -target-version = "py310" -line-length = 100 -exclude = [ - ".git", - ".venv", - "__pycache__", - "*.egg-info", - ".ruff_cache", - "dist", - "build", -] - -[tool.ruff.lint] -select = [ - "E", - "W", - "F", - "I", - "N", - "UP", - "B", - "C4", - "SIM", - "RUF", -] -ignore = [ - "E501", - "N999", -] - -[tool.ruff.lint.isort] -known-first-party = [ - "runpod-flash", -] - -[tool.ruff.format] -quote-style = "double" -indent-style = "space" -skip-magic-trailing-comma = false -line-ending = "auto" - -[tool.mypy] -python_version = "3.10" -warn_return_any = true -warn_unused_configs = true -disallow_untyped_defs = false -ignore_missing_imports = true -strict_optional = true -warn_redundant_casts = true -warn_unused_ignores = true -check_untyped_defs = true - -[tool.pytest.ini_options] -minversion = "8.0" -testpaths = [ - "tests", -] -pythonpath = [ - ".", -] -asyncio_mode = "auto" -addopts = [ - "--strict-markers", - "--strict-config", - "-ra", -] - -[tool.coverage.run] -source = [ - ".", -] -omit = [ - ".venv/*", - "tests/*", - "*/site-packages/*", -] - -[tool.coverage.report] -exclude_lines = [ - "pragma: no cover", - "def __repr__", - "raise AssertionError", - "raise NotImplementedError", - "if __name__ == .__main__.:", - "if TYPE_CHECKING:", -] diff --git a/requirements.txt b/requirements.txt index 494ba78..a73ed1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,271 +1 @@ -# This file was autogenerated by uv via the following command: -# uv pip compile pyproject.toml -o requirements.txt -aiodns==4.0.0 - # via aiohttp -aiohappyeyeballs==2.6.1 - # via aiohttp -aiohttp==3.13.3 - # via - # aiohttp-retry - # runpod -aiohttp-retry==2.9.1 - # via runpod -aiosignal==1.4.0 - # via aiohttp -annotated-doc==0.0.4 - # via fastapi -annotated-types==0.7.0 - # via pydantic -anyio==4.12.1 - # via - # httpx - # starlette - # watchfiles -attrs==25.4.0 - # via aiohttp -backoff==2.2.1 - # via runpod -backports-zstd==1.3.0 - # via aiohttp -bcrypt==5.0.0 - # via paramiko -boto3==1.42.41 - # via runpod -botocore==1.42.41 - # via - # boto3 - # s3transfer -brotli==1.2.0 - # via aiohttp -certifi==2026.1.4 - # via - # httpcore - # httpx - # requests - # sentry-sdk -cffi==2.0.0 - # via - # cryptography - # pycares - # pynacl -charset-normalizer==3.4.4 - # via requests -click==8.3.1 - # via - # rich-toolkit - # runpod - # typer - # uvicorn -cloudpickle==3.1.2 - # via runpod-flash -colorama==0.4.6 - # via runpod -cryptography==45.0.7 - # via - # paramiko - # runpod -dnspython==2.8.0 - # via email-validator -email-validator==2.3.0 - # via - # fastapi - # pydantic -fastapi==0.128.0 - # via runpod -fastapi-cli==0.0.20 - # via fastapi -fastapi-cloud-cli==0.11.0 - # via fastapi-cli -fastar==0.8.0 - # via fastapi-cloud-cli -filelock==3.20.3 - # via runpod -frozenlist==1.8.0 - # via - # aiohttp - # aiosignal -h11==0.16.0 - # via - # httpcore - # uvicorn -httpcore==1.0.9 - # via httpx -httptools==0.7.1 - # via uvicorn -httpx==0.28.1 - # via - # fastapi - # fastapi-cloud-cli -idna==3.11 - # via - # anyio - # email-validator - # httpx - # requests - # yarl -inquirerpy==0.3.4 - # via runpod -invoke==2.2.1 - # via paramiko -itsdangerous==2.2.0 - # via fastapi -jinja2==3.1.6 - # via fastapi -jmespath==1.1.0 - # via - # boto3 - # botocore -markdown-it-py==4.0.0 - # via rich -markupsafe==3.0.3 - # via jinja2 -mdurl==0.1.2 - # via markdown-it-py -multidict==6.7.1 - # via - # aiohttp - # yarl -numpy==2.4.2 - # via runpod-flash-examples (pyproject.toml) -orjson==3.11.7 - # via fastapi -paramiko==4.0.0 - # via runpod -pathspec==1.0.4 - # via runpod-flash -pfzy==0.3.4 - # via inquirerpy -pillow==12.1.0 - # via runpod-flash-examples (pyproject.toml) -prettytable==3.17.0 - # via runpod -prompt-toolkit==3.0.52 - # via - # inquirerpy - # questionary -propcache==0.4.1 - # via - # aiohttp - # yarl -py-cpuinfo==9.0.0 - # via runpod -pycares==5.0.1 - # via aiodns -pycparser==3.0 - # via cffi -pydantic==2.12.5 - # via - # fastapi - # fastapi-cloud-cli - # pydantic-extra-types - # pydantic-settings - # runpod-flash -pydantic-core==2.41.5 - # via pydantic -pydantic-extra-types==2.11.0 - # via fastapi -pydantic-settings==2.12.0 - # via fastapi -pygments==2.19.2 - # via rich -pynacl==1.6.2 - # via paramiko -python-dateutil==2.9.0.post0 - # via botocore -python-dotenv==1.2.1 - # via - # pydantic-settings - # runpod-flash - # uvicorn -python-multipart==0.0.22 - # via - # runpod-flash-examples (pyproject.toml) - # fastapi -pyyaml==6.0.3 - # via - # fastapi - # uvicorn -questionary==2.1.1 - # via runpod-flash -requests==2.32.5 - # via runpod -rich==14.3.2 - # via - # rich-toolkit - # runpod-flash - # typer -rich-toolkit==0.18.1 - # via - # fastapi-cli - # fastapi-cloud-cli -rignore==0.7.6 - # via fastapi-cloud-cli -runpod==1.8.1 - # via runpod-flash -runpod-flash==1.0.0 - # via runpod-flash-examples (pyproject.toml) -s3transfer==0.16.0 - # via boto3 -sentry-sdk==2.51.0 - # via fastapi-cloud-cli -shellingham==1.5.4 - # via typer -six==1.17.0 - # via python-dateutil -starlette==0.50.0 - # via fastapi -structlog==25.5.0 - # via runpod-flash-examples (pyproject.toml) -tomli==2.4.0 - # via runpod -tomlkit==0.14.0 - # via runpod -tqdm==4.67.3 - # via tqdm-loggable -tqdm-loggable==0.2 - # via runpod -typer==0.21.1 - # via - # fastapi-cli - # fastapi-cloud-cli - # runpod-flash -typing-extensions==4.15.0 - # via - # fastapi - # pydantic - # pydantic-core - # pydantic-extra-types - # rich-toolkit - # typer - # typing-inspection -typing-inspection==0.4.2 - # via - # pydantic - # pydantic-settings -ujson==5.11.0 - # via fastapi -urllib3==2.6.3 - # via - # botocore - # requests - # runpod - # sentry-sdk -uvicorn==0.40.0 - # via - # fastapi - # fastapi-cli - # fastapi-cloud-cli -uvloop==0.22.1 - # via uvicorn -watchdog==6.0.0 - # via runpod -watchfiles==1.1.1 - # via uvicorn -wcwidth==0.5.3 - # via - # prettytable - # prompt-toolkit -websockets==16.0 - # via uvicorn -yarl==1.22.0 - # via aiohttp +runpod-flash diff --git a/scripts/sync_example_deps.py b/scripts/sync_example_deps.py deleted file mode 100755 index 0a5dbe4..0000000 --- a/scripts/sync_example_deps.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python3 -"""Consolidate example dependencies into root pyproject.toml. - -Simple script that collects all dependencies from example directories -and adds them to the root pyproject.toml. No conflict resolution - -if pip/uv can't resolve versions, fix manually. - -Usage: - python scripts/sync_example_deps.py # Consolidate - python scripts/sync_example_deps.py --check # Verify (CI mode) - -Make commands: - make consolidate-deps # Run consolidation - make check-deps # Verify in sync -""" - -import sys -from pathlib import Path - -import tomli_w - -try: - import tomllib -except ModuleNotFoundError: - import tomli as tomllib # type: ignore[no-redef] - -ROOT_DIR = Path(__file__).parent.parent -ROOT_PYPROJECT = ROOT_DIR / "pyproject.toml" -EXAMPLES_DIRS = [ - "01_getting_started", - "02_ml_inference", - "03_advanced_workers", - "04_scaling_performance", - "05_data_workflows", - "06_real_world", -] - - -def collect_all_deps() -> set[str]: - """Collect all dependencies from example pyproject.toml files.""" - all_deps = set() - for examples_dir in EXAMPLES_DIRS: - examples_path = ROOT_DIR / examples_dir - if not examples_path.exists(): - continue - for example_dir in examples_path.iterdir(): - if not example_dir.is_dir(): - continue - pyproject_path = example_dir / "pyproject.toml" - if pyproject_path.exists(): - with open(pyproject_path, "rb") as f: - data = tomllib.load(f) - deps = data.get("project", {}).get("dependencies", []) - all_deps.update(deps) - return all_deps - - -def main() -> None: - """Consolidate dependencies or check if consolidation is needed.""" - check_mode = "--check" in sys.argv - - with open(ROOT_PYPROJECT, "rb") as f: - root_data = tomllib.load(f) - - example_deps = collect_all_deps() - current = set(root_data["project"].get("dependencies", [])) - - if check_mode: - missing = example_deps - current - if missing: - print("ERROR: Dependencies need consolidation:") - for dep in sorted(missing): - print(f" Missing: {dep}") - print("\nRun: make consolidate-deps") - sys.exit(1) - print("✓ All example dependencies are in root") - return - - # Consolidate mode - root_data["project"]["dependencies"] = sorted(current | example_deps) - with open(ROOT_PYPROJECT, "wb") as f: - tomli_w.dump(root_data, f) - - new_count = len(example_deps - current) - print(f"✓ Consolidated {new_count} new dependencies") - print("Run 'uv sync' to install") - - -if __name__ == "__main__": - main() diff --git a/uv.lock b/uv.lock index 5e44705..8101c28 100644 --- a/uv.lock +++ b/uv.lock @@ -241,15 +241,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148 }, ] -[[package]] -name = "backports-asyncio-runner" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313 }, -] - [[package]] name = "backports-zstd" version = "1.3.0" @@ -741,110 +732,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] -[[package]] -name = "coverage" -version = "7.13.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/43/3e4ac666cc35f231fa70c94e9f38459299de1a152813f9d2f60fc5f3ecaf/coverage-7.13.3.tar.gz", hash = "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac", size = 826832 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/07/1c8099563a8a6c389a31c2d0aa1497cee86d6248bb4b9ba5e779215db9f9/coverage-7.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b4f345f7265cdbdb5ec2521ffff15fa49de6d6c39abf89fc7ad68aa9e3a55f0", size = 219143 }, - { url = "https://files.pythonhosted.org/packages/69/39/a892d44af7aa092cab70e0cc5cdbba18eeccfe1d6930695dab1742eef9e9/coverage-7.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96c3be8bae9d0333e403cc1a8eb078a7f928b5650bae94a18fb4820cc993fb9b", size = 219663 }, - { url = "https://files.pythonhosted.org/packages/9a/25/9669dcf4c2bb4c3861469e6db20e52e8c11908cf53c14ec9b12e9fd4d602/coverage-7.13.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d6f4a21328ea49d38565b55599e1c02834e76583a6953e5586d65cb1efebd8f8", size = 246424 }, - { url = "https://files.pythonhosted.org/packages/f3/68/d9766c4e298aca62ea5d9543e1dd1e4e1439d7284815244d8b7db1840bfb/coverage-7.13.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fc970575799a9d17d5c3fafd83a0f6ccf5d5117cdc9ad6fbd791e9ead82418b0", size = 248228 }, - { url = "https://files.pythonhosted.org/packages/f0/e2/eea6cb4a4bd443741adf008d4cccec83a1f75401df59b6559aca2bdd9710/coverage-7.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ff33b652b3556b05e204ae20793d1f872161b0fa5ec8a9ac76f8430e152ed6", size = 250103 }, - { url = "https://files.pythonhosted.org/packages/db/77/664280ecd666c2191610842177e2fab9e5dbdeef97178e2078fed46a3d2c/coverage-7.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7df8759ee57b9f3f7b66799b7660c282f4375bef620ade1686d6a7b03699e75f", size = 247107 }, - { url = "https://files.pythonhosted.org/packages/2b/df/2a672eab99e0d0eba52d8a63e47dc92245eee26954d1b2d3c8f7d372151f/coverage-7.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f45c9bcb16bee25a798ccba8a2f6a1251b19de6a0d617bb365d7d2f386c4e20e", size = 248143 }, - { url = "https://files.pythonhosted.org/packages/a5/dc/a104e7a87c13e57a358b8b9199a8955676e1703bb372d79722b54978ae45/coverage-7.13.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:318b2e4753cbf611061e01b6cc81477e1cdfeb69c36c4a14e6595e674caadb56", size = 246148 }, - { url = "https://files.pythonhosted.org/packages/2b/89/e113d3a58dc20b03b7e59aed1e53ebc9ca6167f961876443e002b10e3ae9/coverage-7.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:24db3959de8ee394eeeca89ccb8ba25305c2da9a668dd44173394cbd5aa0777f", size = 246414 }, - { url = "https://files.pythonhosted.org/packages/3f/60/a3fd0a6e8d89b488396019a2268b6a1f25ab56d6d18f3be50f35d77b47dc/coverage-7.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be14d0622125edef21b3a4d8cd2d138c4872bf6e38adc90fd92385e3312f406a", size = 247023 }, - { url = "https://files.pythonhosted.org/packages/19/fa/de4840bb939dbb22ba0648a6d8069fa91c9cf3b3fca8b0d1df461e885b3d/coverage-7.13.3-cp310-cp310-win32.whl", hash = "sha256:53be4aab8ddef18beb6188f3a3fdbf4d1af2277d098d4e618be3a8e6c88e74be", size = 221751 }, - { url = "https://files.pythonhosted.org/packages/de/87/233ff8b7ef62fb63f58c78623b50bef69681111e0c4d43504f422d88cda4/coverage-7.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:bfeee64ad8b4aae3233abb77eb6b52b51b05fa89da9645518671b9939a78732b", size = 222686 }, - { url = "https://files.pythonhosted.org/packages/ec/09/1ac74e37cf45f17eb41e11a21854f7f92a4c2d6c6098ef4a1becb0c6d8d3/coverage-7.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5907605ee20e126eeee2abe14aae137043c2c8af2fa9b38d2ab3b7a6b8137f73", size = 219276 }, - { url = "https://files.pythonhosted.org/packages/2e/cb/71908b08b21beb2c437d0d5870c4ec129c570ca1b386a8427fcdb11cf89c/coverage-7.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a88705500988c8acad8b8fd86c2a933d3aa96bec1ddc4bc5cb256360db7bbd00", size = 219776 }, - { url = "https://files.pythonhosted.org/packages/09/85/c4f3dd69232887666a2c0394d4be21c60ea934d404db068e6c96aa59cd87/coverage-7.13.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bbb5aa9016c4c29e3432e087aa29ebee3f8fda089cfbfb4e6d64bd292dcd1c2", size = 250196 }, - { url = "https://files.pythonhosted.org/packages/9c/cc/560ad6f12010344d0778e268df5ba9aa990aacccc310d478bf82bf3d302c/coverage-7.13.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c2be202a83dde768937a61cdc5d06bf9fb204048ca199d93479488e6247656c", size = 252111 }, - { url = "https://files.pythonhosted.org/packages/f0/66/3193985fb2c58e91f94cfbe9e21a6fdf941e9301fe2be9e92c072e9c8f8c/coverage-7.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f45e32ef383ce56e0ca099b2e02fcdf7950be4b1b56afaab27b4ad790befe5b", size = 254217 }, - { url = "https://files.pythonhosted.org/packages/c5/78/f0f91556bf1faa416792e537c523c5ef9db9b1d32a50572c102b3d7c45b3/coverage-7.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ed2e787249b922a93cd95c671cc9f4c9797a106e81b455c83a9ddb9d34590c0", size = 250318 }, - { url = "https://files.pythonhosted.org/packages/6f/aa/fc654e45e837d137b2c1f3a2cc09b4aea1e8b015acd2f774fa0f3d2ddeba/coverage-7.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:05dd25b21afffe545e808265897c35f32d3e4437663923e0d256d9ab5031fb14", size = 251909 }, - { url = "https://files.pythonhosted.org/packages/73/4d/ab53063992add8a9ca0463c9d92cce5994a29e17affd1c2daa091b922a93/coverage-7.13.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:46d29926349b5c4f1ea4fca95e8c892835515f3600995a383fa9a923b5739ea4", size = 249971 }, - { url = "https://files.pythonhosted.org/packages/29/25/83694b81e46fcff9899694a1b6f57573429cdd82b57932f09a698f03eea5/coverage-7.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fae6a21537519c2af00245e834e5bf2884699cc7c1055738fd0f9dc37a3644ad", size = 249692 }, - { url = "https://files.pythonhosted.org/packages/d4/ef/d68fc304301f4cb4bf6aefa0045310520789ca38dabdfba9dbecd3f37919/coverage-7.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c672d4e2f0575a4ca2bf2aa0c5ced5188220ab806c1bb6d7179f70a11a017222", size = 250597 }, - { url = "https://files.pythonhosted.org/packages/8d/85/240ad396f914df361d0f71e912ddcedb48130c71b88dc4193fe3c0306f00/coverage-7.13.3-cp311-cp311-win32.whl", hash = "sha256:fcda51c918c7a13ad93b5f89a58d56e3a072c9e0ba5c231b0ed81404bf2648fb", size = 221773 }, - { url = "https://files.pythonhosted.org/packages/2f/71/165b3a6d3d052704a9ab52d11ea64ef3426745de517dda44d872716213a7/coverage-7.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:d1a049b5c51b3b679928dd35e47c4a2235e0b6128b479a7596d0ef5b42fa6301", size = 222711 }, - { url = "https://files.pythonhosted.org/packages/51/d0/0ddc9c5934cdd52639c5df1f1eb0fdab51bb52348f3a8d1c7db9c600d93a/coverage-7.13.3-cp311-cp311-win_arm64.whl", hash = "sha256:79f2670c7e772f4917895c3d89aad59e01f3dbe68a4ed2d0373b431fad1dcfba", size = 221377 }, - { url = "https://files.pythonhosted.org/packages/94/44/330f8e83b143f6668778ed61d17ece9dc48459e9e74669177de02f45fec5/coverage-7.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ed48b4170caa2c4420e0cd27dc977caaffc7eecc317355751df8373dddcef595", size = 219441 }, - { url = "https://files.pythonhosted.org/packages/08/e7/29db05693562c2e65bdf6910c0af2fd6f9325b8f43caf7a258413f369e30/coverage-7.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8f2adf4bcffbbec41f366f2e6dffb9d24e8172d16e91da5799c9b7ed6b5716e6", size = 219801 }, - { url = "https://files.pythonhosted.org/packages/90/ae/7f8a78249b02b0818db46220795f8ac8312ea4abd1d37d79ea81db5cae81/coverage-7.13.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01119735c690786b6966a1e9f098da4cd7ca9174c4cfe076d04e653105488395", size = 251306 }, - { url = "https://files.pythonhosted.org/packages/62/71/a18a53d1808e09b2e9ebd6b47dad5e92daf4c38b0686b4c4d1b2f3e42b7f/coverage-7.13.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8bb09e83c603f152d855f666d70a71765ca8e67332e5829e62cb9466c176af23", size = 254051 }, - { url = "https://files.pythonhosted.org/packages/4a/0a/eb30f6455d04c5a3396d0696cad2df0269ae7444bb322f86ffe3376f7bf9/coverage-7.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b607a40cba795cfac6d130220d25962931ce101f2f478a29822b19755377fb34", size = 255160 }, - { url = "https://files.pythonhosted.org/packages/7b/7e/a45baac86274ce3ed842dbb84f14560c673ad30535f397d89164ec56c5df/coverage-7.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:44f14a62f5da2e9aedf9080e01d2cda61df39197d48e323538ec037336d68da8", size = 251709 }, - { url = "https://files.pythonhosted.org/packages/c0/df/dd0dc12f30da11349993f3e218901fdf82f45ee44773596050c8f5a1fb25/coverage-7.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:debf29e0b157769843dff0981cc76f79e0ed04e36bb773c6cac5f6029054bd8a", size = 253083 }, - { url = "https://files.pythonhosted.org/packages/ab/32/fc764c8389a8ce95cb90eb97af4c32f392ab0ac23ec57cadeefb887188d3/coverage-7.13.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:824bb95cd71604031ae9a48edb91fd6effde669522f960375668ed21b36e3ec4", size = 251227 }, - { url = "https://files.pythonhosted.org/packages/dd/ca/d025e9da8f06f24c34d2da9873957cfc5f7e0d67802c3e34d0caa8452130/coverage-7.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8f1010029a5b52dc427c8e2a8dbddb2303ddd180b806687d1acd1bb1d06649e7", size = 250794 }, - { url = "https://files.pythonhosted.org/packages/45/c7/76bf35d5d488ec8f68682eb8e7671acc50a6d2d1c1182de1d2b6d4ffad3b/coverage-7.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cd5dee4fd7659d8306ffa79eeaaafd91fa30a302dac3af723b9b469e549247e0", size = 252671 }, - { url = "https://files.pythonhosted.org/packages/bf/10/1921f1a03a7c209e1cb374f81a6b9b68b03cdb3ecc3433c189bc90e2a3d5/coverage-7.13.3-cp312-cp312-win32.whl", hash = "sha256:f7f153d0184d45f3873b3ad3ad22694fd73aadcb8cdbc4337ab4b41ea6b4dff1", size = 221986 }, - { url = "https://files.pythonhosted.org/packages/3c/7c/f5d93297f8e125a80c15545edc754d93e0ed8ba255b65e609b185296af01/coverage-7.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:03a6e5e1e50819d6d7436f5bc40c92ded7e484e400716886ac921e35c133149d", size = 222793 }, - { url = "https://files.pythonhosted.org/packages/43/59/c86b84170015b4555ebabca8649bdf9f4a1f737a73168088385ed0f947c4/coverage-7.13.3-cp312-cp312-win_arm64.whl", hash = "sha256:51c4c42c0e7d09a822b08b6cf79b3c4db8333fffde7450da946719ba0d45730f", size = 221410 }, - { url = "https://files.pythonhosted.org/packages/81/f3/4c333da7b373e8c8bfb62517e8174a01dcc373d7a9083698e3b39d50d59c/coverage-7.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:853c3d3c79ff0db65797aad79dee6be020efd218ac4510f15a205f1e8d13ce25", size = 219468 }, - { url = "https://files.pythonhosted.org/packages/d6/31/0714337b7d23630c8de2f4d56acf43c65f8728a45ed529b34410683f7217/coverage-7.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f75695e157c83d374f88dcc646a60cb94173304a9258b2e74ba5a66b7614a51a", size = 219839 }, - { url = "https://files.pythonhosted.org/packages/12/99/bd6f2a2738144c98945666f90cae446ed870cecf0421c767475fcf42cdbe/coverage-7.13.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d098709621d0819039f3f1e471ee554f55a0b2ac0d816883c765b14129b5627", size = 250828 }, - { url = "https://files.pythonhosted.org/packages/6f/99/97b600225fbf631e6f5bfd3ad5bcaf87fbb9e34ff87492e5a572ff01bbe2/coverage-7.13.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16d23d6579cf80a474ad160ca14d8b319abaa6db62759d6eef53b2fc979b58c8", size = 253432 }, - { url = "https://files.pythonhosted.org/packages/5f/5c/abe2b3490bda26bd4f5e3e799be0bdf00bd81edebedc2c9da8d3ef288fa8/coverage-7.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00d34b29a59d2076e6f318b30a00a69bf63687e30cd882984ed444e753990cc1", size = 254672 }, - { url = "https://files.pythonhosted.org/packages/31/ba/5d1957c76b40daff53971fe0adb84d9c2162b614280031d1d0653dd010c1/coverage-7.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ab6d72bffac9deb6e6cb0f61042e748de3f9f8e98afb0375a8e64b0b6e11746b", size = 251050 }, - { url = "https://files.pythonhosted.org/packages/69/dc/dffdf3bfe9d32090f047d3c3085378558cb4eb6778cda7de414ad74581ed/coverage-7.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e129328ad1258e49cae0123a3b5fcb93d6c2fa90d540f0b4c7cdcdc019aaa3dc", size = 252801 }, - { url = "https://files.pythonhosted.org/packages/87/51/cdf6198b0f2746e04511a30dc9185d7b8cdd895276c07bdb538e37f1cd50/coverage-7.13.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2213a8d88ed35459bda71597599d4eec7c2ebad201c88f0bfc2c26fd9b0dd2ea", size = 250763 }, - { url = "https://files.pythonhosted.org/packages/d7/1a/596b7d62218c1d69f2475b69cc6b211e33c83c902f38ee6ae9766dd422da/coverage-7.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:00dd3f02de6d5f5c9c3d95e3e036c3c2e2a669f8bf2d3ceb92505c4ce7838f67", size = 250587 }, - { url = "https://files.pythonhosted.org/packages/f7/46/52330d5841ff660f22c130b75f5e1dd3e352c8e7baef5e5fef6b14e3e991/coverage-7.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9bada7bc660d20b23d7d312ebe29e927b655cf414dadcdb6335a2075695bd86", size = 252358 }, - { url = "https://files.pythonhosted.org/packages/36/8a/e69a5be51923097ba7d5cff9724466e74fe486e9232020ba97c809a8b42b/coverage-7.13.3-cp313-cp313-win32.whl", hash = "sha256:75b3c0300f3fa15809bd62d9ca8b170eb21fcf0100eb4b4154d6dc8b3a5bbd43", size = 222007 }, - { url = "https://files.pythonhosted.org/packages/0a/09/a5a069bcee0d613bdd48ee7637fa73bc09e7ed4342b26890f2df97cc9682/coverage-7.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:a2f7589c6132c44c53f6e705e1a6677e2b7821378c22f7703b2cf5388d0d4587", size = 222812 }, - { url = "https://files.pythonhosted.org/packages/3d/4f/d62ad7dfe32f9e3d4a10c178bb6f98b10b083d6e0530ca202b399371f6c1/coverage-7.13.3-cp313-cp313-win_arm64.whl", hash = "sha256:123ceaf2b9d8c614f01110f908a341e05b1b305d6b2ada98763b9a5a59756051", size = 221433 }, - { url = "https://files.pythonhosted.org/packages/04/b2/4876c46d723d80b9c5b695f1a11bf5f7c3dabf540ec00d6edc076ff025e6/coverage-7.13.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:cc7fd0f726795420f3678ac82ff882c7fc33770bd0074463b5aef7293285ace9", size = 220162 }, - { url = "https://files.pythonhosted.org/packages/fc/04/9942b64a0e0bdda2c109f56bda42b2a59d9d3df4c94b85a323c1cae9fc77/coverage-7.13.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d358dc408edc28730aed5477a69338e444e62fba0b7e9e4a131c505fadad691e", size = 220510 }, - { url = "https://files.pythonhosted.org/packages/5a/82/5cfe1e81eae525b74669f9795f37eb3edd4679b873d79d1e6c1c14ee6c1c/coverage-7.13.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5d67b9ed6f7b5527b209b24b3df9f2e5bf0198c1bbf99c6971b0e2dcb7e2a107", size = 261801 }, - { url = "https://files.pythonhosted.org/packages/0b/ec/a553d7f742fd2cd12e36a16a7b4b3582d5934b496ef2b5ea8abeb10903d4/coverage-7.13.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59224bfb2e9b37c1335ae35d00daa3a5b4e0b1a20f530be208fff1ecfa436f43", size = 263882 }, - { url = "https://files.pythonhosted.org/packages/e1/58/8f54a2a93e3d675635bc406de1c9ac8d551312142ff52c9d71b5e533ad45/coverage-7.13.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9306b5299e31e31e0d3b908c66bcb6e7e3ddca143dea0266e9ce6c667346d3", size = 266306 }, - { url = "https://files.pythonhosted.org/packages/1a/be/e593399fd6ea1f00aee79ebd7cc401021f218d34e96682a92e1bae092ff6/coverage-7.13.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:343aaeb5f8bb7bcd38620fd7bc56e6ee8207847d8c6103a1e7b72322d381ba4a", size = 261051 }, - { url = "https://files.pythonhosted.org/packages/5c/e5/e9e0f6138b21bcdebccac36fbfde9cf15eb1bbcea9f5b1f35cd1f465fb91/coverage-7.13.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2182129f4c101272ff5f2f18038d7b698db1bf8e7aa9e615cb48440899ad32e", size = 263868 }, - { url = "https://files.pythonhosted.org/packages/9a/bf/de72cfebb69756f2d4a2dde35efcc33c47d85cd3ebdf844b3914aac2ef28/coverage-7.13.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:94d2ac94bd0cc57c5626f52f8c2fffed1444b5ae8c9fc68320306cc2b255e155", size = 261498 }, - { url = "https://files.pythonhosted.org/packages/f2/91/4a2d313a70fc2e98ca53afd1c8ce67a89b1944cd996589a5b1fe7fbb3e5c/coverage-7.13.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:65436cde5ecabe26fb2f0bf598962f0a054d3f23ad529361326ac002c61a2a1e", size = 260394 }, - { url = "https://files.pythonhosted.org/packages/40/83/25113af7cf6941e779eb7ed8de2a677865b859a07ccee9146d4cc06a03e3/coverage-7.13.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db83b77f97129813dbd463a67e5335adc6a6a91db652cc085d60c2d512746f96", size = 262579 }, - { url = "https://files.pythonhosted.org/packages/1e/19/a5f2b96262977e82fb9aabbe19b4d83561f5d063f18dde3e72f34ffc3b2f/coverage-7.13.3-cp313-cp313t-win32.whl", hash = "sha256:dfb428e41377e6b9ba1b0a32df6db5409cb089a0ed1d0a672dc4953ec110d84f", size = 222679 }, - { url = "https://files.pythonhosted.org/packages/81/82/ef1747b88c87a5c7d7edc3704799ebd650189a9158e680a063308b6125ef/coverage-7.13.3-cp313-cp313t-win_amd64.whl", hash = "sha256:5badd7e596e6b0c89aa8ec6d37f4473e4357f982ce57f9a2942b0221cd9cf60c", size = 223740 }, - { url = "https://files.pythonhosted.org/packages/1c/4c/a67c7bb5b560241c22736a9cb2f14c5034149ffae18630323fde787339e4/coverage-7.13.3-cp313-cp313t-win_arm64.whl", hash = "sha256:989aa158c0eb19d83c76c26f4ba00dbb272485c56e452010a3450bdbc9daafd9", size = 221996 }, - { url = "https://files.pythonhosted.org/packages/5e/b3/677bb43427fed9298905106f39c6520ac75f746f81b8f01104526a8026e4/coverage-7.13.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c6f6169bbdbdb85aab8ac0392d776948907267fcc91deeacf6f9d55f7a83ae3b", size = 219513 }, - { url = "https://files.pythonhosted.org/packages/42/53/290046e3bbf8986cdb7366a42dab3440b9983711eaff044a51b11006c67b/coverage-7.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2f5e731627a3d5ef11a2a35aa0c6f7c435867c7ccbc391268eb4f2ca5dbdcc10", size = 219850 }, - { url = "https://files.pythonhosted.org/packages/ea/2b/ab41f10345ba2e49d5e299be8663be2b7db33e77ac1b85cd0af985ea6406/coverage-7.13.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9db3a3285d91c0b70fab9f39f0a4aa37d375873677efe4e71e58d8321e8c5d39", size = 250886 }, - { url = "https://files.pythonhosted.org/packages/72/2d/b3f6913ee5a1d5cdd04106f257e5fac5d048992ffc2d9995d07b0f17739f/coverage-7.13.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06e49c5897cb12e3f7ecdc111d44e97c4f6d0557b81a7a0204ed70a8b038f86f", size = 253393 }, - { url = "https://files.pythonhosted.org/packages/f0/f6/b1f48810ffc6accf49a35b9943636560768f0812330f7456aa87dc39aff5/coverage-7.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb25061a66802df9fc13a9ba1967d25faa4dae0418db469264fd9860a921dde4", size = 254740 }, - { url = "https://files.pythonhosted.org/packages/57/d0/e59c54f9be0b61808f6bc4c8c4346bd79f02dd6bbc3f476ef26124661f20/coverage-7.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:99fee45adbb1caeb914da16f70e557fb7ff6ddc9e4b14de665bd41af631367ef", size = 250905 }, - { url = "https://files.pythonhosted.org/packages/d5/f7/5291bcdf498bafbee3796bb32ef6966e9915aebd4d0954123c8eae921c32/coverage-7.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:318002f1fd819bdc1651c619268aa5bc853c35fa5cc6d1e8c96bd9cd6c828b75", size = 252753 }, - { url = "https://files.pythonhosted.org/packages/a0/a9/1dcafa918c281554dae6e10ece88c1add82db685be123e1b05c2056ff3fb/coverage-7.13.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:71295f2d1d170b9977dc386d46a7a1b7cbb30e5405492529b4c930113a33f895", size = 250716 }, - { url = "https://files.pythonhosted.org/packages/44/bb/4ea4eabcce8c4f6235df6e059fbc5db49107b24c4bdffc44aee81aeca5a8/coverage-7.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5b1ad2e0dc672625c44bc4fe34514602a9fd8b10d52ddc414dc585f74453516c", size = 250530 }, - { url = "https://files.pythonhosted.org/packages/6d/31/4a6c9e6a71367e6f923b27b528448c37f4e959b7e4029330523014691007/coverage-7.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b2beb64c145593a50d90db5c7178f55daeae129123b0d265bdb3cbec83e5194a", size = 252186 }, - { url = "https://files.pythonhosted.org/packages/27/92/e1451ef6390a4f655dc42da35d9971212f7abbbcad0bdb7af4407897eb76/coverage-7.13.3-cp314-cp314-win32.whl", hash = "sha256:3d1aed4f4e837a832df2f3b4f68a690eede0de4560a2dbc214ea0bc55aabcdb4", size = 222253 }, - { url = "https://files.pythonhosted.org/packages/8a/98/78885a861a88de020c32a2693487c37d15a9873372953f0c3c159d575a43/coverage-7.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f9efbbaf79f935d5fbe3ad814825cbce4f6cdb3054384cb49f0c0f496125fa0", size = 223069 }, - { url = "https://files.pythonhosted.org/packages/eb/fb/3784753a48da58a5337972abf7ca58b1fb0f1bda21bc7b4fae992fd28e47/coverage-7.13.3-cp314-cp314-win_arm64.whl", hash = "sha256:31b6e889c53d4e6687ca63706148049494aace140cffece1c4dc6acadb70a7b3", size = 221633 }, - { url = "https://files.pythonhosted.org/packages/40/f9/75b732d9674d32cdbffe801ed5f770786dd1c97eecedef2125b0d25102dc/coverage-7.13.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c5e9787cec750793a19a28df7edd85ac4e49d3fb91721afcdc3b86f6c08d9aa8", size = 220243 }, - { url = "https://files.pythonhosted.org/packages/cf/7e/2868ec95de5a65703e6f0c87407ea822d1feb3619600fbc3c1c4fa986090/coverage-7.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5b86db331c682fd0e4be7098e6acee5e8a293f824d41487c667a93705d415ca", size = 220515 }, - { url = "https://files.pythonhosted.org/packages/7d/eb/9f0d349652fced20bcaea0f67fc5777bd097c92369f267975732f3dc5f45/coverage-7.13.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:edc7754932682d52cf6e7a71806e529ecd5ce660e630e8bd1d37109a2e5f63ba", size = 261874 }, - { url = "https://files.pythonhosted.org/packages/ee/a5/6619bc4a6c7b139b16818149a3e74ab2e21599ff9a7b6811b6afde99f8ec/coverage-7.13.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3a16d6398666510a6886f67f43d9537bfd0e13aca299688a19daa84f543122f", size = 264004 }, - { url = "https://files.pythonhosted.org/packages/29/b7/90aa3fc645a50c6f07881fca4fd0ba21e3bfb6ce3a7078424ea3a35c74c9/coverage-7.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:303d38b19626c1981e1bb067a9928236d88eb0e4479b18a74812f05a82071508", size = 266408 }, - { url = "https://files.pythonhosted.org/packages/62/55/08bb2a1e4dcbae384e638f0effef486ba5987b06700e481691891427d879/coverage-7.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:284e06eadfe15ddfee2f4ee56631f164ef897a7d7d5a15bca5f0bb88889fc5ba", size = 260977 }, - { url = "https://files.pythonhosted.org/packages/9b/76/8bd4ae055a42d8fb5dd2230e5cf36ff2e05f85f2427e91b11a27fea52ed7/coverage-7.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d401f0864a1d3198422816878e4e84ca89ec1c1bf166ecc0ae01380a39b888cd", size = 263868 }, - { url = "https://files.pythonhosted.org/packages/e3/f9/ba000560f11e9e32ec03df5aa8477242c2d95b379c99ac9a7b2e7fbacb1a/coverage-7.13.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3f379b02c18a64de78c4ccdddf1c81c2c5ae1956c72dacb9133d7dd7809794ab", size = 261474 }, - { url = "https://files.pythonhosted.org/packages/90/4b/4de4de8f9ca7af4733bfcf4baa440121b7dbb3856daf8428ce91481ff63b/coverage-7.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7a482f2da9086971efb12daca1d6547007ede3674ea06e16d7663414445c683e", size = 260317 }, - { url = "https://files.pythonhosted.org/packages/05/71/5cd8436e2c21410ff70be81f738c0dddea91bcc3189b1517d26e0102ccb3/coverage-7.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:562136b0d401992118d9b49fbee5454e16f95f85b120a4226a04d816e33fe024", size = 262635 }, - { url = "https://files.pythonhosted.org/packages/e7/f8/2834bb45bdd70b55a33ec354b8b5f6062fc90e5bb787e14385903a979503/coverage-7.13.3-cp314-cp314t-win32.whl", hash = "sha256:ca46e5c3be3b195098dd88711890b8011a9fa4feca942292bb84714ce5eab5d3", size = 223035 }, - { url = "https://files.pythonhosted.org/packages/26/75/f8290f0073c00d9ae14056d2b84ab92dff21d5370e464cb6cb06f52bf580/coverage-7.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:06d316dbb3d9fd44cca05b2dbcfbef22948493d63a1f28e828d43e6cc505fed8", size = 224142 }, - { url = "https://files.pythonhosted.org/packages/03/01/43ac78dfea8946c4a9161bbc034b5549115cb2b56781a4b574927f0d141a/coverage-7.13.3-cp314-cp314t-win_arm64.whl", hash = "sha256:299d66e9218193f9dc6e4880629ed7c4cd23486005166247c283fb98531656c3", size = 222166 }, - { url = "https://files.pythonhosted.org/packages/7d/fb/70af542d2d938c778c9373ce253aa4116dbe7c0a5672f78b2b2ae0e1b94b/coverage-7.13.3-py3-none-any.whl", hash = "sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910", size = 211237 }, -] - -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, -] - [[package]] name = "cryptography" version = "45.0.7" @@ -1337,15 +1224,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, ] -[[package]] -name = "iniconfig" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, -] - [[package]] name = "inquirerpy" version = "0.3.4" @@ -1398,79 +1276,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419 }, ] -[[package]] -name = "librt" -version = "0.7.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521 }, - { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456 }, - { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392 }, - { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959 }, - { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717 }, - { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585 }, - { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497 }, - { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052 }, - { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477 }, - { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806 }, - { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507 }, - { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455 }, - { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956 }, - { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364 }, - { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034 }, - { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295 }, - { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470 }, - { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713 }, - { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803 }, - { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080 }, - { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383 }, - { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472 }, - { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986 }, - { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422 }, - { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478 }, - { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439 }, - { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483 }, - { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376 }, - { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234 }, - { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057 }, - { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293 }, - { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574 }, - { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500 }, - { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019 }, - { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015 }, - { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161 }, - { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015 }, - { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038 }, - { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006 }, - { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888 }, - { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126 }, - { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262 }, - { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600 }, - { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049 }, - { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689 }, - { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808 }, - { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614 }, - { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955 }, - { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370 }, - { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224 }, - { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541 }, - { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657 }, - { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835 }, - { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885 }, - { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161 }, - { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008 }, - { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199 }, - { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317 }, - { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334 }, - { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031 }, - { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581 }, - { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731 }, - { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173 }, - { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668 }, - { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550 }, -] - [[package]] name = "markdown-it-py" version = "4.0.0" @@ -1715,210 +1520,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319 }, ] -[[package]] -name = "mypy" -version = "1.19.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, - { name = "mypy-extensions" }, - { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333 }, - { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102 }, - { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799 }, - { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149 }, - { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105 }, - { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200 }, - { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539 }, - { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163 }, - { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629 }, - { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933 }, - { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754 }, - { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772 }, - { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053 }, - { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134 }, - { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616 }, - { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847 }, - { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976 }, - { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104 }, - { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927 }, - { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730 }, - { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581 }, - { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252 }, - { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848 }, - { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510 }, - { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744 }, - { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815 }, - { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047 }, - { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998 }, - { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476 }, - { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872 }, - { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239 }, -] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, -] - -[[package]] -name = "numpy" -version = "2.2.6" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245 }, - { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048 }, - { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542 }, - { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301 }, - { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320 }, - { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050 }, - { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034 }, - { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185 }, - { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149 }, - { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620 }, - { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963 }, - { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743 }, - { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616 }, - { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579 }, - { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005 }, - { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570 }, - { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548 }, - { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521 }, - { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866 }, - { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455 }, - { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348 }, - { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362 }, - { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103 }, - { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382 }, - { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462 }, - { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618 }, - { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511 }, - { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783 }, - { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506 }, - { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190 }, - { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828 }, - { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006 }, - { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765 }, - { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736 }, - { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719 }, - { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072 }, - { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213 }, - { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632 }, - { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532 }, - { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885 }, - { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467 }, - { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144 }, - { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217 }, - { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014 }, - { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935 }, - { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122 }, - { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143 }, - { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260 }, - { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225 }, - { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374 }, - { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391 }, - { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754 }, - { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476 }, - { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666 }, -] - -[[package]] -name = "numpy" -version = "2.4.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14'", - "python_full_version == '3.13.*'", - "python_full_version >= '3.11' and python_full_version < '3.13'", -] -sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478 }, - { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467 }, - { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172 }, - { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145 }, - { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084 }, - { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477 }, - { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429 }, - { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109 }, - { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915 }, - { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972 }, - { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763 }, - { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963 }, - { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571 }, - { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469 }, - { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820 }, - { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067 }, - { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782 }, - { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128 }, - { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324 }, - { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282 }, - { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210 }, - { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171 }, - { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696 }, - { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322 }, - { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157 }, - { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330 }, - { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968 }, - { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311 }, - { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850 }, - { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210 }, - { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199 }, - { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848 }, - { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082 }, - { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866 }, - { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631 }, - { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254 }, - { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138 }, - { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398 }, - { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064 }, - { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680 }, - { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433 }, - { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181 }, - { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756 }, - { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092 }, - { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770 }, - { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562 }, - { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710 }, - { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205 }, - { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738 }, - { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888 }, - { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556 }, - { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899 }, - { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072 }, - { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886 }, - { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567 }, - { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372 }, - { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306 }, - { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394 }, - { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343 }, - { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045 }, - { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024 }, - { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937 }, - { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844 }, - { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379 }, - { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179 }, - { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755 }, - { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500 }, - { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252 }, - { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142 }, - { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979 }, - { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577 }, -] - [[package]] name = "orjson" version = "3.11.7" @@ -2000,15 +1601,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038 }, ] -[[package]] -name = "packaging" -version = "26.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366 }, -] - [[package]] name = "paramiko" version = "4.0.0" @@ -2042,113 +1634,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/d7/8ff98376b1acc4503253b685ea09981697385ce344d4e3935c2af49e044d/pfzy-0.3.4-py3-none-any.whl", hash = "sha256:5f50d5b2b3207fa72e7ec0ef08372ef652685470974a107d0d4999fc5a903a96", size = 8537 }, ] -[[package]] -name = "pillow" -version = "12.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/41/f73d92b6b883a579e79600d391f2e21cb0df767b2714ecbd2952315dfeef/pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd", size = 5304089 }, - { url = "https://files.pythonhosted.org/packages/94/55/7aca2891560188656e4a91ed9adba305e914a4496800da6b5c0a15f09edf/pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0", size = 4657815 }, - { url = "https://files.pythonhosted.org/packages/e9/d2/b28221abaa7b4c40b7dba948f0f6a708bd7342c4d47ce342f0ea39643974/pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8", size = 6222593 }, - { url = "https://files.pythonhosted.org/packages/71/b8/7a61fb234df6a9b0b479f69e66901209d89ff72a435b49933f9122f94cac/pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1", size = 8027579 }, - { url = "https://files.pythonhosted.org/packages/ea/51/55c751a57cc524a15a0e3db20e5cde517582359508d62305a627e77fd295/pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda", size = 6335760 }, - { url = "https://files.pythonhosted.org/packages/dc/7c/60e3e6f5e5891a1a06b4c910f742ac862377a6fe842f7184df4a274ce7bf/pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7", size = 7027127 }, - { url = "https://files.pythonhosted.org/packages/06/37/49d47266ba50b00c27ba63a7c898f1bb41a29627ced8c09e25f19ebec0ff/pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a", size = 6449896 }, - { url = "https://files.pythonhosted.org/packages/f9/e5/67fd87d2913902462cd9b79c6211c25bfe95fcf5783d06e1367d6d9a741f/pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef", size = 7151345 }, - { url = "https://files.pythonhosted.org/packages/bd/15/f8c7abf82af68b29f50d77c227e7a1f87ce02fdc66ded9bf603bc3b41180/pillow-12.1.0-cp310-cp310-win32.whl", hash = "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09", size = 6325568 }, - { url = "https://files.pythonhosted.org/packages/d4/24/7d1c0e160b6b5ac2605ef7d8be537e28753c0db5363d035948073f5513d7/pillow-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91", size = 7032367 }, - { url = "https://files.pythonhosted.org/packages/f4/03/41c038f0d7a06099254c60f618d0ec7be11e79620fc23b8e85e5b31d9a44/pillow-12.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea", size = 2452345 }, - { url = "https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3", size = 5304057 }, - { url = "https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0", size = 4657811 }, - { url = "https://files.pythonhosted.org/packages/72/64/f9189e44474610daf83da31145fa56710b627b5c4c0b9c235e34058f6b31/pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451", size = 6232243 }, - { url = "https://files.pythonhosted.org/packages/ef/30/0df458009be6a4caca4ca2c52975e6275c387d4e5c95544e34138b41dc86/pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e", size = 8037872 }, - { url = "https://files.pythonhosted.org/packages/e4/86/95845d4eda4f4f9557e25381d70876aa213560243ac1a6d619c46caaedd9/pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84", size = 6345398 }, - { url = "https://files.pythonhosted.org/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0", size = 7034667 }, - { url = "https://files.pythonhosted.org/packages/f9/f6/683b83cb9b1db1fb52b87951b1c0b99bdcfceaa75febf11406c19f82cb5e/pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b", size = 6458743 }, - { url = "https://files.pythonhosted.org/packages/9a/7d/de833d63622538c1d58ce5395e7c6cb7e7dce80decdd8bde4a484e095d9f/pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18", size = 7159342 }, - { url = "https://files.pythonhosted.org/packages/8c/40/50d86571c9e5868c42b81fe7da0c76ca26373f3b95a8dd675425f4a92ec1/pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64", size = 6328655 }, - { url = "https://files.pythonhosted.org/packages/6c/af/b1d7e301c4cd26cd45d4af884d9ee9b6fab893b0ad2450d4746d74a6968c/pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75", size = 7031469 }, - { url = "https://files.pythonhosted.org/packages/48/36/d5716586d887fb2a810a4a61518a327a1e21c8b7134c89283af272efe84b/pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304", size = 2452515 }, - { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642 }, - { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464 }, - { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878 }, - { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868 }, - { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468 }, - { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518 }, - { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829 }, - { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756 }, - { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770 }, - { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406 }, - { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612 }, - { url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543 }, - { url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373 }, - { url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241 }, - { url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410 }, - { url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312 }, - { url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605 }, - { url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617 }, - { url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509 }, - { url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117 }, - { url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151 }, - { url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534 }, - { url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551 }, - { url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087 }, - { url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470 }, - { url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816 }, - { url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472 }, - { url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974 }, - { url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070 }, - { url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176 }, - { url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061 }, - { url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824 }, - { url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911 }, - { url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445 }, - { url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354 }, - { url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547 }, - { url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533 }, - { url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546 }, - { url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163 }, - { url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086 }, - { url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344 }, - { url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114 }, - { url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708 }, - { url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762 }, - { url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265 }, - { url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341 }, - { url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395 }, - { url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413 }, - { url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779 }, - { url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105 }, - { url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571 }, - { url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426 }, - { url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908 }, - { url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733 }, - { url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431 }, - { url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529 }, - { url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981 }, - { url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878 }, - { url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703 }, - { url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927 }, - { url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104 }, - { url = "https://files.pythonhosted.org/packages/8b/bc/224b1d98cffd7164b14707c91aac83c07b047fbd8f58eba4066a3e53746a/pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377", size = 5228605 }, - { url = "https://files.pythonhosted.org/packages/0c/ca/49ca7769c4550107de049ed85208240ba0f330b3f2e316f24534795702ce/pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72", size = 4622245 }, - { url = "https://files.pythonhosted.org/packages/73/48/fac807ce82e5955bcc2718642b94b1bd22a82a6d452aea31cbb678cddf12/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c", size = 5247593 }, - { url = "https://files.pythonhosted.org/packages/d2/95/3e0742fe358c4664aed4fd05d5f5373dcdad0b27af52aa0972568541e3f4/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd", size = 6989008 }, - { url = "https://files.pythonhosted.org/packages/5a/74/fe2ac378e4e202e56d50540d92e1ef4ff34ed687f3c60f6a121bcf99437e/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc", size = 5313824 }, - { url = "https://files.pythonhosted.org/packages/f3/77/2a60dee1adee4e2655ac328dd05c02a955c1cd683b9f1b82ec3feb44727c/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a", size = 5963278 }, - { url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809 }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, -] - [[package]] name = "prettytable" version = "3.17.0" @@ -2597,52 +2082,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801 }, ] -[[package]] -name = "pytest" -version = "9.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801 }, -] - -[[package]] -name = "pytest-asyncio" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, - { name = "pytest" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075 }, -] - -[[package]] -name = "pytest-cov" -version = "7.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "coverage", extra = ["toml"] }, - { name = "pluggy" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424 }, -] - [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -2992,46 +2431,19 @@ name = "runpod-flash-examples" version = "1.0.0" source = { editable = "." } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "pillow" }, - { name = "python-multipart" }, { name = "runpod-flash" }, - { name = "structlog" }, ] [package.dev-dependencies] dev = [ - { name = "mypy" }, - { name = "packaging" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, { name = "ruff" }, - { name = "tomli" }, - { name = "tomli-w" }, ] [package.metadata] -requires-dist = [ - { name = "numpy", specifier = ">=2.0.2" }, - { name = "pillow", specifier = ">=10.0.0" }, - { name = "python-multipart", specifier = ">=0.0.6" }, - { name = "runpod-flash" }, - { name = "structlog", specifier = ">=23.0.0" }, -] +requires-dist = [{ name = "runpod-flash" }] [package.metadata.requires-dev] -dev = [ - { name = "mypy", specifier = ">=1.13.0" }, - { name = "packaging", specifier = ">=21.0.0" }, - { name = "pytest", specifier = ">=8.3.0" }, - { name = "pytest-asyncio", specifier = ">=0.25.0" }, - { name = "pytest-cov", specifier = ">=6.0.0" }, - { name = "ruff", specifier = ">=0.8.0" }, - { name = "tomli", specifier = ">=2.0.0" }, - { name = "tomli-w", specifier = ">=1.0.0" }, -] +dev = [{ name = "ruff", specifier = ">=0.8.0" }] [[package]] name = "s3transfer" @@ -3089,18 +2501,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033 }, ] -[[package]] -name = "structlog" -version = "25.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ef/52/9ba0f43b686e7f3ddfeaa78ac3af750292662284b3661e91ad5494f21dbc/structlog-25.5.0.tar.gz", hash = "sha256:098522a3bebed9153d4570c6d0288abf80a031dfdb2048d59a49e9dc2190fc98", size = 1460830 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl", hash = "sha256:a8453e9b9e636ec59bd9e79bbd4a72f025981b3ba0f5837aebf48f02f37a7f9f", size = 72510 }, -] - [[package]] name = "tomli" version = "2.4.0" @@ -3155,15 +2555,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477 }, ] -[[package]] -name = "tomli-w" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675 }, -] - [[package]] name = "tomlkit" version = "0.14.0"