Skip to content

Commit 89b2372

Browse files
committed
Implement CopyObjectIT
1 parent ff80a15 commit 89b2372

File tree

2 files changed

+624
-3
lines changed

2 files changed

+624
-3
lines changed

s3mock_test.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,27 @@
22
import hashlib
33
import re
44
import time
5+
import uuid
56
from typing import Iterable, Optional
67
import os
8+
from pathlib import Path
79

810
import boto3
11+
import datetime as dt
912
import pytest
1013
from botocore.client import Config
1114
from botocore.exceptions import ClientError
1215
from mypy_boto3_s3.client import S3Client
1316
from mypy_boto3_s3.type_defs import CreateBucketOutputTypeDef, PutObjectOutputTypeDef
17+
from s3transfer.manager import TransferManager
18+
from boto3.s3.transfer import TransferConfig
1419
from testcontainers.core.container import DockerContainer # type: ignore[import-untyped]
1520
from testcontainers.core.wait_strategies import LogMessageWaitStrategy
1621

1722
UPLOAD_FILE_NAME = 'testfile.txt'
23+
UPLOAD_FILE_LENGTH = Path(UPLOAD_FILE_NAME).stat().st_size
24+
REGION = os.getenv("AWS_REGION", "us-east-1")
25+
ONE_MB = 1024 * 1024
1826

1927
container = (
2028
DockerContainer("adobe/s3mock:4.9.0")
@@ -94,6 +102,7 @@ def s3_client(endpoint_url) -> S3Client:
94102
retries={'max_attempts': _MAX_RETRIES},
95103
signature_version='s3v4',
96104
s3={'addressing_style': 'path'},
105+
max_pool_connections=100,
97106
)
98107
return boto3.client(
99108
's3',
@@ -105,6 +114,19 @@ def s3_client(endpoint_url) -> S3Client:
105114
verify=False, # Skip SSL certificate verification (use only in tests)
106115
)
107116

117+
@pytest.fixture(scope="session", autouse=True)
118+
def transfer_manager(s3_client: S3Client) -> TransferManager:
119+
"""
120+
Create a Transfer Manager equivalent with multipart enabled and high concurrency.
121+
"""
122+
transfer_config = TransferConfig(
123+
multipart_threshold=8 * 1024 * 1024, # 8 MiB threshold for multipart
124+
multipart_chunksize=8 * 1024 * 1024, # 8 MiB parts
125+
max_concurrency=100, # similar to CRT maxConcurrency
126+
use_threads=True, # parallel uploads/downloads
127+
)
128+
return TransferManager(s3_client, config=transfer_config)
129+
108130
@pytest.fixture(scope="session", autouse=True)
109131
def s3_client_http(endpoint_url_http) -> S3Client:
110132
config = Config(
@@ -113,6 +135,7 @@ def s3_client_http(endpoint_url_http) -> S3Client:
113135
retries={'max_attempts': _MAX_RETRIES},
114136
signature_version='s3v4',
115137
s3={'addressing_style': 'path'},
138+
max_pool_connections=100,
116139
)
117140
return boto3.client(
118141
's3',
@@ -123,6 +146,10 @@ def s3_client_http(endpoint_url_http) -> S3Client:
123146
endpoint_url=endpoint_url_http,
124147
)
125148

149+
def upload_file_bytes() -> bytes:
150+
with open('testfile.txt', 'rb') as file:
151+
return file.read()
152+
126153
def delete_multipart_uploads(s3_client: S3Client, bucket_name: str) -> None:
127154
"""
128155
Abort all in-progress multipart uploads in the specified bucket.
@@ -218,9 +245,13 @@ def given_bucket(s3_client: S3Client, bucket_name: str) -> CreateBucketOutputTyp
218245
s3_client.get_waiter("bucket_exists").wait(Bucket=bucket_name)
219246
return bucket
220247

221-
def given_object(s3_client: S3Client, bucket_name: str, object_name: str = UPLOAD_FILE_NAME) -> PutObjectOutputTypeDef:
222-
with open('testfile.txt', 'rb') as file:
223-
return s3_client.put_object(Bucket=bucket_name, Key=object_name, Body=file.read())
248+
def given_object(s3_client: S3Client, bucket_name: str, object_name: str = UPLOAD_FILE_NAME, **kwargs) -> PutObjectOutputTypeDef:
249+
return s3_client.put_object(
250+
Bucket=bucket_name,
251+
Key=object_name,
252+
Body=upload_file_bytes(),
253+
** kwargs
254+
)
224255

225256
def compute_md5_etag(data: bytes) -> str:
226257
# S3 single-part ETag is the hex MD5 in quotes
@@ -230,3 +261,14 @@ def compute_md5_etag(data: bytes) -> str:
230261
def compute_sha256_checksum_b64(data: bytes) -> str:
231262
# AWS returns base64-encoded checksum for SHA256
232263
return base64.b64encode(hashlib.sha256(data).digest()).decode("ascii")
264+
265+
def random_name() -> str:
266+
return f"{uuid.uuid4().hex}"[:63]
267+
268+
def special_key() -> str:
269+
# Includes spaces, unicode, URL-reserved chars that require escaping
270+
return 'spécial key/with spaces & symbols?#[]@!$&\'()*+,;=.txt'
271+
272+
def now_utc() -> dt.datetime:
273+
# Use naive UTC timestamps as boto3 expects
274+
return dt.datetime.now(dt.UTC).replace(tzinfo=None)

0 commit comments

Comments
 (0)