hexkit 3.6.0__tar.gz → 3.8.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.
Files changed (56) hide show
  1. {hexkit-3.6.0/src/hexkit.egg-info → hexkit-3.8.0}/PKG-INFO +1 -1
  2. {hexkit-3.6.0 → hexkit-3.8.0}/pyproject.toml +1 -1
  3. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/protocols/objstorage.py +31 -0
  4. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/akafka/testcontainer.py +1 -1
  5. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/s3/provider.py +83 -25
  6. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/s3/testutils/_fixtures.py +14 -13
  7. {hexkit-3.6.0 → hexkit-3.8.0/src/hexkit.egg-info}/PKG-INFO +1 -1
  8. {hexkit-3.6.0 → hexkit-3.8.0}/LICENSE +0 -0
  9. {hexkit-3.6.0 → hexkit-3.8.0}/MANIFEST.in +0 -0
  10. {hexkit-3.6.0 → hexkit-3.8.0}/README.md +0 -0
  11. {hexkit-3.6.0 → hexkit-3.8.0}/setup.cfg +0 -0
  12. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/__init__.py +0 -0
  13. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/__main__.py +0 -0
  14. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/base.py +0 -0
  15. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/config.py +0 -0
  16. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/correlation.py +0 -0
  17. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/custom_types.py +0 -0
  18. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/log.py +0 -0
  19. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/protocols/__init__.py +0 -0
  20. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/protocols/dao.py +0 -0
  21. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/protocols/daopub.py +0 -0
  22. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/protocols/daosub.py +0 -0
  23. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/protocols/eventpub.py +0 -0
  24. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/protocols/eventsub.py +0 -0
  25. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/__init__.py +0 -0
  26. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/akafka/__init__.py +0 -0
  27. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/akafka/config.py +0 -0
  28. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/akafka/provider/__init__.py +0 -0
  29. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/akafka/provider/daosub.py +0 -0
  30. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/akafka/provider/eventpub.py +0 -0
  31. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/akafka/provider/eventsub.py +0 -0
  32. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/akafka/provider/utils.py +0 -0
  33. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/akafka/testutils.py +0 -0
  34. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/mongodb/__init__.py +0 -0
  35. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/mongodb/provider.py +0 -0
  36. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/mongodb/testutils.py +0 -0
  37. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/mongokafka/__init__.py +0 -0
  38. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/mongokafka/provider.py +0 -0
  39. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/mongokafka/testutils.py +0 -0
  40. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/s3/__init__.py +0 -0
  41. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
  42. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
  43. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
  44. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
  45. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
  46. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/s3/testutils/__init__.py +0 -0
  47. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/s3/testutils/_typical_workflow.py +0 -0
  48. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/s3/testutils/_utils.py +0 -0
  49. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/testing/__init__.py +0 -0
  50. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/providers/testing/eventpub.py +0 -0
  51. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/py.typed +0 -0
  52. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit/utils.py +0 -0
  53. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit.egg-info/SOURCES.txt +0 -0
  54. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit.egg-info/dependency_links.txt +0 -0
  55. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit.egg-info/requires.txt +0 -0
  56. {hexkit-3.6.0 → hexkit-3.8.0}/src/hexkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hexkit
3
- Version: 3.6.0
3
+ Version: 3.8.0
4
4
  Summary: A Toolkit for Building Microservices using the Hexagonal Architecture
5
5
  Author-email: "German Human Genome Phenome Archive (GHGA)" <contact@ghga.de>
6
6
  License: Apache 2.0
@@ -23,7 +23,7 @@ classifiers = [
23
23
  "Intended Audience :: Developers",
24
24
  ]
25
25
  name = "hexkit"
26
- version = "3.6.0"
26
+ version = "3.8.0"
27
27
  description = "A Toolkit for Building Microservices using the Hexagonal Architecture"
28
28
  dependencies = [
29
29
  "pydantic >=2, <3",
@@ -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() # type: ignore
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)
@@ -22,6 +22,7 @@ Utilities for testing are located in `./testutils.py`.
22
22
  # ruff: noqa: PLR0913
