hexkit 7.2.1__tar.gz → 8.0.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 (79) hide show
  1. {hexkit-7.2.1/src/hexkit.egg-info → hexkit-8.0.0}/PKG-INFO +25 -16
  2. {hexkit-7.2.1 → hexkit-8.0.0}/pyproject.toml +30 -18
  3. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/protocols/dao.py +43 -20
  4. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/protocols/daopub.py +9 -17
  5. hexkit-8.0.0/src/hexkit/protocols/kvstore.py +55 -0
  6. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/protocols/objstorage.py +48 -0
  7. hexkit-8.0.0/src/hexkit/providers/mongodb/__init__.py +43 -0
  8. hexkit-8.0.0/src/hexkit/providers/mongodb/config.py +55 -0
  9. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/mongodb/migrations/_manager.py +1 -1
  10. hexkit-8.0.0/src/hexkit/providers/mongodb/provider/__init__.py +54 -0
  11. hexkit-8.0.0/src/hexkit/providers/mongodb/provider/client.py +102 -0
  12. hexkit-7.2.1/src/hexkit/providers/mongodb/provider.py → hexkit-8.0.0/src/hexkit/providers/mongodb/provider/dao.py +92 -169
  13. hexkit-8.0.0/src/hexkit/providers/mongodb/provider/kvstore.py +230 -0
  14. hexkit-8.0.0/src/hexkit/providers/mongodb/provider/utils.py +66 -0
  15. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/mongodb/testutils.py +4 -7
  16. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/mongokafka/__init__.py +1 -1
  17. {hexkit-7.2.1/src/hexkit/providers/mongokafka/provider → hexkit-8.0.0/src/hexkit/providers/mongokafka}/config.py +1 -1
  18. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/mongokafka/provider/__init__.py +2 -3
  19. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/mongokafka/provider/daopub.py +11 -9
  20. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/mongokafka/provider/persistent_pub.py +6 -1
  21. hexkit-8.0.0/src/hexkit/providers/redis/__init__.py +35 -0
  22. hexkit-8.0.0/src/hexkit/providers/redis/config.py +48 -0
  23. {hexkit-7.2.1/src/hexkit/providers/mongodb → hexkit-8.0.0/src/hexkit/providers/redis/provider}/__init__.py +17 -4
  24. hexkit-8.0.0/src/hexkit/providers/redis/provider/client.py +53 -0
  25. hexkit-8.0.0/src/hexkit/providers/redis/provider/kvstore.py +250 -0
  26. hexkit-8.0.0/src/hexkit/providers/redis/testutils.py +147 -0
  27. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/s3/provider.py +42 -11
  28. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/s3/testutils/_utils.py +5 -5
  29. hexkit-8.0.0/src/hexkit/providers/testing/dao.py +475 -0
  30. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/testing/s3.py +26 -0
  31. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/utils.py +9 -1
  32. {hexkit-7.2.1 → hexkit-8.0.0/src/hexkit.egg-info}/PKG-INFO +25 -16
  33. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit.egg-info/SOURCES.txt +14 -2
  34. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit.egg-info/requires.txt +27 -15
  35. hexkit-7.2.1/src/hexkit/providers/testing/dao.py +0 -146
  36. {hexkit-7.2.1 → hexkit-8.0.0}/LICENSE +0 -0
  37. {hexkit-7.2.1 → hexkit-8.0.0}/MANIFEST.in +0 -0
  38. {hexkit-7.2.1 → hexkit-8.0.0}/README.md +0 -0
  39. {hexkit-7.2.1 → hexkit-8.0.0}/setup.cfg +0 -0
  40. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/__init__.py +0 -0
  41. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/__main__.py +0 -0
  42. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/base.py +0 -0
  43. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/config.py +0 -0
  44. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/correlation.py +0 -0
  45. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/custom_types.py +0 -0
  46. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/log.py +0 -0
  47. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/opentelemetry.py +0 -0
  48. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/protocols/__init__.py +0 -0
  49. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/protocols/daosub.py +0 -0
  50. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/protocols/eventpub.py +0 -0
  51. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/protocols/eventsub.py +0 -0
  52. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/__init__.py +0 -0
  53. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/akafka/__init__.py +0 -0
  54. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/akafka/config.py +0 -0
  55. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/akafka/provider/__init__.py +0 -0
  56. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/akafka/provider/daosub.py +0 -0
  57. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/akafka/provider/eventpub.py +0 -0
  58. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/akafka/provider/eventsub.py +0 -0
  59. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/akafka/provider/utils.py +0 -0
  60. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/akafka/testcontainer.py +0 -0
  61. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/akafka/testutils.py +0 -0
  62. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/mongodb/migrations/__init__.py +0 -0
  63. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/mongodb/migrations/_utils.py +0 -0
  64. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/mongodb/migrations/helpers.py +0 -0
  65. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/mongokafka/testutils.py +0 -0
  66. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/s3/__init__.py +0 -0
  67. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
  68. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
  69. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
  70. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
  71. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
  72. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/s3/testutils/__init__.py +0 -0
  73. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/s3/testutils/_fixtures.py +0 -0
  74. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/s3/testutils/_typical_workflow.py +0 -0
  75. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/testing/__init__.py +0 -0
  76. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/providers/testing/eventpub.py +0 -0
  77. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit/py.typed +0 -0
  78. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit.egg-info/dependency_links.txt +0 -0
  79. {hexkit-7.2.1 → hexkit-8.0.0}/src/hexkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hexkit
