hexkit 3.6.0__tar.gz → 3.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hexkit-3.6.0/src/hexkit.egg-info → hexkit-3.7.0}/PKG-INFO +1 -1
- {hexkit-3.6.0 → hexkit-3.7.0}/pyproject.toml +1 -1
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/protocols/objstorage.py +31 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/akafka/testcontainer.py +1 -1
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/s3/provider.py +75 -25
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/s3/testutils/_fixtures.py +14 -13
- {hexkit-3.6.0 → hexkit-3.7.0/src/hexkit.egg-info}/PKG-INFO +1 -1
- {hexkit-3.6.0 → hexkit-3.7.0}/LICENSE +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/MANIFEST.in +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/README.md +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/setup.cfg +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/__init__.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/__main__.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/base.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/config.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/correlation.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/custom_types.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/log.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/protocols/__init__.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/protocols/dao.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/protocols/daopub.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/protocols/daosub.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/protocols/eventpub.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/protocols/eventsub.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/__init__.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/akafka/__init__.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/akafka/config.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/akafka/provider/__init__.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/akafka/provider/daosub.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/akafka/provider/eventpub.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/akafka/provider/eventsub.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/akafka/provider/utils.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/akafka/testutils.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/mongodb/__init__.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/mongodb/provider.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/mongodb/testutils.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/mongokafka/__init__.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/mongokafka/provider.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/mongokafka/testutils.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/s3/__init__.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/s3/testutils/__init__.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/s3/testutils/_typical_workflow.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/s3/testutils/_utils.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/testing/__init__.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/providers/testing/eventpub.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/py.typed +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit/utils.py +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit.egg-info/SOURCES.txt +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit.egg-info/dependency_links.txt +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit.egg-info/requires.txt +0 -0
- {hexkit-3.6.0 → hexkit-3.7.0}/src/hexkit.egg-info/top_level.txt +0 -0
|
@@ -122,6 +122,7 @@ class ObjectStorageProtocol(ABC):
|
|
|
122
122
|
object_id: str,
|
|
123
123
|
part_number: int,
|
|
124
124
|
expires_after: int = 3600,
|
|
125
|
+
part_md5: Optional[str] = None,
|
|
125
126
|
) -> str:
|
|
126
127
|
"""Given a id of an instantiated multipart upload along with the corresponding
|
|
127
128
|
bucket and object ID, it returns a presigned URL for uploading a file part with the
|
|
@@ -137,6 +138,7 @@ class ObjectStorageProtocol(ABC):
|
|
|
137
138
|
object_id=object_id,
|
|
138
139
|
part_number=part_number,
|
|
139
140
|
expires_after=expires_after,
|
|
141
|
+
part_md5=part_md5,
|
|
140
142
|
)
|
|
141
143
|
|
|
142
144
|
async def abort_multipart_upload(
|
|
@@ -207,6 +209,12 @@ class ObjectStorageProtocol(ABC):
|
|
|
207
209
|
bucket_id=bucket_id, object_id=object_id, object_md5sum=object_md5sum
|
|
208
210
|
)
|
|
209
211
|
|
|
212
|
+
async def get_object_etag(self, *, bucket_id: str, object_id: str) -> str:
|
|
213
|
+
"""Returns the etag of an object."""
|
|
214
|
+
self._validate_bucket_id(bucket_id)
|
|
215
|
+
self._validate_object_id(object_id)
|
|
216
|
+
return await self._get_object_etag(bucket_id=bucket_id, object_id=object_id)
|
|
217
|
+
|
|
210
218
|
async def get_object_size(self, *, bucket_id: str, object_id: str) -> int:
|
|
211
219
|
"""Returns the size of an object in bytes."""
|
|
212
220
|
self._validate_bucket_id(bucket_id)
|
|
@@ -220,9 +228,15 @@ class ObjectStorageProtocol(ABC):
|
|
|
220
228
|
source_object_id: str,
|
|
221
229
|
dest_bucket_id: str,
|
|
222
230
|
dest_object_id: str,
|
|
231
|
+
abort_failed: bool = True,
|
|
223
232
|
) -> None:
|
|
224
233
|
"""Copy an object from one bucket (`source_bucket_id` and `source_object_id`) to
|
|
225
234
|
another bucket (`dest_bucket_id` and `dest_object_id`).
|
|
235
|
+
|
|
236
|
+
If `abort_failed` is set to true (default), a failed copy operation tries to
|
|
237
|
+
abort the ongoing multipart upload it created (if using multipart mode).
|
|
238
|
+
This only works reliably as long as there are no other ongoing multipart operations for
|
|
239
|
+
the same destination bucket and object ID, in which case this should be set to false.
|
|
226
240
|
"""
|
|
227
241
|
self._validate_bucket_id(source_bucket_id)
|
|
228
242
|
self._validate_object_id(source_object_id)
|
|
@@ -233,6 +247,7 @@ class ObjectStorageProtocol(ABC):
|
|
|
233
247
|
source_object_id=source_object_id,
|
|
234
248
|
dest_bucket_id=dest_bucket_id,
|
|
235
249
|
dest_object_id=dest_object_id,
|
|
250
|
+
abort_failed=abort_failed,
|
|
236
251
|
)
|
|
237
252
|
|
|
238
253
|
async def delete_object(self, *, bucket_id: str, object_id: str) -> None:
|
|
@@ -336,6 +351,7 @@ class ObjectStorageProtocol(ABC):
|
|
|
336
351
|
object_id: str,
|
|
337
352
|
part_number: int,
|
|
338
353
|
expires_after: int = 3600,
|
|
354
|
+
part_md5: Optional[str] = None,
|
|
339
355
|
) -> str:
|
|
340
356
|
"""
|
|
341
357
|
Given a id of an instantiated multipart upload along with the corresponding
|
|
@@ -402,6 +418,15 @@ class ObjectStorageProtocol(ABC):
|
|
|
402
418
|
"""
|
|
403
419
|
...
|
|
404
420
|
|
|
421
|
+
@abstractmethod
|
|
422
|
+
async def _get_object_etag(self, *, bucket_id: str, object_id: str) -> str:
|
|
423
|
+
"""Return the etag of an object.
|
|
424
|
+
|
|
425
|
+
*To be implemented by the provider. Input validation is done outside of this
|
|
426
|
+
method.*
|
|
427
|
+
"""
|
|
428
|
+
...
|
|
429
|
+
|
|
405
430
|
@abstractmethod
|
|
406
431
|
async def _get_object_size(self, *, bucket_id: str, object_id: str) -> int:
|
|
407
432
|
"""
|
|
@@ -435,11 +460,17 @@ class ObjectStorageProtocol(ABC):
|
|
|
435
460
|
source_object_id: str,
|
|
436
461
|
dest_bucket_id: str,
|
|
437
462
|
dest_object_id: str,
|
|
463
|
+
abort_failed: bool = True,
|
|
438
464
|
) -> None:
|
|
439
465
|
"""
|
|
440
466
|
Copy an object from one bucket (`source_bucket_id` and `source_object_id`) to
|
|
441
467
|
another bucket (`dest_bucket_id` and `dest_object_id`).
|
|
442
468
|
|
|
469
|
+
If `abort_failed` is set to true (default), a failed copy operation tries to
|
|
470
|
+
abort the ongoing multipart upload it created (if using multipart mode).
|
|
471
|
+
This only works reliably as long as there are no other ongoing multipart operations for
|
|
472
|
+
the same destination bucket and object ID, in which case this should be set to false.
|
|
473
|
+
|
|
443
474
|
*To be implemented by the provider. Input validation is done outside of this
|
|
444
475
|
method.*
|
|
445
476
|
"""
|
|
@@ -151,7 +151,7 @@ class KafkaSSLContainer(DockerContainer):
|
|
|
151
151
|
with tarfile.TarFile(fileobj=archive, mode="w") as tar:
|
|
152
152
|
tarinfo = tarfile.TarInfo(name=path)
|
|
153
153
|
tarinfo.size = len(content)
|
|
154
|
-
tarinfo.mtime = time.time()
|
|
154
|
+
tarinfo.mtime = time.time()
|
|
155
155
|
tar.addfile(tarinfo, BytesIO(content))
|
|
156
156
|
archive.seek(0)
|
|
157
157
|
self.get_wrapped_container().put_archive("/", archive)
|
|
@@ -223,27 +223,33 @@ class S3ObjectStorage(ObjectStorageProtocol):
|
|
|
223
223
|
|
|
224
224
|
async def _does_bucket_exist(self, bucket_id: str) -> bool:
|
|
225
225
|
"""Check whether a bucket with the specified ID (`bucket_id`) exists.
|
|
226
|
-
|
|
226
|
+
|
|
227
|
+
Returns `True` if it exists and `False` otherwise.
|
|
228
|
+
|
|
229
|
+
Note that this method does not need to have the permission to list buckets.
|
|
227
230
|
"""
|
|
228
231
|
try:
|
|
229
|
-
|
|
232
|
+
await asyncio.to_thread(self._client.head_bucket, Bucket=bucket_id)
|
|
230
233
|
except botocore.exceptions.ClientError as error:
|
|
234
|
+
if error.response["Error"]["Code"] == "404":
|
|
235
|
+
return False
|
|
231
236
|
raise self._translate_s3_client_errors(
|
|
232
237
|
error, bucket_id=bucket_id
|
|
233
238
|
) from error
|
|
234
|
-
|
|
235
|
-
return any(bucket["Name"] == bucket_id for bucket in bucket_list["Buckets"])
|
|
239
|
+
return True
|
|
236
240
|
|
|
237
241
|
async def _assert_bucket_exists(self, bucket_id: str) -> None:
|
|
238
|
-
"""
|
|
239
|
-
|
|
242
|
+
"""Assert that the bucket with the specified ID (`bucket_id`) exists.
|
|
243
|
+
|
|
244
|
+
If it does not exist, a BucketNotFoundError is raised.
|
|
240
245
|
"""
|
|
241
246
|
if not await self.does_bucket_exist(bucket_id):
|
|
242
247
|
raise self.BucketNotFoundError(bucket_id=bucket_id)
|
|
243
248
|
|
|
244
249
|
async def _assert_bucket_not_exists(self, bucket_id: str) -> None:
|
|
245
|
-
"""
|
|
246
|
-
|
|
250
|
+
"""Assert that the bucket with the specified ID (`bucket_id`) does not exist.
|
|
251
|
+
|
|
252
|
+
If it exists, a BucketAlreadyExistsError is raised.
|
|
247
253
|
"""
|
|
248
254
|
if await self.does_bucket_exist(bucket_id):
|
|
249
255
|
raise self.BucketAlreadyExistsError(bucket_id=bucket_id)
|
|
@@ -274,7 +280,7 @@ class S3ObjectStorage(ObjectStorageProtocol):
|
|
|
274
280
|
await self._assert_bucket_exists(bucket_id)
|
|
275
281
|
|
|
276
282
|
try:
|
|
277
|
-
bucket = self._resource.Bucket(bucket_id)
|
|
283
|
+
bucket = self._resource.Bucket(bucket_id) # pyright: ignore
|
|
278
284
|
content = await asyncio.to_thread(bucket.objects.all)
|
|
279
285
|
if delete_content:
|
|
280
286
|
await asyncio.to_thread(content.delete)
|
|
@@ -289,7 +295,7 @@ class S3ObjectStorage(ObjectStorageProtocol):
|
|
|
289
295
|
await self._assert_bucket_exists(bucket_id)
|
|
290
296
|
|
|
291
297
|
try:
|
|
292
|
-
bucket = self._resource.Bucket(bucket_id)
|
|
298
|
+
bucket = self._resource.Bucket(bucket_id) # pyright: ignore
|
|
293
299
|
content = await asyncio.to_thread(bucket.objects.all)
|
|
294
300
|
return [object_summary.key for object_summary in content]
|
|
295
301
|
except botocore.exceptions.ClientError as error:
|
|
@@ -306,17 +312,18 @@ class S3ObjectStorage(ObjectStorageProtocol):
|
|
|
306
312
|
Return `True` if checks succeed and `False` otherwise.
|
|
307
313
|
"""
|
|
308
314
|
if object_md5sum is not None:
|
|
309
|
-
raise NotImplementedError("
|
|
315
|
+
raise NotImplementedError("MD5 checking is not yet implemented.")
|
|
310
316
|
|
|
311
317
|
try:
|
|
312
|
-
|
|
313
|
-
self._client.head_object,
|
|
314
|
-
Bucket=bucket_id,
|
|
315
|
-
Key=object_id,
|
|
318
|
+
await asyncio.to_thread(
|
|
319
|
+
self._client.head_object, Bucket=bucket_id, Key=object_id
|
|
316
320
|
)
|
|
317
|
-
except botocore.exceptions.ClientError:
|
|
318
|
-
|
|
319
|
-
|
|
321
|
+
except botocore.exceptions.ClientError as error:
|
|
322
|
+
if error.response["Error"]["Code"] == "404":
|
|
323
|
+
return False
|
|
324
|
+
raise self._translate_s3_client_errors(
|
|
325
|
+
error, bucket_id=bucket_id, object_id=object_id
|
|
326
|
+
) from error
|
|
320
327
|
return True
|
|
321
328
|
|
|
322
329
|
async def _assert_object_exists(self, *, bucket_id: str, object_id: str) -> None:
|
|
@@ -451,7 +458,7 @@ class S3ObjectStorage(ObjectStorageProtocol):
|
|
|
451
458
|
)
|
|
452
459
|
|
|
453
460
|
async def _init_multipart_upload(self, *, bucket_id: str, object_id: str) -> str:
|
|
454
|
-
"""Initiates a
|
|
461
|
+
"""Initiates a multipart upload procedure. Returns the upload ID."""
|
|
455
462
|
await self._assert_no_multipart_upload(bucket_id=bucket_id, object_id=object_id)
|
|
456
463
|
|
|
457
464
|
try:
|
|
@@ -473,6 +480,7 @@ class S3ObjectStorage(ObjectStorageProtocol):
|
|
|
473
480
|
object_id: str,
|
|
474
481
|
part_number: int,
|
|
475
482
|
expires_after: int = 3600,
|
|
483
|
+
part_md5: Optional[str] = None,
|
|
476
484
|
) -> str:
|
|
477
485
|
"""Given a id of an instantiated multipart upload along with the corresponding
|
|
478
486
|
bucket and object ID, it returns a presigned URL for uploading a file part with the
|
|
@@ -491,16 +499,20 @@ class S3ObjectStorage(ObjectStorageProtocol):
|
|
|
491
499
|
upload_id=upload_id, bucket_id=bucket_id, object_id=object_id
|
|
492
500
|
)
|
|
493
501
|
|
|
502
|
+
params = {
|
|
503
|
+
"Bucket": bucket_id,
|
|
504
|
+
"Key": object_id,
|
|
505
|
+
"UploadId": upload_id,
|
|
506
|
+
"PartNumber": part_number,
|
|
507
|
+
}
|
|
508
|
+
# add additional parameters if any were passed
|
|
509
|
+
if part_md5:
|
|
510
|
+
params["ContentMD5"] = part_md5
|
|
494
511
|
try:
|
|
495
512
|
return await asyncio.to_thread(
|
|
496
513
|
self._client.generate_presigned_url,
|
|
497
514
|
ClientMethod="upload_part",
|
|
498
|
-
Params=
|
|
499
|
-
"Bucket": bucket_id,
|
|
500
|
-
"Key": object_id,
|
|
501
|
-
"UploadId": upload_id,
|
|
502
|
-
"PartNumber": part_number,
|
|
503
|
-
},
|
|
515
|
+
Params=params,
|
|
504
516
|
ExpiresIn=expires_after,
|
|
505
517
|
)
|
|
506
518
|
except botocore.exceptions.ClientError as error:
|
|
@@ -757,6 +769,22 @@ class S3ObjectStorage(ObjectStorageProtocol):
|
|
|
757
769
|
|
|
758
770
|
return presigned_url
|
|
759
771
|
|
|
772
|
+
async def _get_object_etag(self, *, bucket_id: str, object_id: str) -> str:
|
|
773
|
+
"""Return the etag of an object."""
|
|
774
|
+
await self._assert_object_exists(bucket_id=bucket_id, object_id=object_id)
|
|
775
|
+
|
|
776
|
+
object_metadata = await self._get_object_metadata(
|
|
777
|
+
bucket_id=bucket_id, object_id=object_id
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
if "ETag" not in object_metadata:
|
|
781
|
+
raise self.ObjectError(
|
|
782
|
+
f"Could not get the etag of the object with ID '{object_id}' in"
|
|
783
|
+
+ f" bucket '{bucket_id}'."
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
return object_metadata["ETag"]
|
|
787
|
+
|
|
760
788
|
async def _get_object_size(self, *, bucket_id: str, object_id: str) -> int:
|
|
761
789
|
"""Returns the size of an object in bytes."""
|
|
762
790
|
await self._assert_object_exists(bucket_id=bucket_id, object_id=object_id)
|
|
@@ -795,9 +823,15 @@ class S3ObjectStorage(ObjectStorageProtocol):
|
|
|
795
823
|
source_object_id: str,
|
|
796
824
|
dest_bucket_id: str,
|
|
797
825
|
dest_object_id: str,
|
|
826
|
+
abort_failed: bool = True,
|
|
798
827
|
) -> None:
|
|
799
828
|
"""Copy an object from one bucket (`source_bucket_id` and `source_object_id`) to
|
|
800
829
|
another bucket (`dest_bucket_id` and `dest_object_id`).
|
|
830
|
+
|
|
831
|
+
If `abort_failed` is set to true (default), a failed copy operation tries to
|
|
832
|
+
abort the ongoing multipart upload it created (if using multipart mode).
|
|
833
|
+
This only works reliably as long as there are no other ongoing multipart operations for
|
|
834
|
+
the same destination bucket and object ID, in which case this should be set to false.
|
|
801
835
|
"""
|
|
802
836
|
file_size = await self._get_object_size(
|
|
803
837
|
bucket_id=source_bucket_id, object_id=source_object_id
|
|
@@ -825,6 +859,22 @@ class S3ObjectStorage(ObjectStorageProtocol):
|
|
|
825
859
|
Config=transfer_config,
|
|
826
860
|
)
|
|
827
861
|
except botocore.exceptions.ClientError as error:
|
|
862
|
+
if abort_failed:
|
|
863
|
+
# try to find and abort the multipart operation, if multipart copy mode is used
|
|
864
|
+
# There should only be one ongoing multipart upload/copy at at a time as long
|
|
865
|
+
# as a new object ID is generated for each attempt
|
|
866
|
+
try:
|
|
867
|
+
upload_ids = await self._list_multipart_upload_for_object(
|
|
868
|
+
bucket_id=dest_bucket_id, object_id=dest_object_id
|
|
869
|
+
)
|
|
870
|
+
if len(upload_ids) == 1:
|
|
871
|
+
await self._abort_multipart_upload(
|
|
872
|
+
upload_id=upload_ids[0],
|
|
873
|
+
bucket_id=dest_bucket_id,
|
|
874
|
+
object_id=dest_object_id,
|
|
875
|
+
)
|
|
876
|
+
except botocore.exceptions.ClientError:
|
|
877
|
+
pass
|
|
828
878
|
raise self._translate_s3_client_errors(error) from error
|
|
829
879
|
|
|
830
880
|
async def _delete_object(self, *, bucket_id: str, object_id: str) -> None:
|
|
@@ -24,7 +24,7 @@ from collections.abc import AsyncGenerator, Generator
|
|
|
24
24
|
from contextlib import contextmanager
|
|
25
25
|
from pathlib import Path
|
|
26
26
|
from tempfile import NamedTemporaryFile
|
|
27
|
-
from typing import
|
|
27
|
+
from typing import NamedTuple, Optional, Union
|
|
28
28
|
|
|
29
29
|
try:
|
|
30
30
|
from typing import Self
|
|
@@ -64,7 +64,7 @@ __all__ = [
|
|
|
64
64
|
]
|
|
65
65
|
|
|
66
66
|
|
|
67
|
-
LOCALSTACK_IMAGE = "localstack/localstack:3.
|
|
67
|
+
LOCALSTACK_IMAGE = "localstack/localstack:3.8.1"
|
|
68
68
|
|
|
69
69
|
TEST_FILE_DIR = Path(__file__).parent.parent.resolve() / "test_files"
|
|
70
70
|
|
|
@@ -223,19 +223,23 @@ class S3ContainerFixture(LocalStackContainer):
|
|
|
223
223
|
|
|
224
224
|
def __init__(
|
|
225
225
|
self,
|
|
226
|
-
|
|
226
|
+
image: str = LOCALSTACK_IMAGE,
|
|
227
|
+
edge_port: int = 4566,
|
|
227
228
|
region_name: Optional[str] = None,
|
|
228
|
-
**kwargs
|
|
229
|
+
**kwargs,
|
|
229
230
|
) -> None:
|
|
230
231
|
"""Initialize the container."""
|
|
231
|
-
super().__init__(
|
|
232
|
+
super().__init__(
|
|
233
|
+
image=image, edge_port=edge_port, region_name=region_name, **kwargs
|
|
234
|
+
)
|
|
232
235
|
|
|
233
236
|
def __enter__(self) -> Self:
|
|
234
237
|
"""Enter the container context."""
|
|
235
238
|
super().__enter__()
|
|
236
|
-
|
|
239
|
+
url = self.get_url()
|
|
240
|
+
url = url.replace("localhost", "127.0.0.1")
|
|
237
241
|
s3_config = S3Config( # type: ignore [call-arg]
|
|
238
|
-
s3_endpoint_url=
|
|
242
|
+
s3_endpoint_url=url,
|
|
239
243
|
s3_access_key_id="test",
|
|
240
244
|
s3_secret_access_key=SecretStr("test"),
|
|
241
245
|
)
|
|
@@ -375,8 +379,8 @@ class FederatedS3Fixture:
|
|
|
375
379
|
|
|
376
380
|
# Add the dummy items
|
|
377
381
|
for bucket, objects in contents.items():
|
|
378
|
-
for
|
|
379
|
-
with temp_file_object(bucket,
|
|
382
|
+
for obj in objects:
|
|
383
|
+
with temp_file_object(bucket, obj, 1) as file:
|
|
380
384
|
await storage.populate_file_objects([file])
|
|
381
385
|
|
|
382
386
|
|
|
@@ -418,10 +422,7 @@ def _s3_multi_container_fixture(
|
|
|
418
422
|
|
|
419
423
|
if not storage_aliases:
|
|
420
424
|
raise RuntimeError("The 'storage_aliases' list must not be empty.")
|
|
421
|
-
s3_containers = {
|
|
422
|
-
alias: S3ContainerFixture(name=f"{alias}_s3_container")
|
|
423
|
-
for alias in storage_aliases
|
|
424
|
-
}
|
|
425
|
+
s3_containers = {alias: S3ContainerFixture() for alias in storage_aliases}
|
|
425
426
|
with S3MultiContainerFixture(s3_containers) as s3_multi_container:
|
|
426
427
|
yield s3_multi_container
|
|
427
428
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|