diff --git a/src/deltaglider/adapters/metrics_cloudwatch.py b/src/deltaglider/adapters/metrics_cloudwatch.py index b85fcd2..3920630 100644 --- a/src/deltaglider/adapters/metrics_cloudwatch.py +++ b/src/deltaglider/adapters/metrics_cloudwatch.py @@ -10,6 +10,11 @@ from ..ports.metrics import MetricsPort logger = logging.getLogger(__name__) +# Constants for byte conversions +BYTES_PER_KB = 1024 +BYTES_PER_MB = 1024 * 1024 +BYTES_PER_GB = 1024 * 1024 * 1024 + class CloudWatchMetricsAdapter(MetricsPort): """CloudWatch implementation of MetricsPort for AWS-native metrics.""" @@ -160,11 +165,11 @@ class CloudWatchMetricsAdapter(MetricsPort): # Size metrics if any(x in name_lower for x in ["size", "bytes"]): - if value > 1024 * 1024 * 1024: # > 1GB + if value > BYTES_PER_GB: # > 1GB return "Gigabytes" - elif value > 1024 * 1024: # > 1MB + elif value > BYTES_PER_MB: # > 1MB return "Megabytes" - elif value > 1024: # > 1KB + elif value > BYTES_PER_KB: # > 1KB return "Kilobytes" return "Bytes" diff --git a/src/deltaglider/client.py b/src/deltaglider/client.py index 3afa880..0917e15 100644 --- a/src/deltaglider/client.py +++ b/src/deltaglider/client.py @@ -6,6 +6,7 @@ from dataclasses import dataclass, field from pathlib import Path from typing import Any +from .adapters.storage_s3 import S3StorageAdapter from .core import DeltaService, DeltaSpace, ObjectKey @@ -279,8 +280,6 @@ class DeltaGliderClient: ListObjectsResponse with objects and common prefixes """ # Use storage adapter's list_objects method if available - from .adapters.storage_s3 import S3StorageAdapter - if hasattr(self.service.storage, "list_objects"): # Use list_objects method if available result = self.service.storage.list_objects( @@ -364,8 +363,6 @@ class DeltaGliderClient: Returns: Response dict with deletion details """ - from .core.models import ObjectKey - # Use core service's delta-aware delete object_key = ObjectKey(bucket=Bucket, key=Key) delete_result = self.service.delete(object_key) @@ -413,8 +410,6 @@ class DeltaGliderClient: Returns: Response dict with deleted objects """ - from .core.models import ObjectKey - deleted = [] errors = [] delta_info = [] @@ -1051,6 +1046,26 @@ class DeltaGliderClient: direct_objects=direct_count, ) + def _try_boto3_presigned_operation(self, operation: str, **kwargs: Any) -> Any | None: + """Try to generate presigned operation using boto3 client, return None if not available.""" + storage_adapter = self.service.storage + + # Check if storage adapter has boto3 client + if hasattr(storage_adapter, "client"): + try: + if operation == "url": + return str(storage_adapter.client.generate_presigned_url(**kwargs)) + elif operation == "post": + return dict(storage_adapter.client.generate_presigned_post(**kwargs)) + except AttributeError: + # storage_adapter does not have a 'client' attribute + pass + except Exception as e: + # Fall back to manual construction if needed + self.service.logger.warning(f"Failed to generate presigned {operation}: {e}") + + return None + def generate_presigned_url( self, ClientMethod: str, @@ -1067,23 +1082,15 @@ class DeltaGliderClient: Returns: Presigned URL string """ - # Access the underlying S3 client through storage adapter - # Note: service.storage should be an S3StorageAdapter instance - storage_adapter = self.service.storage - - # Check if storage adapter has boto3 client - if hasattr(storage_adapter, "client"): - try: - # Use boto3's native presigned URL generation - url = storage_adapter.client.generate_presigned_url( - ClientMethod=ClientMethod, - Params=Params, - ExpiresIn=ExpiresIn, - ) - return str(url) - except Exception as e: - # Fall back to manual URL construction if needed - self.service.logger.warning(f"Failed to generate presigned URL: {e}") + # Try boto3 first, fallback to manual construction + url = self._try_boto3_presigned_operation( + "url", + ClientMethod=ClientMethod, + Params=Params, + ExpiresIn=ExpiresIn, + ) + if url is not None: + return str(url) # Fallback: construct URL manually (less secure, for dev/testing only) bucket = Params.get("Bucket", "") @@ -1118,22 +1125,17 @@ class DeltaGliderClient: Returns: Dict with 'url' and 'fields' for form submission """ - storage_adapter = self.service.storage - - # Check if storage adapter has boto3 client - if hasattr(storage_adapter, "client"): - try: - # Use boto3's native presigned POST generation - response = storage_adapter.client.generate_presigned_post( - Bucket=Bucket, - Key=Key, - Fields=Fields, - Conditions=Conditions, - ExpiresIn=ExpiresIn, - ) - return dict(response) - except Exception as e: - self.service.logger.warning(f"Failed to generate presigned POST: {e}") + # Try boto3 first, fallback to manual construction + response = self._try_boto3_presigned_operation( + "post", + Bucket=Bucket, + Key=Key, + Fields=Fields, + Conditions=Conditions, + ExpiresIn=ExpiresIn, + ) + if response is not None: + return dict(response) # Fallback: return minimal structure for compatibility if self.endpoint_url: diff --git a/tests/integration/test_client.py b/tests/integration/test_client.py index c539eed..67fe4a0 100644 --- a/tests/integration/test_client.py +++ b/tests/integration/test_client.py @@ -1,6 +1,7 @@ """Tests for the DeltaGlider client with boto3-compatible APIs.""" import hashlib +from datetime import UTC, datetime from pathlib import Path import pytest @@ -30,7 +31,7 @@ class MockStorage: key=key, size=obj["size"], etag=obj.get("etag", "mock-etag"), - last_modified=obj.get("last_modified"), + last_modified=obj.get("last_modified", datetime.now(UTC)), metadata=obj.get("metadata", {}), ) return None @@ -39,7 +40,9 @@ class MockStorage: """Mock list operation for StoragePort interface.""" for key, _obj in self.objects.items(): if key.startswith(prefix): - yield self.head(key) + obj_head = self.head(key) + if obj_head is not None: + yield obj_head def list_objects(self, bucket, prefix="", delimiter="", max_keys=1000, start_after=None): """Mock list_objects operation for S3 features."""