3
- Version: 7.2.1
3
+ Version: 8.0.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
@@ -18,39 +18,45 @@ Classifier: Intended Audience :: Developers
18
18
  Requires-Python: >=3.10
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
- Requires-Dist: opentelemetry-api<2,>=1.37
22
- Requires-Dist: pydantic<3,>=2.11
23
- Requires-Dist: pydantic_settings<3,>=2.10
21
+ Requires-Dist: opentelemetry-api<2,>=1.39
22
+ Requires-Dist: pydantic<3,>=2.12
23
+ Requires-Dist: pydantic_settings<3,>=2.12
24
24
  Requires-Dist: PyYAML<7,>=6
25
25
  Provides-Extra: akafka
26
- Requires-Dist: aiokafka[lz4,snappy,zstd]~=0.12.0; extra == "akafka"
26
+ Requires-Dist: aiokafka[lz4,snappy,zstd]~=0.13.0; extra == "akafka"
27
27
  Requires-Dist: jsonschema<5,>=4.25; extra == "akafka"
28
28
  Provides-Extra: s3
29
- Requires-Dist: boto3<2,>=1.40; extra == "s3"
30
- Requires-Dist: botocore<2,>=1.40; extra == "s3"
29
+ Requires-Dist: boto3<2,>=1.42; extra == "s3"
30
+ Requires-Dist: botocore<2,>=1.42; extra == "s3"
31
31
  Provides-Extra: mongodb
32
32
  Requires-Dist: pymongo<5,>=4.15; extra == "mongodb"
33
+ Provides-Extra: redis
34
+ Requires-Dist: redis<8,>=7.1; extra == "redis"
33
35
  Provides-Extra: opentelemetry-base
34
- Requires-Dist: opentelemetry-sdk<2,>=1.37.0; extra == "opentelemetry-base"
35
- Requires-Dist: opentelemetry-exporter-otlp<2,>=1.37.0; extra == "opentelemetry-base"
36
- Requires-Dist: opentelemetry-instrumentation>=0.58b0; extra == "opentelemetry-base"
37
- Requires-Dist: opentelemetry-instrumentation-httpx>=0.58b0; extra == "opentelemetry-base"
38
- Requires-Dist: opentelemetry-distro>=0.58b0; extra == "opentelemetry-base"
36
+ Requires-Dist: opentelemetry-sdk<2,>=1.39.1; extra == "opentelemetry-base"
37
+ Requires-Dist: opentelemetry-exporter-otlp<2,>=1.39.1; extra == "opentelemetry-base"
38
+ Requires-Dist: opentelemetry-instrumentation>=0.60b1; extra == "opentelemetry-base"
39
+ Requires-Dist: opentelemetry-instrumentation-httpx>=0.60b1; extra == "opentelemetry-base"
40
+ Requires-Dist: opentelemetry-distro>=0.60b1; extra == "opentelemetry-base"
39
41
  Provides-Extra: opentelemetry-akafka