23
23
 
24
24
  import asyncio
25
+ import re
25
26
  from functools import lru_cache
26
27
  from pathlib import Path
27
28
  from typing import Any, Optional
@@ -31,6 +32,7 @@ import botocore.client
31
32
  import botocore.config
32
33
  import botocore.configloader
33
34
  import botocore.exceptions
35
+ import botocore.handlers
34
36
  from boto3.s3.transfer import TransferConfig
35
37
  from pydantic import Field, SecretStr
36
38
  from pydantic_settings import BaseSettings
@@ -39,6 +41,10 @@ from hexkit.protocols.objstorage import ObjectStorageProtocol, PresignedPostURL
39
41
  from hexkit.utils import calc_part_size
40
42
 
41
43
  __all__ = ["ObjectStorageProtocol", "PresignedPostURL"]
44
+ # Allow colon character in bucket names to accommodate Ceph multi tenancy S3
45
+ botocore.handlers.VALID_BUCKET = re.compile(
46
+ r"^(?:[a-zA-Z0-9_]{1,191}:)?[a-z0-9\-]{3,63}$"
47
+ )
42
48
 
43
49
 
44
50
  class S3Config(BaseSettings):
@@ -116,6 +122,8 @@ def read_aws_config_ini(aws_config_ini: Path) -> botocore.config.Config:
116
122
  class S3ObjectStorage(ObjectStorageProtocol):
117
123
  """S3-based provider implementing the ObjectStorageProtocol."""
118
124
 
