diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index beb7dc7..4160f36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,8 +30,15 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - cache: "npm" - cache-dependency-path: "src/py_s3_storacha/js/package-lock.json" + + - name: šŸ“¦ Cache npm dependencies + uses: actions/cache@v4 + with: + path: src/py_s3_storacha/js/node_modules + key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('src/py_s3_storacha/js/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node-${{ matrix.node-version }}- + ${{ runner.os }}-node- - name: šŸ“¦ Install Python dependencies run: | diff --git a/.gitignore b/.gitignore index 91b15d4..82e396c 100644 --- a/.gitignore +++ b/.gitignore @@ -217,5 +217,6 @@ downloaded.txt # JavaScript dependencies src/py_s3_storacha/js/node_modules/ -src/py_s3_storacha/js/package-lock.json + +# Config files with credentials config.json diff --git a/examples/diagnose_s3.py b/examples/diagnose_s3.py index 938dfd1..ecee507 100755 --- a/examples/diagnose_s3.py +++ b/examples/diagnose_s3.py @@ -11,90 +11,96 @@ load_dotenv() -def test_s3_connection(access_key, secret_key, region, bucket, endpoint=None, service_name="S3"): +def test_s3_connection( + access_key, secret_key, region, bucket, endpoint=None, service_name="S3" +): """Test S3 connection with given configuration""" - - print(f"\n{'='*70}") + + print(f"\n{'=' * 70}") print(f"Testing {service_name}") - print(f"{'='*70}") + print(f"{'=' * 70}") print(f"Bucket: {bucket}") print(f"Region: {region}") print(f"Endpoint: {endpoint or 'Default AWS'}") print() - + try: # Create S3 client config = { - 'aws_access_key_id': access_key, - 'aws_secret_access_key': secret_key, - 'region_name': region, + "aws_access_key_id": access_key, + "aws_secret_access_key": secret_key, + "region_name": region, } - + if endpoint: - config['endpoint_url'] = endpoint - - s3 = boto3.client('s3', **config) - + config["endpoint_url"] = endpoint + + s3 = boto3.client("s3", **config) + # Test 1: List buckets print("Test 1: Listing all buckets...") try: response = s3.list_buckets() - buckets = [b['Name'] for b in response.get('Buckets', [])] + buckets = [b["Name"] for b in response.get("Buckets", [])] print(f"āœ“ Success! Found {len(buckets)} buckets") if buckets: print(f" Buckets: {', '.join(buckets[:5])}") if len(buckets) > 5: print(f" ... and {len(buckets) - 5} more") except ClientError as e: - print(f"āœ— Failed: {e.response['Error']['Code']} - {e.response['Error']['Message']}") + print( + f"āœ— Failed: {e.response['Error']['Code']} - {e.response['Error']['Message']}" + ) except Exception as e: print(f"āœ— Failed: {type(e).__name__}: {e}") - + # Test 2: Get bucket location print(f"\nTest 2: Getting bucket location for '{bucket}'...") try: response = s3.get_bucket_location(Bucket=bucket) - location = response.get('LocationConstraint') or 'us-east-1' + location = response.get("LocationConstraint") or "us-east-1" print(f"āœ“ Success! Bucket location: {location}") except ClientError as e: - print(f"āœ— Failed: {e.response['Error']['Code']} - {e.response['Error']['Message']}") + print( + f"āœ— Failed: {e.response['Error']['Code']} - {e.response['Error']['Message']}" + ) except Exception as e: print(f"āœ— Failed: {type(e).__name__}: {e}") - + # Test 3: List objects in bucket print(f"\nTest 3: Listing objects in bucket '{bucket}'...") try: response = s3.list_objects_v2(Bucket=bucket, MaxKeys=10) - count = response.get('KeyCount', 0) + count = response.get("KeyCount", 0) print(f"āœ“ Success! Found {count} objects (showing first 10)") - - if 'Contents' in response: - for obj in response['Contents'][:5]: - size_mb = obj['Size'] / (1024 * 1024) + + if "Contents" in response: + for obj in response["Contents"][:5]: + size_mb = obj["Size"] / (1024 * 1024) print(f" - {obj['Key']} ({size_mb:.2f} MB)") if count > 5: print(f" ... and {count - 5} more") - + return True - + except ClientError as e: - error_code = e.response['Error']['Code'] - error_msg = e.response['Error']['Message'] + error_code = e.response["Error"]["Code"] + error_msg = e.response["Error"]["Message"] print(f"āœ— Failed: {error_code} - {error_msg}") - - if error_code == 'AccessDenied': + + if error_code == "AccessDenied": print("\n šŸ’” Possible causes:") print(" - IAM user lacks s3:ListBucket permission") print(" - Bucket policy denies access") print(" - Credentials are invalid or expired") - elif error_code == 'NoSuchBucket': + elif error_code == "NoSuchBucket": print("\n šŸ’” Possible causes:") print(" - Bucket name is incorrect") print(" - Bucket is in a different region") print(" - Bucket doesn't exist") - + return False - + except EndpointConnectionError as e: print(f"āœ— Connection Failed: {e}") print("\n šŸ’” Possible causes:") @@ -102,75 +108,70 @@ def test_s3_connection(access_key, secret_key, region, bucket, endpoint=None, se print(" - Network connectivity issues") print(" - Firewall blocking connection") return False - + except Exception as e: print(f"āœ— Failed: {type(e).__name__}: {e}") return False - + except Exception as e: print(f"āœ— Failed to create S3 client: {type(e).__name__}: {e}") return False def main(): - print("="*70) + print("=" * 70) print("S3 Connection Diagnostics") - print("="*70) - + print("=" * 70) + # Load credentials from .env access_key = os.getenv("S3_ACCESS_KEY_ID") secret_key = os.getenv("S3_SECRET_ACCESS_KEY") region = os.getenv("S3_REGION", "us-east-1") bucket = os.getenv("S3_BUCKET_NAME") custom_endpoint = os.getenv("S3_ENDPOINT_URL") - + if not all([access_key, secret_key, bucket]): print("\nāŒ Missing required environment variables:") print(" S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_BUCKET_NAME") return 1 - - print(f"\nCredentials loaded from .env:") - print(f" Access Key: {access_key[:10]}...") + + print("\nCredentials loaded from .env:") + if access_key: + print(f" Access Key: {access_key[:10]}...") print(f" Bucket: {bucket}") print(f" Region: {region}") if custom_endpoint: print(f" Custom Endpoint: {custom_endpoint}") - + # Test different endpoint configurations test_configs = [] - + # 1. Custom endpoint from .env (if provided) if custom_endpoint: - test_configs.append({ - 'name': 'Custom Endpoint (from .env)', - 'endpoint': custom_endpoint - }) - + test_configs.append( + {"name": "Custom Endpoint (from .env)", "endpoint": custom_endpoint} + ) + # 2. Standard AWS S3 (no custom endpoint) - test_configs.append({ - 'name': 'Standard AWS S3', - 'endpoint': None - }) - + test_configs.append({"name": "Standard AWS S3", "endpoint": None}) + # 3. AWS S3 with explicit regional endpoint - test_configs.append({ - 'name': 'AWS S3 Regional Endpoint', - 'endpoint': f'https://s3.{region}.amazonaws.com' - }) - + test_configs.append( + { + "name": "AWS S3 Regional Endpoint", + "endpoint": f"https://s3.{region}.amazonaws.com", + } + ) + # 4. AWS S3 with path-style endpoint - test_configs.append({ - 'name': 'AWS S3 Path-Style', - 'endpoint': f'https://s3.{region}.amazonaws.com' - }) - + test_configs.append( + {"name": "AWS S3 Path-Style", "endpoint": f"https://s3.{region}.amazonaws.com"} + ) + # 5. LocalStack (if it looks like local testing) - if custom_endpoint and 'localhost' in custom_endpoint: - test_configs.append({ - 'name': 'LocalStack', - 'endpoint': 'http://localhost:4566' - }) - + if custom_endpoint and "localhost" in custom_endpoint: + test_configs.append({"name": "LocalStack", "endpoint": "http://localhost:4566"}) + # Run tests results = [] for config in test_configs: @@ -179,70 +180,70 @@ def main(): secret_key=secret_key, region=region, bucket=bucket, - endpoint=config['endpoint'], - service_name=config['name'] + endpoint=config["endpoint"], + service_name=config["name"], ) - results.append((config['name'], success)) - + results.append((config["name"], success)) + # Summary - print("\n" + "="*70) + print("\n" + "=" * 70) print("Summary") - print("="*70) - + print("=" * 70) + successful = [name for name, success in results if success] failed = [name for name, success in results if not success] - + if successful: print(f"\nāœ… Working configurations ({len(successful)}):") for name in successful: print(f" - {name}") - + if failed: print(f"\nāŒ Failed configurations ({len(failed)}):") for name in failed: print(f" - {name}") - + # Recommendations - print("\n" + "="*70) + print("\n" + "=" * 70) print("Recommendations") - print("="*70) - + print("=" * 70) + if successful: - print(f"\nāœ… Use this configuration in your .env:") - winning_config = [c for c in test_configs if c['name'] == successful[0]][0] - - if winning_config['endpoint']: + print("\nāœ… Use this configuration in your .env:") + winning_config = [c for c in test_configs if c["name"] == successful[0]][0] + + if winning_config["endpoint"]: print(f"\nS3_ENDPOINT_URL={winning_config['endpoint']}") else: - print(f"\n# Remove or comment out S3_ENDPOINT_URL for standard AWS") - print(f"# S3_ENDPOINT_URL=") - + print("\n# Remove or comment out S3_ENDPOINT_URL for standard AWS") + print("# S3_ENDPOINT_URL=") + print(f"S3_REGION={region}") print(f"S3_BUCKET_NAME={bucket}") - + else: print("\nāŒ No working configuration found. Possible issues:") print("\n1. Check AWS Credentials:") print(" - Verify access key and secret key are correct") print(" - Check if credentials have expired") print(" - Ensure IAM user has necessary permissions") - + print("\n2. Check Bucket Configuration:") print(" - Verify bucket name is correct") print(" - Check if bucket exists in the specified region") print(" - Verify bucket is not in a different AWS account") - + print("\n3. Check IAM Permissions:") print(" Required permissions:") print(" - s3:ListBucket") print(" - s3:GetObject") print(" - s3:GetBucketLocation") - + print("\n4. Test with AWS CLI:") print(f" aws s3 ls s3://{bucket}/ --region {region}") - - print("\n" + "="*70) - + + print("\n" + "=" * 70) + return 0 if successful else 1 diff --git a/examples/migrate.py b/examples/migrate.py index 1bf58ba..3a6fb99 100644 --- a/examples/migrate.py +++ b/examples/migrate.py @@ -5,13 +5,18 @@ import json import sys -from pathlib import Path -from py_s3_storacha import S3Config, StorachaConfig, MigrationConfig, S3ToStorachaMigrator, MigrationRequest +from py_s3_storacha import ( + S3Config, + StorachaConfig, + MigrationConfig, + S3ToStorachaMigrator, + MigrationRequest, +) def load_config(config_path: str = "examples/config.json"): """Load configuration from JSON file""" - with open(config_path, 'r') as f: + with open(config_path, "r") as f: return json.load(f) @@ -19,75 +24,75 @@ def list_s3_files(s3_config: S3Config, prefix: str = ""): """List all files in S3 bucket""" import boto3 from botocore.exceptions import ClientError - - print("\n" + "="*70) + + print("\n" + "=" * 70) print("šŸ“‚ Listing S3 Bucket Contents") - print("="*70) + print("=" * 70) print(f"Bucket: {s3_config.bucket_name}") print(f"Region: {s3_config.region}") if prefix: print(f"Prefix: {prefix}") print() - + # Create S3 client client_config = { - 'aws_access_key_id': s3_config.access_key_id, - 'aws_secret_access_key': s3_config.secret_access_key, - 'region_name': s3_config.region, + "aws_access_key_id": s3_config.access_key_id, + "aws_secret_access_key": s3_config.secret_access_key, + "region_name": s3_config.region, } - + if s3_config.endpoint_url: - client_config['endpoint_url'] = s3_config.endpoint_url - - s3 = boto3.client('s3', **client_config) - + client_config["endpoint_url"] = s3_config.endpoint_url + + s3 = boto3.client("s3", **client_config) + try: # List all objects - paginator = s3.get_paginator('list_objects_v2') + paginator = s3.get_paginator("list_objects_v2") pages = paginator.paginate(Bucket=s3_config.bucket_name, Prefix=prefix) - + all_objects = [] total_size = 0 - + for page in pages: - if 'Contents' in page: - for obj in page['Contents']: + if "Contents" in page: + for obj in page["Contents"]: all_objects.append(obj) - total_size += obj['Size'] - + total_size += obj["Size"] + if not all_objects: print("āš ļø No files found in bucket") return [] - + # Display results print(f"āœ… Found {len(all_objects)} files") print(f"šŸ’¾ Total size: {total_size:,} bytes ({total_size / (1024**2):.2f} MB)") print() print("Files:") print("-" * 70) - + for i, obj in enumerate(all_objects, 1): - size_mb = obj['Size'] / (1024**2) - modified = obj['LastModified'].strftime('%Y-%m-%d %H:%M:%S') + size_mb = obj["Size"] / (1024**2) + modified = obj["LastModified"].strftime("%Y-%m-%d %H:%M:%S") print(f"{i:3d}. {obj['Key']}") print(f" Size: {size_mb:.2f} MB | Modified: {modified}") - + print("-" * 70) - + return all_objects - + except ClientError as e: - error_code = e.response['Error']['Code'] - error_msg = e.response['Error']['Message'] + error_code = e.response["Error"]["Code"] + error_msg = e.response["Error"]["Message"] print(f"āŒ Error: {error_code} - {error_msg}") - - if error_code == 'AccessDenied': + + if error_code == "AccessDenied": print("\nšŸ’” Check your AWS credentials and IAM permissions") - elif error_code == 'NoSuchBucket': + elif error_code == "NoSuchBucket": print("\nšŸ’” Bucket doesn't exist or is in a different region") - + return [] - + except Exception as e: print(f"āŒ Unexpected error: {type(e).__name__}: {e}") return [] @@ -98,98 +103,98 @@ def migrate_to_storacha( storacha_config: StorachaConfig, migration_config: MigrationConfig, source_prefix: str = "", - dest_prefix: str = "files/" + dest_prefix: str = "files/", ): """Migrate files from S3 to Storacha""" - - print("\n" + "="*70) + + print("\n" + "=" * 70) print("šŸš€ Starting Migration to Storacha") - print("="*70) + print("=" * 70) print(f"Source: s3://{s3_config.bucket_name}/{source_prefix}") print(f"Destination: storacha://{storacha_config.space_name}/{dest_prefix}") print() - + if migration_config.dry_run: print("āš ļø DRY RUN MODE - No actual migration will occur") else: print("āš ļø ACTUAL MIGRATION - Data will be transferred to Storacha!") - + print() - + # Create migrator print("šŸ”§ Creating migrator...") migrator = S3ToStorachaMigrator( s3_config=s3_config, storacha_config=storacha_config, - migration_config=migration_config + migration_config=migration_config, ) - + # Create migration request - request = MigrationRequest( - source_path=source_prefix, - destination_path=dest_prefix - ) - + request = MigrationRequest(source_path=source_prefix, destination_path=dest_prefix) + # Start migration print("ā³ Starting migration...") print("-" * 70) - + # Run async migration import asyncio + result = asyncio.run(migrator.migrate(request)) - + print("-" * 70) - + # Display results - print("\n" + "="*70) + print("\n" + "=" * 70) print("šŸ“Š Migration Results") - print("="*70) - + print("=" * 70) + if result.success: - print(f"āœ… Status: SUCCESS") + print("āœ… Status: SUCCESS") else: - print(f"āŒ Status: FAILED") - if result.error: - print(f"Error: {result.error}") - + print("āŒ Status: FAILED") + if result.errors: + print(f"Errors: {', '.join(result.errors)}") + print(f"šŸ“¦ Objects migrated: {result.objects_migrated}") - print(f"šŸ’¾ Total size: {result.total_size_bytes:,} bytes ({result.total_size_bytes / (1024**2):.2f} MB)") + print( + f"šŸ’¾ Total size: {result.total_size_bytes:,} bytes ({result.total_size_bytes / (1024**2):.2f} MB)" + ) print(f"ā±ļø Duration: {result.duration_seconds:.2f} seconds") - + if result.warnings: print(f"\nāš ļø Warnings ({len(result.warnings)}):") for i, warning in enumerate(result.warnings, 1): print(f"{i}. {warning}") - + if result.errors: print(f"\nāŒ Errors ({len(result.errors)}):") for i, error in enumerate(result.errors, 1): print(f"{i}. {error}") - + if result.skipped_objects: print(f"\nā­ļø Skipped ({len(result.skipped_objects)}):") for obj in result.skipped_objects[:5]: print(f" - {obj}") if len(result.skipped_objects) > 5: print(f" ... and {len(result.skipped_objects) - 5} more") - + if result.failed_objects: print(f"\nāŒ Failed ({len(result.failed_objects)}):") for obj in result.failed_objects[:5]: print(f" - {obj}") if len(result.failed_objects) > 5: print(f" ... and {len(result.failed_objects) - 5} more") - - print("="*70) - + + print("=" * 70) + return result def main(): - print("="*70) + print("=" * 70) print("S3 to Storacha Migration Tool") - print("="*70) - + print("=" * 70) + # Load configuration try: config = load_config() @@ -197,55 +202,55 @@ def main(): except Exception as e: print(f"āŒ Failed to load configuration: {e}") return 1 - + # Create config objects s3_config = S3Config( - access_key_id=config['s3']['access_key_id'], - secret_access_key=config['s3']['secret_access_key'], - region=config['s3']['region'], - bucket_name=config['s3']['bucket_name'], - endpoint_url=config['s3'].get('endpoint_url') + access_key_id=config["s3"]["access_key_id"], + secret_access_key=config["s3"]["secret_access_key"], + region=config["s3"]["region"], + bucket_name=config["s3"]["bucket_name"], + endpoint_url=config["s3"].get("endpoint_url"), ) - + storacha_config = StorachaConfig( - api_key=config['storacha']['api_key'], - endpoint_url=config['storacha']['endpoint_url'], - space_name=config['storacha']['space_name'] + api_key=config["storacha"]["api_key"], + endpoint_url=config["storacha"]["endpoint_url"], + space_name=config["storacha"]["space_name"], ) - + migration_config = MigrationConfig( - batch_size=config['migration']['batch_size'], - timeout_seconds=config['migration']['timeout_seconds'], - retry_attempts=config['migration']['retry_attempts'], - verbose=config['migration']['verbose'], - dry_run=config['migration']['dry_run'] + batch_size=config["migration"]["batch_size"], + timeout_seconds=config["migration"]["timeout_seconds"], + retry_attempts=config["migration"]["retry_attempts"], + verbose=config["migration"]["verbose"], + dry_run=config["migration"]["dry_run"], ) - + # Step 1: List all files in S3 objects = list_s3_files(s3_config) - + if not objects: print("\nāŒ No files to migrate") return 1 - + # Step 2: Ask for confirmation - print("\n" + "="*70) + print("\n" + "=" * 70) print("āš ļø Migration Confirmation") - print("="*70) + print("=" * 70) print(f"You are about to migrate {len(objects)} files from S3 to Storacha") print(f"Total size: {sum(obj['Size'] for obj in objects) / (1024**2):.2f} MB") - + if migration_config.dry_run: print("\nāœ“ Running in DRY RUN mode - no actual migration will occur") - response = 'y' + response = "y" else: print("\nāš ļø This will upload data to Storacha/IPFS") response = input("\nProceed with migration? (y/n): ").lower().strip() - - if response != 'y': + + if response != "y": print("\nāŒ Migration cancelled") return 0 - + # Step 3: Migrate to Storacha # Use empty string for root (not "/") to match all objects result = migrate_to_storacha( @@ -253,9 +258,9 @@ def main(): storacha_config=storacha_config, migration_config=migration_config, source_prefix="Aunty Funke documents/", # Migrate files from this folder - dest_prefix="migrated/" + dest_prefix="migrated/", ) - + if result.success: print("\nšŸŽ‰ Migration completed successfully!") print("Your files are now on Storacha/IPFS!") diff --git a/src/py_s3_storacha/__init__.py b/src/py_s3_storacha/__init__.py index e483d73..90dfd4d 100644 --- a/src/py_s3_storacha/__init__.py +++ b/src/py_s3_storacha/__init__.py @@ -77,7 +77,7 @@ __all__ = [ # Configuration "S3Config", - "StorachaConfig", + "StorachaConfig", "MigrationConfig", "ConfigurationParser", # Exceptions diff --git a/src/py_s3_storacha/api.py b/src/py_s3_storacha/api.py index 0130c7e..16c5549 100644 --- a/src/py_s3_storacha/api.py +++ b/src/py_s3_storacha/api.py @@ -1,23 +1,21 @@ """Python API layer for S3 to Storacha migration operations.""" import asyncio -import logging import time -from typing import Optional, Dict, Any, List -from datetime import datetime +from typing import Optional, Dict, Any from .config import S3Config, StorachaConfig, MigrationConfig from .js_wrapper import JSWrapperManager from .models import ( - MigrationRequest, - MigrationResult, - MigrationProgress, + MigrationRequest, + MigrationResult, + MigrationProgress, MigrationStatus, - ProgressCallback + ProgressCallback, ) from .progress import ProgressReporter from .exceptions import MigrationError, ConfigurationError, JSWrapperError -from .error_handler import with_error_handling, get_retry_handler +from .error_handler import with_error_handling from .logging_config import get_logger @@ -26,16 +24,16 @@ class S3ToStorachaMigrator: """Main class for orchestrating S3 to Storacha migrations.""" - + def __init__( self, s3_config: S3Config, storacha_config: StorachaConfig, migration_config: Optional[MigrationConfig] = None, - js_script_path: Optional[str] = None + js_script_path: Optional[str] = None, ) -> None: """Initialize the migrator with configuration. - + Args: s3_config: S3 connection and authentication configuration storacha_config: Storacha connection and authentication configuration @@ -45,76 +43,86 @@ def __init__( self.s3_config = s3_config self.storacha_config = storacha_config self.migration_config = migration_config or MigrationConfig() - + # Initialize JavaScript wrapper manager self.js_wrapper = JSWrapperManager(js_script_path) - + # Initialize progress reporter self._progress_reporter = ProgressReporter() - + # Migration state self._current_migration: Optional[MigrationRequest] = None self._migration_start_time: Optional[float] = None self._cancelled = False - - @with_error_handling("migration", error_types=(MigrationError, JSWrapperError, asyncio.TimeoutError)) + + @with_error_handling( + "migration", error_types=(MigrationError, JSWrapperError, asyncio.TimeoutError) + ) async def migrate( self, request: MigrationRequest, - progress_callback: Optional[ProgressCallback] = None + progress_callback: Optional[ProgressCallback] = None, ) -> MigrationResult: """Execute a migration operation. - + Args: request: Migration request parameters progress_callback: Optional callback for progress updates - + Returns: Migration result with status and statistics - + Raises: MigrationError: If migration fails ConfigurationError: If configuration is invalid """ - logger.info(f"Starting migration from {request.source_path} to {request.destination_path}") - + logger.info( + f"Starting migration from {request.source_path} to {request.destination_path}" + ) + # Validate inputs self._validate_migration_request(request) - + # Set up migration state self._current_migration = request self._migration_start_time = time.time() self._cancelled = False - + # Set up progress reporting self._progress_reporter.reset() if progress_callback: self._progress_reporter.add_callback(progress_callback) - + try: # Validate environment before starting await self.js_wrapper.validate_environment() - + # Execute migration workflow result = await self._execute_migration_workflow(request) - - logger.info(f"Migration completed successfully: {result.objects_migrated} objects migrated") + + logger.info( + f"Migration completed successfully: {result.objects_migrated} objects migrated" + ) return result - + except Exception as e: logger.error(f"Migration failed: {e}") - + # Create failure result - duration = time.time() - self._migration_start_time if self._migration_start_time else 0 + duration = ( + time.time() - self._migration_start_time + if self._migration_start_time + else 0 + ) result = MigrationResult( success=False, status=MigrationStatus.FAILED, objects_migrated=0, total_size_bytes=0, duration_seconds=duration, - errors=[str(e)] + errors=[str(e)], ) - + # Re-raise as MigrationError if not already if not isinstance(e, MigrationError): raise MigrationError( @@ -122,56 +130,58 @@ async def migrate( operation="migrate", source_path=request.source_path, destination_path=request.destination_path, - original_error=e + original_error=e, ) raise - + finally: # Clean up migration state self._current_migration = None self._migration_start_time = None self._cancelled = False - + # Clean up progress reporting if progress_callback: self._progress_reporter.remove_callback(progress_callback) - - async def _execute_migration_workflow(self, request: MigrationRequest) -> MigrationResult: + + async def _execute_migration_workflow( + self, request: MigrationRequest + ) -> MigrationResult: """Execute the core migration workflow. - + Args: request: Migration request parameters - + Returns: Migration result - + Raises: MigrationError: If any step of the workflow fails """ # Prepare JavaScript wrapper input js_input = self._prepare_js_input(request) - + # Report initial progress self._progress_reporter.update_progress( current_object="Initializing migration...", current_operation="initialization", completed_objects=0, - transferred_bytes=0 + transferred_bytes=0, ) - + try: # Execute JavaScript wrapper with progress monitoring js_result = await self._execute_with_progress_monitoring(js_input) - + # Parse and validate JavaScript result result = self._parse_js_result(js_result, request) - + return result - + except asyncio.TimeoutError: raise MigrationError.timeout_error( operation="migration", - timeout_seconds=self.migration_config.timeout_seconds + timeout_seconds=self.migration_config.timeout_seconds, ) except JSWrapperError as e: raise MigrationError( @@ -179,59 +189,59 @@ async def _execute_migration_workflow(self, request: MigrationRequest) -> Migrat operation="javascript_execution", source_path=request.source_path, destination_path=request.destination_path, - original_error=e + original_error=e, ) - + def _validate_migration_request(self, request: MigrationRequest) -> None: """Validate migration request parameters. - + Args: request: Migration request to validate - + Raises: MigrationError: If request validation fails """ try: # Basic request validation is handled by MigrationRequest.__post_init__ # Additional validation can be added here - + # Validate path formats (basic checks) if not request.source_path.strip(): raise MigrationError.validation_error( "Source path cannot be empty or whitespace only", - source_path=request.source_path + source_path=request.source_path, ) - + if not request.destination_path.strip(): raise MigrationError.validation_error( "Destination path cannot be empty or whitespace only", - destination_path=request.destination_path + destination_path=request.destination_path, ) - + # Validate pattern syntax if provided if request.include_pattern and not request.include_pattern.strip(): raise MigrationError.validation_error( "Include pattern cannot be empty or whitespace only" ) - + if request.exclude_pattern and not request.exclude_pattern.strip(): raise MigrationError.validation_error( "Exclude pattern cannot be empty or whitespace only" ) - + except ValueError as e: raise MigrationError.validation_error( str(e), source_path=request.source_path, - destination_path=request.destination_path + destination_path=request.destination_path, ) - + def _prepare_js_input(self, request: MigrationRequest) -> Dict[str, Any]: """Prepare input data for JavaScript wrapper execution. - + Args: request: Migration request parameters - + Returns: Dictionary containing all data needed by JavaScript wrapper """ @@ -242,16 +252,16 @@ def _prepare_js_input(self, request: MigrationRequest) -> Dict[str, Any]: "region": self.s3_config.region, "bucketName": self.s3_config.bucket_name, } - + if self.s3_config.endpoint_url: s3_data["endpointUrl"] = self.s3_config.endpoint_url - + storacha_data = { "apiKey": self.storacha_config.api_key, "endpointUrl": self.storacha_config.endpoint_url, "spaceName": self.storacha_config.space_name, } - + migration_data = { "sourcePath": request.source_path, "destinationPath": request.destination_path, @@ -263,28 +273,26 @@ def _prepare_js_input(self, request: MigrationRequest) -> Dict[str, Any]: "overwriteExisting": request.overwrite_existing, "verifyChecksums": request.verify_checksums, } - + if request.include_pattern: migration_data["includePattern"] = request.include_pattern - + if request.exclude_pattern: migration_data["excludePattern"] = request.exclude_pattern - - return { - "s3": s3_data, - "storacha": storacha_data, - "migration": migration_data - } - - async def _execute_with_progress_monitoring(self, js_input: Dict[str, Any]) -> Dict[str, Any]: + + return {"s3": s3_data, "storacha": storacha_data, "migration": migration_data} + + async def _execute_with_progress_monitoring( + self, js_input: Dict[str, Any] + ) -> Dict[str, Any]: """Execute JavaScript wrapper with progress monitoring and timeout handling. - + Args: js_input: Input data for JavaScript wrapper - + Returns: JavaScript wrapper result - + Raises: asyncio.TimeoutError: If operation times out MigrationError: If operation is cancelled @@ -292,25 +300,21 @@ async def _execute_with_progress_monitoring(self, js_input: Dict[str, Any]) -> D # Create a task for the JavaScript execution js_task = asyncio.create_task( self.js_wrapper.execute_migration( - js_input["s3"], - js_input["storacha"], - js_input["migration"] + js_input["s3"], js_input["storacha"], js_input["migration"] ) ) - + # Create a task for progress monitoring - progress_task = asyncio.create_task( - self._monitor_progress() - ) - + progress_task = asyncio.create_task(self._monitor_progress()) + try: # Wait for either completion or timeout done, pending = await asyncio.wait( [js_task, progress_task], timeout=self.migration_config.timeout_seconds, - return_when=asyncio.FIRST_COMPLETED + return_when=asyncio.FIRST_COMPLETED, ) - + # Cancel any pending tasks for task in pending: task.cancel() @@ -318,22 +322,21 @@ async def _execute_with_progress_monitoring(self, js_input: Dict[str, Any]) -> D await task except asyncio.CancelledError: pass - + # Check if we timed out if not done: js_task.cancel() progress_task.cancel() raise asyncio.TimeoutError() - + # Check if migration was cancelled if self._cancelled: js_task.cancel() progress_task.cancel() raise MigrationError( - "Migration was cancelled by user", - operation="migration_cancelled" + "Migration was cancelled by user", operation="migration_cancelled" ) - + # Get the result from the JavaScript task if js_task in done: return await js_task @@ -341,87 +344,95 @@ async def _execute_with_progress_monitoring(self, js_input: Dict[str, Any]) -> D # This shouldn't happen, but handle it gracefully raise MigrationError( "JavaScript execution completed unexpectedly", - operation="javascript_execution" + operation="javascript_execution", ) - + except asyncio.TimeoutError: # Clean up tasks js_task.cancel() progress_task.cancel() raise - + async def _monitor_progress(self) -> None: """Monitor migration progress and provide periodic updates. - + This method runs in parallel with the JavaScript execution and provides periodic progress updates based on estimated progress. """ progress_interval = 2.0 # Update progress every 2 seconds start_time = time.time() - + # Simulate progress updates (in a real implementation, this would # communicate with the JavaScript process to get actual progress) estimated_objects = 100 # Default estimate estimated_bytes = 1024 * 1024 * 100 # 100MB default estimate - + # Set initial totals self._progress_reporter.update_progress( total_objects=estimated_objects, total_bytes=estimated_bytes, - current_operation="migrating" + current_operation="migrating", ) - + while not self._cancelled: try: await asyncio.sleep(progress_interval) - + elapsed_time = time.time() - start_time - + # Create estimated progress (this is a placeholder - in a real # implementation, we would get actual progress from the JS process) - estimated_completion = min(elapsed_time / self.migration_config.timeout_seconds, 0.95) - + estimated_completion = min( + elapsed_time / self.migration_config.timeout_seconds, 0.95 + ) + self._progress_reporter.update_progress( - current_object=f"Processing objects... (estimated)", + current_object="Processing objects... (estimated)", completed_objects=int(estimated_objects * estimated_completion), transferred_bytes=int(estimated_bytes * estimated_completion), - current_operation="migrating" + current_operation="migrating", ) - + except asyncio.CancelledError: break except Exception as e: logger.warning(f"Error in progress monitoring: {e}") break - - def _parse_js_result(self, js_result: Dict[str, Any], request: MigrationRequest) -> MigrationResult: + + def _parse_js_result( + self, js_result: Dict[str, Any], request: MigrationRequest + ) -> MigrationResult: """Parse JavaScript wrapper result into Python-native types. - + Args: js_result: Raw result from JavaScript wrapper request: Original migration request - + Returns: Parsed migration result - + Raises: MigrationError: If result parsing fails """ try: # Calculate duration - duration = time.time() - self._migration_start_time if self._migration_start_time else 0 - + duration = ( + time.time() - self._migration_start_time + if self._migration_start_time + else 0 + ) + # Extract basic result data success = js_result.get("success", False) objects_migrated = js_result.get("objectsMigrated", 0) total_size_bytes = js_result.get("totalSizeBytes", 0) - + # Extract error and warning lists errors = js_result.get("errors", []) warnings = js_result.get("warnings", []) skipped_objects = js_result.get("skippedObjects", []) failed_objects = js_result.get("failedObjects", []) - + # Determine status if success and not errors and not failed_objects: status = MigrationStatus.COMPLETED @@ -429,7 +440,7 @@ def _parse_js_result(self, js_result: Dict[str, Any], request: MigrationRequest) status = MigrationStatus.FAILED else: status = MigrationStatus.COMPLETED # Completed with warnings - + # Create result object result = MigrationResult( success=success, @@ -439,10 +450,14 @@ def _parse_js_result(self, js_result: Dict[str, Any], request: MigrationRequest) duration_seconds=duration, errors=errors if isinstance(errors, list) else [str(errors)], warnings=warnings if isinstance(warnings, list) else [str(warnings)], - skipped_objects=skipped_objects if isinstance(skipped_objects, list) else [], - failed_objects=failed_objects if isinstance(failed_objects, list) else [] + skipped_objects=skipped_objects + if isinstance(skipped_objects, list) + else [], + failed_objects=failed_objects + if isinstance(failed_objects, list) + else [], ) - + # Report final progress self._progress_reporter.update_progress( current_object="Migration completed", @@ -450,23 +465,23 @@ def _parse_js_result(self, js_result: Dict[str, Any], request: MigrationRequest) total_objects=objects_migrated, transferred_bytes=total_size_bytes, total_bytes=total_size_bytes, - current_operation="completed" + current_operation="completed", ) - + return result - + except (KeyError, TypeError, ValueError) as e: raise MigrationError( f"Failed to parse JavaScript wrapper result: {e}", operation="result_parsing", source_path=request.source_path, destination_path=request.destination_path, - original_error=e + original_error=e, ) - + def cancel_migration(self) -> None: """Cancel the current migration operation. - + Note: This sets a cancellation flag, but the actual cancellation depends on the JavaScript wrapper implementation. """ @@ -475,59 +490,61 @@ def cancel_migration(self) -> None: self._cancelled = True else: logger.warning("No active migration to cancel") - + @property def is_migration_active(self) -> bool: """Check if a migration is currently active.""" return self._current_migration is not None - + @property def current_migration_request(self) -> Optional[MigrationRequest]: """Get the current migration request if active.""" return self._current_migration - + def add_progress_callback(self, callback: ProgressCallback) -> None: """Add a progress callback to receive updates during migration. - + Args: callback: Function to call with progress updates """ self._progress_reporter.add_callback(callback) - + def remove_progress_callback(self, callback: ProgressCallback) -> None: """Remove a progress callback. - + Args: callback: Function to remove from callbacks """ self._progress_reporter.remove_callback(callback) - + def get_current_progress(self) -> Optional[MigrationProgress]: """Get current migration progress if a migration is active. - + Returns: Current migration progress, or None if no migration is active """ if not self.is_migration_active: return None - + return self._progress_reporter.get_current_progress() - + @property def migration_duration(self) -> Optional[float]: """Get the duration of the current migration in seconds. - + Returns: Duration in seconds, or None if no migration is active """ if not self._migration_start_time: return None - + return time.time() - self._migration_start_time # Convenience function for simple migrations -@with_error_handling("simple_migration", error_types=(MigrationError, ConfigurationError, JSWrapperError)) +@with_error_handling( + "simple_migration", error_types=(MigrationError, ConfigurationError, JSWrapperError) +) async def migrate_s3_to_storacha( s3_config: S3Config, storacha_config: StorachaConfig, @@ -535,10 +552,10 @@ async def migrate_s3_to_storacha( destination_path: str, migration_config: Optional[MigrationConfig] = None, progress_callback: Optional[ProgressCallback] = None, - **kwargs + **kwargs, ) -> MigrationResult: """Convenience function for simple S3 to Storacha migrations. - + Args: s3_config: S3 connection and authentication configuration storacha_config: Storacha connection and authentication configuration @@ -547,26 +564,24 @@ async def migrate_s3_to_storacha( migration_config: Migration-specific settings (optional) progress_callback: Optional callback for progress updates **kwargs: Additional parameters for MigrationRequest - + Returns: Migration result with status and statistics - + Raises: MigrationError: If migration fails ConfigurationError: If configuration is invalid """ # Create migration request request = MigrationRequest( - source_path=source_path, - destination_path=destination_path, - **kwargs + source_path=source_path, destination_path=destination_path, **kwargs ) - + # Create migrator and execute migrator = S3ToStorachaMigrator( s3_config=s3_config, storacha_config=storacha_config, - migration_config=migration_config + migration_config=migration_config, ) - - return await migrator.migrate(request, progress_callback) \ No newline at end of file + + return await migrator.migrate(request, progress_callback) diff --git a/src/py_s3_storacha/auth_helper.py b/src/py_s3_storacha/auth_helper.py index a6c8843..a92d2eb 100644 --- a/src/py_s3_storacha/auth_helper.py +++ b/src/py_s3_storacha/auth_helper.py @@ -10,20 +10,17 @@ class StorachaAuthHelper: """Helper class for managing Storacha authentication.""" - + @staticmethod def check_cli_installed() -> Tuple[bool, Optional[str]]: """Check if Storacha CLI is installed. - + Returns: Tuple of (is_installed, version_string) """ try: result = subprocess.run( - ["storacha", "--version"], - capture_output=True, - text=True, - timeout=5 + ["storacha", "--version"], capture_output=True, text=True, timeout=5 ) if result.returncode == 0: version = result.stdout.strip() @@ -31,20 +28,17 @@ def check_cli_installed() -> Tuple[bool, Optional[str]]: return False, None except (subprocess.SubprocessError, FileNotFoundError): return False, None - + @staticmethod def check_authenticated() -> Tuple[bool, Optional[str]]: """Check if user is authenticated with Storacha CLI. - + Returns: Tuple of (is_authenticated, user_did) """ try: result = subprocess.run( - ["storacha", "whoami"], - capture_output=True, - text=True, - timeout=5 + ["storacha", "whoami"], capture_output=True, text=True, timeout=5 ) if result.returncode == 0: user_did = result.stdout.strip() @@ -52,51 +46,48 @@ def check_authenticated() -> Tuple[bool, Optional[str]]: return False, None except (subprocess.SubprocessError, FileNotFoundError): return False, None - + @staticmethod def list_spaces() -> List[dict]: """List available Storacha spaces. - + Returns: List of space dictionaries with 'did', 'name', and 'current' keys """ try: result = subprocess.run( - ["storacha", "space", "ls"], - capture_output=True, - text=True, - timeout=10 + ["storacha", "space", "ls"], capture_output=True, text=True, timeout=10 ) - + if result.returncode != 0: return [] - + spaces = [] - for line in result.stdout.strip().split('\n'): + for line in result.stdout.strip().split("\n"): if not line.strip(): continue - + # Parse format: "* did:key:... space-name" or " did:key:... space-name" - is_current = line.startswith('*') - parts = line.strip().lstrip('*').strip().split(None, 1) - + is_current = line.startswith("*") + parts = line.strip().lstrip("*").strip().split(None, 1) + if len(parts) >= 1: space = { - 'did': parts[0], - 'name': parts[1] if len(parts) > 1 else None, - 'current': is_current + "did": parts[0], + "name": parts[1] if len(parts) > 1 else None, + "current": is_current, } spaces.append(space) - + return spaces - + except (subprocess.SubprocessError, FileNotFoundError): return [] - + @staticmethod def install_cli() -> bool: """Install Storacha CLI using npm. - + Returns: True if installation successful """ @@ -106,102 +97,104 @@ def install_cli() -> bool: ["npm", "install", "-g", "@storacha/cli"], capture_output=True, text=True, - timeout=120 + timeout=120, ) - + if result.returncode == 0: print("āœ“ Storacha CLI installed successfully") return True else: print(f"āœ— Installation failed: {result.stderr}") return False - + except Exception as e: print(f"āœ— Installation failed: {e}") return False - + @staticmethod def login(email: str) -> bool: """Login to Storacha with email. - + Args: email: Email address for authentication - + Returns: True if login successful """ try: print(f"\nLogging in to Storacha with: {email}") print("āš ļø Check your email for verification link!\n") - + result = subprocess.run( ["storacha", "login", email], - timeout=300 # 5 minutes for user to verify email + timeout=300, # 5 minutes for user to verify email ) - + return result.returncode == 0 - + except subprocess.TimeoutExpired: print("\nāœ— Login timed out - verification link not clicked") return False except Exception as e: print(f"\nāœ— Login failed: {e}") return False - + @staticmethod def create_space(name: str) -> bool: """Create a new Storacha space. - + Args: name: Name for the new space - + Returns: True if space created successfully """ try: print(f"\nCreating space: {name}") - + result = subprocess.run( ["storacha", "space", "create", name], capture_output=True, text=True, - timeout=60 + timeout=60, ) - + if result.returncode == 0: print(f"āœ“ Space '{name}' created successfully") return True else: print(f"āœ— Failed to create space: {result.stderr}") return False - + except Exception as e: print(f"āœ— Failed to create space: {e}") return False - + @classmethod - def setup_authentication(cls, email: Optional[str] = None, space_name: Optional[str] = None) -> bool: + def setup_authentication( + cls, email: Optional[str] = None, space_name: Optional[str] = None + ) -> bool: """Interactive setup for Storacha authentication. - + Args: email: Email address (will prompt if not provided) space_name: Space name (will prompt if not provided) - + Returns: True if setup successful """ - print("\n" + "="*60) + print("\n" + "=" * 60) print("Storacha Authentication Setup") - print("="*60 + "\n") - + print("=" * 60 + "\n") + # Check if CLI is installed cli_installed, version = cls.check_cli_installed() - + if not cli_installed: print("Storacha CLI is not installed.") response = input("Install it now? (y/n): ").strip().lower() - - if response == 'y': + + if response == "y": if not cls.install_cli(): return False else: @@ -210,56 +203,56 @@ def setup_authentication(cls, email: Optional[str] = None, space_name: Optional[ return False else: print(f"āœ“ Storacha CLI installed: {version}") - + # Check if authenticated authenticated, user_did = cls.check_authenticated() - + if not authenticated: print("\nNot authenticated with Storacha.") - + if not email: email = input("Enter your email address: ").strip() - + if not cls.login(email): return False - + print("āœ“ Authentication successful") else: print(f"āœ“ Already authenticated: {user_did}") - + # Check spaces spaces = cls.list_spaces() - + if spaces: print(f"\nāœ“ Found {len(spaces)} space(s):") for space in spaces: - marker = "*" if space['current'] else " " - name = space['name'] or "(unnamed)" + marker = "*" if space["current"] else " " + name = space["name"] or "(unnamed)" print(f" {marker} {name} - {space['did']}") else: print("\nNo spaces found.") - + if not space_name: space_name = input("Enter name for new space: ").strip() - + if not cls.create_space(space_name): return False - - print("\n" + "="*60) + + print("\n" + "=" * 60) print("āœ“ Setup Complete!") - print("="*60) + print("=" * 60) print("\nYou can now use py-s3-storacha for migrations.") print("The JavaScript client will use these credentials automatically.\n") - + return True - + @classmethod def print_status(cls): """Print current authentication status.""" - print("\n" + "="*60) + print("\n" + "=" * 60) print("Storacha Authentication Status") - print("="*60 + "\n") - + print("=" * 60 + "\n") + # Check CLI cli_installed, version = cls.check_cli_installed() if cli_installed: @@ -267,7 +260,7 @@ def print_status(cls): else: print("āœ— CLI not installed") print(" Install: npm install -g @storacha/cli") - + # Check authentication authenticated, user_did = cls.check_authenticated() if authenticated: @@ -275,61 +268,49 @@ def print_status(cls): else: print("āœ— Not authenticated") print(" Login: storacha login your-email@example.com") - + # List spaces spaces = cls.list_spaces() if spaces: print(f"\nāœ“ Spaces ({len(spaces)}):") for space in spaces: - marker = "*" if space['current'] else " " - name = space['name'] or "(unnamed)" + marker = "*" if space["current"] else " " + name = space["name"] or "(unnamed)" print(f" {marker} {name}") print(f" DID: {space['did']}") else: print("\nāœ— No spaces found") print(" Create: storacha space create my-space") - - print("\n" + "="*60 + "\n") + + print("\n" + "=" * 60 + "\n") def main(): """Main entry point for auth helper.""" import argparse - + parser = argparse.ArgumentParser( description="Manage Storacha authentication for py-s3-storacha" ) + parser.add_argument("--setup", action="store_true", help="Run interactive setup") + parser.add_argument("--email", help="Email address for authentication") + parser.add_argument("--space", help="Space name to create") parser.add_argument( - "--setup", - action="store_true", - help="Run interactive setup" - ) - parser.add_argument( - "--email", - help="Email address for authentication" + "--status", action="store_true", help="Show authentication status" ) - parser.add_argument( - "--space", - help="Space name to create" - ) - parser.add_argument( - "--status", - action="store_true", - help="Show authentication status" - ) - + args = parser.parse_args() - + helper = StorachaAuthHelper() - + if args.status: helper.print_status() sys.exit(0) - + if args.setup: success = helper.setup_authentication(email=args.email, space_name=args.space) sys.exit(0 if success else 1) - + # Default: show status helper.print_status() diff --git a/src/py_s3_storacha/cli.py b/src/py_s3_storacha/cli.py index 3c29c7a..dcfeb35 100644 --- a/src/py_s3_storacha/cli.py +++ b/src/py_s3_storacha/cli.py @@ -11,9 +11,8 @@ from .config import S3Config, StorachaConfig, MigrationConfig, ConfigurationParser from .api import S3ToStorachaMigrator from .models import MigrationRequest, MigrationResult, MigrationProgress -from .progress import create_console_progress_callback from .exceptions import S3StorachaError, ConfigurationError, MigrationError -from .error_handler import with_error_handling, get_error_handler +from .error_handler import with_error_handling from .logging_config import setup_logging, get_logger @@ -24,16 +23,16 @@ class CLIArgumentParser: """Handles command-line argument parsing and validation.""" - + def __init__(self) -> None: """Initialize the argument parser.""" self.parser = self._create_parser() - + def _create_parser(self) -> argparse.ArgumentParser: """Create and configure the argument parser.""" parser = argparse.ArgumentParser( - prog='py-s3-storacha', - description='Migrate objects from AWS S3 to Storacha storage', + prog="py-s3-storacha", + description="Migrate objects from AWS S3 to Storacha storage", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: @@ -61,228 +60,216 @@ def _create_parser(self) -> argparse.ArgumentParser: S3_ENDPOINT_URL Custom S3 endpoint URL STORACHA_API_KEY Storacha API key STORACHA_ENDPOINT_URL Storacha endpoint URL - """ + """, ) - + # S3 Configuration - s3_group = parser.add_argument_group('S3 Configuration') + s3_group = parser.add_argument_group("S3 Configuration") s3_group.add_argument( - '--s3-access-key', - help='S3 access key ID (default: AWS_ACCESS_KEY_ID env var)' + "--s3-access-key", + help="S3 access key ID (default: AWS_ACCESS_KEY_ID env var)", ) s3_group.add_argument( - '--s3-secret-key', - help='S3 secret access key (default: AWS_SECRET_ACCESS_KEY env var)' + "--s3-secret-key", + help="S3 secret access key (default: AWS_SECRET_ACCESS_KEY env var)", ) s3_group.add_argument( - '--s3-region', - help='S3 region (default: AWS_DEFAULT_REGION env var or us-east-1)' + "--s3-region", + help="S3 region (default: AWS_DEFAULT_REGION env var or us-east-1)", ) s3_group.add_argument( - '--s3-bucket', - required=True, - help='S3 bucket name (required)' + "--s3-bucket", required=True, help="S3 bucket name (required)" ) s3_group.add_argument( - '--s3-endpoint-url', - help='Custom S3 endpoint URL (default: S3_ENDPOINT_URL env var)' + "--s3-endpoint-url", + help="Custom S3 endpoint URL (default: S3_ENDPOINT_URL env var)", ) - + # Storacha Configuration - storacha_group = parser.add_argument_group('Storacha Configuration') + storacha_group = parser.add_argument_group("Storacha Configuration") storacha_group.add_argument( - '--storacha-api-key', - help='Storacha API key (default: STORACHA_API_KEY env var)' + "--storacha-api-key", + help="Storacha API key (default: STORACHA_API_KEY env var)", ) storacha_group.add_argument( - '--storacha-endpoint-url', - help='Storacha endpoint URL (default: STORACHA_ENDPOINT_URL env var or https://api.storacha.network)' + "--storacha-endpoint-url", + help="Storacha endpoint URL (default: STORACHA_ENDPOINT_URL env var or https://api.storacha.network)", ) storacha_group.add_argument( - '--storacha-space', - required=True, - help='Storacha space name (required)' + "--storacha-space", required=True, help="Storacha space name (required)" ) - + # Migration Parameters - migration_group = parser.add_argument_group('Migration Parameters') + migration_group = parser.add_argument_group("Migration Parameters") migration_group.add_argument( - '--source-path', + "--source-path", required=True, - help='Source path in S3 (e.g., "folder/" or "file.txt") (required)' + help='Source path in S3 (e.g., "folder/" or "file.txt") (required)', ) migration_group.add_argument( - '--dest-path', - required=True, - help='Destination path in Storacha (required)' + "--dest-path", required=True, help="Destination path in Storacha (required)" ) migration_group.add_argument( - '--include-pattern', - help='Include only objects matching this pattern (glob syntax)' + "--include-pattern", + help="Include only objects matching this pattern (glob syntax)", ) migration_group.add_argument( - '--exclude-pattern', - help='Exclude objects matching this pattern (glob syntax)' + "--exclude-pattern", + help="Exclude objects matching this pattern (glob syntax)", ) migration_group.add_argument( - '--overwrite-existing', - action='store_true', - help='Overwrite existing objects in destination' + "--overwrite-existing", + action="store_true", + help="Overwrite existing objects in destination", ) migration_group.add_argument( - '--no-verify-checksums', - action='store_true', - help='Skip checksum verification during migration' + "--no-verify-checksums", + action="store_true", + help="Skip checksum verification during migration", ) - + # Migration Options - options_group = parser.add_argument_group('Migration Options') + options_group = parser.add_argument_group("Migration Options") options_group.add_argument( - '--batch-size', + "--batch-size", type=int, default=100, - help='Number of objects to process in each batch (default: 100)' + help="Number of objects to process in each batch (default: 100)", ) options_group.add_argument( - '--timeout', + "--timeout", type=int, default=300, - help='Timeout for migration operation in seconds (default: 300)' + help="Timeout for migration operation in seconds (default: 300)", ) options_group.add_argument( - '--retry-attempts', + "--retry-attempts", type=int, default=3, - help='Number of retry attempts for failed operations (default: 3)' + help="Number of retry attempts for failed operations (default: 3)", ) options_group.add_argument( - '--dry-run', - action='store_true', - help='Show what would be migrated without actually doing it' + "--dry-run", + action="store_true", + help="Show what would be migrated without actually doing it", ) options_group.add_argument( - '--verbose', - action='store_true', - help='Enable verbose output' + "--verbose", action="store_true", help="Enable verbose output" ) options_group.add_argument( - '--quiet', - action='store_true', - help='Suppress progress output (errors still shown)' + "--quiet", + action="store_true", + help="Suppress progress output (errors still shown)", ) - + # Configuration File - config_group = parser.add_argument_group('Configuration') + config_group = parser.add_argument_group("Configuration") config_group.add_argument( - '--config-file', + "--config-file", type=Path, - help='Load configuration from file (JSON, YAML, or TOML)' + help="Load configuration from file (JSON, YAML, or TOML)", ) - + # Version parser.add_argument( - '--version', - action='version', - version=f'%(prog)s {self._get_version()}' + "--version", action="version", version=f"%(prog)s {self._get_version()}" ) - + return parser - + def _get_version(self) -> str: """Get the package version.""" try: from . import __version__ + return __version__ except ImportError: return "unknown" - + def parse_args(self, args: Optional[List[str]] = None) -> argparse.Namespace: """Parse command-line arguments. - + Args: args: List of arguments to parse (default: sys.argv) - + Returns: Parsed arguments namespace """ return self.parser.parse_args(args) - + def validate_args(self, args: argparse.Namespace) -> None: """Validate parsed arguments. - + Args: args: Parsed arguments to validate - + Raises: ConfigurationError: If validation fails """ errors = [] - + # Validate batch size if args.batch_size <= 0: errors.append("Batch size must be greater than 0") - + # Validate timeout if args.timeout <= 0: errors.append("Timeout must be greater than 0") - + # Validate retry attempts if args.retry_attempts < 0: errors.append("Retry attempts cannot be negative") - + # Validate paths if not args.source_path.strip(): errors.append("Source path cannot be empty or whitespace only") - + if not args.dest_path.strip(): errors.append("Destination path cannot be empty or whitespace only") - + # Validate patterns if args.include_pattern and not args.include_pattern.strip(): errors.append("Include pattern cannot be empty or whitespace only") - + if args.exclude_pattern and not args.exclude_pattern.strip(): errors.append("Exclude pattern cannot be empty or whitespace only") - + # Validate conflicting options if args.verbose and args.quiet: errors.append("Cannot specify both --verbose and --quiet") - + # Validate config file exists if specified if args.config_file and not args.config_file.exists(): errors.append(f"Configuration file not found: {args.config_file}") - + if errors: - raise ConfigurationError( - f"Argument validation failed: {'; '.join(errors)}" - ) + raise ConfigurationError(f"Argument validation failed: {'; '.join(errors)}") class CLIConfigurationLoader: """Loads configuration from various sources.""" - + def __init__(self) -> None: """Initialize the configuration loader.""" self.config_parser = ConfigurationParser() - + def load_configuration( - self, - args: argparse.Namespace + self, args: argparse.Namespace ) -> tuple[S3Config, StorachaConfig, MigrationConfig]: """Load configuration from arguments, environment, and config files. - + Args: args: Parsed command-line arguments - + Returns: Tuple of (S3Config, StorachaConfig, MigrationConfig) - + Raises: ConfigurationError: If configuration loading fails """ # Start with empty configuration config_data: Dict[str, Any] = {} - + # Load from config file if specified if args.config_file: try: @@ -292,169 +279,167 @@ def load_configuration( raise ConfigurationError( f"Failed to load configuration file {args.config_file}: {e}" ) - + # Load from environment variables env_config = self._load_from_environment() config_data.update(env_config) - + # Override with command-line arguments cli_config = self._load_from_cli_args(args) config_data.update(cli_config) - + # Create configuration objects try: s3_config = self._create_s3_config(config_data) storacha_config = self._create_storacha_config(config_data) migration_config = self._create_migration_config(config_data) - + return s3_config, storacha_config, migration_config - + except Exception as e: raise ConfigurationError(f"Failed to create configuration: {e}") - + def _load_from_environment(self) -> Dict[str, Any]: """Load configuration from environment variables.""" env_config = {} - + # S3 environment variables - if os.getenv('AWS_ACCESS_KEY_ID'): - env_config['s3_access_key_id'] = os.getenv('AWS_ACCESS_KEY_ID') - - if os.getenv('AWS_SECRET_ACCESS_KEY'): - env_config['s3_secret_access_key'] = os.getenv('AWS_SECRET_ACCESS_KEY') - - if os.getenv('AWS_DEFAULT_REGION'): - env_config['s3_region'] = os.getenv('AWS_DEFAULT_REGION') - - if os.getenv('S3_ENDPOINT_URL'): - env_config['s3_endpoint_url'] = os.getenv('S3_ENDPOINT_URL') - + if os.getenv("AWS_ACCESS_KEY_ID"): + env_config["s3_access_key_id"] = os.getenv("AWS_ACCESS_KEY_ID") + + if os.getenv("AWS_SECRET_ACCESS_KEY"): + env_config["s3_secret_access_key"] = os.getenv("AWS_SECRET_ACCESS_KEY") + + if os.getenv("AWS_DEFAULT_REGION"): + env_config["s3_region"] = os.getenv("AWS_DEFAULT_REGION") + + if os.getenv("S3_ENDPOINT_URL"): + env_config["s3_endpoint_url"] = os.getenv("S3_ENDPOINT_URL") + # Storacha environment variables - if os.getenv('STORACHA_API_KEY'): - env_config['storacha_api_key'] = os.getenv('STORACHA_API_KEY') - - if os.getenv('STORACHA_ENDPOINT_URL'): - env_config['storacha_endpoint_url'] = os.getenv('STORACHA_ENDPOINT_URL') - + if os.getenv("STORACHA_API_KEY"): + env_config["storacha_api_key"] = os.getenv("STORACHA_API_KEY") + + if os.getenv("STORACHA_ENDPOINT_URL"): + env_config["storacha_endpoint_url"] = os.getenv("STORACHA_ENDPOINT_URL") + return env_config - + def _load_from_cli_args(self, args: argparse.Namespace) -> Dict[str, Any]: """Load configuration from command-line arguments.""" cli_config = {} - + # S3 configuration if args.s3_access_key: - cli_config['s3_access_key_id'] = args.s3_access_key - + cli_config["s3_access_key_id"] = args.s3_access_key + if args.s3_secret_key: - cli_config['s3_secret_access_key'] = args.s3_secret_key - + cli_config["s3_secret_access_key"] = args.s3_secret_key + if args.s3_region: - cli_config['s3_region'] = args.s3_region - + cli_config["s3_region"] = args.s3_region + if args.s3_bucket: - cli_config['s3_bucket_name'] = args.s3_bucket - + cli_config["s3_bucket_name"] = args.s3_bucket + if args.s3_endpoint_url: - cli_config['s3_endpoint_url'] = args.s3_endpoint_url - + cli_config["s3_endpoint_url"] = args.s3_endpoint_url + # Storacha configuration if args.storacha_api_key: - cli_config['storacha_api_key'] = args.storacha_api_key - + cli_config["storacha_api_key"] = args.storacha_api_key + if args.storacha_endpoint_url: - cli_config['storacha_endpoint_url'] = args.storacha_endpoint_url - + cli_config["storacha_endpoint_url"] = args.storacha_endpoint_url + if args.storacha_space: - cli_config['storacha_space_name'] = args.storacha_space - + cli_config["storacha_space_name"] = args.storacha_space + # Migration configuration - cli_config['batch_size'] = args.batch_size - cli_config['timeout_seconds'] = args.timeout - cli_config['retry_attempts'] = args.retry_attempts - cli_config['dry_run'] = args.dry_run - cli_config['verbose'] = args.verbose - + cli_config["batch_size"] = args.batch_size + cli_config["timeout_seconds"] = args.timeout + cli_config["retry_attempts"] = args.retry_attempts + cli_config["dry_run"] = args.dry_run + cli_config["verbose"] = args.verbose + return cli_config - + def _create_s3_config(self, config_data: Dict[str, Any]) -> S3Config: """Create S3Config from configuration data.""" # Required fields - access_key_id = config_data.get('s3_access_key_id') - secret_access_key = config_data.get('s3_secret_access_key') - bucket_name = config_data.get('s3_bucket_name') - + access_key_id = config_data.get("s3_access_key_id") + secret_access_key = config_data.get("s3_secret_access_key") + bucket_name = config_data.get("s3_bucket_name") + # Check for required fields if not access_key_id: raise ConfigurationError( "S3 access key ID is required (use --s3-access-key or AWS_ACCESS_KEY_ID)" ) - + if not secret_access_key: raise ConfigurationError( "S3 secret access key is required (use --s3-secret-key or AWS_SECRET_ACCESS_KEY)" ) - + if not bucket_name: - raise ConfigurationError( - "S3 bucket name is required (use --s3-bucket)" - ) - + raise ConfigurationError("S3 bucket name is required (use --s3-bucket)") + # Optional fields with defaults - region = config_data.get('s3_region', 'us-east-1') - endpoint_url = config_data.get('s3_endpoint_url') - + region = config_data.get("s3_region", "us-east-1") + endpoint_url = config_data.get("s3_endpoint_url") + return S3Config( access_key_id=access_key_id, secret_access_key=secret_access_key, region=region, bucket_name=bucket_name, - endpoint_url=endpoint_url + endpoint_url=endpoint_url, ) - + def _create_storacha_config(self, config_data: Dict[str, Any]) -> StorachaConfig: """Create StorachaConfig from configuration data.""" # Required fields - api_key = config_data.get('storacha_api_key') - space_name = config_data.get('storacha_space_name') - + api_key = config_data.get("storacha_api_key") + space_name = config_data.get("storacha_space_name") + # Check for required fields if not api_key: raise ConfigurationError( "Storacha API key is required (use --storacha-api-key or STORACHA_API_KEY)" ) - + if not space_name: raise ConfigurationError( "Storacha space name is required (use --storacha-space)" ) - + # Optional fields with defaults - endpoint_url = config_data.get('storacha_endpoint_url', 'https://api.storacha.network') - + endpoint_url = config_data.get( + "storacha_endpoint_url", "https://api.storacha.network" + ) + return StorachaConfig( - api_key=api_key, - endpoint_url=endpoint_url, - space_name=space_name + api_key=api_key, endpoint_url=endpoint_url, space_name=space_name ) - + def _create_migration_config(self, config_data: Dict[str, Any]) -> MigrationConfig: """Create MigrationConfig from configuration data.""" return MigrationConfig( - batch_size=config_data.get('batch_size', 100), - timeout_seconds=config_data.get('timeout_seconds', 300), - retry_attempts=config_data.get('retry_attempts', 3), - dry_run=config_data.get('dry_run', False), - verbose=config_data.get('verbose', False) + batch_size=config_data.get("batch_size", 100), + timeout_seconds=config_data.get("timeout_seconds", 300), + retry_attempts=config_data.get("retry_attempts", 3), + dry_run=config_data.get("dry_run", False), + verbose=config_data.get("verbose", False), ) class CLIProgressDisplay: """Handles progress display for CLI operations.""" - + def __init__(self, quiet: bool = False, verbose: bool = False) -> None: """Initialize progress display. - + Args: quiet: Suppress progress output verbose: Enable verbose output @@ -462,82 +447,83 @@ def __init__(self, quiet: bool = False, verbose: bool = False) -> None: self.quiet = quiet self.verbose = verbose self._last_progress_line = "" - + def create_progress_callback(self): """Create a progress callback function for the CLI.""" if self.quiet: return None - + def progress_callback(progress: MigrationProgress) -> None: """Handle progress updates.""" if self.verbose: # Verbose mode: show detailed progress - print(f"[{progress.current_operation or 'migrating'}] " - f"{progress.current_object} " - f"({progress.objects_completed}/{progress.total_objects} objects, " - f"{progress.bytes_transferred:,}/{progress.total_bytes:,} bytes, " - f"{progress.progress_percentage:.1f}%)") + print( + f"[{progress.current_operation or 'migrating'}] " + f"{progress.current_object} " + f"({progress.objects_completed}/{progress.total_objects} objects, " + f"{progress.bytes_transferred:,}/{progress.total_bytes:,} bytes, " + f"{progress.progress_percentage:.1f}%)" + ) else: # Normal mode: show progress bar progress_bar = self._create_progress_bar( - progress.progress_percentage, - width=40 + progress.progress_percentage, width=40 ) - + # Clear previous line and show new progress if self._last_progress_line: - print('\r' + ' ' * len(self._last_progress_line) + '\r', end='') - + print("\r" + " " * len(self._last_progress_line) + "\r", end="") + progress_line = ( f"Progress: {progress_bar} " f"{progress.objects_completed}/{progress.total_objects} objects " f"({progress.progress_percentage:.1f}%)" ) - - print(progress_line, end='', flush=True) + + print(progress_line, end="", flush=True) self._last_progress_line = progress_line - + return progress_callback - + def _create_progress_bar(self, percentage: float, width: int = 40) -> str: """Create a text-based progress bar.""" filled = int(width * percentage / 100) - bar = 'ā–ˆ' * filled + 'ā–‘' * (width - filled) + bar = "ā–ˆ" * filled + "ā–‘" * (width - filled) return f"[{bar}]" - + def print_result(self, result: MigrationResult) -> None: """Print migration result.""" # Clear progress line if needed if self._last_progress_line and not self.quiet: - print('\r' + ' ' * len(self._last_progress_line) + '\r', end='') - + print("\r" + " " * len(self._last_progress_line) + "\r", end="") + if result.success: - print(f"āœ“ Migration completed successfully!") + print("āœ“ Migration completed successfully!") print(f" Objects migrated: {result.objects_migrated:,}") print(f" Total size: {result.total_size_bytes:,} bytes") print(f" Duration: {result.duration_seconds:.2f} seconds") - + if result.warnings: print(f" Warnings: {len(result.warnings)}") if self.verbose: for warning in result.warnings: print(f" - {warning}") - + if result.skipped_objects: print(f" Skipped objects: {len(result.skipped_objects)}") if self.verbose: for skipped in result.skipped_objects: print(f" - {skipped}") else: - print(f"āœ— Migration failed!") + print("āœ— Migration failed!") print(f" Objects migrated: {result.objects_migrated:,}") print(f" Duration: {result.duration_seconds:.2f} seconds") - + if result.errors: print(f" Errors: {len(result.errors)}") for error in result.errors: print(f" - {error}") - + if result.failed_objects: print(f" Failed objects: {len(result.failed_objects)}") if self.verbose: @@ -554,105 +540,107 @@ def print_result(self, result: MigrationResult) -> None: class CLIExecutor: """Handles CLI execution workflow.""" - + def __init__(self) -> None: """Initialize CLI executor.""" self.arg_parser = CLIArgumentParser() self.config_loader = CLIConfigurationLoader() - - @with_error_handling("cli_execution", error_types=(Exception,), exclude_types=(KeyboardInterrupt,)) + + @with_error_handling( + "cli_execution", error_types=(Exception,), exclude_types=(KeyboardInterrupt,) + ) async def execute(self, args: Optional[List[str]] = None) -> int: """Execute CLI workflow. - + Args: args: Command-line arguments (default: sys.argv) - + Returns: Exit code (0 for success, non-zero for failure) """ try: # Parse arguments parsed_args = self.arg_parser.parse_args(args) - + # Validate arguments self.arg_parser.validate_args(parsed_args) - + # Configure logging self._configure_logging(parsed_args) - + # Load configuration - s3_config, storacha_config, migration_config = self.config_loader.load_configuration(parsed_args) - + s3_config, storacha_config, migration_config = ( + self.config_loader.load_configuration(parsed_args) + ) + # Create migration request migration_request = self._create_migration_request(parsed_args) - + # Set up progress display progress_display = CLIProgressDisplay( - quiet=parsed_args.quiet, - verbose=parsed_args.verbose + quiet=parsed_args.quiet, verbose=parsed_args.verbose ) progress_callback = progress_display.create_progress_callback() - + # Execute migration logger.info("Starting S3 to Storacha migration") - + if migration_config.dry_run: print("DRY RUN MODE - No actual migration will be performed") - + migrator = S3ToStorachaMigrator( s3_config=s3_config, storacha_config=storacha_config, - migration_config=migration_config + migration_config=migration_config, ) - + result = await migrator.migrate( - request=migration_request, - progress_callback=progress_callback + request=migration_request, progress_callback=progress_callback ) - + # Display result progress_display.print_result(result) - + # Return appropriate exit code return 0 if result.success else 1 - + except KeyboardInterrupt: print("\nāœ— Migration cancelled by user") logger.info("Migration cancelled by user") return 130 # Standard exit code for SIGINT - + except ConfigurationError as e: print(f"āœ— Configuration error: {e}") logger.error(f"Configuration error: {e}") return 2 - + except MigrationError as e: print(f"āœ— Migration error: {e}") logger.error(f"Migration error: {e}") return 3 - + except S3StorachaError as e: print(f"āœ— Error: {e}") logger.error(f"S3StorachaError: {e}") return 4 - + except Exception as e: print(f"āœ— Unexpected error: {e}") logger.exception("Unexpected error during CLI execution") return 5 - + def _configure_logging(self, args: argparse.Namespace) -> None: """Configure logging based on CLI arguments.""" if args.verbose: logging.getLogger().setLevel(logging.DEBUG) - logging.getLogger('py_s3_storacha').setLevel(logging.DEBUG) + logging.getLogger("py_s3_storacha").setLevel(logging.DEBUG) elif args.quiet: logging.getLogger().setLevel(logging.ERROR) - logging.getLogger('py_s3_storacha').setLevel(logging.ERROR) + logging.getLogger("py_s3_storacha").setLevel(logging.ERROR) else: logging.getLogger().setLevel(logging.INFO) - logging.getLogger('py_s3_storacha').setLevel(logging.INFO) - + logging.getLogger("py_s3_storacha").setLevel(logging.INFO) + def _create_migration_request(self, args: argparse.Namespace) -> MigrationRequest: """Create migration request from CLI arguments.""" return MigrationRequest( @@ -661,21 +649,21 @@ def _create_migration_request(self, args: argparse.Namespace) -> MigrationReques include_pattern=args.include_pattern, exclude_pattern=args.exclude_pattern, overwrite_existing=args.overwrite_existing, - verify_checksums=not args.no_verify_checksums + verify_checksums=not args.no_verify_checksums, ) def main(args: Optional[List[str]] = None) -> int: """Main CLI entry point. - + Args: args: Command-line arguments (default: sys.argv) - + Returns: Exit code (0 for success, non-zero for failure) """ executor = CLIExecutor() - + try: # Run the async executor return asyncio.run(executor.execute(args)) @@ -688,5 +676,5 @@ def main(args: Optional[List[str]] = None) -> int: return 5 -if __name__ == '__main__': - sys.exit(main()) \ No newline at end of file +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/py_s3_storacha/config.py b/src/py_s3_storacha/config.py index 2279982..4058af7 100644 --- a/src/py_s3_storacha/config.py +++ b/src/py_s3_storacha/config.py @@ -1,6 +1,6 @@ """Configuration data classes and validation for S3 to Storacha migration.""" -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Optional, Dict, Any, Union import os import json @@ -10,13 +10,13 @@ @dataclass class S3Config: """Configuration for S3 connection and authentication.""" - + access_key_id: str secret_access_key: str region: str bucket_name: str endpoint_url: Optional[str] = None - + def __post_init__(self) -> None: """Validate S3 configuration after initialization.""" if not self.access_key_id: @@ -27,7 +27,7 @@ def __post_init__(self) -> None: raise ValueError("S3 region is required") if not self.bucket_name: raise ValueError("S3 bucket_name is required") - + @classmethod def from_dict(cls, data: Dict[str, Any]) -> "S3Config": """Create S3Config from dictionary.""" @@ -36,9 +36,9 @@ def from_dict(cls, data: Dict[str, Any]) -> "S3Config": secret_access_key=data.get("secret_access_key", ""), region=data.get("region", ""), bucket_name=data.get("bucket_name", ""), - endpoint_url=data.get("endpoint_url") + endpoint_url=data.get("endpoint_url"), ) - + @classmethod def from_env(cls, prefix: str = "S3_") -> "S3Config": """Create S3Config from environment variables.""" @@ -47,18 +47,18 @@ def from_env(cls, prefix: str = "S3_") -> "S3Config": secret_access_key=os.getenv(f"{prefix}SECRET_ACCESS_KEY", ""), region=os.getenv(f"{prefix}REGION", ""), bucket_name=os.getenv(f"{prefix}BUCKET_NAME", ""), - endpoint_url=os.getenv(f"{prefix}ENDPOINT_URL") + endpoint_url=os.getenv(f"{prefix}ENDPOINT_URL"), ) @dataclass class StorachaConfig: """Configuration for Storacha connection and authentication.""" - + api_key: str endpoint_url: str space_name: str - + def __post_init__(self) -> None: """Validate Storacha configuration after initialization.""" if not self.api_key: @@ -67,36 +67,36 @@ def __post_init__(self) -> None: raise ValueError("Storacha endpoint_url is required") if not self.space_name: raise ValueError("Storacha space_name is required") - + @classmethod def from_dict(cls, data: Dict[str, Any]) -> "StorachaConfig": """Create StorachaConfig from dictionary.""" return cls( api_key=data.get("api_key", ""), endpoint_url=data.get("endpoint_url", ""), - space_name=data.get("space_name", "") + space_name=data.get("space_name", ""), ) - + @classmethod def from_env(cls, prefix: str = "STORACHA_") -> "StorachaConfig": """Create StorachaConfig from environment variables.""" return cls( api_key=os.getenv(f"{prefix}API_KEY", ""), endpoint_url=os.getenv(f"{prefix}ENDPOINT_URL", ""), - space_name=os.getenv(f"{prefix}SPACE_NAME", "") + space_name=os.getenv(f"{prefix}SPACE_NAME", ""), ) @dataclass class MigrationConfig: """Configuration for migration-specific settings.""" - + batch_size: int = 100 timeout_seconds: int = 300 retry_attempts: int = 3 verbose: bool = False dry_run: bool = False - + def __post_init__(self) -> None: """Validate migration configuration after initialization.""" if self.batch_size <= 0: @@ -105,7 +105,7 @@ def __post_init__(self) -> None: raise ValueError("timeout_seconds must be greater than 0") if self.retry_attempts < 0: raise ValueError("retry_attempts must be non-negative") - + @classmethod def from_dict(cls, data: Dict[str, Any]) -> "MigrationConfig": """Create MigrationConfig from dictionary.""" @@ -114,51 +114,55 @@ def from_dict(cls, data: Dict[str, Any]) -> "MigrationConfig": timeout_seconds=data.get("timeout_seconds", 300), retry_attempts=data.get("retry_attempts", 3), verbose=data.get("verbose", False), - dry_run=data.get("dry_run", False) + dry_run=data.get("dry_run", False), ) class ConfigurationParser: """Utility class for parsing configuration from various sources.""" - + @staticmethod def parse_from_file(file_path: Union[str, Path]) -> Dict[str, Any]: """Parse configuration from JSON or environment file.""" path = Path(file_path) - + if not path.exists(): raise FileNotFoundError(f"Configuration file not found: {file_path}") - - if path.suffix.lower() == '.json': - with open(path, 'r') as f: + + if path.suffix.lower() == ".json": + with open(path, "r") as f: return json.load(f) - + # Handle .env style files config = {} - with open(path, 'r') as f: + with open(path, "r") as f: for line in f: line = line.strip() - if line and not line.startswith('#') and '=' in line: - key, value = line.split('=', 1) - config[key.strip()] = value.strip().strip('"\'') - + if line and not line.startswith("#") and "=" in line: + key, value = line.split("=", 1) + config[key.strip()] = value.strip().strip("\"'") + return config - + @staticmethod - def create_configs_from_dict(data: Dict[str, Any]) -> tuple[S3Config, StorachaConfig, MigrationConfig]: + def create_configs_from_dict( + data: Dict[str, Any], + ) -> tuple[S3Config, StorachaConfig, MigrationConfig]: """Create all configuration objects from a single dictionary.""" s3_data = data.get("s3", {}) storacha_data = data.get("storacha", {}) migration_data = data.get("migration", {}) - + s3_config = S3Config.from_dict(s3_data) storacha_config = StorachaConfig.from_dict(storacha_data) migration_config = MigrationConfig.from_dict(migration_data) - + return s3_config, storacha_config, migration_config - + @staticmethod - def create_configs_from_file(file_path: Union[str, Path]) -> tuple[S3Config, StorachaConfig, MigrationConfig]: + def create_configs_from_file( + file_path: Union[str, Path], + ) -> tuple[S3Config, StorachaConfig, MigrationConfig]: """Create all configuration objects from a configuration file.""" data = ConfigurationParser.parse_from_file(file_path) - return ConfigurationParser.create_configs_from_dict(data) \ No newline at end of file + return ConfigurationParser.create_configs_from_dict(data) diff --git a/src/py_s3_storacha/error_handler.py b/src/py_s3_storacha/error_handler.py index 0c86acf..857af5f 100644 --- a/src/py_s3_storacha/error_handler.py +++ b/src/py_s3_storacha/error_handler.py @@ -1,10 +1,9 @@ """Comprehensive error handling system for S3 to Storacha migration operations.""" import asyncio -import json import re import time -from typing import Optional, Dict, Any, List, Callable, TypeVar, Union +from typing import Optional, Dict, Any, Callable, TypeVar, Union from functools import wraps import logging @@ -12,174 +11,192 @@ S3StorachaError, JSWrapperError, ConfigurationError, - MigrationError + MigrationError, ) from .logging_config import get_logger -T = TypeVar('T') +T = TypeVar("T") class ErrorHandler: """Comprehensive error handling system for S3 to Storacha operations.""" - + def __init__(self, logger: Optional[logging.Logger] = None) -> None: """Initialize error handler with optional logger. - + Args: logger: Logger instance for error reporting. If None, uses default logger. """ self.logger = logger or get_logger(__name__) - + # JavaScript error patterns for parsing subprocess errors self.js_error_patterns = { - 'syntax_error': re.compile(r'SyntaxError:\s*(.+)', re.IGNORECASE), - 'reference_error': re.compile(r'ReferenceError:\s*(.+)', re.IGNORECASE), - 'type_error': re.compile(r'TypeError:\s*(.+)', re.IGNORECASE), - 'network_error': re.compile(r'(ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET)', re.IGNORECASE), - 'auth_error': re.compile(r'(Unauthorized|Forbidden|Invalid.*credentials)', re.IGNORECASE), - 'not_found': re.compile(r'(Not Found|404|NoSuchBucket|NoSuchKey)', re.IGNORECASE), - 'permission_error': re.compile(r'(Access Denied|Permission denied|EACCES)', re.IGNORECASE) + "syntax_error": re.compile(r"SyntaxError:\s*(.+)", re.IGNORECASE), + "reference_error": re.compile(r"ReferenceError:\s*(.+)", re.IGNORECASE), + "type_error": re.compile(r"TypeError:\s*(.+)", re.IGNORECASE), + "network_error": re.compile( + r"(ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET)", re.IGNORECASE + ), + "auth_error": re.compile( + r"(Unauthorized|Forbidden|Invalid.*credentials)", re.IGNORECASE + ), + "not_found": re.compile( + r"(Not Found|404|NoSuchBucket|NoSuchKey)", re.IGNORECASE + ), + "permission_error": re.compile( + r"(Access Denied|Permission denied|EACCES)", re.IGNORECASE + ), } - - def handle_js_error(self, stderr: str, returncode: int, command: Optional[str] = None) -> JSWrapperError: + + def handle_js_error( + self, stderr: str, returncode: int, command: Optional[str] = None + ) -> JSWrapperError: """Parse JavaScript subprocess errors and create appropriate Python exceptions. - + Args: stderr: Standard error output from JavaScript process returncode: Process return code command: Command that was executed (optional) - + Returns: JSWrapperError: Appropriate exception based on error analysis """ self.logger.error(f"JavaScript wrapper failed with return code {returncode}") self.logger.debug(f"JavaScript stderr: {stderr}") - + # Try to parse structured error information error_info = self._parse_js_error(stderr) - + # Create appropriate error message - if error_info['type'] == 'network': + if error_info["type"] == "network": message = f"Network error in JavaScript wrapper: {error_info['message']}" - elif error_info['type'] == 'auth': - message = f"Authentication error in JavaScript wrapper: {error_info['message']}" - elif error_info['type'] == 'not_found': - message = f"Resource not found in JavaScript wrapper: {error_info['message']}" - elif error_info['type'] == 'permission': + elif error_info["type"] == "auth": + message = ( + f"Authentication error in JavaScript wrapper: {error_info['message']}" + ) + elif error_info["type"] == "not_found": + message = ( + f"Resource not found in JavaScript wrapper: {error_info['message']}" + ) + elif error_info["type"] == "permission": message = f"Permission error in JavaScript wrapper: {error_info['message']}" - elif error_info['type'] in ['syntax', 'reference', 'type']: + elif error_info["type"] in ["syntax", "reference", "type"]: message = f"JavaScript {error_info['type']} error: {error_info['message']}" else: message = f"JavaScript wrapper execution failed: {error_info['message']}" - + # Create exception with parsed information error = JSWrapperError.from_process_result( return_code=returncode, stdout="", # stdout not provided in this context stderr=stderr, - command=command + command=command, ) error.message = message - error.add_context("error_type", error_info['type']) - + error.add_context("error_type", error_info["type"]) + return error - - def handle_network_error(self, error: Exception, operation: str = "unknown") -> MigrationError: + + def handle_network_error( + self, error: Exception, operation: str = "unknown" + ) -> MigrationError: """Handle network-related errors with appropriate context. - + Args: error: Original network exception operation: Operation that failed - + Returns: MigrationError: Network error with migration context """ self.logger.error(f"Network error during {operation}: {error}") - + # Determine specific network error type error_str = str(error).lower() - if 'timeout' in error_str or 'timed out' in error_str: + if "timeout" in error_str or "timed out" in error_str: message = f"Network timeout during {operation}" - elif 'connection refused' in error_str or 'econnrefused' in error_str: + elif "connection refused" in error_str or "econnrefused" in error_str: message = f"Connection refused during {operation}" - elif 'not found' in error_str or 'enotfound' in error_str: + elif "not found" in error_str or "enotfound" in error_str: message = f"Host not found during {operation}" - elif 'unauthorized' in error_str or 'forbidden' in error_str: + elif "unauthorized" in error_str or "forbidden" in error_str: message = f"Authentication failed during {operation}" else: message = f"Network error during {operation}: {str(error)}" - + return MigrationError.network_error( - message=message, - operation=operation, - original_error=error + message=message, operation=operation, original_error=error ) - - def handle_config_error(self, config_issue: str, config_type: str = "unknown") -> ConfigurationError: + + def handle_config_error( + self, config_issue: str, config_type: str = "unknown" + ) -> ConfigurationError: """Handle configuration validation errors. - + Args: config_issue: Description of the configuration issue config_type: Type of configuration that failed - + Returns: ConfigurationError: Configuration error with context """ self.logger.error(f"Configuration error in {config_type}: {config_issue}") - + return ConfigurationError( message=f"Configuration validation failed: {config_issue}", - config_type=config_type + config_type=config_type, ) - + def _parse_js_error(self, stderr: str) -> Dict[str, str]: """Parse JavaScript error output to extract error type and message. - + Args: stderr: Standard error output from JavaScript process - + Returns: Dict containing error type and message """ # Default error info error_info = { - 'type': 'unknown', - 'message': stderr.strip() if stderr else 'Unknown JavaScript error' + "type": "unknown", + "message": stderr.strip() if stderr else "Unknown JavaScript error", } - + if not stderr: return error_info - + # Try to match against known error patterns for error_type, pattern in self.js_error_patterns.items(): match = pattern.search(stderr) if match: - error_info['type'] = error_type.replace('_error', '') - error_info['message'] = match.group(1) if match.groups() else match.group(0) + error_info["type"] = error_type.replace("_error", "") + error_info["message"] = ( + match.group(1) if match.groups() else match.group(0) + ) break - + # If no specific pattern matched, try to extract first line as message - if error_info['type'] == 'unknown': - lines = stderr.strip().split('\n') + if error_info["type"] == "unknown": + lines = stderr.strip().split("\n") if lines: - error_info['message'] = lines[0] - + error_info["message"] = lines[0] + return error_info class RetryHandler: """Handles retry logic for various operations with exponential backoff.""" - + def __init__( self, max_retries: int = 3, base_delay: float = 1.0, max_delay: float = 60.0, backoff_factor: float = 2.0, - logger: Optional[logging.Logger] = None + logger: Optional[logging.Logger] = None, ) -> None: """Initialize retry handler with configuration. - + Args: max_retries: Maximum number of retry attempts base_delay: Base delay between retries in seconds @@ -192,143 +209,155 @@ def __init__( self.max_delay = max_delay self.backoff_factor = backoff_factor self.logger = logger or get_logger(__name__) - + def retry_on_error( self, error_types: Union[type, tuple] = (Exception,), - exclude_types: Union[type, tuple] = (ConfigurationError,) + exclude_types: Union[type, tuple] = (ConfigurationError,), ) -> Callable: """Decorator for retrying functions on specific error types. - + Args: error_types: Exception types to retry on exclude_types: Exception types to never retry - + Returns: Decorator function """ + def decorator(func: Callable[..., T]) -> Callable[..., T]: @wraps(func) def sync_wrapper(*args, **kwargs) -> T: - return self._execute_with_retry(func, args, kwargs, error_types, exclude_types) - + return self._execute_with_retry( + func, args, kwargs, error_types, exclude_types + ) + @wraps(func) async def async_wrapper(*args, **kwargs) -> T: - return await self._execute_with_retry_async(func, args, kwargs, error_types, exclude_types) - + return await self._execute_with_retry_async( + func, args, kwargs, error_types, exclude_types + ) + # Return appropriate wrapper based on function type if asyncio.iscoroutinefunction(func): - return async_wrapper + return async_wrapper # type: ignore[return-value] else: - return sync_wrapper - + return sync_wrapper # type: ignore[return-value] + return decorator - + def _execute_with_retry( self, func: Callable[..., T], args: tuple, kwargs: dict, error_types: Union[type, tuple], - exclude_types: Union[type, tuple] + exclude_types: Union[type, tuple], ) -> T: """Execute function with retry logic (synchronous). - + Args: func: Function to execute args: Function arguments kwargs: Function keyword arguments error_types: Exception types to retry on exclude_types: Exception types to never retry - + Returns: Function result - + Raises: Last exception if all retries fail """ last_exception = None - + for attempt in range(self.max_retries + 1): try: return func(*args, **kwargs) - except exclude_types as e: + except exclude_types as e: # type: ignore[misc] # Don't retry on excluded error types - self.logger.debug(f"Not retrying excluded error type: {type(e).__name__}") + self.logger.debug( + f"Not retrying excluded error type: {type(e).__name__}" + ) raise - except error_types as e: + except error_types as e: # type: ignore[misc] last_exception = e - + if attempt < self.max_retries: delay = min( - self.base_delay * (self.backoff_factor ** attempt), - self.max_delay + self.base_delay * (self.backoff_factor**attempt), self.max_delay ) self.logger.warning( f"Attempt {attempt + 1} failed: {e}. Retrying in {delay:.2f} seconds..." ) time.sleep(delay) else: - self.logger.error(f"All {self.max_retries + 1} attempts failed. Last error: {e}") - + self.logger.error( + f"All {self.max_retries + 1} attempts failed. Last error: {e}" + ) + # Re-raise the last exception if all retries failed if last_exception: raise last_exception - + # This should never be reached, but just in case raise RuntimeError("Retry logic failed unexpectedly") - + async def _execute_with_retry_async( self, func: Callable[..., T], args: tuple, kwargs: dict, error_types: Union[type, tuple], - exclude_types: Union[type, tuple] + exclude_types: Union[type, tuple], ) -> T: """Execute async function with retry logic. - + Args: func: Async function to execute args: Function arguments kwargs: Function keyword arguments error_types: Exception types to retry on exclude_types: Exception types to never retry - + Returns: Function result - + Raises: Last exception if all retries fail """ last_exception = None - + for attempt in range(self.max_retries + 1): try: - return await func(*args, **kwargs) - except exclude_types as e: + result = func(*args, **kwargs) + return await result # type: ignore[misc] + except exclude_types as e: # type: ignore[misc] # Don't retry on excluded error types - self.logger.debug(f"Not retrying excluded error type: {type(e).__name__}") + self.logger.debug( + f"Not retrying excluded error type: {type(e).__name__}" + ) raise - except error_types as e: + except error_types as e: # type: ignore[misc] last_exception = e - + if attempt < self.max_retries: delay = min( - self.base_delay * (self.backoff_factor ** attempt), - self.max_delay + self.base_delay * (self.backoff_factor**attempt), self.max_delay ) self.logger.warning( f"Attempt {attempt + 1} failed: {e}. Retrying in {delay:.2f} seconds..." ) await asyncio.sleep(delay) else: - self.logger.error(f"All {self.max_retries + 1} attempts failed. Last error: {e}") - + self.logger.error( + f"All {self.max_retries + 1} attempts failed. Last error: {e}" + ) + # Re-raise the last exception if all retries failed if last_exception: raise last_exception - + # This should never be reached, but just in case raise RuntimeError("Retry logic failed unexpectedly") @@ -340,7 +369,7 @@ async def _execute_with_retry_async( def get_error_handler() -> ErrorHandler: """Get global error handler instance. - + Returns: ErrorHandler: Global error handler instance """ @@ -352,7 +381,7 @@ def get_error_handler() -> ErrorHandler: def get_retry_handler() -> RetryHandler: """Get global retry handler instance. - + Returns: RetryHandler: Global retry handler instance """ @@ -365,23 +394,26 @@ def get_retry_handler() -> RetryHandler: def with_error_handling( operation: str, error_types: Union[type, tuple] = (Exception,), - exclude_types: Union[type, tuple] = (ConfigurationError,) + exclude_types: Union[type, tuple] = (ConfigurationError,), ) -> Callable: """Decorator that adds comprehensive error handling to functions. - + Args: operation: Name of the operation for error context error_types: Exception types to handle and potentially retry exclude_types: Exception types to never retry - + Returns: Decorator function """ + def decorator(func: Callable[..., T]) -> Callable[..., T]: error_handler = get_error_handler() retry_handler = get_retry_handler() - - @retry_handler.retry_on_error(error_types=error_types, exclude_types=exclude_types) + + @retry_handler.retry_on_error( + error_types=error_types, exclude_types=exclude_types + ) @wraps(func) def wrapper(*args, **kwargs) -> T: try: @@ -397,33 +429,31 @@ def wrapper(*args, **kwargs) -> T: raise except Exception as e: # Handle other exceptions by converting to appropriate error type - if 'network' in str(e).lower() or 'connection' in str(e).lower(): + if "network" in str(e).lower() or "connection" in str(e).lower(): raise error_handler.handle_network_error(e, operation) else: # Wrap in generic S3StorachaError raise S3StorachaError( message=f"Unexpected error during {operation}: {str(e)}", context={"operation": operation}, - original_error=e + original_error=e, ) - + return wrapper - + return decorator def handle_subprocess_error( - returncode: int, - stderr: str, - command: Optional[str] = None + returncode: int, stderr: str, command: Optional[str] = None ) -> JSWrapperError: """Handle subprocess execution errors. - + Args: returncode: Process return code stderr: Standard error output command: Command that was executed - + Returns: JSWrapperError: Appropriate exception for the subprocess error """ @@ -435,16 +465,16 @@ def handle_validation_error( message: str, config_type: str, field_name: Optional[str] = None, - field_value: Optional[Any] = None + field_value: Optional[Any] = None, ) -> ConfigurationError: """Handle configuration validation errors. - + Args: message: Error message config_type: Type of configuration field_name: Name of the field that failed validation field_value: Value that failed validation - + Returns: ConfigurationError: Configuration validation error """ @@ -453,12 +483,12 @@ def handle_validation_error( config_type=config_type, field_name=field_name, field_value=field_value, - reason=message + reason=message, ) else: return ConfigurationError( message=message, config_type=config_type, field_name=field_name, - field_value=field_value - ) \ No newline at end of file + field_value=field_value, + ) diff --git a/src/py_s3_storacha/exceptions.py b/src/py_s3_storacha/exceptions.py index aa1f6f1..9b5f4a3 100644 --- a/src/py_s3_storacha/exceptions.py +++ b/src/py_s3_storacha/exceptions.py @@ -5,15 +5,15 @@ class S3StorachaError(Exception): """Base exception class for all S3 to Storacha operations.""" - + def __init__( - self, - message: str, + self, + message: str, context: Optional[Dict[str, Any]] = None, - original_error: Optional[Exception] = None + original_error: Optional[Exception] = None, ) -> None: """Initialize base exception with message and optional context. - + Args: message: Human-readable error message context: Additional context information about the error @@ -23,20 +23,20 @@ def __init__( self.message = message self.context = context or {} self.original_error = original_error - + def __str__(self) -> str: """Return formatted error message with context.""" base_message = self.message - + if self.context: context_str = ", ".join(f"{k}={v}" for k, v in self.context.items()) base_message = f"{base_message} (Context: {context_str})" - + if self.original_error: base_message = f"{base_message} (Caused by: {self.original_error})" - + return base_message - + def add_context(self, key: str, value: Any) -> None: """Add additional context information to the exception.""" self.context[key] = value @@ -44,7 +44,7 @@ def add_context(self, key: str, value: Any) -> None: class JSWrapperError(S3StorachaError): """Exception raised when JavaScript wrapper execution fails.""" - + def __init__( self, message: str, @@ -52,10 +52,10 @@ def __init__( stdout: Optional[str] = None, stderr: Optional[str] = None, context: Optional[Dict[str, Any]] = None, - original_error: Optional[Exception] = None + original_error: Optional[Exception] = None, ) -> None: """Initialize JavaScript wrapper error. - + Args: message: Human-readable error message return_code: Process return code @@ -68,38 +68,31 @@ def __init__( self.return_code = return_code self.stdout = stdout self.stderr = stderr - + # Add process information to context if return_code is not None: self.add_context("return_code", return_code) if stderr: self.add_context("stderr", stderr[:500]) # Limit stderr length - + @classmethod def from_process_result( - cls, - return_code: int, - stdout: str, - stderr: str, - command: Optional[str] = None + cls, return_code: int, stdout: str, stderr: str, command: Optional[str] = None ) -> "JSWrapperError": """Create JSWrapperError from subprocess execution result.""" message = f"JavaScript wrapper execution failed with return code {return_code}" - + if command: message = f"{message} (Command: {command})" - + return cls( - message=message, - return_code=return_code, - stdout=stdout, - stderr=stderr + message=message, return_code=return_code, stdout=stdout, stderr=stderr ) class ConfigurationError(S3StorachaError): """Exception raised when configuration validation fails.""" - + def __init__( self, message: str, @@ -107,10 +100,10 @@ def __init__( field_name: Optional[str] = None, field_value: Optional[Any] = None, context: Optional[Dict[str, Any]] = None, - original_error: Optional[Exception] = None + original_error: Optional[Exception] = None, ) -> None: """Initialize configuration error. - + Args: message: Human-readable error message config_type: Type of configuration that failed (e.g., 'S3Config') @@ -123,7 +116,7 @@ def __init__( self.config_type = config_type self.field_name = field_name self.field_value = field_value - + # Add configuration details to context if config_type: self.add_context("config_type", config_type) @@ -131,29 +124,23 @@ def __init__( self.add_context("field_name", field_name) if field_value is not None: # Mask sensitive values - if field_name and any(sensitive in field_name.lower() - for sensitive in ['key', 'secret', 'password', 'token']): + if field_name and any( + sensitive in field_name.lower() + for sensitive in ["key", "secret", "password", "token"] + ): self.add_context("field_value", "***MASKED***") else: self.add_context("field_value", str(field_value)[:100]) - + @classmethod def missing_field(cls, config_type: str, field_name: str) -> "ConfigurationError": """Create ConfigurationError for missing required field.""" message = f"Missing required field '{field_name}' in {config_type}" - return cls( - message=message, - config_type=config_type, - field_name=field_name - ) - + return cls(message=message, config_type=config_type, field_name=field_name) + @classmethod def invalid_field( - cls, - config_type: str, - field_name: str, - field_value: Any, - reason: str + cls, config_type: str, field_name: str, field_value: Any, reason: str ) -> "ConfigurationError": """Create ConfigurationError for invalid field value.""" message = f"Invalid value for field '{field_name}' in {config_type}: {reason}" @@ -161,13 +148,13 @@ def invalid_field( message=message, config_type=config_type, field_name=field_name, - field_value=field_value + field_value=field_value, ) class MigrationError(S3StorachaError): """Exception raised during migration operations.""" - + def __init__( self, message: str, @@ -176,10 +163,10 @@ def __init__( destination_path: Optional[str] = None, objects_processed: Optional[int] = None, context: Optional[Dict[str, Any]] = None, - original_error: Optional[Exception] = None + original_error: Optional[Exception] = None, ) -> None: """Initialize migration error. - + Args: message: Human-readable error message operation: Migration operation that failed (e.g., 'upload', 'download') @@ -194,7 +181,7 @@ def __init__( self.source_path = source_path self.destination_path = destination_path self.objects_processed = objects_processed - + # Add migration details to context if operation: self.add_context("operation", operation) @@ -204,47 +191,42 @@ def __init__( self.add_context("destination_path", destination_path) if objects_processed is not None: self.add_context("objects_processed", objects_processed) - + @classmethod def network_error( - cls, - message: str, - operation: str, - original_error: Exception + cls, message: str, operation: str, original_error: Exception ) -> "MigrationError": """Create MigrationError for network-related failures.""" return cls( message=f"Network error during {operation}: {message}", operation=operation, - original_error=original_error + original_error=original_error, ) - + @classmethod def timeout_error( cls, operation: str, timeout_seconds: int, - objects_processed: Optional[int] = None + objects_processed: Optional[int] = None, ) -> "MigrationError": """Create MigrationError for timeout failures.""" message = f"Operation '{operation}' timed out after {timeout_seconds} seconds" return cls( - message=message, - operation=operation, - objects_processed=objects_processed + message=message, operation=operation, objects_processed=objects_processed ) - + @classmethod def validation_error( cls, message: str, source_path: Optional[str] = None, - destination_path: Optional[str] = None + destination_path: Optional[str] = None, ) -> "MigrationError": """Create MigrationError for data validation failures.""" return cls( message=f"Validation error: {message}", operation="validation", source_path=source_path, - destination_path=destination_path - ) \ No newline at end of file + destination_path=destination_path, + ) diff --git a/src/py_s3_storacha/js/package-lock.json b/src/py_s3_storacha/js/package-lock.json new file mode 100644 index 0000000..fd38f6e --- /dev/null +++ b/src/py_s3_storacha/js/package-lock.json @@ -0,0 +1,2983 @@ +{ + "name": "s3-to-storacha-js", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "s3-to-storacha-js", + "version": "0.0.1", + "dependencies": { + "@aws-sdk/client-s3": "^3.0.0", + "@storacha/client": "^1.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.917.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.917.0.tgz", + "integrity": "sha512-3L73mDCpH7G0koFv3p3WkkEKqC5wn2EznKtNMrJ6hczPIr2Cu6DJz8VHeTZp9wFZLPrIBmh3ZW1KiLujT5Fd2w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/credential-provider-node": "3.917.0", + "@aws-sdk/middleware-bucket-endpoint": "3.914.0", + "@aws-sdk/middleware-expect-continue": "3.917.0", + "@aws-sdk/middleware-flexible-checksums": "3.916.0", + "@aws-sdk/middleware-host-header": "3.914.0", + "@aws-sdk/middleware-location-constraint": "3.914.0", + "@aws-sdk/middleware-logger": "3.914.0", + "@aws-sdk/middleware-recursion-detection": "3.914.0", + "@aws-sdk/middleware-sdk-s3": "3.916.0", + "@aws-sdk/middleware-ssec": "3.914.0", + "@aws-sdk/middleware-user-agent": "3.916.0", + "@aws-sdk/region-config-resolver": "3.914.0", + "@aws-sdk/signature-v4-multi-region": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-endpoints": "3.916.0", + "@aws-sdk/util-user-agent-browser": "3.914.0", + "@aws-sdk/util-user-agent-node": "3.916.0", + "@aws-sdk/xml-builder": "3.914.0", + "@smithy/config-resolver": "^4.4.0", + "@smithy/core": "^3.17.1", + "@smithy/eventstream-serde-browser": "^4.2.3", + "@smithy/eventstream-serde-config-resolver": "^4.3.3", + "@smithy/eventstream-serde-node": "^4.2.3", + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/hash-blob-browser": "^4.2.4", + "@smithy/hash-node": "^4.2.3", + "@smithy/hash-stream-node": "^4.2.3", + "@smithy/invalid-dependency": "^4.2.3", + "@smithy/md5-js": "^4.2.3", + "@smithy/middleware-content-length": "^4.2.3", + "@smithy/middleware-endpoint": "^4.3.5", + "@smithy/middleware-retry": "^4.4.5", + "@smithy/middleware-serde": "^4.2.3", + "@smithy/middleware-stack": "^4.2.3", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/node-http-handler": "^4.4.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.4", + "@smithy/util-defaults-mode-node": "^4.2.6", + "@smithy/util-endpoints": "^3.2.3", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-retry": "^4.2.3", + "@smithy/util-stream": "^4.5.4", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.3", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.916.0.tgz", + "integrity": "sha512-Eu4PtEUL1MyRvboQnoq5YKg0Z9vAni3ccebykJy615xokVZUdA3di2YxHM/hykDQX7lcUC62q9fVIvh0+UNk/w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/middleware-host-header": "3.914.0", + "@aws-sdk/middleware-logger": "3.914.0", + "@aws-sdk/middleware-recursion-detection": "3.914.0", + "@aws-sdk/middleware-user-agent": "3.916.0", + "@aws-sdk/region-config-resolver": "3.914.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-endpoints": "3.916.0", + "@aws-sdk/util-user-agent-browser": "3.914.0", + "@aws-sdk/util-user-agent-node": "3.916.0", + "@smithy/config-resolver": "^4.4.0", + "@smithy/core": "^3.17.1", + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/hash-node": "^4.2.3", + "@smithy/invalid-dependency": "^4.2.3", + "@smithy/middleware-content-length": "^4.2.3", + "@smithy/middleware-endpoint": "^4.3.5", + "@smithy/middleware-retry": "^4.4.5", + "@smithy/middleware-serde": "^4.2.3", + "@smithy/middleware-stack": "^4.2.3", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/node-http-handler": "^4.4.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.4", + "@smithy/util-defaults-mode-node": "^4.2.6", + "@smithy/util-endpoints": "^3.2.3", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-retry": "^4.2.3", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.916.0.tgz", + "integrity": "sha512-1JHE5s6MD5PKGovmx/F1e01hUbds/1y3X8rD+Gvi/gWVfdg5noO7ZCerpRsWgfzgvCMZC9VicopBqNHCKLykZA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@aws-sdk/xml-builder": "3.914.0", + "@smithy/core": "^3.17.1", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/signature-v4": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.916.0.tgz", + "integrity": "sha512-3gDeqOXcBRXGHScc6xb7358Lyf64NRG2P08g6Bu5mv1Vbg9PKDyCAZvhKLkG7hkdfAM8Yc6UJNhbFxr1ud/tCQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.916.0.tgz", + "integrity": "sha512-NmooA5Z4/kPFJdsyoJgDxuqXC1C6oPMmreJjbOPqcwo6E/h2jxaG8utlQFgXe5F9FeJsMx668dtxVxSYnAAqHQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/node-http-handler": "^4.4.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/util-stream": "^4.5.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.917.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.917.0.tgz", + "integrity": "sha512-rvQ0QamLySRq+Okc0ZqFHZ3Fbvj3tYuWNIlzyEKklNmw5X5PM1idYKlOJflY2dvUGkIqY3lUC9SC2WL+1s7KIw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/credential-provider-env": "3.916.0", + "@aws-sdk/credential-provider-http": "3.916.0", + "@aws-sdk/credential-provider-process": "3.916.0", + "@aws-sdk/credential-provider-sso": "3.916.0", + "@aws-sdk/credential-provider-web-identity": "3.917.0", + "@aws-sdk/nested-clients": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/credential-provider-imds": "^4.2.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.917.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.917.0.tgz", + "integrity": "sha512-n7HUJ+TgU9wV/Z46yR1rqD9hUjfG50AKi+b5UXTlaDlVD8bckg40i77ROCllp53h32xQj/7H0yBIYyphwzLtmg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.916.0", + "@aws-sdk/credential-provider-http": "3.916.0", + "@aws-sdk/credential-provider-ini": "3.917.0", + "@aws-sdk/credential-provider-process": "3.916.0", + "@aws-sdk/credential-provider-sso": "3.916.0", + "@aws-sdk/credential-provider-web-identity": "3.917.0", + "@aws-sdk/types": "3.914.0", + "@smithy/credential-provider-imds": "^4.2.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.916.0.tgz", + "integrity": "sha512-SXDyDvpJ1+WbotZDLJW1lqP6gYGaXfZJrgFSXIuZjHb75fKeNRgPkQX/wZDdUvCwdrscvxmtyJorp2sVYkMcvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.916.0.tgz", + "integrity": "sha512-gu9D+c+U/Dp1AKBcVxYHNNoZF9uD4wjAKYCjgSN37j4tDsazwMEylbbZLuRNuxfbXtizbo4/TiaxBXDbWM7AkQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.916.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/token-providers": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.917.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.917.0.tgz", + "integrity": "sha512-pZncQhFbwW04pB0jcD5OFv3x2gAddDYCVxyJVixgyhSw7bKCYxqu6ramfq1NxyVpmm+qsw+ijwi/3cCmhUHF/A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/nested-clients": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.914.0.tgz", + "integrity": "sha512-mHLsVnPPp4iq3gL2oEBamfpeETFV0qzxRHmcnCfEP3hualV8YF8jbXGmwPCPopUPQDpbYDBHYtXaoClZikCWPQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "@smithy/util-config-provider": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.917.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.917.0.tgz", + "integrity": "sha512-UPBq1ZP2CaxwbncWSbVqkhYXQrmfNiqAtHyBxi413hjRVZ4JhQ1UyH7pz5yqiG8zx2/+Po8cUD4SDUwJgda4nw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.916.0.tgz", + "integrity": "sha512-CBRRg6slHHBYAm26AWY/pECHK0vVO/peDoNhZiAzUNt4jV6VftotjszEJ904pKGOr7/86CfZxtCnP3CCs3lQjA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-stream": "^4.5.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.914.0.tgz", + "integrity": "sha512-7r9ToySQ15+iIgXMF/h616PcQStByylVkCshmQqcdeynD/lCn2l667ynckxW4+ql0Q+Bo/URljuhJRxVJzydNA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.914.0.tgz", + "integrity": "sha512-Mpd0Sm9+GN7TBqGnZg1+dO5QZ/EOYEcDTo7KfvoyrXScMlxvYm9fdrUVMmLdPn/lntweZGV3uNrs+huasGOOTA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.914.0.tgz", + "integrity": "sha512-/gaW2VENS5vKvJbcE1umV4Ag3NuiVzpsANxtrqISxT3ovyro29o1RezW/Avz/6oJqjnmgz8soe9J1t65jJdiNg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.914.0.tgz", + "integrity": "sha512-yiAjQKs5S2JKYc+GrkvGMwkUvhepXDigEXpSJqUseR/IrqHhvGNuOxDxq+8LbDhM4ajEW81wkiBbU+Jl9G82yQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@aws/lambda-invoke-store": "^0.0.1", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.916.0.tgz", + "integrity": "sha512-pjmzzjkEkpJObzmTthqJPq/P13KoNFuEi/x5PISlzJtHofCNcyXeVAQ90yvY2dQ6UXHf511Rh1/ytiKy2A8M0g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/core": "^3.17.1", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/signature-v4": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-stream": "^4.5.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.914.0.tgz", + "integrity": "sha512-V1Oae/oLVbpNb9uWs+v80GKylZCdsbqs2c2Xb1FsAUPtYeSnxFuAWsF3/2AEMSSpFe0dTC5KyWr/eKl2aim9VQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.916.0.tgz", + "integrity": "sha512-mzF5AdrpQXc2SOmAoaQeHpDFsK2GE6EGcEACeNuoESluPI2uYMpuuNMYrUufdnIAIyqgKlis0NVxiahA5jG42w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-endpoints": "3.916.0", + "@smithy/core": "^3.17.1", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.916.0.tgz", + "integrity": "sha512-tgg8e8AnVAer0rcgeWucFJ/uNN67TbTiDHfD+zIOPKep0Z61mrHEoeT/X8WxGIOkEn4W6nMpmS4ii8P42rNtnA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/middleware-host-header": "3.914.0", + "@aws-sdk/middleware-logger": "3.914.0", + "@aws-sdk/middleware-recursion-detection": "3.914.0", + "@aws-sdk/middleware-user-agent": "3.916.0", + "@aws-sdk/region-config-resolver": "3.914.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-endpoints": "3.916.0", + "@aws-sdk/util-user-agent-browser": "3.914.0", + "@aws-sdk/util-user-agent-node": "3.916.0", + "@smithy/config-resolver": "^4.4.0", + "@smithy/core": "^3.17.1", + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/hash-node": "^4.2.3", + "@smithy/invalid-dependency": "^4.2.3", + "@smithy/middleware-content-length": "^4.2.3", + "@smithy/middleware-endpoint": "^4.3.5", + "@smithy/middleware-retry": "^4.4.5", + "@smithy/middleware-serde": "^4.2.3", + "@smithy/middleware-stack": "^4.2.3", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/node-http-handler": "^4.4.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.4", + "@smithy/util-defaults-mode-node": "^4.2.6", + "@smithy/util-endpoints": "^3.2.3", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-retry": "^4.2.3", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.914.0.tgz", + "integrity": "sha512-KlmHhRbn1qdwXUdsdrJ7S/MAkkC1jLpQ11n+XvxUUUCGAJd1gjC7AjxPZUM7ieQ2zcb8bfEzIU7al+Q3ZT0u7Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/config-resolver": "^4.4.0", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.916.0.tgz", + "integrity": "sha512-fuzUMo6xU7e0NBzBA6TQ4FUf1gqNbg4woBSvYfxRRsIfKmSMn9/elXXn4sAE5UKvlwVQmYnb6p7dpVRPyFvnQA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/protocol-http": "^5.3.3", + "@smithy/signature-v4": "^5.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.916.0.tgz", + "integrity": "sha512-13GGOEgq5etbXulFCmYqhWtpcEQ6WI6U53dvXbheW0guut8fDFJZmEv7tKMTJgiybxh7JHd0rWcL9JQND8DwoQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.916.0", + "@aws-sdk/nested-clients": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.914.0.tgz", + "integrity": "sha512-kQWPsRDmom4yvAfyG6L1lMmlwnTzm1XwMHOU+G5IFlsP4YEaMtXidDzW/wiivY0QFrhfCz/4TVmu0a2aPU57ug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.893.0.tgz", + "integrity": "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.916.0.tgz", + "integrity": "sha512-bAgUQwvixdsiGNcuZSDAOWbyHlnPtg8G8TyHD6DTfTmKTHUW6tAn+af/ZYJPXEzXhhpwgJqi58vWnsiDhmr7NQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@smithy/util-endpoints": "^3.2.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", + "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.914.0.tgz", + "integrity": "sha512-rMQUrM1ECH4kmIwlGl9UB0BtbHy6ZuKdWFrIknu8yGTRI/saAucqNTh5EI1vWBxZ0ElhK5+g7zOnUuhSmVQYUA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.914.0", + "@smithy/types": "^4.8.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.916.0.tgz", + "integrity": "sha512-CwfWV2ch6UdjuSV75ZU99N03seEUb31FIUrXBnwa6oONqj/xqXwrxtlUMLx6WH3OJEE4zI3zt5PjlTdGcVwf4g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.914.0.tgz", + "integrity": "sha512-k75evsBD5TcIjedycYS7QXQ98AmOtbnxRJOPtCo0IwYRmy7UvqgS/gBL5SmrIqeV6FDSYRQMgdBxSMp6MLmdew==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.0.1.tgz", + "integrity": "sha512-ORHRQ2tmvnBXc8t/X9Z8IcSbBA4xTLKuN873FopzklHMeqBst7YG0d+AX97inkvDX+NChYtSr+qGfcqGFaI8Zw==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@ipld/car": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/@ipld/car/-/car-5.4.2.tgz", + "integrity": "sha512-gfyrJvePyXnh2Fbj8mPg4JYvEZ3izhk8C9WgAle7xIYbrJNSXmNQ6BxAls8Gof97vvGbCROdxbTWRmHJtTCbcg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@ipld/dag-cbor": "^9.0.7", + "cborg": "^4.0.5", + "multiformats": "^13.0.0", + "varint": "^6.0.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@ipld/dag-cbor": { + "version": "9.2.5", + "resolved": "https://registry.npmjs.org/@ipld/dag-cbor/-/dag-cbor-9.2.5.tgz", + "integrity": "sha512-84wSr4jv30biui7endhobYhXBQzQE4c/wdoWlFrKcfiwH+ofaPg8fwsM8okX9cOzkkrsAsNdDyH3ou+kiLquwQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "cborg": "^4.0.0", + "multiformats": "^13.1.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@ipld/dag-json": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/@ipld/dag-json/-/dag-json-10.2.5.tgz", + "integrity": "sha512-Q4Fr3IBDEN8gkpgNefynJ4U/ZO5Kwr7WSUMBDbZx0c37t0+IwQCTM9yJh8l5L4SRFjm31MuHwniZ/kM+P7GQ3Q==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "cborg": "^4.0.0", + "multiformats": "^13.1.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@ipld/dag-pb": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@ipld/dag-pb/-/dag-pb-4.1.5.tgz", + "integrity": "sha512-w4PZ2yPqvNmlAir7/2hsCRMqny1EY5jj26iZcSgxREJexmbAc2FI21jp26MqiNdfgAxvkCnf2N/TJI18GaDNwA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "multiformats": "^13.1.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@ipld/dag-ucan": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/@ipld/dag-ucan/-/dag-ucan-3.4.5.tgz", + "integrity": "sha512-wiWhH0Ju7WgnumsarHCYtlo+1NRy+WIsXyAB7g2sDqVsKCyEJiLLmjeZzKqNv+qMxLnUS8bQ287cPiQ//s/oJQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@ipld/dag-cbor": "^9.0.0", + "@ipld/dag-json": "^10.0.0", + "multiformats": "^13.3.1" + } + }, + "node_modules/@ipld/unixfs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@ipld/unixfs/-/unixfs-3.0.0.tgz", + "integrity": "sha512-Tj3/BPOlnemcZQ2ETIZAO8hqAs9KNzWyX5J9+JCL9jDwvYwjxeYjqJ3v+9DusNvTBmJhZnGVP6ijUHrsuOLp+g==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@ipld/dag-pb": "^4.0.0", + "@multiformats/murmur3": "^2.1.3", + "@perma/map": "^1.0.2", + "actor": "^2.3.1", + "multiformats": "^13.0.1", + "protobufjs": "^7.1.2", + "rabin-rs": "^2.1.0" + } + }, + "node_modules/@multiformats/murmur3": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@multiformats/murmur3/-/murmur3-2.1.8.tgz", + "integrity": "sha512-6vId1C46ra3R1sbJUOFCZnsUIveR9oF20yhPmAFxPm0JfrX3/ZRCgP3YDrBzlGoEppOXnA9czHeYc0T9mB6hbA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "multiformats": "^13.0.0", + "murmurhash3js-revisited": "^3.0.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/ed25519": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.5.tgz", + "integrity": "sha512-xuS0nwRMQBvSxDa7UxMb61xTiH3MxTgUfhyPUALVIe0FlOAz4sjELwyDRyUvqeEYfRSG9qNjFIycqLZppg4RSA==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT" + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@perma/map": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@perma/map/-/map-1.0.3.tgz", + "integrity": "sha512-Bf5njk0fnJGTFE2ETntq0N1oJ6YdCPIpTDn3R3KYZJQdeYSOCNL7mBrFlGnbqav8YQhJA/p81pvHINX9vAtHkQ==", + "license": "(Apache-2.0 AND MIT)", + "dependencies": { + "@multiformats/murmur3": "^2.1.0", + "murmurhash3js-revisited": "^3.0.0" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.3.tgz", + "integrity": "sha512-xWL9Mf8b7tIFuAlpjKtRPnHrR8XVrwTj5NPYO/QwZPtc0SDLsPxb56V5tzi5yspSMytISHybifez+4jlrx0vkQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.0.tgz", + "integrity": "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.1.tgz", + "integrity": "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.0.tgz", + "integrity": "sha512-Kkmz3Mup2PGp/HNJxhCWkLNdlajJORLSjwkcfrj0E7nu6STAEdcMR1ir5P9/xOmncx8xXfru0fbUYLlZog/cFg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.3", + "@smithy/types": "^4.8.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-endpoints": "^3.2.3", + "@smithy/util-middleware": "^4.2.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.17.1.tgz", + "integrity": "sha512-V4Qc2CIb5McABYfaGiIYLTmo/vwNIK7WXI5aGveBd9UcdhbOMwcvIMxIw/DJj1S9QgOMa/7FBkarMdIC0EOTEQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.2.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-stream": "^4.5.4", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.3.tgz", + "integrity": "sha512-hA1MQ/WAHly4SYltJKitEsIDVsNmXcQfYBRv2e+q04fnqtAX5qXaybxy/fhUeAMCnQIdAjaGDb04fMHQefWRhw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.3.tgz", + "integrity": "sha512-rcr0VH0uNoMrtgKuY7sMfyKqbHc4GQaQ6Yp4vwgm+Z6psPuOgL+i/Eo/QWdXRmMinL3EgFM0Z1vkfyPyfzLmjw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.8.0", + "@smithy/util-hex-encoding": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.3.tgz", + "integrity": "sha512-EcS0kydOr2qJ3vV45y7nWnTlrPmVIMbUFOZbMG80+e2+xePQISX9DrcbRpVRFTS5Nqz3FiEbDcTCAV0or7bqdw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.3.tgz", + "integrity": "sha512-GewKGZ6lIJ9APjHFqR2cUW+Efp98xLu1KmN0jOWxQ1TN/gx3HTUPVbLciFD8CfScBj2IiKifqh9vYFRRXrYqXA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.3.tgz", + "integrity": "sha512-uQobOTQq2FapuSOlmGLUeGTpvcBLE5Fc7XjERUSk4dxEi4AhTwuyHYZNAvL4EMUp7lzxxkKDFaJ1GY0ovrj0Kg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.3.tgz", + "integrity": "sha512-QIvH/CKOk1BZPz/iwfgbh1SQD5Y0lpaw2kLA8zpLRRtYMPXeYUEWh+moTaJyqDaKlbrB174kB7FSRFiZ735tWw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.2.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.4.tgz", + "integrity": "sha512-bwigPylvivpRLCm+YK9I5wRIYjFESSVwl8JQ1vVx/XhCw0PtCi558NwTnT2DaVCl5pYlImGuQTSwMsZ+pIavRw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.3", + "@smithy/querystring-builder": "^4.2.3", + "@smithy/types": "^4.8.0", + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.4.tgz", + "integrity": "sha512-W7eIxD+rTNsLB/2ynjmbdeP7TgxRXprfvqQxKFEfy9HW2HeD7t+g+KCIrY0pIn/GFjA6/fIpH+JQnfg5TTk76Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.2.0", + "@smithy/chunked-blob-reader-native": "^4.2.1", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.3.tgz", + "integrity": "sha512-6+NOdZDbfuU6s1ISp3UOk5Rg953RJ2aBLNLLBEcamLjHAg1Po9Ha7QIB5ZWhdRUVuOUrT8BVFR+O2KIPmw027g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.3.tgz", + "integrity": "sha512-EXMSa2yiStVII3x/+BIynyOAZlS7dGvI7RFrzXa/XssBgck/7TXJIvnjnCu328GY/VwHDC4VeDyP1S4rqwpYag==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.3.tgz", + "integrity": "sha512-Cc9W5DwDuebXEDMpOpl4iERo8I0KFjTnomK2RMdhhR87GwrSmUmwMxS4P5JdRf+LsjOdIqumcerwRgYMr/tZ9Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.3.tgz", + "integrity": "sha512-5+4bUEJQi/NRgzdA5SVXvAwyvEnD0ZAiKzV3yLO6dN5BG8ScKBweZ8mxXXUtdxq+Dx5k6EshKk0XJ7vgvIPSnA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.3.tgz", + "integrity": "sha512-/atXLsT88GwKtfp5Jr0Ks1CSa4+lB+IgRnkNrrYP0h1wL4swHNb0YONEvTceNKNdZGJsye+W2HH8W7olbcPUeA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.5.tgz", + "integrity": "sha512-SIzKVTvEudFWJbxAaq7f2GvP3jh2FHDpIFI6/VAf4FOWGFZy0vnYMPSRj8PGYI8Hjt29mvmwSRgKuO3bK4ixDw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.17.1", + "@smithy/middleware-serde": "^4.2.3", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@smithy/util-middleware": "^4.2.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.5.tgz", + "integrity": "sha512-DCaXbQqcZ4tONMvvdz+zccDE21sLcbwWoNqzPLFlZaxt1lDtOE2tlVpRSwcTOJrjJSUThdgEYn7HrX5oLGlK9A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/service-error-classification": "^4.2.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-retry": "^4.2.3", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.3.tgz", + "integrity": "sha512-8g4NuUINpYccxiCXM5s1/V+uLtts8NcX4+sPEbvYQDZk4XoJfDpq5y2FQxfmUL89syoldpzNzA0R9nhzdtdKnQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.3.tgz", + "integrity": "sha512-iGuOJkH71faPNgOj/gWuEGS6xvQashpLwWB1HjHq1lNNiVfbiJLpZVbhddPuDbx9l4Cgl0vPLq5ltRfSaHfspA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.3.tgz", + "integrity": "sha512-NzI1eBpBSViOav8NVy1fqOlSfkLgkUjUTlohUSgAEhHaFWA3XJiLditvavIP7OpvTjDp5u2LhtlBhkBlEisMwA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.3.tgz", + "integrity": "sha512-MAwltrDB0lZB/H6/2M5PIsISSwdI5yIh6DaBB9r0Flo9nx3y0dzl/qTMJPd7tJvPdsx6Ks/cwVzheGNYzXyNbQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/querystring-builder": "^4.2.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.3.tgz", + "integrity": "sha512-+1EZ+Y+njiefCohjlhyOcy1UNYjT+1PwGFHCxA/gYctjg3DQWAU19WigOXAco/Ql8hZokNehpzLd0/+3uCreqQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.3.tgz", + "integrity": "sha512-Mn7f/1aN2/jecywDcRDvWWWJF4uwg/A0XjFMJtj72DsgHTByfjRltSqcT9NyE9RTdBSN6X1RSXrhn/YWQl8xlw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.3.tgz", + "integrity": "sha512-LOVCGCmwMahYUM/P0YnU/AlDQFjcu+gWbFJooC417QRB/lDJlWSn8qmPSDp+s4YVAHOgtgbNG4sR+SxF/VOcJQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "@smithy/util-uri-escape": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.3.tgz", + "integrity": "sha512-cYlSNHcTAX/wc1rpblli3aUlLMGgKZ/Oqn8hhjFASXMCXjIqeuQBei0cnq2JR8t4RtU9FpG6uyl6PxyArTiwKA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.3.tgz", + "integrity": "sha512-NkxsAxFWwsPsQiwFG2MzJ/T7uIR6AQNh1SzcxSUnmmIqIQMlLRQDKhc17M7IYjiuBXhrQRjQTo3CxX+DobS93g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.3.3.tgz", + "integrity": "sha512-9f9Ixej0hFhroOK2TxZfUUDR13WVa8tQzhSzPDgXe5jGL3KmaM9s8XN7RQwqtEypI82q9KHnKS71CJ+q/1xLtQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.3.tgz", + "integrity": "sha512-CmSlUy+eEYbIEYN5N3vvQTRfqt0lJlQkaQUIf+oizu7BbDut0pozfDjBGecfcfWf7c62Yis4JIEgqQ/TCfodaA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.1.tgz", + "integrity": "sha512-Ngb95ryR5A9xqvQFT5mAmYkCwbXvoLavLFwmi7zVg/IowFPCfiqRfkOKnbc/ZRL8ZKJ4f+Tp6kSu6wjDQb8L/g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.17.1", + "@smithy/middleware-endpoint": "^4.3.5", + "@smithy/middleware-stack": "^4.2.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "@smithy/util-stream": "^4.5.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.8.0.tgz", + "integrity": "sha512-QpELEHLO8SsQVtqP+MkEgCYTFW0pleGozfs3cZ183ZBj9z3VC1CX1/wtFMK64p+5bhtZo41SeLK1rBRtd25nHQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.3.tgz", + "integrity": "sha512-I066AigYvY3d9VlU3zG9XzZg1yT10aNqvCaBTw9EPgu5GrsEl1aUkcMvhkIXascYH1A8W0LQo3B1Kr1cJNcQEw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.4.tgz", + "integrity": "sha512-qI5PJSW52rnutos8Bln8nwQZRpyoSRN6k2ajyoUHNMUzmWqHnOJCnDELJuV6m5PML0VkHI+XcXzdB+6awiqYUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.6.tgz", + "integrity": "sha512-c6M/ceBTm31YdcFpgfgQAJaw3KbaLuRKnAz91iMWFLSrgxRpYm03c3bu5cpYojNMfkV9arCUelelKA7XQT36SQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.0", + "@smithy/credential-provider-imds": "^4.2.3", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.3.tgz", + "integrity": "sha512-aCfxUOVv0CzBIkU10TubdgKSx5uRvzH064kaiPEWfNIvKOtNpu642P4FP1hgOFkjQIkDObrfIDnKMKkeyrejvQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.3.tgz", + "integrity": "sha512-v5ObKlSe8PWUHCqEiX2fy1gNv6goiw6E5I/PN2aXg3Fb/hse0xeaAnSpXDiWl7x6LamVKq7senB+m5LOYHUAHw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.3.tgz", + "integrity": "sha512-lLPWnakjC0q9z+OtiXk+9RPQiYPNAovt2IXD3CP4LkOnd9NpUsxOjMx1SnoUVB7Orb7fZp67cQMtTBKMFDvOGg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.2.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.4.tgz", + "integrity": "sha512-+qDxSkiErejw1BAIXUFBSfM5xh3arbz1MmxlbMCKanDDZtVEQ7PSKW9FQS0Vud1eI/kYn0oCTVKyNzRlq+9MUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/node-http-handler": "^4.4.3", + "@smithy/types": "^4.8.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.3.tgz", + "integrity": "sha512-5+nU///E5sAdD7t3hs4uwvCTWQtTR8JwKwOCSJtBRx0bY1isDo1QwH87vRK86vlFLBTISqoDA2V6xvP6nF1isQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.3", + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", + "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@storacha/access": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@storacha/access/-/access-1.6.2.tgz", + "integrity": "sha512-gps6StohcrYGbd34BdkyKOnuoEwFQVp96GSCiAOS1IKUyNktjx4+bRuZcHtjIRTwJlR2lr7uK8mVqdeywf7JRw==", + "license": "(Apache-2.0 OR MIT)", + "dependencies": { + "@ipld/car": "^5.4.0", + "@ipld/dag-ucan": "^3.4.5", + "@scure/bip39": "^1.2.1", + "@storacha/capabilities": "^1.10.0", + "@storacha/did-mailto": "^1.0.2", + "@storacha/one-webcrypto": "^1.0.1", + "@ucanto/client": "^9.0.2", + "@ucanto/core": "^10.4.5", + "@ucanto/interface": "^11.0.1", + "@ucanto/principal": "^9.0.3", + "@ucanto/transport": "^9.2.1", + "@ucanto/validator": "^10.0.1", + "bigint-mod-arith": "^3.1.2", + "conf": "11.0.2", + "multiformats": "^13.3.6", + "p-defer": "^4.0.0", + "type-fest": "^4.9.0", + "uint8arrays": "^5.1.0" + } + }, + "node_modules/@storacha/blob-index": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@storacha/blob-index/-/blob-index-1.2.2.tgz", + "integrity": "sha512-JC8EVXYbMdcNSn3jAitykLoRPfbqtq9TFhdpLIVN/6kWGj6Z0vtMcCCxmHFrWRGwGwma76MW3X9gsv8ANzomAg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@ipld/dag-cbor": "^9.0.6", + "@storacha/capabilities": "^1.10.0", + "@storacha/one-webcrypto": "^1.0.1", + "@ucanto/core": "^10.4.5", + "@ucanto/interface": "^11.0.1", + "carstream": "^2.1.0", + "multiformats": "^13.3.6", + "sade": "^1.8.1", + "uint8arrays": "^5.0.3" + }, + "bin": { + "blob-index": "src/bin.js" + }, + "engines": { + "node": ">=16.15" + } + }, + "node_modules/@storacha/capabilities": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@storacha/capabilities/-/capabilities-1.10.0.tgz", + "integrity": "sha512-rdKOuKuiifNi46ZpXXHhAhQeuOevZGkASg0rhy62RRXnbgfcCt6cBna9NL9ULYbycL/Im1DKpotaEf83E6sNEQ==", + "license": "(Apache-2.0 OR MIT)", + "dependencies": { + "@ucanto/core": "^10.4.5", + "@ucanto/interface": "^11.0.1", + "@ucanto/principal": "^9.0.3", + "@ucanto/transport": "^9.2.1", + "@ucanto/validator": "^10.0.1", + "@web3-storage/data-segment": "^5.2.0", + "multiformats": "^13.3.6" + } + }, + "node_modules/@storacha/client": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/@storacha/client/-/client-1.8.4.tgz", + "integrity": "sha512-6KYwWtaBljuMZ/oFa/2ki4gUDm4OjSXiRzPaj4yFW1Es0kXBDrPm7NL0mhWpxmYK+kgal0/asXBU2RLwzXWenQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@ipld/dag-ucan": "^3.4.5", + "@storacha/access": "^1.6.2", + "@storacha/blob-index": "^1.2.2", + "@storacha/capabilities": "^1.10.0", + "@storacha/did-mailto": "^1.0.2", + "@storacha/filecoin-client": "^1.0.13", + "@storacha/upload-client": "^1.3.4", + "@ucanto/client": "^9.0.2", + "@ucanto/core": "^10.4.5", + "@ucanto/interface": "^11.0.1", + "@ucanto/principal": "^9.0.3", + "@ucanto/transport": "^9.2.1", + "environment": "^1.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@storacha/did-mailto": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@storacha/did-mailto/-/did-mailto-1.0.2.tgz", + "integrity": "sha512-0ee1t15yG4xdyIYz2iL+wP06VnGQf8zJ/xpszjf4sdP2sTgAVzx92LfEgHcuIbFF4sabcJXzHFAcMi8l8u64Mg==", + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">=16.15" + } + }, + "node_modules/@storacha/filecoin-client": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@storacha/filecoin-client/-/filecoin-client-1.0.13.tgz", + "integrity": "sha512-UyV39V6aJQHsfVa5+OirxRrZW6gYjdzY6zG2YbxkC3ZdXZIVZZ+CEq2rDPaaWpnKZIkAvXWZ4RN0F9r5C9H7SQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@ipld/dag-ucan": "^3.4.5", + "@storacha/capabilities": "^1.10.0", + "@ucanto/client": "^9.0.2", + "@ucanto/core": "^10.4.5", + "@ucanto/interface": "^11.0.1", + "@ucanto/transport": "^9.2.1" + } + }, + "node_modules/@storacha/one-webcrypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@storacha/one-webcrypto/-/one-webcrypto-1.0.1.tgz", + "integrity": "sha512-bD+vWmcgsEBqU0Dz04BR43SA03bBoLTAY29vaKasY9Oe8cb6XIP0/vkm0OS2UwKC13c8uRgFW4rjJUgDCNLejQ==", + "license": "MIT" + }, + "node_modules/@storacha/upload-client": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@storacha/upload-client/-/upload-client-1.3.4.tgz", + "integrity": "sha512-HzxtIti3Wk1EUMT8nPF1LzOGk8eSHbdpm99JIzJvCLmVaolC0N2X/nmbk3Zm6WUaWhU2RZVk4bJNF6bDTW9+lQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@ipld/car": "^5.2.2", + "@ipld/dag-cbor": "^9.0.6", + "@ipld/dag-ucan": "^3.4.5", + "@ipld/unixfs": "^3.0.0", + "@storacha/blob-index": "^1.2.2", + "@storacha/capabilities": "^1.10.0", + "@storacha/filecoin-client": "^1.0.13", + "@ucanto/client": "^9.0.2", + "@ucanto/core": "^10.4.5", + "@ucanto/interface": "^11.0.1", + "@ucanto/transport": "^9.2.1", + "@web3-storage/data-segment": "^5.1.0", + "ipfs-utils": "^9.0.14", + "multiformats": "^13.3.6", + "p-retry": "^5.1.2", + "varint": "^6.0.0" + } + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", + "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/retry": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", + "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", + "license": "MIT" + }, + "node_modules/@ucanto/client": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@ucanto/client/-/client-9.0.2.tgz", + "integrity": "sha512-XbG8rw/fc1hPSFKDQ/z1NpIuc4Lm79J4NWbP+dedvDnb5LmOBMHE8bjjEPD4xGO2eh6fWQSZ4YTYk5wjkLz+3Q==", + "license": "(Apache-2.0 AND MIT)", + "dependencies": { + "@ucanto/core": "^10.4.5", + "@ucanto/interface": "^11.0.1" + } + }, + "node_modules/@ucanto/core": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/@ucanto/core/-/core-10.4.5.tgz", + "integrity": "sha512-NKVgAFjpPu1QFyKjYHlaeJbSvst3oA79S7XNSFFQGhNKhGJ45ro67Fa5zxdy5jlU5jhSox3+T81VsmsszfBRYw==", + "license": "(Apache-2.0 AND MIT)", + "dependencies": { + "@ipld/car": "^5.1.0", + "@ipld/dag-cbor": "^9.0.0", + "@ipld/dag-ucan": "^3.4.5", + "@ucanto/interface": "^11.0.1", + "multiformats": "^13.3.1" + } + }, + "node_modules/@ucanto/interface": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@ucanto/interface/-/interface-11.0.1.tgz", + "integrity": "sha512-d0yoPFXdbb3EyM3sBom5VVcVyz58HT+57qi/ywSY1dbZAm4c7GZss0lpfcI5OXQREzGCzJYAg1e7pFwqbATnmQ==", + "license": "(Apache-2.0 AND MIT)", + "dependencies": { + "@ipld/dag-ucan": "^3.4.5", + "multiformats": "^13.3.1" + } + }, + "node_modules/@ucanto/principal": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@ucanto/principal/-/principal-9.0.3.tgz", + "integrity": "sha512-MFLvckOpQA46VSeLWyB7xCysL+ub5vnbCEtHbWhNE3qkzeo14v4wSThd60odPhxj83QXrzbveiebp0gcaEvXJg==", + "license": "(Apache-2.0 AND MIT)", + "dependencies": { + "@ipld/dag-ucan": "^3.4.5", + "@noble/curves": "^1.2.0", + "@noble/ed25519": "^1.7.3", + "@noble/hashes": "^1.3.2", + "@ucanto/interface": "^11.0.1", + "multiformats": "^13.3.1", + "one-webcrypto": "^1.0.3" + } + }, + "node_modules/@ucanto/transport": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@ucanto/transport/-/transport-9.2.1.tgz", + "integrity": "sha512-/Dhr+2vjpGO5GTKsSl8XcPQFtFdZvIA9Yk/yzzY9DHFsdI+tXrUjLWYwXcidx8EIdx5Xd9oitF4wodIZuiW8GQ==", + "license": "(Apache-2.0 AND MIT)", + "dependencies": { + "@ucanto/core": "^10.4.5", + "@ucanto/interface": "^11.0.1" + } + }, + "node_modules/@ucanto/validator": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@ucanto/validator/-/validator-10.0.1.tgz", + "integrity": "sha512-J1/2NAfvs55CxDEuHKMTjoLgoVrFqBbZqErYwL7TDx8Pu2V9sv7rc2wZS5dRxcM1VCEzHXY3SWeLOJuH8VgKrw==", + "license": "(Apache-2.0 AND MIT)", + "dependencies": { + "@ipld/car": "^5.4.0", + "@ipld/dag-cbor": "^9.2.2", + "@ucanto/core": "^10.4.5", + "@ucanto/interface": "^11.0.1", + "multiformats": "^13.3.1" + } + }, + "node_modules/@web3-storage/data-segment": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@web3-storage/data-segment/-/data-segment-5.3.0.tgz", + "integrity": "sha512-zFJ4m+pEKqtKatJNsFrk/2lHeFSbkXZ6KKXjBe7/2ayA9wAar7T/unewnOcZrrZTnCWmaxKsXWqdMFy9bXK9dw==", + "license": "(Apache-2.0 AND MIT)", + "dependencies": { + "@ipld/dag-cbor": "^9.2.1", + "multiformats": "^13.3.0", + "sync-multihash-sha2": "^1.0.0" + } + }, + "node_modules/actor": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/actor/-/actor-2.3.1.tgz", + "integrity": "sha512-ST/3wnvcP2tKDXnum7nLCLXm+/rsf8vPocXH2Fre6D8FQwNkGDd4JEitBlXj007VQJfiGYRQvXqwOBZVi+JtRg==", + "license": "(Apache-2.0 AND MIT)" + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/any-signal": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/any-signal/-/any-signal-3.0.1.tgz", + "integrity": "sha512-xgZgJtKEa9YmDqXodIgl7Fl1C8yNXr8w6gXjqK3LW4GcEiYT+6AQfJSE/8SPsEpLLmcvbv8YU+qet94UewHxqg==", + "license": "MIT" + }, + "node_modules/atomically": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.0.3.tgz", + "integrity": "sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==", + "dependencies": { + "stubborn-fs": "^1.2.5", + "when-exit": "^2.1.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bigint-mod-arith": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/bigint-mod-arith/-/bigint-mod-arith-3.3.1.tgz", + "integrity": "sha512-pX/cYW3dCa87Jrzv6DAr8ivbbJRzEX5yGhdt8IutnX/PCIXfpx+mabWNK/M8qqh+zQ0J3thftUBHW0ByuUlG0w==", + "license": "MIT", + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/bowser": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", + "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browser-readablestream-to-it": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/browser-readablestream-to-it/-/browser-readablestream-to-it-1.0.3.tgz", + "integrity": "sha512-+12sHB+Br8HIh6VAMVEG5r3UXCyESIgDW7kzk3BjIXa43DVqVwL7GC5TW3jeh+72dtcH99pPVpw0X8i0jt+/kw==", + "license": "ISC" + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/carstream": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/carstream/-/carstream-2.3.0.tgz", + "integrity": "sha512-2YwFg5Kxs2tqVCJv7sthWbYoUpALCYBBfTdpQcpicV7ipi6bBb1h9M4MNb1vm+724f39lUNp5VWhW43IFxfPlA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@ipld/dag-cbor": "^9.0.3", + "multiformats": "^13.0.1", + "uint8arraylist": "^2.4.3" + } + }, + "node_modules/cborg": { + "version": "4.2.18", + "resolved": "https://registry.npmjs.org/cborg/-/cborg-4.2.18.tgz", + "integrity": "sha512-uzhkd5HOaLccokqeZa5B0Qz7/aa9C12pmUq5yU3vcy6I6OhTKdPHSzOuBPZfcoQHdcx8Emz/dWZbPNNfF/puvg==", + "license": "Apache-2.0", + "bin": { + "cborg": "lib/bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/conf": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/conf/-/conf-11.0.2.tgz", + "integrity": "sha512-jjyhlQ0ew/iwmtwsS2RaB6s8DBifcE2GYBEaw2SJDUY/slJJbNfY4GlDVzOs/ff8cM/Wua5CikqXgbFl5eu85A==", + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", + "atomically": "^2.0.0", + "debounce-fn": "^5.1.2", + "dot-prop": "^7.2.0", + "env-paths": "^3.0.0", + "json-schema-typed": "^8.0.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/debounce-fn": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-5.1.2.tgz", + "integrity": "sha512-Sr4SdOZ4vw6eQDvPYNxHogvrxmCIld/VenC5JbNrFwMiwd7lY/Z18ZFfo+EWNG4DD9nFlAujWAo/wGuOPHmy5A==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-7.2.0.tgz", + "integrity": "sha512-Ol/IPXUARn9CSbkrdV4VJo7uCy1I3VuSiWCaFSg+8BdUOzF9n3jefIpcgAydvUZbTdEBZs2vEiTiS9m61ssiDA==", + "license": "MIT", + "dependencies": { + "type-fest": "^2.11.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/electron-fetch": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/electron-fetch/-/electron-fetch-1.9.1.tgz", + "integrity": "sha512-M9qw6oUILGVrcENMSRRefE1MbHPIz0h79EKIeJWK9v563aT9Qkh8aEHPO1H5vi970wPirNY+jO9OpFoLiMsMGA==", + "license": "MIT", + "dependencies": { + "encoding": "^0.1.13" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-3.0.1.tgz", + "integrity": "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/get-iterator": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-iterator/-/get-iterator-1.0.2.tgz", + "integrity": "sha512-v+dm9bNVfOYsY1OrhaCrmyOcYoSeVvbt+hHZ0Au+T+p1y+0Uyj9aMaGIeUTT6xdpRbWzDeYKvfOslPhggQMcsg==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipfs-utils": { + "version": "9.0.14", + "resolved": "https://registry.npmjs.org/ipfs-utils/-/ipfs-utils-9.0.14.tgz", + "integrity": "sha512-zIaiEGX18QATxgaS0/EOQNoo33W0islREABAcxXE8n7y2MGAlB+hdsxXn4J0hGZge8IqVQhW8sWIb+oJz2yEvg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "any-signal": "^3.0.0", + "browser-readablestream-to-it": "^1.0.0", + "buffer": "^6.0.1", + "electron-fetch": "^1.7.2", + "err-code": "^3.0.1", + "is-electron": "^2.2.0", + "iso-url": "^1.1.5", + "it-all": "^1.0.4", + "it-glob": "^1.0.1", + "it-to-stream": "^1.0.0", + "merge-options": "^3.0.4", + "nanoid": "^3.1.20", + "native-fetch": "^3.0.0", + "node-fetch": "^2.6.8", + "react-native-fetch-api": "^3.0.0", + "stream-to-it": "^0.2.2" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/is-electron": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz", + "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==", + "license": "MIT" + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/iso-url": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iso-url/-/iso-url-1.2.1.tgz", + "integrity": "sha512-9JPDgCN4B7QPkLtYAAOrEuAWvP9rWvR5offAr0/SeF046wIkglqH3VXgYYP6NcsKslH80UIVgmPqNe3j7tG2ng==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/it-all": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/it-all/-/it-all-1.0.6.tgz", + "integrity": "sha512-3cmCc6Heqe3uWi3CVM/k51fa/XbMFpQVzFoDsV0IZNHSQDyAXl3c4MjHkFX5kF3922OGj7Myv1nSEUgRtcuM1A==", + "license": "ISC" + }, + "node_modules/it-glob": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/it-glob/-/it-glob-1.0.2.tgz", + "integrity": "sha512-Ch2Dzhw4URfB9L/0ZHyY+uqOnKvBNeS/SMcRiPmJfpHiM0TsUZn+GkpcZxAoF3dJVdPm/PuIk3A4wlV7SUo23Q==", + "license": "ISC", + "dependencies": { + "@types/minimatch": "^3.0.4", + "minimatch": "^3.0.4" + } + }, + "node_modules/it-to-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/it-to-stream/-/it-to-stream-1.0.0.tgz", + "integrity": "sha512-pLULMZMAB/+vbdvbZtebC0nWBTbG581lk6w8P7DfIIIKUfa8FbY7Oi0FxZcFPbxvISs7A9E+cMpLDBc1XhpAOA==", + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "fast-fifo": "^1.0.0", + "get-iterator": "^1.0.2", + "p-defer": "^3.0.0", + "p-fifo": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, + "node_modules/it-to-stream/node_modules/p-defer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", + "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.1.tgz", + "integrity": "sha512-XQmWYj2Sm4kn4WeTYvmpKEbyPsL7nBsb647c7pMe6l02/yx2+Jfc4dT6UZkEXnIUb5LhD55r2HPsJ1milQ4rDg==", + "license": "BSD-2-Clause" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "license": "MIT", + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/multiformats": { + "version": "13.4.1", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.4.1.tgz", + "integrity": "sha512-VqO6OSvLrFVAYYjgsr8tyv62/rCQhPgsZUXLTqoFLSgdkgiUYKYeArbt1uWLlEpkjxQe+P0+sHlbPEte1Bi06Q==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/murmurhash3js-revisited": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/murmurhash3js-revisited/-/murmurhash3js-revisited-3.0.0.tgz", + "integrity": "sha512-/sF3ee6zvScXMb1XFJ8gDsSnY+X8PbOyjIuBhtgis10W2Jx4ZjIhikUCIF9c4gpJxVnQIsPAFrSwTCuAjicP6g==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/native-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/native-fetch/-/native-fetch-3.0.0.tgz", + "integrity": "sha512-G3Z7vx0IFb/FQ4JxvtqGABsOTIqRWvgQz6e+erkB+JJD6LrszQtMozEHI4EkmgZQvnGHrpLVzUWk7t4sJCIkVw==", + "license": "MIT", + "peerDependencies": { + "node-fetch": "*" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/one-webcrypto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/one-webcrypto/-/one-webcrypto-1.0.3.tgz", + "integrity": "sha512-fu9ywBVBPx0gS9K0etIROTiCkvI5S1TDjFsYFb3rC1ewFxeOqsbzq7aIMBHsYfrTHBcGXJaONXXjTl8B01cW1Q==", + "license": "MIT" + }, + "node_modules/p-defer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-4.0.1.tgz", + "integrity": "sha512-Mr5KC5efvAK5VUptYEIopP1bakB85k2IWXaRC0rsh1uwn1L6M0LVml8OIQ4Gudg4oyZakf7FmeRLkMMtZW1i5A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-fifo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-fifo/-/p-fifo-1.0.0.tgz", + "integrity": "sha512-IjoCxXW48tqdtDFz6fqo5q1UfFVjjVZe8TC1QRflvNUJtNfCUhxOUw6MOVZhDPjqhSzc26xKdugsO17gmzd5+A==", + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.0.0", + "p-defer": "^3.0.0" + } + }, + "node_modules/p-fifo/node_modules/p-defer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", + "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-5.1.2.tgz", + "integrity": "sha512-couX95waDu98NfNZV+i/iLt+fdVxmI7CbrrdC2uDWfPdUAApyxT4wmDlyOtR5KtTDmkDO0zDScDjDou9YHhd9g==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.1", + "retry": "^0.13.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/rabin-rs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/rabin-rs/-/rabin-rs-2.1.0.tgz", + "integrity": "sha512-5y72gAXPzIBsAMHcpxZP8eMDuDT98qMP1BqSDHRbHkJJXEgWIN1lA47LxUqzsK6jknOJtgfkQr9v+7qMlFDm6g==", + "license": "(Apache-2.0 AND MIT)" + }, + "node_modules/react-native-fetch-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-native-fetch-api/-/react-native-fetch-api-3.0.0.tgz", + "integrity": "sha512-g2rtqPjdroaboDKTsJCTlcmtw54E25OjyaunUP0anOZn4Fuo2IKs8BVfe02zVggA/UysbmfSnRJIqtNkAgggNA==", + "license": "MIT", + "dependencies": { + "p-defer": "^3.0.0" + } + }, + "node_modules/react-native-fetch-api/node_modules/p-defer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", + "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stream-to-it": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/stream-to-it/-/stream-to-it-0.2.4.tgz", + "integrity": "sha512-4vEbkSs83OahpmBybNJXlJd7d6/RxzkkSdT3I0mnGt79Xd2Kk+e1JqbvAvsQfCeKj3aKb0QIWkyK3/n0j506vQ==", + "license": "MIT", + "dependencies": { + "get-iterator": "^1.0.2" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/stubborn-fs": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-1.2.5.tgz", + "integrity": "sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==" + }, + "node_modules/sync-multihash-sha2": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/sync-multihash-sha2/-/sync-multihash-sha2-1.0.0.tgz", + "integrity": "sha512-A5gVpmtKF0ov+/XID0M0QRJqF2QxAsj3x/LlDC8yivzgoYCoWkV+XaZPfVu7Vj1T/hYzYS1tfjwboSbXjqocug==", + "license": "(Apache-2.0 AND MIT)", + "dependencies": { + "@noble/hashes": "^1.3.1" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uint8arraylist": { + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/uint8arraylist/-/uint8arraylist-2.4.8.tgz", + "integrity": "sha512-vc1PlGOzglLF0eae1M8mLRTBivsvrGsdmJ5RbK3e+QRvRLOZfZhQROTwH/OfyF3+ZVUg9/8hE8bmKP2CvP9quQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "uint8arrays": "^5.0.1" + } + }, + "node_modules/uint8arrays": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-5.1.0.tgz", + "integrity": "sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "multiformats": "^13.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/varint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/when-exit": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.4.tgz", + "integrity": "sha512-4rnvd3A1t16PWzrBUcSDZqcAmsUIy4minDXT/CZ8F2mVDgd65i4Aalimgz1aQkRGU0iH5eT5+6Rx2TK8o443Pg==", + "license": "MIT" + } + } +} diff --git a/src/py_s3_storacha/js_wrapper.py b/src/py_s3_storacha/js_wrapper.py index 33e4068..558e748 100644 --- a/src/py_s3_storacha/js_wrapper.py +++ b/src/py_s3_storacha/js_wrapper.py @@ -4,13 +4,14 @@ import json import shutil import subprocess -import sys from pathlib import Path -from typing import Dict, Any, Optional, Tuple, List -import logging +from typing import Dict, Any, Optional, Tuple from .exceptions import JSWrapperError, ConfigurationError -from .error_handler import with_error_handling, handle_subprocess_error, get_retry_handler +from .error_handler import ( + with_error_handling, + handle_subprocess_error, +) from .logging_config import get_logger @@ -19,10 +20,10 @@ class JSWrapperManager: """Manages JavaScript subprocess execution and communication.""" - + def __init__(self, js_script_path: Optional[str] = None) -> None: """Initialize JavaScript wrapper manager. - + Args: js_script_path: Path to the JavaScript implementation script. If None, will attempt to locate automatically. @@ -31,87 +32,92 @@ def __init__(self, js_script_path: Optional[str] = None) -> None: self._nodejs_path: Optional[str] = None self._nodejs_version: Optional[str] = None self._validated = False - + async def validate_environment(self) -> None: """Validate Node.js environment and JavaScript script availability. - + Raises: JSWrapperError: If Node.js is not available or version is incompatible ConfigurationError: If JavaScript script cannot be found """ if self._validated: return - + # Validate Node.js availability await self._validate_nodejs() - + # Validate JavaScript script path self._validate_js_script_path() - + self._validated = True logger.info( f"JavaScript environment validated: Node.js {self._nodejs_version} at {self._nodejs_path}" ) - - @with_error_handling("javascript_migration", error_types=(JSWrapperError, OSError, asyncio.TimeoutError)) + + @with_error_handling( + "javascript_migration", + error_types=(JSWrapperError, OSError, asyncio.TimeoutError), + ) async def execute_migration( self, s3_config: Dict[str, Any], storacha_config: Dict[str, Any], - migration_params: Dict[str, Any] + migration_params: Dict[str, Any], ) -> Dict[str, Any]: """Execute migration via JavaScript subprocess. - + Args: s3_config: S3 configuration dictionary storacha_config: Storacha configuration dictionary migration_params: Migration parameters dictionary - + Returns: Dictionary containing migration results - + Raises: JSWrapperError: If subprocess execution fails ConfigurationError: If environment is not properly configured """ await self.validate_environment() - + # Prepare input data for JavaScript process input_data = { "s3": s3_config, "storacha": storacha_config, - "migration": migration_params + "migration": migration_params, } - + logger.debug(f"Executing JavaScript migration with params: {migration_params}") - + # Execute JavaScript subprocess result = await self._execute_js_subprocess(input_data) - + logger.info("JavaScript migration completed successfully") return result - - async def _execute_js_subprocess(self, input_data: Dict[str, Any]) -> Dict[str, Any]: + + async def _execute_js_subprocess( + self, input_data: Dict[str, Any] + ) -> Dict[str, Any]: """Execute JavaScript subprocess with JSON communication. - + Args: input_data: Data to send to JavaScript process via stdin - + Returns: Parsed JSON response from JavaScript process - + Raises: JSWrapperError: If subprocess execution fails or returns invalid data """ if not self._nodejs_path or not self.js_script_path: raise JSWrapperError("JavaScript environment not properly validated") - + # Prepare command cmd = [self._nodejs_path, str(self.js_script_path)] input_json = json.dumps(input_data) - + logger.debug(f"Executing command: {' '.join(cmd)}") - + try: # Create subprocess process = await asyncio.create_subprocess_exec( @@ -119,28 +125,30 @@ async def _execute_js_subprocess(self, input_data: Dict[str, Any]) -> Dict[str, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, - cwd=Path(self.js_script_path).parent if self.js_script_path else None + cwd=Path(self.js_script_path).parent if self.js_script_path else None, ) - + # Communicate with subprocess - stdout_data, stderr_data = await process.communicate(input_json.encode('utf-8')) - + stdout_data, stderr_data = await process.communicate( + input_json.encode("utf-8") + ) + # Decode output - stdout = stdout_data.decode('utf-8') if stdout_data else "" - stderr = stderr_data.decode('utf-8') if stderr_data else "" - + stdout = stdout_data.decode("utf-8") if stdout_data else "" + stderr = stderr_data.decode("utf-8") if stderr_data else "" + logger.debug(f"Process return code: {process.returncode}") if stderr: logger.debug(f"Process stderr: {stderr}") - + # Check return code if process.returncode != 0: raise handle_subprocess_error( - returncode=process.returncode, + returncode=process.returncode or 1, stderr=stderr, - command=' '.join(cmd) + command=" ".join(cmd), ) - + # Parse JSON response try: result = json.loads(stdout) @@ -149,74 +157,73 @@ async def _execute_js_subprocess(self, input_data: Dict[str, Any]) -> Dict[str, raise JSWrapperError( f"Invalid JSON response from JavaScript process: {e}", context={"stdout": stdout[:500], "stderr": stderr[:500]}, - original_error=e + original_error=e, ) - + except asyncio.TimeoutError as e: raise JSWrapperError( - "JavaScript subprocess execution timed out", - original_error=e + "JavaScript subprocess execution timed out", original_error=e ) except OSError as e: raise JSWrapperError( - f"Failed to execute JavaScript subprocess: {e}", - original_error=e + f"Failed to execute JavaScript subprocess: {e}", original_error=e ) - + async def _validate_nodejs(self) -> None: """Validate Node.js availability and version. - + Raises: JSWrapperError: If Node.js is not available or version is incompatible """ # Try to find Node.js executable - nodejs_candidates = ['node', 'nodejs'] - + nodejs_candidates = ["node", "nodejs"] + for candidate in nodejs_candidates: nodejs_path = shutil.which(candidate) if nodejs_path: self._nodejs_path = nodejs_path break - + if not self._nodejs_path: raise JSWrapperError( "Node.js not found. Please install Node.js to use this library.", context={ "installation_guide": "Visit https://nodejs.org/ for installation instructions", - "searched_executables": nodejs_candidates - } + "searched_executables": nodejs_candidates, + }, ) - + # Check Node.js version try: process = await asyncio.create_subprocess_exec( - self._nodejs_path, '--version', + self._nodejs_path, + "--version", stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE + stderr=asyncio.subprocess.PIPE, ) - + stdout_data, stderr_data = await process.communicate() - + if process.returncode != 0: - stderr = stderr_data.decode('utf-8') if stderr_data else "" + stderr = stderr_data.decode("utf-8") if stderr_data else "" raise JSWrapperError( f"Failed to get Node.js version: {stderr}", - return_code=process.returncode + return_code=process.returncode, ) - - version_output = stdout_data.decode('utf-8').strip() + + version_output = stdout_data.decode("utf-8").strip() self._nodejs_version = version_output - + # Basic version validation (Node.js versions start with 'v') - if not version_output.startswith('v'): + if not version_output.startswith("v"): raise JSWrapperError( f"Unexpected Node.js version format: {version_output}", - context={"expected_format": "v.."} + context={"expected_format": "v.."}, ) - + # Extract major version for compatibility check try: - major_version = int(version_output[1:].split('.')[0]) + major_version = int(version_output[1:].split(".")[0]) if major_version < 14: logger.warning( f"Node.js version {version_output} may not be fully supported. " @@ -224,16 +231,15 @@ async def _validate_nodejs(self) -> None: ) except (ValueError, IndexError): logger.warning(f"Could not parse Node.js version: {version_output}") - + except OSError as e: raise JSWrapperError( - f"Failed to execute Node.js version check: {e}", - original_error=e + f"Failed to execute Node.js version check: {e}", original_error=e ) - + def _validate_js_script_path(self) -> None: """Validate JavaScript script path availability. - + Raises: ConfigurationError: If JavaScript script cannot be found or accessed """ @@ -243,42 +249,42 @@ def _validate_js_script_path(self) -> None: Path(__file__).parent / "js" / "s3-to-storacha.js", Path(__file__).parent.parent.parent / "js" / "s3-to-storacha.js", Path.cwd() / "js" / "s3-to-storacha.js", - Path.cwd() / "s3-to-storacha.js" + Path.cwd() / "s3-to-storacha.js", ] - + for path in possible_paths: if path.exists() and path.is_file(): self.js_script_path = str(path) break - + if not self.js_script_path: raise ConfigurationError( "JavaScript script not found. Please specify js_script_path or ensure " "the JavaScript implementation is available in a standard location.", context={ "searched_paths": [str(p) for p in possible_paths], - "config_field": "js_script_path" - } + "config_field": "js_script_path", + }, ) - + # Validate the specified or found path script_path = Path(self.js_script_path) - + if not script_path.exists(): raise ConfigurationError( f"JavaScript script not found at specified path: {self.js_script_path}", field_name="js_script_path", - field_value=self.js_script_path + field_value=self.js_script_path, ) - + if not script_path.is_file(): raise ConfigurationError( f"JavaScript script path is not a file: {self.js_script_path}", field_name="js_script_path", - field_value=self.js_script_path + field_value=self.js_script_path, ) - - if not script_path.suffix.lower() in ['.js', '.mjs']: + + if script_path.suffix.lower() not in [".js", ".mjs"]: logger.warning( f"JavaScript script does not have a .js or .mjs extension: {self.js_script_path}" ) @@ -286,73 +292,69 @@ def _validate_js_script_path(self) -> None: def validate_nodejs_environment() -> Tuple[str, str]: """Validate Node.js environment synchronously. - + Returns: Tuple of (nodejs_path, nodejs_version) - + Raises: JSWrapperError: If Node.js is not available """ # Try to find Node.js executable - nodejs_candidates = ['node', 'nodejs'] + nodejs_candidates = ["node", "nodejs"] nodejs_path = None - + for candidate in nodejs_candidates: path = shutil.which(candidate) if path: nodejs_path = path break - + if not nodejs_path: raise JSWrapperError( "Node.js not found. Please install Node.js to use this library.", context={ "installation_guide": "Visit https://nodejs.org/ for installation instructions", - "searched_executables": nodejs_candidates - } + "searched_executables": nodejs_candidates, + }, ) - + # Check Node.js version try: result = subprocess.run( - [nodejs_path, '--version'], - capture_output=True, - text=True, - timeout=10 + [nodejs_path, "--version"], capture_output=True, text=True, timeout=10 ) - + if result.returncode != 0: raise JSWrapperError( f"Failed to get Node.js version: {result.stderr}", - return_code=result.returncode + return_code=result.returncode, ) - + version = result.stdout.strip() - + # Basic version validation - if not version.startswith('v'): + if not version.startswith("v"): raise JSWrapperError( f"Unexpected Node.js version format: {version}", - context={"expected_format": "v.."} + context={"expected_format": "v.."}, ) - + return nodejs_path, version - + except subprocess.TimeoutExpired: raise JSWrapperError("Node.js version check timed out") except OSError as e: raise JSWrapperError( - f"Failed to execute Node.js version check: {e}", - original_error=e + f"Failed to execute Node.js version check: {e}", original_error=e ) def find_js_script(script_name: str = "s3-to-storacha.js") -> Optional[str]: """Find JavaScript script in common locations. - + Args: script_name: Name of the JavaScript script to find - + Returns: Path to the JavaScript script if found, None otherwise """ @@ -360,11 +362,11 @@ def find_js_script(script_name: str = "s3-to-storacha.js") -> Optional[str]: Path(__file__).parent / "js" / script_name, Path(__file__).parent.parent.parent / "js" / script_name, Path.cwd() / "js" / script_name, - Path.cwd() / script_name + Path.cwd() / script_name, ] - + for path in possible_paths: if path.exists() and path.is_file(): return str(path) - - return None \ No newline at end of file + + return None diff --git a/src/py_s3_storacha/logging_config.py b/src/py_s3_storacha/logging_config.py index caf92ef..65d43a1 100644 --- a/src/py_s3_storacha/logging_config.py +++ b/src/py_s3_storacha/logging_config.py @@ -12,6 +12,7 @@ class LogLevel(Enum): """Enumeration of supported log levels.""" + DEBUG = "DEBUG" INFO = "INFO" WARNING = "WARNING" @@ -21,16 +22,16 @@ class LogLevel(Enum): class StructuredFormatter(logging.Formatter): """Custom formatter that outputs structured JSON logs.""" - + def __init__(self, include_extra: bool = True) -> None: """Initialize structured formatter. - + Args: include_extra: Whether to include extra fields in log records """ super().__init__() self.include_extra = include_extra - + def format(self, record: logging.LogRecord) -> str: """Format log record as structured JSON.""" log_data = { @@ -40,23 +41,40 @@ def format(self, record: logging.LogRecord) -> str: "message": record.getMessage(), "module": record.module, "function": record.funcName, - "line": record.lineno + "line": record.lineno, } - + # Add exception information if present if record.exc_info: log_data["exception"] = self.formatException(record.exc_info) - + # Add extra fields if enabled if self.include_extra: extra_fields = {} for key, value in record.__dict__.items(): if key not in { - 'name', 'msg', 'args', 'levelname', 'levelno', 'pathname', - 'filename', 'module', 'lineno', 'funcName', 'created', - 'msecs', 'relativeCreated', 'thread', 'threadName', - 'processName', 'process', 'getMessage', 'exc_info', - 'exc_text', 'stack_info', 'message' + "name", + "msg", + "args", + "levelname", + "levelno", + "pathname", + "filename", + "module", + "lineno", + "funcName", + "created", + "msecs", + "relativeCreated", + "thread", + "threadName", + "processName", + "process", + "getMessage", + "exc_info", + "exc_text", + "stack_info", + "message", }: # Ensure value is JSON serializable try: @@ -64,27 +82,27 @@ def format(self, record: logging.LogRecord) -> str: extra_fields[key] = value except (TypeError, ValueError): extra_fields[key] = str(value) - + if extra_fields: log_data["extra"] = extra_fields - + return json.dumps(log_data, default=str) class SimpleFormatter(logging.Formatter): """Simple human-readable formatter for console output.""" - + def __init__(self) -> None: """Initialize simple formatter with timestamp and level colors.""" super().__init__( fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - datefmt="%Y-%m-%d %H:%M:%S" + datefmt="%Y-%m-%d %H:%M:%S", ) class LoggingConfig: """Configuration class for logging setup.""" - + def __init__( self, level: Union[str, LogLevel] = LogLevel.INFO, @@ -93,10 +111,10 @@ def __init__( max_file_size: int = 10 * 1024 * 1024, # 10MB backup_count: int = 5, console_output: bool = True, - include_extra: bool = True + include_extra: bool = True, ) -> None: """Initialize logging configuration. - + Args: level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) format_type: Format type ('simple' or 'structured') @@ -113,7 +131,7 @@ def __init__( self.backup_count = backup_count self.console_output = console_output self.include_extra = include_extra - + @classmethod def from_env(cls) -> "LoggingConfig": """Create logging configuration from environment variables.""" @@ -121,10 +139,11 @@ def from_env(cls) -> "LoggingConfig": level=os.getenv("S3_STORACHA_LOG_LEVEL", "INFO"), format_type=os.getenv("S3_STORACHA_LOG_FORMAT", "simple"), log_file=os.getenv("S3_STORACHA_LOG_FILE"), - console_output=os.getenv("S3_STORACHA_LOG_CONSOLE", "true").lower() == "true", - include_extra=os.getenv("S3_STORACHA_LOG_EXTRA", "true").lower() == "true" + console_output=os.getenv("S3_STORACHA_LOG_CONSOLE", "true").lower() + == "true", + include_extra=os.getenv("S3_STORACHA_LOG_EXTRA", "true").lower() == "true", ) - + @classmethod def from_dict(cls, config_dict: Dict[str, Any]) -> "LoggingConfig": """Create logging configuration from dictionary.""" @@ -135,65 +154,65 @@ def from_dict(cls, config_dict: Dict[str, Any]) -> "LoggingConfig": max_file_size=config_dict.get("max_file_size", 10 * 1024 * 1024), backup_count=config_dict.get("backup_count", 5), console_output=config_dict.get("console_output", True), - include_extra=config_dict.get("include_extra", True) + include_extra=config_dict.get("include_extra", True), ) def setup_logging(config: Optional[LoggingConfig] = None) -> None: """Set up logging configuration for the application. - + Args: config: Logging configuration. If None, uses environment variables. """ if config is None: config = LoggingConfig.from_env() - + # Get root logger for the package root_logger = logging.getLogger("py_s3_storacha") root_logger.setLevel(getattr(logging, config.level.value)) - + # Clear any existing handlers root_logger.handlers.clear() - + # Create formatter based on format type if config.format_type == "structured": formatter = StructuredFormatter(include_extra=config.include_extra) else: formatter = SimpleFormatter() - + # Add console handler if enabled if config.console_output: console_handler = logging.StreamHandler(sys.stdout) console_handler.setLevel(getattr(logging, config.level.value)) console_handler.setFormatter(formatter) root_logger.addHandler(console_handler) - + # Add file handler if log file is specified if config.log_file: # Ensure log directory exists config.log_file.parent.mkdir(parents=True, exist_ok=True) - + # Use rotating file handler to manage log file size file_handler = logging.handlers.RotatingFileHandler( filename=config.log_file, maxBytes=config.max_file_size, backupCount=config.backup_count, - encoding="utf-8" + encoding="utf-8", ) file_handler.setLevel(getattr(logging, config.level.value)) file_handler.setFormatter(formatter) root_logger.addHandler(file_handler) - + # Prevent propagation to avoid duplicate logs root_logger.propagate = False def get_logger(name: str) -> logging.Logger: """Get a logger instance for the specified module. - + Args: name: Logger name (typically __name__ from the calling module) - + Returns: Configured logger instance """ @@ -203,7 +222,7 @@ def get_logger(name: str) -> logging.Logger: name = "py_s3_storacha.main" else: name = f"py_s3_storacha.{name}" - + return logging.getLogger(name) @@ -211,10 +230,10 @@ def log_function_call( logger: logging.Logger, func_name: str, args: Optional[Dict[str, Any]] = None, - level: str = "DEBUG" + level: str = "DEBUG", ) -> None: """Log function call with arguments. - + Args: logger: Logger instance func_name: Name of the function being called @@ -222,18 +241,22 @@ def log_function_call( level: Log level for the message """ log_level = getattr(logging, level.upper()) - + if args: # Mask sensitive arguments safe_args = {} for key, value in args.items(): - if any(sensitive in key.lower() - for sensitive in ['key', 'secret', 'password', 'token']): + if any( + sensitive in key.lower() + for sensitive in ["key", "secret", "password", "token"] + ): safe_args[key] = "***MASKED***" else: safe_args[key] = value - - logger.log(log_level, f"Calling {func_name}", extra={"function_args": safe_args}) + + logger.log( + log_level, f"Calling {func_name}", extra={"function_args": safe_args} + ) else: logger.log(log_level, f"Calling {func_name}") @@ -242,64 +265,49 @@ def log_performance( logger: logging.Logger, operation: str, duration_seconds: float, - additional_metrics: Optional[Dict[str, Any]] = None + additional_metrics: Optional[Dict[str, Any]] = None, ) -> None: """Log performance metrics for an operation. - + Args: logger: Logger instance operation: Name of the operation duration_seconds: Duration of the operation in seconds additional_metrics: Additional metrics to log """ - metrics = { - "operation": operation, - "duration_seconds": round(duration_seconds, 3) - } - + metrics = {"operation": operation, "duration_seconds": round(duration_seconds, 3)} + if additional_metrics: metrics.update(additional_metrics) - + logger.info(f"Performance: {operation} completed", extra={"performance": metrics}) def configure_third_party_loggers(level: str = "WARNING") -> None: """Configure third-party library loggers to reduce noise. - + Args: level: Log level to set for third-party loggers """ - third_party_loggers = [ - "urllib3", - "requests", - "boto3", - "botocore", - "asyncio" - ] - + third_party_loggers = ["urllib3", "requests", "boto3", "botocore", "asyncio"] + log_level = getattr(logging, level.upper()) - + for logger_name in third_party_loggers: logging.getLogger(logger_name).setLevel(log_level) # Default logging setup function for convenience def setup_default_logging( - level: str = "INFO", - format_type: str = "simple", - log_file: Optional[str] = None + level: str = "INFO", format_type: str = "simple", log_file: Optional[str] = None ) -> None: """Set up default logging configuration. - + Args: level: Logging level format_type: Format type ('simple' or 'structured') log_file: Optional log file path """ - config = LoggingConfig( - level=level, - format_type=format_type, - log_file=log_file - ) + config = LoggingConfig(level=level, format_type=format_type, log_file=log_file) setup_logging(config) - configure_third_party_loggers() \ No newline at end of file + configure_third_party_loggers() diff --git a/src/py_s3_storacha/models.py b/src/py_s3_storacha/models.py index 45d97a5..ac4ba28 100644 --- a/src/py_s3_storacha/models.py +++ b/src/py_s3_storacha/models.py @@ -2,12 +2,13 @@ from dataclasses import dataclass, field from datetime import datetime -from typing import List, Optional, Callable, Any, Dict +from typing import List, Optional, Callable from enum import Enum class MigrationStatus(Enum): """Status of a migration operation.""" + NOT_STARTED = "not_started" IN_PROGRESS = "in_progress" COMPLETED = "completed" @@ -18,12 +19,13 @@ class MigrationStatus(Enum): @dataclass class S3Object: """Represents an S3 object to be migrated.""" + key: str size: int last_modified: datetime etag: str storage_class: Optional[str] = None - + def __post_init__(self) -> None: """Validate S3 object data after initialization.""" if not self.key: @@ -35,6 +37,7 @@ def __post_init__(self) -> None: @dataclass class MigrationProgress: """Progress information for a migration operation.""" + current_object: str objects_completed: int total_objects: int @@ -42,14 +45,14 @@ class MigrationProgress: total_bytes: int estimated_time_remaining: Optional[float] = None current_operation: Optional[str] = None - + @property def progress_percentage(self) -> float: """Calculate progress as a percentage.""" if self.total_objects == 0: return 0.0 return (self.objects_completed / self.total_objects) * 100.0 - + @property def bytes_percentage(self) -> float: """Calculate bytes transferred as a percentage.""" @@ -61,6 +64,7 @@ def bytes_percentage(self) -> float: @dataclass class MigrationResult: """Result of a migration operation.""" + success: bool status: MigrationStatus objects_migrated: int @@ -70,12 +74,12 @@ class MigrationResult: warnings: List[str] = field(default_factory=list) skipped_objects: List[str] = field(default_factory=list) failed_objects: List[str] = field(default_factory=list) - + @property def has_errors(self) -> bool: """Check if the migration had any errors.""" return len(self.errors) > 0 or len(self.failed_objects) > 0 - + @property def has_warnings(self) -> bool: """Check if the migration had any warnings.""" @@ -85,13 +89,14 @@ def has_warnings(self) -> bool: @dataclass class MigrationRequest: """Request parameters for a migration operation.""" + source_path: str destination_path: str include_pattern: Optional[str] = None exclude_pattern: Optional[str] = None overwrite_existing: bool = False verify_checksums: bool = True - + def __post_init__(self) -> None: """Validate migration request after initialization.""" if not self.source_path: @@ -101,4 +106,4 @@ def __post_init__(self) -> None: # Type alias for progress callback function -ProgressCallback = Callable[[MigrationProgress], None] \ No newline at end of file +ProgressCallback = Callable[[MigrationProgress], None] diff --git a/src/py_s3_storacha/progress.py b/src/py_s3_storacha/progress.py index 9237dcb..4620767 100644 --- a/src/py_s3_storacha/progress.py +++ b/src/py_s3_storacha/progress.py @@ -2,9 +2,8 @@ import time import threading -from typing import Optional, Dict, Any, List +from typing import Optional, List from dataclasses import dataclass, field -from datetime import datetime, timedelta from .models import MigrationProgress, ProgressCallback @@ -12,7 +11,7 @@ @dataclass class ProgressTracker: """Tracks migration progress with timing and estimation capabilities.""" - + start_time: float = field(default_factory=time.time) total_objects: int = 0 total_bytes: int = 0 @@ -20,11 +19,11 @@ class ProgressTracker: transferred_bytes: int = 0 current_object: str = "" current_operation: str = "" - + # Progress history for rate calculation _progress_history: List[tuple[float, int, int]] = field(default_factory=list) _lock: threading.Lock = field(default_factory=threading.Lock) - + def update( self, completed_objects: Optional[int] = None, @@ -32,10 +31,10 @@ def update( current_object: Optional[str] = None, current_operation: Optional[str] = None, total_objects: Optional[int] = None, - total_bytes: Optional[int] = None + total_bytes: Optional[int] = None, ) -> None: """Update progress tracking information. - + Args: completed_objects: Number of objects completed transferred_bytes: Number of bytes transferred @@ -57,24 +56,26 @@ def update( self.total_objects = total_objects if total_bytes is not None: self.total_bytes = total_bytes - + # Record progress history for rate calculation current_time = time.time() - self._progress_history.append((current_time, self.completed_objects, self.transferred_bytes)) - + self._progress_history.append( + (current_time, self.completed_objects, self.transferred_bytes) + ) + # Keep only recent history (last 10 entries) if len(self._progress_history) > 10: self._progress_history = self._progress_history[-10:] - + def get_progress(self) -> MigrationProgress: """Get current progress information. - + Returns: MigrationProgress object with current state and estimates """ with self._lock: estimated_time_remaining = self._calculate_estimated_time_remaining() - + return MigrationProgress( current_object=self.current_object, objects_completed=self.completed_objects, @@ -82,67 +83,67 @@ def get_progress(self) -> MigrationProgress: bytes_transferred=self.transferred_bytes, total_bytes=self.total_bytes, estimated_time_remaining=estimated_time_remaining, - current_operation=self.current_operation + current_operation=self.current_operation, ) - + def _calculate_estimated_time_remaining(self) -> Optional[float]: """Calculate estimated time remaining based on progress history. - + Returns: Estimated seconds remaining, or None if cannot be calculated """ if len(self._progress_history) < 2: return None - + # Use the most recent progress points to calculate rate recent_history = self._progress_history[-5:] # Last 5 data points - + if len(recent_history) < 2: return None - + # Calculate rates time_diff = recent_history[-1][0] - recent_history[0][0] objects_diff = recent_history[-1][1] - recent_history[0][1] bytes_diff = recent_history[-1][2] - recent_history[0][2] - + if time_diff <= 0: return None - + # Calculate remaining work remaining_objects = max(0, self.total_objects - self.completed_objects) remaining_bytes = max(0, self.total_bytes - self.transferred_bytes) - + # Estimate time based on object rate and byte rate estimated_times = [] - + if objects_diff > 0 and remaining_objects > 0: object_rate = objects_diff / time_diff time_by_objects = remaining_objects / object_rate estimated_times.append(time_by_objects) - + if bytes_diff > 0 and remaining_bytes > 0: byte_rate = bytes_diff / time_diff time_by_bytes = remaining_bytes / byte_rate estimated_times.append(time_by_bytes) - + if not estimated_times: return None - + # Return the average of available estimates return sum(estimated_times) / len(estimated_times) - + @property def elapsed_time(self) -> float: """Get elapsed time since tracking started.""" return time.time() - self.start_time - + @property def objects_per_second(self) -> float: """Get current objects per second rate.""" if self.elapsed_time <= 0: return 0.0 return self.completed_objects / self.elapsed_time - + @property def bytes_per_second(self) -> float: """Get current bytes per second rate.""" @@ -153,33 +154,33 @@ def bytes_per_second(self) -> float: class ProgressReporter: """Manages progress reporting with multiple callback support.""" - + def __init__(self) -> None: """Initialize progress reporter.""" self._callbacks: List[ProgressCallback] = [] self._tracker = ProgressTracker() self._lock = threading.Lock() - + def add_callback(self, callback: ProgressCallback) -> None: """Add a progress callback. - + Args: callback: Function to call with progress updates """ with self._lock: if callback not in self._callbacks: self._callbacks.append(callback) - + def remove_callback(self, callback: ProgressCallback) -> None: """Remove a progress callback. - + Args: callback: Function to remove from callbacks """ with self._lock: if callback in self._callbacks: self._callbacks.remove(callback) - + def update_progress( self, completed_objects: Optional[int] = None, @@ -187,10 +188,10 @@ def update_progress( current_object: Optional[str] = None, current_operation: Optional[str] = None, total_objects: Optional[int] = None, - total_bytes: Optional[int] = None + total_bytes: Optional[int] = None, ) -> None: """Update progress and notify all callbacks. - + Args: completed_objects: Number of objects completed transferred_bytes: Number of bytes transferred @@ -206,33 +207,34 @@ def update_progress( current_object=current_object, current_operation=current_operation, total_objects=total_objects, - total_bytes=total_bytes + total_bytes=total_bytes, ) - + # Get current progress progress = self._tracker.get_progress() - + # Notify all callbacks with self._lock: callbacks_to_call = self._callbacks.copy() - + for callback in callbacks_to_call: try: callback(progress) except Exception as e: # Log error but don't let callback failures stop progress reporting import logging + logger = logging.getLogger(__name__) logger.warning(f"Progress callback failed: {e}") - + def get_current_progress(self) -> MigrationProgress: """Get current progress without triggering callbacks. - + Returns: Current migration progress """ return self._tracker.get_progress() - + def reset(self) -> None: """Reset progress tracking.""" with self._lock: @@ -241,54 +243,60 @@ def reset(self) -> None: def create_console_progress_callback(show_details: bool = True) -> ProgressCallback: """Create a progress callback that prints to console. - + Args: show_details: Whether to show detailed progress information - + Returns: Progress callback function """ + def console_callback(progress: MigrationProgress) -> None: """Print progress to console.""" if show_details: - print(f"\rProgress: {progress.progress_percentage:.1f}% " - f"({progress.objects_completed}/{progress.total_objects} objects, " - f"{progress.bytes_percentage:.1f}% bytes) - {progress.current_object}", - end="", flush=True) + print( + f"\rProgress: {progress.progress_percentage:.1f}% " + f"({progress.objects_completed}/{progress.total_objects} objects, " + f"{progress.bytes_percentage:.1f}% bytes) - {progress.current_object}", + end="", + flush=True, + ) else: - print(f"\rProgress: {progress.progress_percentage:.1f}%", end="", flush=True) - + print( + f"\rProgress: {progress.progress_percentage:.1f}%", end="", flush=True + ) + return console_callback def create_logging_progress_callback( - logger_name: str = __name__, - log_interval: int = 10 + logger_name: str = __name__, log_interval: int = 10 ) -> ProgressCallback: """Create a progress callback that logs progress periodically. - + Args: logger_name: Name of logger to use log_interval: Log every N progress updates - + Returns: Progress callback function """ import logging + logger = logging.getLogger(logger_name) - + call_count = 0 - + def logging_callback(progress: MigrationProgress) -> None: """Log progress periodically.""" nonlocal call_count call_count += 1 - + if call_count % log_interval == 0: logger.info( f"Migration progress: {progress.progress_percentage:.1f}% " f"({progress.objects_completed}/{progress.total_objects} objects, " f"{progress.bytes_transferred}/{progress.total_bytes} bytes)" ) - - return logging_callback \ No newline at end of file + + return logging_callback diff --git a/src/py_s3_storacha/setup_helpers.py b/src/py_s3_storacha/setup_helpers.py index cf68dd2..37216eb 100644 --- a/src/py_s3_storacha/setup_helpers.py +++ b/src/py_s3_storacha/setup_helpers.py @@ -2,7 +2,6 @@ import subprocess import sys -import shutil from pathlib import Path from typing import Optional, Tuple import logging @@ -12,16 +11,13 @@ def check_nodejs_installed() -> Tuple[bool, Optional[str]]: """Check if Node.js is installed and get version. - + Returns: Tuple of (is_installed, version_string) """ try: result = subprocess.run( - ["node", "--version"], - capture_output=True, - text=True, - timeout=5 + ["node", "--version"], capture_output=True, text=True, timeout=5 ) if result.returncode == 0: version = result.stdout.strip() @@ -33,16 +29,13 @@ def check_nodejs_installed() -> Tuple[bool, Optional[str]]: def check_npm_installed() -> Tuple[bool, Optional[str]]: """Check if npm is installed and get version. - + Returns: Tuple of (is_installed, version_string) """ try: result = subprocess.run( - ["npm", "--version"], - capture_output=True, - text=True, - timeout=5 + ["npm", "--version"], capture_output=True, text=True, timeout=5 ) if result.returncode == 0: version = result.stdout.strip() @@ -54,7 +47,7 @@ def check_npm_installed() -> Tuple[bool, Optional[str]]: def get_js_directory() -> Path: """Get the JavaScript implementation directory. - + Returns: Path to the js directory """ @@ -63,16 +56,16 @@ def get_js_directory() -> Path: def check_js_dependencies_installed() -> bool: """Check if JavaScript dependencies are installed. - + Returns: True if node_modules exists and has packages """ js_dir = get_js_directory() node_modules = js_dir / "node_modules" - + if not node_modules.exists(): return False - + # Check for key dependencies required_packages = ["@storacha/client", "@aws-sdk/client-s3"] for package in required_packages: @@ -81,19 +74,19 @@ def check_js_dependencies_installed() -> bool: package_dir = node_modules / package_path if not package_dir.exists(): return False - + return True def install_js_dependencies(force: bool = False) -> bool: """Install JavaScript dependencies using npm. - + Args: force: If True, reinstall even if already installed - + Returns: True if installation successful - + Raises: RuntimeError: If Node.js/npm not available or installation fails """ @@ -101,7 +94,7 @@ def install_js_dependencies(force: bool = False) -> bool: if not force and check_js_dependencies_installed(): logger.info("JavaScript dependencies already installed") return True - + # Check Node.js node_installed, node_version = check_nodejs_installed() if not node_installed: @@ -111,9 +104,9 @@ def install_js_dependencies(force: bool = False) -> bool: " - Or use: brew install node (macOS)\n" " - Or use: apt install nodejs (Ubuntu/Debian)" ) - + logger.info(f"Found Node.js {node_version}") - + # Check npm npm_installed, npm_version = check_npm_installed() if not npm_installed: @@ -122,41 +115,39 @@ def install_js_dependencies(force: bool = False) -> bool: " - Usually comes with Node.js\n" " - Or install separately: https://www.npmjs.com/get-npm" ) - + logger.info(f"Found npm {npm_version}") - + # Install dependencies js_dir = get_js_directory() - + if not js_dir.exists(): raise RuntimeError(f"JavaScript directory not found: {js_dir}") - + package_json = js_dir / "package.json" if not package_json.exists(): raise RuntimeError(f"package.json not found: {package_json}") - + logger.info("Installing JavaScript dependencies...") logger.info(f"Running: npm install in {js_dir}") - + try: result = subprocess.run( ["npm", "install"], cwd=str(js_dir), capture_output=True, text=True, - timeout=300 # 5 minutes timeout + timeout=300, # 5 minutes timeout ) - + if result.returncode != 0: raise RuntimeError( - f"npm install failed:\n" - f"stdout: {result.stdout}\n" - f"stderr: {result.stderr}" + f"npm install failed:\nstdout: {result.stdout}\nstderr: {result.stderr}" ) - + logger.info("JavaScript dependencies installed successfully") return True - + except subprocess.TimeoutExpired: raise RuntimeError("npm install timed out after 5 minutes") except Exception as e: @@ -165,7 +156,7 @@ def install_js_dependencies(force: bool = False) -> bool: def verify_installation() -> dict: """Verify the complete installation. - + Returns: Dictionary with installation status """ @@ -176,107 +167,107 @@ def verify_installation() -> dict: "npm_version": None, "js_dependencies_installed": False, "js_script_exists": False, - "ready": False + "ready": False, } - + # Check Node.js node_installed, node_version = check_nodejs_installed() status["nodejs_installed"] = node_installed status["nodejs_version"] = node_version - + # Check npm npm_installed, npm_version = check_npm_installed() status["npm_installed"] = npm_installed status["npm_version"] = npm_version - + # Check JS dependencies status["js_dependencies_installed"] = check_js_dependencies_installed() - + # Check JS script js_script = get_js_directory() / "s3-to-storacha.js" status["js_script_exists"] = js_script.exists() - + # Overall ready status - status["ready"] = all([ - status["nodejs_installed"], - status["npm_installed"], - status["js_dependencies_installed"], - status["js_script_exists"] - ]) - + status["ready"] = all( + [ + status["nodejs_installed"], + status["npm_installed"], + status["js_dependencies_installed"], + status["js_script_exists"], + ] + ) + return status def print_installation_status(): """Print installation status to console.""" status = verify_installation() - - print("\n" + "="*60) + + print("\n" + "=" * 60) print("py-s3-storacha Installation Status") - print("="*60) - + print("=" * 60) + # Node.js if status["nodejs_installed"]: print(f"āœ“ Node.js: {status['nodejs_version']}") else: print("āœ— Node.js: Not installed") print(" Install from: https://nodejs.org") - + # npm if status["npm_installed"]: print(f"āœ“ npm: {status['npm_version']}") else: print("āœ— npm: Not installed") - + # JS dependencies if status["js_dependencies_installed"]: print("āœ“ JavaScript dependencies: Installed") else: print("āœ— JavaScript dependencies: Not installed") print(" Run: python -m py_s3_storacha.setup_helpers") - + # JS script if status["js_script_exists"]: print("āœ“ JavaScript implementation: Found") else: print("āœ— JavaScript implementation: Not found") - - print("="*60) - + + print("=" * 60) + if status["ready"]: print("āœ“ Installation complete - ready to use!") else: print("⚠ Installation incomplete - see issues above") - - print("="*60 + "\n") - + + print("=" * 60 + "\n") + return status["ready"] def main(): """Main entry point for setup helper.""" import argparse - + parser = argparse.ArgumentParser( description="Install JavaScript dependencies for py-s3-storacha" ) parser.add_argument( - "--force", - action="store_true", - help="Force reinstall even if already installed" + "--force", action="store_true", help="Force reinstall even if already installed" ) parser.add_argument( "--check", action="store_true", - help="Check installation status without installing" + help="Check installation status without installing", ) - + args = parser.parse_args() - + if args.check: ready = print_installation_status() sys.exit(0 if ready else 1) - + # Install dependencies try: print("Installing JavaScript dependencies...") diff --git a/tests/conftest.py b/tests/conftest.py index 28efd14..4d86d8a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,7 +11,7 @@ def valid_s3_config_dict() -> Dict[str, Any]: "access_key_id": "AKIAIOSFODNN7EXAMPLE", "secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "region": "us-east-1", - "bucket_name": "test-bucket" + "bucket_name": "test-bucket", } @@ -21,7 +21,7 @@ def valid_storacha_config_dict() -> Dict[str, Any]: return { "api_key": "storacha_test_key_123", "endpoint_url": "https://api.storacha.network", - "space_name": "test-space" + "space_name": "test-space", } @@ -33,19 +33,17 @@ def valid_migration_config_dict() -> Dict[str, Any]: "timeout_seconds": 300, "retry_attempts": 3, "verbose": False, - "dry_run": False + "dry_run": False, } @pytest.fixture def complete_config_dict( - valid_s3_config_dict, - valid_storacha_config_dict, - valid_migration_config_dict + valid_s3_config_dict, valid_storacha_config_dict, valid_migration_config_dict ) -> Dict[str, Any]: """Fixture providing complete configuration dictionary with all sections.""" return { "s3": valid_s3_config_dict, "storacha": valid_storacha_config_dict, - "migration": valid_migration_config_dict + "migration": valid_migration_config_dict, } diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 4d48635..75e7076 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -4,8 +4,6 @@ import os import json import tempfile -from pathlib import Path -from typing import Dict, Any from py_s3_storacha.config import ( S3Config, @@ -17,22 +15,22 @@ class TestS3Config: """Test cases for S3Config data class.""" - + def test_valid_s3_config(self): """Test creating S3Config with valid parameters.""" config = S3Config( access_key_id="AKIAIOSFODNN7EXAMPLE", secret_access_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", region="us-east-1", - bucket_name="test-bucket" + bucket_name="test-bucket", ) - + assert config.access_key_id == "AKIAIOSFODNN7EXAMPLE" assert config.secret_access_key == "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" assert config.region == "us-east-1" assert config.bucket_name == "test-bucket" assert config.endpoint_url is None - + def test_s3_config_with_endpoint(self): """Test S3Config with custom endpoint URL.""" config = S3Config( @@ -40,11 +38,11 @@ def test_s3_config_with_endpoint(self): secret_access_key="test_secret", region="us-west-2", bucket_name="test-bucket", - endpoint_url="https://s3.custom.endpoint.com" + endpoint_url="https://s3.custom.endpoint.com", ) - + assert config.endpoint_url == "https://s3.custom.endpoint.com" - + def test_s3_config_missing_access_key(self): """Test S3Config validation fails with missing access_key_id.""" with pytest.raises(ValueError, match="access_key_id is required"): @@ -52,9 +50,9 @@ def test_s3_config_missing_access_key(self): access_key_id="", secret_access_key="test_secret", region="us-east-1", - bucket_name="test-bucket" + bucket_name="test-bucket", ) - + def test_s3_config_missing_secret_key(self): """Test S3Config validation fails with missing secret_access_key.""" with pytest.raises(ValueError, match="secret_access_key is required"): @@ -62,9 +60,9 @@ def test_s3_config_missing_secret_key(self): access_key_id="test_key", secret_access_key="", region="us-east-1", - bucket_name="test-bucket" + bucket_name="test-bucket", ) - + def test_s3_config_missing_region(self): """Test S3Config validation fails with missing region.""" with pytest.raises(ValueError, match="region is required"): @@ -72,9 +70,9 @@ def test_s3_config_missing_region(self): access_key_id="test_key", secret_access_key="test_secret", region="", - bucket_name="test-bucket" + bucket_name="test-bucket", ) - + def test_s3_config_missing_bucket_name(self): """Test S3Config validation fails with missing bucket_name.""" with pytest.raises(ValueError, match="bucket_name is required"): @@ -82,9 +80,9 @@ def test_s3_config_missing_bucket_name(self): access_key_id="test_key", secret_access_key="test_secret", region="us-east-1", - bucket_name="" + bucket_name="", ) - + def test_s3_config_from_dict(self): """Test creating S3Config from dictionary.""" data = { @@ -92,27 +90,24 @@ def test_s3_config_from_dict(self): "secret_access_key": "test_secret", "region": "eu-west-1", "bucket_name": "my-bucket", - "endpoint_url": "https://custom.endpoint.com" + "endpoint_url": "https://custom.endpoint.com", } - + config = S3Config.from_dict(data) - + assert config.access_key_id == "test_key" assert config.secret_access_key == "test_secret" assert config.region == "eu-west-1" assert config.bucket_name == "my-bucket" assert config.endpoint_url == "https://custom.endpoint.com" - + def test_s3_config_from_dict_missing_fields(self): """Test S3Config.from_dict with missing fields raises validation error.""" - data = { - "access_key_id": "test_key", - "region": "us-east-1" - } - + data = {"access_key_id": "test_key", "region": "us-east-1"} + with pytest.raises(ValueError): S3Config.from_dict(data) - + def test_s3_config_from_env(self, monkeypatch): """Test creating S3Config from environment variables.""" monkeypatch.setenv("S3_ACCESS_KEY_ID", "env_key") @@ -120,24 +115,24 @@ def test_s3_config_from_env(self, monkeypatch): monkeypatch.setenv("S3_REGION", "ap-south-1") monkeypatch.setenv("S3_BUCKET_NAME", "env-bucket") monkeypatch.setenv("S3_ENDPOINT_URL", "https://env.endpoint.com") - + config = S3Config.from_env() - + assert config.access_key_id == "env_key" assert config.secret_access_key == "env_secret" assert config.region == "ap-south-1" assert config.bucket_name == "env-bucket" assert config.endpoint_url == "https://env.endpoint.com" - + def test_s3_config_from_env_custom_prefix(self, monkeypatch): """Test S3Config.from_env with custom prefix.""" monkeypatch.setenv("AWS_ACCESS_KEY_ID", "aws_key") monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "aws_secret") monkeypatch.setenv("AWS_REGION", "us-west-1") monkeypatch.setenv("AWS_BUCKET_NAME", "aws-bucket") - + config = S3Config.from_env(prefix="AWS_") - + assert config.access_key_id == "aws_key" assert config.secret_access_key == "aws_secret" assert config.region == "us-west-1" @@ -146,68 +141,64 @@ def test_s3_config_from_env_custom_prefix(self, monkeypatch): class TestStorachaConfig: """Test cases for StorachaConfig data class.""" - + def test_valid_storacha_config(self): """Test creating StorachaConfig with valid parameters.""" config = StorachaConfig( api_key="storacha_api_key_123", endpoint_url="https://api.storacha.network", - space_name="my-space" + space_name="my-space", ) - + assert config.api_key == "storacha_api_key_123" assert config.endpoint_url == "https://api.storacha.network" assert config.space_name == "my-space" - + def test_storacha_config_missing_api_key(self): """Test StorachaConfig validation fails with missing api_key.""" with pytest.raises(ValueError, match="api_key is required"): StorachaConfig( api_key="", endpoint_url="https://api.storacha.network", - space_name="my-space" + space_name="my-space", ) - + def test_storacha_config_missing_endpoint_url(self): """Test StorachaConfig validation fails with missing endpoint_url.""" with pytest.raises(ValueError, match="endpoint_url is required"): - StorachaConfig( - api_key="test_key", - endpoint_url="", - space_name="my-space" - ) - + StorachaConfig(api_key="test_key", endpoint_url="", space_name="my-space") + def test_storacha_config_missing_space_name(self): """Test StorachaConfig validation fails with missing space_name.""" with pytest.raises(ValueError, match="space_name is required"): StorachaConfig( api_key="test_key", endpoint_url="https://api.storacha.network", - space_name="" + space_name="", ) - + def test_storacha_config_from_dict(self): """Test creating StorachaConfig from dictionary.""" data = { "api_key": "dict_api_key", "endpoint_url": "https://dict.endpoint.com", - "space_name": "dict-space" + "space_name": "dict-space", } - + config = StorachaConfig.from_dict(data) - + assert config.api_key == "dict_api_key" assert config.endpoint_url == "https://dict.endpoint.com" assert config.space_name == "dict-space" - + def test_storacha_config_from_env(self, monkeypatch): """Test creating StorachaConfig from environment variables.""" monkeypatch.setenv("STORACHA_API_KEY", "env_api_key") monkeypatch.setenv("STORACHA_ENDPOINT_URL", "https://env.storacha.com") monkeypatch.setenv("STORACHA_SPACE_NAME", "env-space") - + config = StorachaConfig.from_env() - + assert config.api_key == "env_api_key" assert config.endpoint_url == "https://env.storacha.com" assert config.space_name == "env-space" @@ -215,17 +206,17 @@ def test_storacha_config_from_env(self, monkeypatch): class TestMigrationConfig: """Test cases for MigrationConfig data class.""" - + def test_default_migration_config(self): """Test MigrationConfig with default values.""" config = MigrationConfig() - + assert config.batch_size == 100 assert config.timeout_seconds == 300 assert config.retry_attempts == 3 assert config.verbose is False assert config.dry_run is False - + def test_custom_migration_config(self): """Test MigrationConfig with custom values.""" config = MigrationConfig( @@ -233,41 +224,41 @@ def test_custom_migration_config(self): timeout_seconds=600, retry_attempts=5, verbose=True, - dry_run=True + dry_run=True, ) - + assert config.batch_size == 50 assert config.timeout_seconds == 600 assert config.retry_attempts == 5 assert config.verbose is True assert config.dry_run is True - + def test_migration_config_invalid_batch_size(self): """Test MigrationConfig validation fails with invalid batch_size.""" with pytest.raises(ValueError, match="batch_size must be greater than 0"): MigrationConfig(batch_size=0) - + with pytest.raises(ValueError, match="batch_size must be greater than 0"): MigrationConfig(batch_size=-10) - + def test_migration_config_invalid_timeout(self): """Test MigrationConfig validation fails with invalid timeout_seconds.""" with pytest.raises(ValueError, match="timeout_seconds must be greater than 0"): MigrationConfig(timeout_seconds=0) - + with pytest.raises(ValueError, match="timeout_seconds must be greater than 0"): MigrationConfig(timeout_seconds=-100) - + def test_migration_config_invalid_retry_attempts(self): """Test MigrationConfig validation fails with negative retry_attempts.""" with pytest.raises(ValueError, match="retry_attempts must be non-negative"): MigrationConfig(retry_attempts=-1) - + def test_migration_config_zero_retry_attempts(self): """Test MigrationConfig allows zero retry_attempts.""" config = MigrationConfig(retry_attempts=0) assert config.retry_attempts == 0 - + def test_migration_config_from_dict(self): """Test creating MigrationConfig from dictionary.""" data = { @@ -275,23 +266,23 @@ def test_migration_config_from_dict(self): "timeout_seconds": 900, "retry_attempts": 10, "verbose": True, - "dry_run": False + "dry_run": False, } - + config = MigrationConfig.from_dict(data) - + assert config.batch_size == 200 assert config.timeout_seconds == 900 assert config.retry_attempts == 10 assert config.verbose is True assert config.dry_run is False - + def test_migration_config_from_dict_partial(self): """Test MigrationConfig.from_dict with partial data uses defaults.""" data = {"batch_size": 150} - + config = MigrationConfig.from_dict(data) - + assert config.batch_size == 150 assert config.timeout_seconds == 300 # default assert config.retry_attempts == 3 # default @@ -299,7 +290,7 @@ def test_migration_config_from_dict_partial(self): class TestConfigurationParser: """Test cases for ConfigurationParser utility class.""" - + def test_parse_json_file(self): """Test parsing configuration from JSON file.""" config_data = { @@ -307,25 +298,25 @@ def test_parse_json_file(self): "access_key_id": "json_key", "secret_access_key": "json_secret", "region": "us-east-1", - "bucket_name": "json-bucket" + "bucket_name": "json-bucket", }, "storacha": { "api_key": "json_storacha_key", "endpoint_url": "https://json.storacha.com", - "space_name": "json-space" - } + "space_name": "json-space", + }, } - - with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: json.dump(config_data, f) temp_path = f.name - + try: parsed = ConfigurationParser.parse_from_file(temp_path) assert parsed == config_data finally: os.unlink(temp_path) - + def test_parse_env_file(self): """Test parsing configuration from .env style file.""" env_content = """ @@ -337,11 +328,11 @@ def test_parse_env_file(self): STORACHA_API_KEY=env_storacha_key """ - - with tempfile.NamedTemporaryFile(mode='w', suffix='.env', delete=False) as f: + + with tempfile.NamedTemporaryFile(mode="w", suffix=".env", delete=False) as f: f.write(env_content) temp_path = f.name - + try: parsed = ConfigurationParser.parse_from_file(temp_path) assert parsed["S3_ACCESS_KEY_ID"] == "env_key" @@ -351,12 +342,12 @@ def test_parse_env_file(self): assert parsed["STORACHA_API_KEY"] == "env_storacha_key" finally: os.unlink(temp_path) - + def test_parse_nonexistent_file(self): """Test parsing nonexistent file raises FileNotFoundError.""" with pytest.raises(FileNotFoundError, match="Configuration file not found"): ConfigurationParser.parse_from_file("/nonexistent/path/config.json") - + def test_create_configs_from_dict(self): """Test creating all config objects from dictionary.""" data = { @@ -364,28 +355,27 @@ def test_create_configs_from_dict(self): "access_key_id": "test_key", "secret_access_key": "test_secret", "region": "us-east-1", - "bucket_name": "test-bucket" + "bucket_name": "test-bucket", }, "storacha": { "api_key": "test_storacha_key", "endpoint_url": "https://test.storacha.com", - "space_name": "test-space" + "space_name": "test-space", }, - "migration": { - "batch_size": 250, - "timeout_seconds": 450 - } + "migration": {"batch_size": 250, "timeout_seconds": 450}, } - - s3_config, storacha_config, migration_config = ConfigurationParser.create_configs_from_dict(data) - + + s3_config, storacha_config, migration_config = ( + ConfigurationParser.create_configs_from_dict(data) + ) + assert s3_config.access_key_id == "test_key" assert s3_config.bucket_name == "test-bucket" assert storacha_config.api_key == "test_storacha_key" assert storacha_config.space_name == "test-space" assert migration_config.batch_size == 250 assert migration_config.timeout_seconds == 450 - + def test_create_configs_from_file(self): """Test creating all config objects from JSON file.""" config_data = { @@ -393,26 +383,25 @@ def test_create_configs_from_file(self): "access_key_id": "file_key", "secret_access_key": "file_secret", "region": "eu-central-1", - "bucket_name": "file-bucket" + "bucket_name": "file-bucket", }, "storacha": { "api_key": "file_storacha_key", "endpoint_url": "https://file.storacha.com", - "space_name": "file-space" + "space_name": "file-space", }, - "migration": { - "batch_size": 75, - "verbose": True - } + "migration": {"batch_size": 75, "verbose": True}, } - - with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: json.dump(config_data, f) temp_path = f.name - + try: - s3_config, storacha_config, migration_config = ConfigurationParser.create_configs_from_file(temp_path) - + s3_config, storacha_config, migration_config = ( + ConfigurationParser.create_configs_from_file(temp_path) + ) + assert s3_config.access_key_id == "file_key" assert s3_config.region == "eu-central-1" assert storacha_config.endpoint_url == "https://file.storacha.com" diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py index c5f875b..706fc08 100644 --- a/tests/unit/test_exceptions.py +++ b/tests/unit/test_exceptions.py @@ -1,7 +1,6 @@ """Unit tests for custom exception classes.""" import pytest -from typing import Dict, Any from py_s3_storacha.exceptions import ( S3StorachaError, @@ -13,55 +12,53 @@ class TestS3StorachaError: """Test cases for base S3StorachaError exception.""" - + def test_basic_error_creation(self): """Test creating basic S3StorachaError with message only.""" error = S3StorachaError("Test error message") - + assert error.message == "Test error message" assert error.context == {} assert error.original_error is None assert str(error) == "Test error message" - + def test_error_with_context(self): """Test S3StorachaError with context information.""" context = {"operation": "test_op", "file": "test.txt"} error = S3StorachaError("Error occurred", context=context) - + assert error.context == context assert "operation=test_op" in str(error) assert "file=test.txt" in str(error) - + def test_error_with_original_error(self): """Test S3StorachaError with original exception.""" original = ValueError("Original error") error = S3StorachaError("Wrapped error", original_error=original) - + assert error.original_error is original assert "Caused by: Original error" in str(error) - + def test_add_context(self): """Test adding context to existing error.""" error = S3StorachaError("Test error") - + error.add_context("key1", "value1") error.add_context("key2", 42) - + assert error.context["key1"] == "value1" assert error.context["key2"] == 42 assert "key1=value1" in str(error) assert "key2=42" in str(error) - + def test_error_string_formatting(self): """Test complete error string formatting with all components.""" original = RuntimeError("Original issue") context = {"step": "validation", "count": 5} error = S3StorachaError( - "Main error message", - context=context, - original_error=original + "Main error message", context=context, original_error=original ) - + error_str = str(error) assert "Main error message" in error_str assert "step=validation" in error_str @@ -71,124 +68,124 @@ def test_error_string_formatting(self): class TestJSWrapperError: """Test cases for JSWrapperError exception.""" - + def test_basic_js_wrapper_error(self): """Test creating basic JSWrapperError.""" error = JSWrapperError("JavaScript execution failed") - + assert error.message == "JavaScript execution failed" assert error.return_code is None assert error.stdout is None assert error.stderr is None - + def test_js_wrapper_error_with_process_info(self): """Test JSWrapperError with process execution details.""" error = JSWrapperError( "Process failed", return_code=1, stdout="Some output", - stderr="Error details" + stderr="Error details", ) - + assert error.return_code == 1 assert error.stdout == "Some output" assert error.stderr == "Error details" assert error.context["return_code"] == 1 assert "Error details" in error.context["stderr"] - + def test_js_wrapper_error_stderr_truncation(self): """Test that long stderr is truncated in context.""" long_stderr = "x" * 1000 - error = JSWrapperError( - "Process failed", - return_code=1, - stderr=long_stderr - ) - + error = JSWrapperError("Process failed", return_code=1, stderr=long_stderr) + # stderr should be truncated to 500 chars in context assert len(error.context["stderr"]) == 500 assert error.stderr == long_stderr # Original should be preserved - + def test_from_process_result(self): """Test creating JSWrapperError from process result.""" error = JSWrapperError.from_process_result( return_code=127, stdout="", stderr="Command not found", - command="node script.js" + command="node script.js", ) - + assert error.return_code == 127 assert error.stderr == "Command not found" assert "return code 127" in error.message assert "Command: node script.js" in error.message - + def test_from_process_result_without_command(self): """Test from_process_result without command parameter.""" error = JSWrapperError.from_process_result( - return_code=1, - stdout="output", - stderr="error" + return_code=1, stdout="output", stderr="error" ) - + assert "return code 1" in error.message assert "Command:" not in error.message class TestConfigurationError: """Test cases for ConfigurationError exception.""" - + def test_basic_configuration_error(self): """Test creating basic ConfigurationError.""" error = ConfigurationError("Invalid configuration") - + assert error.message == "Invalid configuration" assert error.config_type is None assert error.field_name is None assert error.field_value is None - + def test_configuration_error_with_details(self): """Test ConfigurationError with field details.""" error = ConfigurationError( "Invalid value", config_type="S3Config", field_name="region", - field_value="invalid-region" + field_value="invalid-region", ) - + assert error.config_type == "S3Config" assert error.field_name == "region" assert error.field_value == "invalid-region" assert error.context["config_type"] == "S3Config" assert error.context["field_name"] == "region" assert "invalid-region" in error.context["field_value"] - + def test_configuration_error_masks_sensitive_fields(self): """Test that sensitive field values are masked in context.""" - sensitive_fields = ["api_key", "secret_key", "password", "token", "Secret_Access_Key"] - + sensitive_fields = [ + "api_key", + "secret_key", + "password", + "token", + "Secret_Access_Key", + ] + for field_name in sensitive_fields: error = ConfigurationError( "Sensitive field error", config_type="TestConfig", field_name=field_name, - field_value="super_secret_value_123" + field_value="super_secret_value_123", ) - + assert error.field_value == "super_secret_value_123" # Original preserved assert error.context["field_value"] == "***MASKED***" # Context masked - + def test_configuration_error_non_sensitive_field(self): """Test that non-sensitive fields are not masked.""" error = ConfigurationError( "Invalid region", config_type="S3Config", field_name="region", - field_value="us-invalid-1" + field_value="us-invalid-1", ) - + assert error.context["field_value"] == "us-invalid-1" - + def test_configuration_error_long_value_truncation(self): """Test that long field values are truncated in context.""" long_value = "x" * 200 @@ -196,30 +193,27 @@ def test_configuration_error_long_value_truncation(self): "Value too long", config_type="TestConfig", field_name="description", - field_value=long_value + field_value=long_value, ) - + assert len(error.context["field_value"]) == 100 assert error.field_value == long_value # Original preserved - + def test_missing_field_factory(self): """Test missing_field factory method.""" error = ConfigurationError.missing_field("S3Config", "access_key_id") - + assert "Missing required field 'access_key_id'" in error.message assert "S3Config" in error.message assert error.config_type == "S3Config" assert error.field_name == "access_key_id" - + def test_invalid_field_factory(self): """Test invalid_field factory method.""" error = ConfigurationError.invalid_field( - "StorachaConfig", - "timeout", - -10, - "must be positive" + "StorachaConfig", "timeout", -10, "must be positive" ) - + assert "Invalid value for field 'timeout'" in error.message assert "StorachaConfig" in error.message assert "must be positive" in error.message @@ -230,17 +224,17 @@ def test_invalid_field_factory(self): class TestMigrationError: """Test cases for MigrationError exception.""" - + def test_basic_migration_error(self): """Test creating basic MigrationError.""" error = MigrationError("Migration failed") - + assert error.message == "Migration failed" assert error.operation is None assert error.source_path is None assert error.destination_path is None assert error.objects_processed is None - + def test_migration_error_with_details(self): """Test MigrationError with operation details.""" error = MigrationError( @@ -248,67 +242,59 @@ def test_migration_error_with_details(self): operation="upload", source_path="s3://bucket/file.txt", destination_path="storacha://space/file.txt", - objects_processed=42 + objects_processed=42, ) - + assert error.operation == "upload" assert error.source_path == "s3://bucket/file.txt" assert error.destination_path == "storacha://space/file.txt" assert error.objects_processed == 42 assert error.context["operation"] == "upload" assert error.context["objects_processed"] == 42 - + def test_network_error_factory(self): """Test network_error factory method.""" original = ConnectionError("Connection refused") - error = MigrationError.network_error( - "Failed to connect", - "download", - original - ) - + error = MigrationError.network_error("Failed to connect", "download", original) + assert "Network error during download" in error.message assert "Failed to connect" in error.message assert error.operation == "download" assert error.original_error is original - + def test_timeout_error_factory(self): """Test timeout_error factory method.""" - error = MigrationError.timeout_error( - "upload", - 300, - objects_processed=15 - ) - + error = MigrationError.timeout_error("upload", 300, objects_processed=15) + assert "timed out after 300 seconds" in error.message assert "upload" in error.message assert error.operation == "upload" assert error.objects_processed == 15 - + def test_timeout_error_without_objects_processed(self): """Test timeout_error without objects_processed.""" error = MigrationError.timeout_error("sync", 600) - + assert "timed out after 600 seconds" in error.message assert error.objects_processed is None - + def test_validation_error_factory(self): """Test validation_error factory method.""" error = MigrationError.validation_error( "File size mismatch", source_path="s3://bucket/file.txt", - destination_path="storacha://space/file.txt" + destination_path="storacha://space/file.txt", ) - + assert "Validation error: File size mismatch" in error.message assert error.operation == "validation" assert error.source_path == "s3://bucket/file.txt" assert error.destination_path == "storacha://space/file.txt" - + def test_validation_error_without_paths(self): """Test validation_error without path parameters.""" error = MigrationError.validation_error("Invalid format") - + assert "Validation error: Invalid format" in error.message assert error.source_path is None assert error.destination_path is None @@ -316,51 +302,51 @@ def test_validation_error_without_paths(self): class TestExceptionInheritance: """Test exception inheritance and polymorphism.""" - + def test_all_exceptions_inherit_from_base(self): """Test that all custom exceptions inherit from S3StorachaError.""" assert issubclass(JSWrapperError, S3StorachaError) assert issubclass(ConfigurationError, S3StorachaError) assert issubclass(MigrationError, S3StorachaError) - + def test_all_exceptions_inherit_from_exception(self): """Test that all custom exceptions inherit from Exception.""" assert issubclass(S3StorachaError, Exception) assert issubclass(JSWrapperError, Exception) assert issubclass(ConfigurationError, Exception) assert issubclass(MigrationError, Exception) - + def test_catch_specific_exception(self): """Test catching specific exception types.""" with pytest.raises(ConfigurationError): raise ConfigurationError("Config error") - + with pytest.raises(JSWrapperError): raise JSWrapperError("JS error") - + with pytest.raises(MigrationError): raise MigrationError("Migration error") - + def test_catch_base_exception(self): """Test catching base exception catches all custom exceptions.""" exceptions = [ ConfigurationError("Config error"), JSWrapperError("JS error"), - MigrationError("Migration error") + MigrationError("Migration error"), ] - + for exc in exceptions: with pytest.raises(S3StorachaError): raise exc - + def test_exception_context_preserved_through_inheritance(self): """Test that context functionality works in all exception types.""" exceptions = [ JSWrapperError("JS error", return_code=1), ConfigurationError("Config error", config_type="TestConfig"), - MigrationError("Migration error", operation="test") + MigrationError("Migration error", operation="test"), ] - + for exc in exceptions: exc.add_context("test_key", "test_value") assert exc.context["test_key"] == "test_value"