40
42
  Requires-Dist: hexkit[opentelemetry-base]; extra == "opentelemetry-akafka"
41
43
  Requires-Dist: hexkit[akafka]; extra == "opentelemetry-akafka"
42
- Requires-Dist: opentelemetry-instrumentation-aiokafka>=0.58b0; extra == "opentelemetry-akafka"
44
+ Requires-Dist: opentelemetry-instrumentation-aiokafka>=0.60b1; extra == "opentelemetry-akafka"
43
45
  Provides-Extra: opentelemetry-mongodb
44
46
  Requires-Dist: hexkit[opentelemetry-base]; extra == "opentelemetry-mongodb"
45
47
  Requires-Dist: hexkit[mongodb]; extra == "opentelemetry-mongodb"
46
- Requires-Dist: opentelemetry-instrumentation-pymongo>=0.58b0; extra == "opentelemetry-mongodb"
48
+ Requires-Dist: opentelemetry-instrumentation-pymongo>=0.60b1; extra == "opentelemetry-mongodb"
47
49
  Provides-Extra: opentelemetry-fastapi
48
50
  Requires-Dist: hexkit[opentelemetry-base]; extra == "opentelemetry-fastapi"
49
- Requires-Dist: opentelemetry-instrumentation-fastapi>=0.58b0; extra == "opentelemetry-fastapi"
51
+ Requires-Dist: opentelemetry-instrumentation-fastapi>=0.60b1; extra == "opentelemetry-fastapi"
50
52
  Provides-Extra: opentelemetry-s3
51
53
  Requires-Dist: hexkit[opentelemetry-base]; extra == "opentelemetry-s3"
52
54
  Requires-Dist: hexkit[s3]; extra == "opentelemetry-s3"
53
- Requires-Dist: opentelemetry-instrumentation-botocore>=0.58b0; extra == "opentelemetry-s3"
55
+ Requires-Dist: opentelemetry-instrumentation-botocore>=0.60b1; extra == "opentelemetry-s3"
56
+ Provides-Extra: opentelemetry-redis
57
+ Requires-Dist: hexkit[opentelemetry-base]; extra == "opentelemetry-redis"
58
+ Requires-Dist: hexkit[redis]; extra == "opentelemetry-redis"
59
+ Requires-Dist: opentelemetry-instrumentation-redis>=0.60b1; extra == "opentelemetry-redis"
54
60
  Provides-Extra: opentelemetry
55
61
  Requires-Dist: hexkit[opentelemetry-akafka]; extra == "opentelemetry"
56
62
  Requires-Dist: hexkit[opentelemetry-mongodb]; extra == "opentelemetry"
@@ -65,6 +71,9 @@ Requires-Dist: testcontainers<5,>=4.13; extra == "test-s3"
65
71
  Provides-Extra: test-mongodb
66
72
  Requires-Dist: hexkit[mongodb]; extra == "test-mongodb"
67
73
  Requires-Dist: testcontainers[mongo]<5,>=4.13; extra == "test-mongodb"
74
+ Provides-Extra: test-redis
75
+ Requires-Dist: hexkit[redis]; extra == "test-redis"
76
+ Requires-Dist: testcontainers[redis]<5,>=4.13; extra == "test-redis"
68
77
  Provides-Extra: test
69
78
  Requires-Dist: hexkit[test-akafka,test-mongodb,test-s3]; extra == "test"
70
79
  Provides-Extra: all
@@ -1,6 +1,6 @@
1
1
  [build-system]
2
2
  requires = [
3
- "setuptools>=80.9",
3
+ "setuptools>=80.10",
4
4
  ]
5
5
  build-backend = "setuptools.build_meta"
6
6
 
@@ -23,12 +23,12 @@ classifiers = [
23
23
  "Intended Audience :: Developers",
24
24
  ]
25
25
  name = "hexkit"
26
- version = "7.2.1"
26
+ version = "8.0.0"
27
27
  description = "A Toolkit for Building Microservices using the Hexagonal Architecture"
28
28
  dependencies = [
29
- "opentelemetry-api >=1.37, <2",
30
- "pydantic >=2.11, <3",
31
- "pydantic_settings >=2.10, <3",
29
+ "opentelemetry-api >=1.39, <2",
30
+ "pydantic >=2.12, <3",
31
+ "pydantic_settings >=2.12, <3",
32
32
  "PyYAML >=6, <7",
33
33
  ]