125
+ _re_bucket_id = botocore.handlers.VALID_BUCKET
126
+
119
127
  def __init__(
120
128
  self,
121
129
  *,
@@ -223,27 +231,33 @@ class S3ObjectStorage(ObjectStorageProtocol):
223
231
 
224
232
  async def _does_bucket_exist(self, bucket_id: str) -> bool:
225
233
  """Check whether a bucket with the specified ID (`bucket_id`) exists.
226
- Return `True` if it exists and `False` otherwise.
234
+
235
+ Returns `True` if it exists and `False` otherwise.
236
+
237
+ Note that this method does not need to have the permission to list buckets.
227
238
  """
228
239
  try:
229
- bucket_list = await asyncio.to_thread(self._client.list_buckets)
240
+ await asyncio.to_thread(self._client.head_bucket, Bucket=bucket_id)
230
241
  except botocore.exceptions.ClientError as error:
242
+ if error.response["Error"]["Code"] == "404":
243
+ return False
231
244
  raise self._translate_s3_client_errors(
232
245
  error, bucket_id=bucket_id
233
246
  ) from error
234
-
235
- return any(bucket["Name"] == bucket_id for bucket in bucket_list["Buckets"])
247
+ return True
236
248
 
237
249
  async def _assert_bucket_exists(self, bucket_id: str) -> None:
238
- """Checks if the bucket with specified ID (`bucket_id`) exists and throws an
239
- BucketNotFoundError otherwise.
250
+ """Assert that the bucket with the specified ID (`bucket_id`) exists.
251
+
252
+ If it does not exist, a BucketNotFoundError is raised.
240
253
  """
241
254
  if not await self.does_bucket_exist(bucket_id):
242
255
  raise self.BucketNotFoundError(bucket_id=bucket_id)
243
256
 
244
257
  async def _assert_bucket_not_exists(self, bucket_id: str) -> None:
245
- """Checks if the bucket with specified ID (`bucket_id`) exists. If so, it throws
246
- an BucketAlreadyExistsError.
258
+ """Assert that the bucket with the specified ID (`bucket_id`) does not exist.
259
+
260
+ If it exists, a BucketAlreadyExistsError is raised.
247
261
  """
248
262
  if await self.does_bucket_exist(bucket_id):
249
263
  raise self.BucketAlreadyExistsError(bucket_id=bucket_id)
@@ -274,7 +288,7 @@ class S3ObjectStorage(ObjectStorageProtocol):
274
288
  await self._assert_bucket_exists(bucket_id)
275
289
 
276
290
  try:
277
- bucket = self._resource.Bucket(bucket_id)
291
+ bucket = self._resource.Bucket(bucket_id) # pyright: ignore
278
292
  content = await asyncio.to_thread(bucket.objects.all)
279
293
  if delete_content:
280
294
  await asyncio.to_thread(content.delete)
@@ -289,7 +303,7 @@ class S3ObjectStorage(ObjectStorageProtocol):
289
303
  await self._assert_bucket_exists(bucket_id)
290
304
 
291
305
  try:
292
- bucket = self._resource.Bucket(bucket_id)
306
+ bucket = self._resource.Bucket(bucket_id) # pyright: ignore
293
307
  content = await asyncio.to_thread(bucket.objects.all)
294
308
  return [object_summary.key for object_summary in content]
295
309
  except botocore.exceptions.ClientError as error:
@@ -306,17 +320,18 @@ class S3ObjectStorage(ObjectStorageProtocol):
306
320
  Return `True` if checks succeed and `False` otherwise.
307
321
  """
308
322
  if object_md5sum is not None:
309
- raise NotImplementedError("Md5 checking is not yet implemented.")
323
+ raise NotImplementedError("MD5 checking is not yet implemented.")
310
324
 
311
325
  try:
312
- _ = await asyncio.to_thread(
313
- self._client.head_object,
314
- Bucket=bucket_id,
315
- Key=object_id,
326
+ await asyncio.to_thread(
327
+ self._client.head_object, Bucket=bucket_id, Key=object_id
316
328
  )
317
- except botocore.exceptions.ClientError:
318
- return False
319
-
329
+ except botocore.exceptions.ClientError as error:
330
+ if error.response["Error"]["Code"] == "404":
331
+ return False
332
+ raise self._translate_s3_client_errors(
333
+ error, bucket_id=bucket_id, object_id=object_id
334
+ ) from error
320
335
  return True
321
336
 
322
337
  async def _assert_object_exists(self, *, bucket_id: str, object_id: str) -> None:
@@ -451,7 +466,7 @@ class S3ObjectStorage(ObjectStorageProtocol):
451
466
  )
452
467
 
453
468
  async def _init_multipart_upload(self, *, bucket_id: str, object_id: str) -> str:
454
- """Initiates a mulipart upload procedure. Returns the upload ID."""
469
+ """Initiates a multipart upload procedure. Returns the upload ID."""
455
470
  await self._assert_no_multipart_upload(bucket_id=bucket_id, object_id=object_id)
456
471
 
457
472
  try:
@@ -473,6 +488,7 @@ class S3ObjectStorage(ObjectStorageProtocol):
473
488
  object_id: str,
474
489
  part_number: int,
475
490
  expires_after: int = 3600,
491
+ part_md5: Optional[str] = None,
476
492
  ) -> str:
477
493
  """Given a id of an instantiated multipart upload along with the corresponding
478
494
  bucket and object ID, it returns a presigned URL for uploading a file part with the
@@ -491,16 +507,20 @@ class S3ObjectStorage(ObjectStorageProtocol):
491
507
  upload_id=upload_id, bucket_id=bucket_id, object_id=object_id
492
508
  )
493
509
 
510
+ params = {
511
+ "Bucket": bucket_id,
512
+ "Key": object_id,
513
+ "UploadId": upload_id,
514
+ "PartNumber": part_number,
515
+ }
516
+ # add additional parameters if any were passed
517
+ if part_md5:
518
+ params["ContentMD5"] = part_md5
494
519
  try:
495
520
  return await asyncio.to_thread(
496
521
  self._client.generate_presigned_url,
497
522
  ClientMethod="upload_part",
498
- Params={
499
- "Bucket": bucket_id,
500
- "Key": object_id,
501
- "UploadId": upload_id,
502
- "PartNumber": part_number,
503
- },
523
+ Params=params,
504
524
  ExpiresIn=expires_after,
505
525
  )
