hexkit 3.5.0__tar.gz → 3.6.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.5.0/src/hexkit.egg-info → hexkit-3.6.0}/PKG-INFO +1 -1
  2. {hexkit-3.5.0 → hexkit-3.6.0}/pyproject.toml +1 -1
  3. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/config.py +1 -1
  4. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/protocols/dao.py +14 -0
  5. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/mongodb/provider.py +12 -0
  6. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/mongokafka/provider.py +16 -2
  7. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/s3/testutils/__init__.py +18 -0
  8. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/s3/testutils/_fixtures.py +148 -0
  9. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/s3/testutils/_utils.py +2 -2
  10. {hexkit-3.5.0 → hexkit-3.6.0/src/hexkit.egg-info}/PKG-INFO +1 -1
  11. {hexkit-3.5.0 → hexkit-3.6.0}/LICENSE +0 -0
  12. {hexkit-3.5.0 → hexkit-3.6.0}/MANIFEST.in +0 -0
  13. {hexkit-3.5.0 → hexkit-3.6.0}/README.md +0 -0
  14. {hexkit-3.5.0 → hexkit-3.6.0}/setup.cfg +0 -0
  15. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/__init__.py +0 -0
  16. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/__main__.py +0 -0
  17. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/base.py +0 -0
  18. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/correlation.py +0 -0
  19. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/custom_types.py +0 -0
  20. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/log.py +0 -0
  21. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/protocols/__init__.py +0 -0
  22. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/protocols/daopub.py +0 -0
  23. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/protocols/daosub.py +0 -0
  24. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/protocols/eventpub.py +0 -0
  25. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/protocols/eventsub.py +0 -0
  26. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/protocols/objstorage.py +0 -0
  27. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/__init__.py +0 -0
  28. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/akafka/__init__.py +0 -0
  29. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/akafka/config.py +0 -0
  30. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/akafka/provider/__init__.py +0 -0
  31. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/akafka/provider/daosub.py +0 -0
  32. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/akafka/provider/eventpub.py +0 -0
  33. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/akafka/provider/eventsub.py +0 -0
  34. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/akafka/provider/utils.py +0 -0
  35. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/akafka/testcontainer.py +0 -0
  36. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/akafka/testutils.py +0 -0
  37. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/mongodb/__init__.py +0 -0
  38. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/mongodb/testutils.py +0 -0
  39. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/mongokafka/__init__.py +0 -0
  40. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/mongokafka/testutils.py +0 -0
  41. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/s3/__init__.py +0 -0
  42. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/s3/provider.py +0 -0
  43. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
  44. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
  45. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
  46. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
  47. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
  48. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/s3/testutils/_typical_workflow.py +0 -0
  49. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/testing/__init__.py +0 -0
  50. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/providers/testing/eventpub.py +0 -0
  51. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/py.typed +0 -0
  52. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit/utils.py +0 -0
  53. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit.egg-info/SOURCES.txt +0 -0
  54. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit.egg-info/dependency_links.txt +0 -0
  55. {hexkit-3.5.0 → hexkit-3.6.0}/src/hexkit.egg-info/requires.txt +0 -0
  56. {hexkit-3.5.0 → hexkit-3.6.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.5.0
3
+ Version: 3.6.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.5.0"
26
+ version = "3.6.0"
27
27
  description = "A Toolkit for Building Microservices using the Hexagonal Architecture"
28
28
  dependencies = [
29
29
  "pydantic >=2, <3",
@@ -143,7 +143,7 @@ def config_from_yaml(
143
143
  model_config = SettingsConfigDict(frozen=True, env_prefix=f"{prefix}_")
144
144
 
145
145
  @classmethod
146
- def settings_customise_sources( # noqa: PLR0913
146
+ def settings_customise_sources(
147
147
  cls,
148
148
  settings_cls: type[BaseSettings],
149
149
  init_settings: PydanticBaseSettingsSource,
@@ -21,6 +21,7 @@ with the database.
21
21
  # ruff: noqa: PLR0913
22
22
 
23
23
  import typing
24
+ import warnings
24
25
  from abc import ABC, abstractmethod
25
26
  from collections.abc import AsyncGenerator, AsyncIterator, Collection, Mapping
26
27
  from contextlib import AbstractAsyncContextManager
@@ -29,6 +30,7 @@ from typing import Any, Optional, TypeVar, Union, overload
29
30
  from uuid import uuid4
30
31
 
31
32
  from pydantic import BaseModel
33
+ from typing_extensions import deprecated
32
34
 
33
35
  from hexkit.utils import FieldNotInModelError, validate_fields_in_model
34
36
 
@@ -259,6 +261,9 @@ class DaoNaturalId(DaoCommons[Dto], typing.Protocol[Dto]):
259
261
  ...
260
262
 
261
263
 
264
+ @deprecated(
265
+ "uuid4_id_generator is deprecated as of v3.6 and will be removed in hexkit v4."
266
+ )
262
267
  async def uuid4_id_generator() -> AsyncGenerator[str, None]:
263
268
  """Generates a new ID using the UUID4 algorithm.
264
269
  This is an AsyncGenerator to be compliant with the id_generator requirements of the
@@ -441,6 +446,15 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
441
446
  # instantiate the default ID generator:
442
447
  id_generator = uuid4_id_generator()
443
448
 
449
+ if dto_creation_model is not None:
450
+ dep_msg = (
451
+ "The DAO API (and existing providers) are deprecated as of v3.6."
452
+ + " They will be simplified in hexkit v4."
453
+ + " Please plan to provide IDs during resource creation. The same model"
454
+ + " should be used for both creation and retrieval."
455
+ )
456
+ warnings.warn(dep_msg, category=DeprecationWarning, stacklevel=2)
457
+
444
458
  return await self._get_dao(
445
459
  name=name,
446
460
  dto_model=dto_model,
@@ -22,6 +22,7 @@ Utilities for testing are located in `./testutils.py`.
22
22
  # ruff: noqa: PLR0913
23
23
 
24
24
  import json
25
+ import warnings
25
26
  from abc import ABC
26
27
  from collections.abc import AsyncGenerator, AsyncIterator, Collection, Mapping
27
28
  from contextlib import AbstractAsyncContextManager
@@ -163,6 +164,17 @@ class MongoDbDaoBase(ABC, Generic[Dto]):
163
164
  self._document_to_dto = document_to_dto
164
165
  self._dto_to_document = dto_to_document
165
166
 
167
+ warnings.warn(
168
+ "The MongoDbDaoBase class and subclasses are deprecated as of v3.6 and will"
169
+ + " be replaced in hexkit v4 with the new MongoDbDao class. To replace the"
170
+ + " MongoDbDaoNaturalId: just use the new class. To replace the"
171
+ + " MongoDbDaoSurrogateId: use the new class, remove the creation model and"
172
+ + " id_generator parameters, and make sure the supplied DTO model includes"
173
+ + " the ID field with a default factory for generating new IDs.",
174
+ category=DeprecationWarning,
175
+ stacklevel=2,
176
+ )
177
+
166
178
  async def get_by_id(self, id_: str) -> Dto:
167
179
  """Get a resource by providing its ID.
168
180
 
@@ -255,7 +255,14 @@ class MongoKafkaDaoPublisher(Generic[Dto]):
255
255
  document = self._dao._dto_to_document(dto)
256
256
  document.setdefault("__metadata__", {})["correlation_id"] = correlation_id
257
257
  result = await self._collection.replace_one(
258
- {"_id": document["_id"], "__metadata__.deleted": False}, document
258
+ {
259
+ "_id": document["_id"],
260
+ "$or": [
261
+ {"__metadata__": {"$exists": False}},
262
+ {"__metadata__.deleted": False},
263
+ ],
264
+ },
265
+ document,
259
266
  )
260
267
  if result.matched_count == 0:
261
268
  raise ResourceNotFoundError(id_=document["_id"])
@@ -282,7 +289,14 @@ class MongoKafkaDaoPublisher(Generic[Dto]):
282
289
  },
283
290
  }
284
291
  result = await self._collection.replace_one(
285
- {"_id": document["_id"], "__metadata__.deleted": False}, document
292
+ {
293
+ "_id": document["_id"],
294
+ "$or": [
295
+ {"__metadata__": {"$exists": False}},
296
+ {"__metadata__.deleted": False},
297
+ ],
298
+ },
299
+ document,
286
300
  )
287
301
  if result.matched_count == 0:
288
302
  raise ResourceNotFoundError(id_=id_)
@@ -20,18 +20,27 @@ from ._fixtures import (
20
20
  MEBIBYTE,
21
21
  TEST_FILE_DIR,
22
22
  TEST_FILE_PATHS,
23
+ FederatedS3Fixture,
23
24
  FileObject,
24
25
  S3ContainerFixture,
25
26
  S3Fixture,
27
+ S3MultiContainerFixture,
26
28
  calc_md5,
29
+ clean_federated_s3_fixture,
27
30
  clean_s3_fixture,
31
+ federated_s3_fixture,
32
+ get_clean_federated_s3_fixture,
28
33
  get_clean_s3_fixture,
34
+ get_persistent_federated_s3_fixture,
29
35
  get_persistent_s3_fixture,
30
36
  get_s3_container_fixture,
37
+ get_s3_multi_container_fixture,
38
+ persistent_federated_s3_fixture,
31
39
  persistent_s3_fixture,
32
40
  populate_storage,
33
41
  s3_container_fixture,
34
42
  s3_fixture,
43
+ s3_multi_container_fixture,
35
44
  temp_file_object,
36
45
  tmp_file,
37
46
  upload_file,
@@ -73,4 +82,13 @@ __all__ = [
73
82
  "upload_part",
74
83
  "upload_part_of_size",
75
84
  "upload_part_via_url",
85
+ "FederatedS3Fixture",
86
+ "S3MultiContainerFixture",
87
+ "clean_federated_s3_fixture",
88
+ "federated_s3_fixture",
89
+ "get_clean_federated_s3_fixture",
90
+ "get_s3_multi_container_fixture",
91
+ "get_persistent_federated_s3_fixture",
92
+ "s3_multi_container_fixture",
93
+ "persistent_federated_s3_fixture",
76
94
  ]
@@ -344,3 +344,151 @@ def tmp_file() -> Generator[FileObject, None, None]:
344
344
  """A fixture that provides a temporary file."""
345
345
  with temp_file_object() as temp_file:
346
346
  yield temp_file
347
+
348
+
349
+ class FederatedS3Fixture:
350
+ """Fixture containing multiple S3 fixtures to simulate federated storage."""
351
+
352
+ def __init__(self, storages: dict[str, S3Fixture]):
353
+ self.storages = storages
354
+
355
+ def get_configs_by_alias(self) -> dict[str, S3Config]:
356
+ """Get the S3Config instance for each object storage in the fixture."""
357
+ return {alias: self.storages[alias].config for alias in self.storages}
358
+
359
+ async def populate_dummy_items(self, alias: str, contents: dict[str, list[str]]):
360
+ """Convenience function to populate a specific S3Fixture.
361
+
362
+ Args:
363
+ - `alias`: The alias of the S3Fixture to populate.
364
+ - `contents`: A dictionary with bucket names as keys and lists of object names
365
+ as values. The buckets can be empty, and the objects are created with a size of
366
+ 1 byte.
367
+ """
368
+ if alias not in self.storages:
369
+ # This would indicate some kind of mismatch between config and fixture
370
+ raise RuntimeError(f"Alias '{alias}' not found in the federated S3 fixture")
371
+ storage = self.storages[alias]
372
+
373
+ # Populate the buckets so even empty buckets are established
374
+ await storage.populate_buckets([bucket for bucket in contents])
375
+
376
+ # Add the dummy items
377
+ for bucket, objects in contents.items():
378
+ for object in objects:
379
+ with temp_file_object(bucket, object, 1) as file:
380
+ await storage.populate_file_objects([file])
381
+
382
+
383
+ class S3MultiContainerFixture:
384
+ """Fixture for managing multiple running S3 test containers in order to mimic
385
+ multiple object storages.
386
+
387
+ Without this fixture, separate S3Fixture instances would access the same
388
+ underlying storage resources.
389
+ """
390
+
391
+ def __init__(self, s3_containers: dict[str, S3ContainerFixture]):
392
+ self.s3_containers = s3_containers
393
+
394
+ def __enter__(self):
395
+ """Enter the context manager and start the S3 containers."""
396
+ for container in self.s3_containers.values():
397
+ container.__enter__()
398
+ return self
399
+
400
+ def __exit__(self, exc_type, exc_val, exc_tb):
401
+ """Exit the context manager and clean up the S3 containers."""
402
+ for container in self.s3_containers.values():
403
+ container.__exit__(exc_type, exc_val, exc_tb)
404
+
405
+
406
+ def _s3_multi_container_fixture(
407
+ request,
408
+ ) -> Generator[S3MultiContainerFixture, None, None]:
409
+ """Fixture function for getting multiple running S3 test containers."""
410
+ try:
411
+ storage_aliases = request.getfixturevalue("storage_aliases")
412
+ except pytest.FixtureLookupError as err:
413
+ raise NotImplementedError(
414
+ "You must provide a 'storage_aliases' fixture in your test setup."
415
+ + " It must have the same scope as 's3_multi_container'"
416
+ + " and it must return a list of the storage aliases to be used."
417
+ ) from err
418
+
419
+ if not storage_aliases:
420
+ 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
+ with S3MultiContainerFixture(s3_containers) as s3_multi_container:
426
+ yield s3_multi_container
427
+
428
+
429
+ def get_s3_multi_container_fixture(
430
+ scope: PytestScope = "session", name: str = "s3_multi_container"
431
+ ):
432
+ """Get a fixture containing multiple LocalStack test containers.
433
+
434
+ By default, the session scope is used for LocalStack test containers.
435
+ Requires that a 'storage_aliases' fixture is provided in the test setup.
436
+ """
437
+ return pytest.fixture(_s3_multi_container_fixture, scope=scope, name=name)
438
+
439
+
440
+ s3_multi_container_fixture = get_s3_multi_container_fixture()
441
+
442
+
443
+ def _persistent_federated_s3_fixture(
444
+ s3_multi_container: S3MultiContainerFixture,
445
+ ) -> Generator[FederatedS3Fixture, None, None]:
446
+ """Fixture function that creates a persistent FederatedS3Fixture.
447
+
448
+ The state of each S3 storage in the fixture is not cleaned up.
449
+ """
450
+ s3_fixtures = {}
451
+ for alias, container in s3_multi_container.s3_containers.items():
452
+ config = container.s3_config
453
+ storage = S3ObjectStorage(config=config)
454
+ s3_fixtures[alias] = S3Fixture(config=config, storage=storage)
455
+ yield FederatedS3Fixture(s3_fixtures)
456
+
457
+
458
+ def get_persistent_federated_s3_fixture(
459
+ scope: PytestScope = "function", name: str = "federated_s3"
460
+ ):
461
+ """Get a federated S3 storage fixture with desired scope.
462
+
463
+ The state of the S3 storage is not cleaned up by the fixture.
464
+ """
465
+ return pytest.fixture(_persistent_federated_s3_fixture, scope=scope, name=name)
466
+
467
+
468
+ persistent_federated_s3_fixture = get_persistent_federated_s3_fixture()
469
+
470
+
471
+ async def _clean_federated_s3_fixture(
472
+ s3_multi_container: S3MultiContainerFixture,
473
+ ) -> AsyncGenerator[FederatedS3Fixture, None]:
474
+ """Fixture function that creates a clean FederatedS3Fixture instance.
475
+
476
+ The state of each S3 storage is cleaned up before yielding the fixture.
477
+ """
478
+ for federated_s3_fixture in _persistent_federated_s3_fixture(s3_multi_container):
479
+ for s3_fixture in federated_s3_fixture.storages.values():
480
+ await s3_fixture.delete_buckets()
481
+ yield federated_s3_fixture
482
+
483
+
484
+ def get_clean_federated_s3_fixture(
485
+ scope: PytestScope = "function", name: str = "federated_s3"
486
+ ):
487
+ """Get a federated S3 storage fixture with desired scope.
488
+
489
+ The state of the S3 storage is not cleaned up by the fixture.
490
+ """
491
+ return pytest_asyncio.fixture(_clean_federated_s3_fixture, scope=scope, name=name)
492
+
493
+
494
+ federated_s3_fixture = clean_federated_s3_fixture = get_clean_federated_s3_fixture()
@@ -44,7 +44,7 @@ class FileObject(BaseModel):
44
44
  bucket_id: str
45
45
  object_id: str
46
46
 
47
- @computed_field # type: ignore [misc]
47
+ @computed_field # type: ignore [prop-decorator]
48
48
  @property
49
49
  def content(self) -> bytes:
50
50
  """Extract the content from the file at the provided path"""
@@ -53,7 +53,7 @@ class FileObject(BaseModel):
53
53
  with open(self.file_path, "rb") as file:
54
54
  return file.read()
55
55
 
56
- @computed_field # type: ignore [misc]
56
+ @computed_field # type: ignore [prop-decorator]
57
57
  @property
58
58
  def md5(self) -> str:
59
59
  """Calculate the md5 hash of the content"""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hexkit
3
- Version: 3.5.0
3
+ Version: 3.6.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