34
34
 
@@ -37,41 +37,49 @@ text = "Apache 2.0"
37
37
 
38
38
  [project.optional-dependencies]
39
39
  akafka = [
40
- "aiokafka[zstd,lz4,snappy]~=0.12.0",
40
+ "aiokafka[zstd,lz4,snappy]~=0.13.0",
41
41
  "jsonschema >=4.25, <5",
42
42
  ]
43
43
  s3 = [
44
- "boto3 >=1.40, <2",
45
- "botocore >=1.40, <2",
44
+ "boto3 >=1.42, <2",
45
+ "botocore >=1.42, <2",
46
46
  ]
47
47
  mongodb = [
48
48
  "pymongo >=4.15, <5",
49
49
  ]
50
+ redis = [
51
+ "redis >=7.1, <8",
52
+ ]
50
53
  opentelemetry-base = [
51
- "opentelemetry-sdk >=1.37.0, <2",
52
- "opentelemetry-exporter-otlp >=1.37.0, <2",
53
- "opentelemetry-instrumentation >=0.58b0",
54
- "opentelemetry-instrumentation-httpx >=0.58b0",
55
- "opentelemetry-distro >=0.58b0",
54
+ "opentelemetry-sdk >=1.39.1, <2",
55
+ "opentelemetry-exporter-otlp >=1.39.1, <2",
56
+ "opentelemetry-instrumentation >=0.60b1",
57
+ "opentelemetry-instrumentation-httpx >=0.60b1",
58
+ "opentelemetry-distro >=0.60b1",
56
59
  ]
57
60
  opentelemetry-akafka = [
58
61
  "hexkit[opentelemetry-base]",
59
62
  "hexkit[akafka]",
60
- "opentelemetry-instrumentation-aiokafka >=0.58b0",
63
+ "opentelemetry-instrumentation-aiokafka >=0.60b1",
61
64
  ]
62
65
  opentelemetry-mongodb = [
63
66
  "hexkit[opentelemetry-base]",
64
67
  "hexkit[mongodb]",
65
- "opentelemetry-instrumentation-pymongo >=0.58b0",
68
+ "opentelemetry-instrumentation-pymongo >=0.60b1",
66
69
  ]
67
70
  opentelemetry-fastapi = [
68
71
  "hexkit[opentelemetry-base]",
69
- "opentelemetry-instrumentation-fastapi >=0.58b0",
72
+ "opentelemetry-instrumentation-fastapi >=0.60b1",
70
73
  ]
71
74
  opentelemetry-s3 = [
72
75
  "hexkit[opentelemetry-base]",
73
76
  "hexkit[s3]",
74
- "opentelemetry-instrumentation-botocore >=0.58b0",
77
+ "opentelemetry-instrumentation-botocore >=0.60b1",
78
+ ]
79
+ opentelemetry-redis = [
80
+ "hexkit[opentelemetry-base]",
81
+ "hexkit[redis]",
82
+ "opentelemetry-instrumentation-redis >=0.60b1",
75
83
  ]
