feat: Implement boto3-compatible dict responses (Phase 2)

Changed list_objects() to return boto3-compatible dict instead of custom
ListObjectsResponse dataclass. This makes DeltaGlider a true drop-in replacement
for boto3.client('s3').

Changes:
- list_objects() now returns dict[str, Any] with boto3-compatible structure:
  * Contents: list[S3Object] (dict with Key, Size, LastModified, etc.)
  * CommonPrefixes: list[dict] for folder simulation
  * IsTruncated, NextContinuationToken for pagination
  * DeltaGlider metadata stored in standard Metadata field

- Updated all client methods that use list_objects() to work with dict responses:
  * find_similar_files()
  * get_bucket_stats()
  * CLI ls command

- Updated all tests to use dict access (response['Contents']) instead of
  dataclass access (response.contents)

- Updated examples/boto3_compatible_types.py to demonstrate usage

- DeltaGlider-specific metadata now in Metadata field:
  * deltaglider-is-delta: "true"/"false"
  * deltaglider-original-size: string number
  * deltaglider-compression-ratio: string number or "unknown"
  * deltaglider-reference-key: optional string

Benefits:
- True drop-in replacement for boto3
- No learning curve - if you know boto3, you know DeltaGlider
- Works with any boto3-compatible library
- Type safety through TypedDict (no boto3 import needed)
- Zero runtime overhead (TypedDict compiles to plain dict)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Simone Scarduzio
2025-10-08 14:23:50 +02:00
parent 38134f28f5
commit 69ed9056d2
7 changed files with 476 additions and 82 deletions

View File

@@ -10,7 +10,6 @@ from deltaglider import create_client
from deltaglider.client import (
BucketStats,
CompressionEstimate,
ListObjectsResponse,
ObjectInfo,
)
@@ -279,27 +278,35 @@ class TestBoto3Compatibility:
assert response["ContentLength"] == len(content)
def test_list_objects(self, client):
"""Test list_objects with various options."""
"""Test list_objects with various options (boto3-compatible dict response)."""
# List all objects (default: FetchMetadata=False)
response = client.list_objects(Bucket="test-bucket")
assert isinstance(response, ListObjectsResponse)
assert response.key_count > 0
assert len(response.contents) > 0
# Response is now a boto3-compatible dict (not ListObjectsResponse)
assert isinstance(response, dict)
assert response["KeyCount"] > 0
assert len(response["Contents"]) > 0
# Verify S3Object structure
for obj in response["Contents"]:
assert "Key" in obj
assert "Size" in obj
assert "LastModified" in obj
assert "Metadata" in obj # DeltaGlider metadata
# Test with FetchMetadata=True (should only affect delta files)
response_with_metadata = client.list_objects(Bucket="test-bucket", FetchMetadata=True)
assert isinstance(response_with_metadata, ListObjectsResponse)
assert response_with_metadata.key_count > 0
assert isinstance(response_with_metadata, dict)
assert response_with_metadata["KeyCount"] > 0
def test_list_objects_with_delimiter(self, client):
"""Test list_objects with delimiter for folder simulation."""
"""Test list_objects with delimiter for folder simulation (boto3-compatible dict response)."""
response = client.list_objects(Bucket="test-bucket", Prefix="", Delimiter="/")
# Should have common prefixes for folders
assert len(response.common_prefixes) > 0
assert {"Prefix": "folder1/"} in response.common_prefixes
assert {"Prefix": "folder2/"} in response.common_prefixes
assert len(response.get("CommonPrefixes", [])) > 0
assert {"Prefix": "folder1/"} in response["CommonPrefixes"]
assert {"Prefix": "folder2/"} in response["CommonPrefixes"]
def test_delete_object(self, client):
"""Test delete_object."""

View File

@@ -53,8 +53,11 @@ class TestSDKFiltering:
client = DeltaGliderClient(service)
response = client.list_objects(Bucket="test-bucket", Prefix="releases/")
# Response is now a boto3-compatible dict
contents = response["Contents"]
# Verify .delta suffix is stripped
keys = [obj.key for obj in response.contents]
keys = [obj["Key"] for obj in contents]
assert "releases/app-v1.zip" in keys
assert "releases/app-v2.zip" in keys
assert "releases/README.md" in keys
@@ -63,8 +66,12 @@ class TestSDKFiltering:
for key in keys:
assert not key.endswith(".delta"), f"Found .delta suffix in: {key}"
# Verify is_delta flag is set correctly
delta_objects = [obj for obj in response.contents if obj.is_delta]
# Verify is_delta flag is set correctly in Metadata
delta_objects = [
obj
for obj in contents
if obj.get("Metadata", {}).get("deltaglider-is-delta") == "true"
]
assert len(delta_objects) == 2
def test_list_objects_filters_reference_bin(self):
@@ -106,15 +113,18 @@ class TestSDKFiltering:
client = DeltaGliderClient(service)
response = client.list_objects(Bucket="test-bucket", Prefix="releases/")
# Response is now a boto3-compatible dict
contents = response["Contents"]
# Verify NO reference.bin files in output
keys = [obj.key for obj in response.contents]
keys = [obj["Key"] for obj in contents]
for key in keys:
assert not key.endswith("reference.bin"), f"Found reference.bin in: {key}"
# Should only have the app.zip (with .delta stripped)
assert len(response.contents) == 1
assert response.contents[0].key == "releases/app.zip"
assert response.contents[0].is_delta is True
assert len(contents) == 1
assert contents[0]["Key"] == "releases/app.zip"
assert contents[0].get("Metadata", {}).get("deltaglider-is-delta") == "true"
def test_list_objects_combined_filtering(self):
"""Test filtering of both .delta and reference.bin together."""
@@ -170,12 +180,15 @@ class TestSDKFiltering:
client = DeltaGliderClient(service)
response = client.list_objects(Bucket="test-bucket", Prefix="data/")
# Response is now a boto3-compatible dict
contents = response["Contents"]
# Should filter out 2 reference.bin files
# Should strip .delta from 3 files
# Should keep 1 regular file as-is
assert len(response.contents) == 4 # 3 deltas + 1 regular file
assert len(contents) == 4 # 3 deltas + 1 regular file
keys = [obj.key for obj in response.contents]
keys = [obj["Key"] for obj in contents]
expected_keys = ["data/file1.zip", "data/file2.zip", "data/file3.txt", "data/sub/app.jar"]
assert sorted(keys) == sorted(expected_keys)