506
526
  except botocore.exceptions.ClientError as error:
@@ -757,6 +777,22 @@ class S3ObjectStorage(ObjectStorageProtocol):
757
777
 
758
778
  return presigned_url
759
779
 
780
+ async def _get_object_etag(self, *, bucket_id: str, object_id: str) -> str:
781
+ """Return the etag of an object."""
782
+ await self._assert_object_exists(bucket_id=bucket_id, object_id=object_id)
783
+
784
+ object_metadata = await self._get_object_metadata(
785
+ bucket_id=bucket_id, object_id=object_id
786
+ )
787
+
788
+ if "ETag" not in object_metadata:
789
+ raise self.ObjectError(
790
+ f"Could not get the etag of the object with ID '{object_id}' in"
791
+ + f" bucket '{bucket_id}'."
792
+ )
793
+
794
+ return object_metadata["ETag"]
795
+
760
796
  async def _get_object_size(self, *, bucket_id: str, object_id: str) -> int:
761
797
  """Returns the size of an object in bytes."""
762
798
  await self._assert_object_exists(bucket_id=bucket_id, object_id=object_id)
@@ -795,9 +831,15 @@ class S3ObjectStorage(ObjectStorageProtocol):
795
831
  source_object_id: str,
796
832
  dest_bucket_id: str,
797
833
  dest_object_id: str,
834
+ abort_failed: bool = True,
798
835
  ) -> None:
799
836
  """Copy an object from one bucket (`source_bucket_id` and `source_object_id`) to
800
837
  another bucket (`dest_bucket_id` and `dest_object_id`).
838
+
839
+ If `abort_failed` is set to true (default), a failed copy operation tries to
840
+ abort the ongoing multipart upload it created (if using multipart mode).
841
+ This only works reliably as long as there are no other ongoing multipart operations for
842
+ the same destination bucket and object ID, in which case this should be set to false.
801
843
  """
802
844
  file_size = await self._get_object_size(
803
845
  bucket_id=source_bucket_id, object_id=source_object_id
@@ -825,6 +867,22 @@ class S3ObjectStorage(ObjectStorageProtocol):
825
867
  Config=transfer_config,
826
868
  )
827
869
  except botocore.exceptions.ClientError as error:
870
+ if abort_failed:
871
+ # try to find and abort the multipart operation, if multipart copy mode is used
872
+ # There should only be one ongoing multipart upload/copy at at a time as long
873
+ # as a new object ID is generated for each attempt
874
+ try:
875
+ upload_ids = await self._list_multipart_upload_for_object(
876
+ bucket_id=dest_bucket_id, object_id=dest_object_id
877
+ )
878
+ if len(upload_ids) == 1:
879
+ await self._abort_multipart_upload(
880
+ upload_id=upload_ids[0],
881
+ bucket_id=dest_bucket_id,
882
+ object_id=dest_object_id,
883
+ )
884
+ except botocore.exceptions.ClientError:
885
+ pass
828
886
  raise self._translate_s3_client_errors(error) from error
829
887
 
830
888
  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 Any, NamedTuple, Optional, Union
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.5.0"
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
- port: int = 4566,
226
+ image: str = LOCALSTACK_IMAGE,
227
+ edge_port: int = 4566,
227
228
  region_name: Optional[str] = None,
228
- **kwargs: Any,
229
+ **kwargs,
229
230
  ) -> None:
230
231
  """Initialize the container."""
231
- super().__init__(image=LOCALSTACK_IMAGE)
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
- s3_endpoint_url = self.get_url()
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=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 object in objects:
379
- with temp_file_object(bucket, object, 1) as file:
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hexkit
3
- Version: 3.6.0
3
+ Version: 3.8.0
4
4
  Summary: A Toolkit for Building Microservices using the Hexagonal Architecture
5
5
  Author-email: "German Human Genome Phenome Archive (GHGA)" <contact@ghga.de>
6
6
  License: Apache 2.0
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