76
84
  opentelemetry = [
77
85
  "hexkit[opentelemetry-akafka]",
@@ -91,6 +99,10 @@ test-mongodb = [
91
99
  "hexkit[mongodb]",
92
100
  "testcontainers[mongo] >=4.13, <5",
93
101
  ]
102
+ test-redis = [
103
+ "hexkit[redis]",
104
+ "testcontainers[redis] >=4.13, <5",
105
+ ]
94
106
  test = [
95
107
  "hexkit[test-akafka,test-s3,test-mongodb]",
96
108
  ]
@@ -209,7 +221,7 @@ check_untyped_defs = true
209
221
  no_site_packages = false
210
222
 
211
223
  [tool.pytest.ini_options]
212
- minversion = "8.4"
224
+ minversion = "9.0"
213
225
  asyncio_mode = "strict"
214
226
  asyncio_default_fixture_loop_scope = "function"
215
227
 
@@ -23,7 +23,7 @@ from abc import ABC, abstractmethod
23
23
  from collections.abc import AsyncIterator, Collection, Mapping
24
24
  from contextlib import AbstractAsyncContextManager
25
25
  from functools import partial
26
- from typing import Any, TypeVar
26
+ from typing import Any, Generic, TypeVar
27
27
  from uuid import uuid4
28
28
 
29
29
  from pydantic import BaseModel, Field
@@ -39,10 +39,22 @@ __all__ = [
39
39
  "ResourceAlreadyExistsError",
40
40
  "ResourceNotFoundError",
41
41
  "UUID4Field",
42
+ "UniqueConstraintViolationError",
42
43
  ]
43
44
 
45
+
46
+ class IndexBase(ABC):
47
+ """Base from which all DAO index classes must inherit"""
48
+
49
+ @abstractmethod
50
+ def list_field_names(self) -> list[str]:
51
+ """Returns a list of all fields specified by this index"""
52
+ ...
53
+
54
+
44
55
  # Type variable for handling Data Transfer Objects:
45
56
  Dto = TypeVar("Dto", bound=BaseModel)
57
+ Index = TypeVar("Index", bound=IndexBase)
46
58
 
47
59
 
48
60
  class DaoError(RuntimeError):
@@ -65,6 +77,20 @@ class ResourceAlreadyExistsError(DaoError):
65
77
  super().__init__(message)
66
78
 
67
79
 
80
+ class UniqueConstraintViolationError(DaoError):
81
+ """Raised when attempting to insert a resource violated a uniqueness constraint.
82
+
83
+ This does not apply to the default unique index automatically applied to
84
+ the ID field.
85
+ """
86
+
87
+ def __init__(self, *, unique_fields: dict[str, Any]):
88
+ message = (
89
+ f"A resource with the unique field value(s) {unique_fields} already exists."
90
+ )
91
+ super().__init__(message)
92
+
93
+
68
94
  class FindError(DaoError):
69
95
  """Base for all error related to DAO find operations."""
70
96
 
@@ -268,16 +294,19 @@ class DaoFactoryBase:
268
294
  cls,
269
295
  *,
270
296
  dto_model: type[Dto],
271
- fields_to_index: Collection[str] | None,
297
+ indexes: Collection[Index] | None,
272
298
  ) -> None:
273
299
  """Checks that all provided fields are present in the dto_model.
274
300
  Raises IndexFieldsInvalidError otherwise.
275
301
  """
276
- if fields_to_index is None:
302
+ if not indexes:
277
303
  return
278
304
 
279
305
  try:
280
- validate_fields_in_model(model=dto_model, fields=fields_to_index)
306
+ for index in indexes:
307
+ validate_fields_in_model(
308
+ model=dto_model, fields=index.list_field_names()
309
+ )
281
310
  except FieldNotInModelError as error:
282
311
  raise cls.IndexFieldsInvalidError(
283
312
  f"Provided index fields are invalid: {error}"
@@ -289,16 +318,14 @@ class DaoFactoryBase:
289
318
  *,
290
319
  dto_model: type[Dto],
291
320
  id_field: str,
292
- fields_to_index: Collection[str] | None,
321
+ indexes: Collection[Index] | None,
293
322
  ) -> None:
294
323
  """Validates the input parameters of the get_dao method."""
295
324
  cls._validate_dto_model_id(dto_model=dto_model, id_field=id_field)
296
- cls._validate_fields_to_index(
297
- dto_model=dto_model, fields_to_index=fields_to_index
298
- )
325
+ cls._validate_fields_to_index(dto_model=dto_model, indexes=indexes)
299
326
 
300
327
 
301
- class DaoFactoryProtocol(DaoFactoryBase, ABC):
328
+ class DaoFactoryProtocol(DaoFactoryBase, ABC, Generic[Index]):
302
329
  """A protocol describing a factory to produce Data Access Objects (DAO) objects
303
330
  that are enclosed in transactional scopes.
304
331
  """
@@ -309,7 +336,7 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
309
336
  name: str,
310
337
  dto_model: type[Dto],
311
338
  id_field: str,
312
- fields_to_index: Collection[str] | None = None,
339
+ indexes: Collection[Index] | None = None,
313
340
  ) -> Dao[Dto]:
314
341
  """Constructs a DAO for interacting with resources in a database.
315
342
 
@@ -322,9 +349,9 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
322
349
  id_field:
323
350
  The name of the field of the `dto_model` that serves as resource ID.
324
351
  (DAO implementation might use this field as primary key.)
325
- fields_to_index:
326
- Optionally, provide any fields that should be indexed in addition to the
327
- `id_field`. Defaults to None.
352
+ indexes:
353
+ Optionally, provide any indexes that should be created in addition to
354
+ the provider's default index on `id_field`. Defaults to None.
328
355
  Returns:
329
356
  A DAO specific to the provided DTO model.
330
357
 
@@ -334,17 +361,13 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
334
361
  self.IdTypeNotSupportedError:
335
362
  Raised when the id_field of the dto_model has an unexpected type.
336
363
  """
337
- self._validate(
338
- dto_model=dto_model,
339
- id_field=id_field,
340
- fields_to_index=fields_to_index,
341
- )
364
+ self._validate(dto_model=dto_model, id_field=id_field, indexes=indexes)
342
365
 
343
366
  return await self._get_dao(
344
367
  name=name,
345
368
  dto_model=dto_model,
346
369
  id_field=id_field,
347
- fields_to_index=fields_to_index,
370
+ indexes=indexes,
348
371
  )
349
372
 
350
373
  @abstractmethod
@@ -354,7 +377,7 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
354
377
  name: str,
355
378
  dto_model: type[Dto],
356
379
  id_field: str,
357
- fields_to_index: Collection[str] | None,
380
+ indexes: Collection[Index] | None,
358
381
  ) -> Dao[Dto]:
359
382
  """*To be implemented by the provider. Input validation is done outside of this
360
383
  method.*
@@ -26,11 +26,7 @@ from abc import ABC, abstractmethod
26
26
  from collections.abc import Callable, Collection
27
27
 
28
28
  from hexkit.custom_types import JsonObject
29
- from hexkit.protocols.dao import (
30
- Dao,
31
- DaoFactoryBase,
32
- Dto,
33
- )
29
+ from hexkit.protocols.dao import Dao, DaoFactoryBase, Dto, Index
34
30
 
35
31
 
36
32
  class DaoPublisher(Dao[Dto], typing.Protocol[Dto]):
@@ -49,7 +45,7 @@ class DaoPublisher(Dao[Dto], typing.Protocol[Dto]):
49
45
  ...
50
46
 
51
47
 
52
- class DaoPublisherFactoryProtocol(DaoFactoryBase, ABC):
48
+ class DaoPublisherFactoryProtocol(DaoFactoryBase, ABC, typing.Generic[Index]):
53
49
  """A protocol describing a factory to produce Data Access Objects (DAO) objects
54
50
  which automatically publish changes according to the outbox pattern.
55
51
  """
@@ -60,7 +56,7 @@ class DaoPublisherFactoryProtocol(DaoFactoryBase, ABC):
60
56
  name: str,
61
57
  dto_model: type[Dto],
62
58
  id_field: str,
63
- fields_to_index: Collection[str] | None = None,
59
+ indexes: Collection[Index] | None = None,
64
60
  dto_to_event: Callable[[Dto], JsonObject | None],
65
61
  event_topic: str,
66
62
  autopublish: bool = True,
@@ -76,9 +72,9 @@ class DaoPublisherFactoryProtocol(DaoFactoryBase, ABC):
76
72
  id_field:
77
73
  The name of the field of the `dto_model` that serves as resource ID.
78
74
  (DAO implementation might use this field as primary key.)
79
- fields_to_index:
80
- Optionally, provide any fields that should be indexed in addition to the
81
- `id_field`. Defaults to None.
75
+ indexes:
76
+ Optionally, provide any indexes that should be created in addition to
77
+ the provider's default index on `id_field`. Defaults to None.
82
78
  dto_to_event:
83
79
  A function that takes a DTO and returns the payload for an event.
84
80
  If the returned payload is None, the event will not be published.
@@ -97,17 +93,13 @@ class DaoPublisherFactoryProtocol(DaoFactoryBase, ABC):
97
93
  self.IdTypeNotSupportedError:
98
94
  Raised when the id_field of the dto_model has an unexpected type.
99
95
  """
100
- self._validate(
101
- dto_model=dto_model,
102
- id_field=id_field,
103
- fields_to_index=fields_to_index,
104
- )
96
+ self._validate(dto_model=dto_model, id_field=id_field, indexes=indexes)
105
97
 
106
98
  return await self._get_dao(
107
99
  name=name,
108
100
  dto_model=dto_model,
109
101
  id_field=id_field,
110
- fields_to_index=fields_to_index,
102
+ indexes=indexes,
111
103
  dto_to_event=dto_to_event,
112
104
  event_topic=event_topic,
113
105
  autopublish=autopublish,
@@ -120,7 +112,7 @@ class DaoPublisherFactoryProtocol(DaoFactoryBase, ABC):
120
112
  name: str,
121
113
  dto_model: type[Dto],
122
114
  id_field: str,
123
- fields_to_index: Collection[str] | None,
115
+ indexes: Collection[Index] | None,
124
116
  dto_to_event: Callable[[Dto], JsonObject | None],
125
117
  event_topic: str,
126
118
  autopublish: bool,
@@ -0,0 +1,55 @@
1
+ # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
+ # for the German Human Genome-Phenome Archive (GHGA)
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ """Protocol supporting key-value store operations."""
18
+
19
+ from typing import Generic, Protocol, TypeVar
20
+
21
+ V = TypeVar("V")
22
+
23
+
24
+ class KeyValueStoreProtocol(Protocol, Generic[V]):
25
+ """A protocol for a key-value store."""
26
+
27
+ async def get(self, key: str, default: V | None = None) -> V | None:
28
+ """Retrieve the value for the given key.
29
+
30
+ Returns the specified default value if there is no such value in the store.
31
+ """
32
+ pass
33
+
34
+ async def set(self, key: str, value: V) -> None:
35
+ """Set the value for the given key.
36
+
37
+ Providers must reject storing a value of None if they
38
+ support a value domain V that can contain such values.
39
+ """
40
+ pass
41
+
42
+ async def delete(self, key: str) -> None:
43
+ """Delete the value for the given key.
44
+
45
+ Does nothing if there is no such value in the store.
46
+ """
47
+ pass
48
+
49
+ async def exists(self, key: str) -> bool:
50
+ """Check if the given key exists.
51
+
52
+ Default implementation for classes inheriting from this protocol.
53
+ Providers can use something more efficient.
54
+ """
55
+ return (await self.get(key)) is not None
@@ -182,6 +182,31 @@ class ObjectStorageProtocol(ABC):
182
182
  anticipated_part_size=anticipated_part_size,
183
183
  )
184
184
 
185
+ async def list_multipart_uploads_for_object(
186
+ self, *, bucket_id: str, object_id: str
187
+ ) -> list[str]:
188
+ """Lists all active multipart uploads for the given object ID.
189
+
190
+ Raises a `BucketNotFoundError` if the bucket does not exist.
191
+ """
192
+ self._validate_bucket_id(bucket_id)
193
+ self._validate_object_id(object_id)
194
+ return await self._list_multipart_uploads_for_object(
195
+ bucket_id=bucket_id, object_id=object_id
196
+ )
197
+
198
+ async def get_all_multipart_uploads(self, *, bucket_id: str) -> dict[str, str]:
199
+ """Gets all active multipart uploads for the given bucket ID.
200
+
201
+ Returns a dict where the keys are upload IDs and values are object IDs.
202
+ S3 allows multiple ongoing multi-part uploads, so it's possible for some upload
203
+ IDs to map to the same object ID.
204
+
205
+ Raises a `BucketNotFoundError` if the bucket does not exist.
206
+ """
207
+ self._validate_bucket_id(bucket_id)
208
+ return await self._get_all_multipart_uploads(bucket_id=bucket_id)
209
+
185
210
  async def get_object_download_url(
186
211
  self, *, bucket_id: str, object_id: str, expires_after: int = 86400
187
212
  ) -> str:
@@ -404,6 +429,29 @@ class ObjectStorageProtocol(ABC):
404
429
  """
405
430
  ...
406
431
 
432
+ @abstractmethod
433
+ async def _list_multipart_uploads_for_object(
434
+ self, *, bucket_id: str, object_id: str
435
+ ) -> list[str]:
436
+ """Lists all active multipart uploads for the given object ID.
437
+
438
+ *To be implemented by the provider. Input validation is done outside of this
439
+ method.*
440
+ """
441
+ ...
442
+
443
+ @abstractmethod
444
+ async def _get_all_multipart_uploads(self, *, bucket_id: str) -> dict[str, str]:
445
+ """Gets all active multipart uploads for the given bucket ID.
446
+
447
+ Returns a dict where the keys are upload IDs and values are object IDs.
448
+ S3 allows multiple ongoing multi-part uploads, so it's possible for some upload
449
+ IDs to map to the same object ID.
450
+
451
+ *To be implemented by the provider. Input validation is done outside of this
452
+ method.*
453
+ """
454
+
407
455
  @abstractmethod
408
456
  async def _get_object_download_url(
409
457
  self, *, bucket_id: str, object_id: str, expires_after: int = 86400
@@ -0,0 +1,43 @@
1
+ # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
+ # for the German Human Genome-Phenome Archive (GHGA)
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ """Subpackage containing MongoDB-based providers and related utilities."""
18
+
19
+ from .config import MongoDbConfig
20
+ from .provider import (
21
+ ConfiguredMongoClient,
22
+ MongoDbBytesKeyValueStore,
23
+ MongoDbDao,
24
+ MongoDbDaoFactory,
25
+ MongoDbDtoKeyValueStore,
26
+ MongoDbIndex,
27
+ MongoDbJsonKeyValueStore,
28
+ MongoDbStrKeyValueStore,
29
+ translate_pymongo_errors,
30
+ )
31
+
32
+ __all__ = [
33
+ "ConfiguredMongoClient",
34
+ "MongoDbBytesKeyValueStore",
35
+ "MongoDbConfig",
36
+ "MongoDbDao",
37
+ "MongoDbDaoFactory",
38
+ "MongoDbDtoKeyValueStore",
39
+ "MongoDbIndex",
40
+ "MongoDbJsonKeyValueStore",
41
+ "MongoDbStrKeyValueStore",
42
+ "translate_pymongo_errors",
43
+ ]
@@ -0,0 +1,55 @@
1
+ # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
+ # for the German Human Genome-Phenome Archive (GHGA)
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ """MongoDB specific configuration."""
18
+
19
+ from pydantic import Field, MongoDsn, PositiveInt, Secret
20
+ from pydantic_settings import BaseSettings
21
+
22
+ __all__ = ["MongoDbConfig"]
23
+
24
+
25
+ class MongoDbConfig(BaseSettings):
26
+ """Configuration parameters for connecting to a MongoDB server.
27
+
28
+ Inherit your config class from this class if your application uses MongoDB.
29
+ """
30
+
31
+ mongo_dsn: Secret[MongoDsn] = Field(
32
+ ...,
33
+ examples=["mongodb://localhost:27017"],
34
+ description=(
35
+ "MongoDB connection string. Might include credentials."
36
+ + " For more information see:"
37
+ + " https://naiveskill.com/mongodb-connection-string/"
38
+ ),
39
+ )
40
+ db_name: str = Field(
41
+ ...,
42
+ examples=["my-database"],
43
+ description="Name of the database located on the MongoDB server.",
44
+ )
45
+ mongo_timeout: PositiveInt | None = Field(
46
+ default=None,
47
+ examples=[300, 600, None],
48
+ description=(
49
+ "Timeout in seconds for API calls to MongoDB. The timeout applies to all steps"
50
+ + " needed to complete the operation, including server selection, connection"
51
+ + " checkout, serialization, and server-side execution. When the timeout"
52
+ + " expires, PyMongo raises a timeout exception. If set to None, the"
53
+ + " operation will not time out (default MongoDB behavior)."
54
+ ),
55
+ )
@@ -27,7 +27,7 @@ from pymongo import AsyncMongoClient
27
27
  from pymongo.asynchronous.database import AsyncDatabase
28
28
  from pymongo.errors import DuplicateKeyError
29
29
 
30
- from hexkit.providers.mongodb import MongoDbConfig
30
+ from hexkit.providers.mongodb.config import MongoDbConfig
31
31
  from hexkit.providers.mongodb.provider import ConfiguredMongoClient
32
32
 
33
33
  from ._utils import MigrationDefinition, Reversible