hexkit 7.3.0__tar.gz → 8.1.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 (78) hide show
  1. {hexkit-7.3.0 → hexkit-8.1.0}/LICENSE +1 -1
  2. {hexkit-7.3.0/src/hexkit.egg-info → hexkit-8.1.0}/PKG-INFO +6 -6
  3. {hexkit-7.3.0 → hexkit-8.1.0}/pyproject.toml +9 -9
  4. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/__init__.py +1 -1
  5. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/__main__.py +1 -1
  6. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/base.py +1 -1
  7. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/config.py +14 -2
  8. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/correlation.py +1 -1
  9. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/custom_types.py +1 -1
  10. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/log.py +1 -1
  11. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/opentelemetry.py +1 -1
  12. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/protocols/__init__.py +1 -1
  13. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/protocols/dao.py +55 -21
  14. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/protocols/daopub.py +10 -18
  15. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/protocols/daosub.py +1 -1
  16. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/protocols/eventpub.py +1 -1
  17. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/protocols/eventsub.py +1 -1
  18. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/protocols/kvstore.py +1 -1
  19. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/protocols/objstorage.py +49 -1
  20. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/__init__.py +1 -1
  21. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/akafka/__init__.py +1 -1
  22. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/akafka/config.py +1 -1
  23. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/akafka/provider/__init__.py +1 -1
  24. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/akafka/provider/daosub.py +1 -1
  25. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/akafka/provider/eventpub.py +1 -1
  26. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/akafka/provider/eventsub.py +1 -1
  27. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/akafka/provider/utils.py +1 -1
  28. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/akafka/testcontainer.py +1 -1
  29. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/akafka/testutils.py +1 -1
  30. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/__init__.py +3 -1
  31. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/config.py +1 -1
  32. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/migrations/__init__.py +1 -1
  33. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/migrations/_manager.py +1 -1
  34. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/migrations/_utils.py +1 -1
  35. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/migrations/helpers.py +1 -1
  36. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/provider/__init__.py +3 -1
  37. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/provider/client.py +1 -1
  38. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/provider/dao.py +104 -15
  39. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/provider/kvstore.py +1 -1
  40. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/provider/utils.py +1 -1
  41. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongodb/testutils.py +1 -1
  42. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongokafka/__init__.py +1 -1
  43. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongokafka/config.py +1 -1
  44. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongokafka/provider/__init__.py +1 -1
  45. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongokafka/provider/daopub.py +48 -20
  46. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongokafka/provider/persistent_pub.py +1 -1
  47. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/mongokafka/testutils.py +1 -1
  48. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/redis/__init__.py +1 -1
  49. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/redis/config.py +1 -1
  50. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/redis/provider/__init__.py +1 -1
  51. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/redis/provider/client.py +1 -1
  52. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/redis/provider/kvstore.py +1 -1
  53. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/redis/testutils.py +1 -1
  54. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/s3/__init__.py +1 -1
  55. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/s3/provider.py +42 -11
  56. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/s3/test_files/__init__.py +1 -1
  57. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/s3/testutils/__init__.py +1 -1
  58. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/s3/testutils/_fixtures.py +1 -1
  59. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/s3/testutils/_typical_workflow.py +1 -1
  60. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/s3/testutils/_utils.py +1 -1
  61. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/testing/__init__.py +1 -1
  62. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/testing/dao.py +1 -1
  63. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/testing/eventpub.py +1 -1
  64. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/testing/s3.py +27 -1
  65. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/utils.py +1 -1
  66. {hexkit-7.3.0 → hexkit-8.1.0/src/hexkit.egg-info}/PKG-INFO +6 -6
  67. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit.egg-info/requires.txt +5 -5
  68. {hexkit-7.3.0 → hexkit-8.1.0}/MANIFEST.in +0 -0
  69. {hexkit-7.3.0 → hexkit-8.1.0}/README.md +0 -0
  70. {hexkit-7.3.0 → hexkit-8.1.0}/setup.cfg +0 -0
  71. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
  72. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
  73. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
  74. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
  75. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit/py.typed +0 -0
  76. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit.egg-info/SOURCES.txt +0 -0
  77. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit.egg-info/dependency_links.txt +0 -0
  78. {hexkit-7.3.0 → hexkit-8.1.0}/src/hexkit.egg-info/top_level.txt +0 -0
@@ -186,7 +186,7 @@
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
189
+ Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
190
190
  for the German Human Genome-Phenome Archive (GHGA)
191
191
 
192
192
  Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hexkit
3
- Version: 7.3.0
3
+ Version: 8.1.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
@@ -29,7 +29,7 @@ Provides-Extra: s3
29
29
  Requires-Dist: boto3<2,>=1.42; extra == "s3"
30
30
  Requires-Dist: botocore<2,>=1.42; extra == "s3"
31
31
  Provides-Extra: mongodb
32
- Requires-Dist: pymongo<5,>=4.15; extra == "mongodb"
32
+ Requires-Dist: pymongo<5,>=4.16; extra == "mongodb"
33
33
  Provides-Extra: redis
34
34
  Requires-Dist: redis<8,>=7.1; extra == "redis"
35
35
  Provides-Extra: opentelemetry-base
@@ -64,16 +64,16 @@ Requires-Dist: hexkit[opentelemetry-fastapi]; extra == "opentelemetry"
64
64
  Requires-Dist: hexkit[opentelemetry-s3]; extra == "opentelemetry"
65
65
  Provides-Extra: test-akafka
66
66
  Requires-Dist: hexkit[akafka]; extra == "test-akafka"
67
- Requires-Dist: testcontainers[kafka]<5,>=4.13; extra == "test-akafka"
67
+ Requires-Dist: testcontainers[kafka]<5,>=4.14; extra == "test-akafka"
68
68
  Provides-Extra: test-s3
69
69
  Requires-Dist: hexkit[s3]; extra == "test-s3"
70
- Requires-Dist: testcontainers<5,>=4.13; extra == "test-s3"
70
+ Requires-Dist: testcontainers<5,>=4.14; extra == "test-s3"
71
71
  Provides-Extra: test-mongodb
72
72
  Requires-Dist: hexkit[mongodb]; extra == "test-mongodb"
73
- Requires-Dist: testcontainers[mongo]<5,>=4.13; extra == "test-mongodb"
73
+ Requires-Dist: testcontainers[mongo]<5,>=4.14; extra == "test-mongodb"
74
74
  Provides-Extra: test-redis
75
75
  Requires-Dist: hexkit[redis]; extra == "test-redis"
76
- Requires-Dist: testcontainers[redis]<5,>=4.13; extra == "test-redis"
76
+ Requires-Dist: testcontainers[redis]<5,>=4.14; extra == "test-redis"
77
77
  Provides-Extra: test
78
78
  Requires-Dist: hexkit[test-akafka,test-mongodb,test-s3]; extra == "test"
79
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,7 +23,7 @@ classifiers = [
23
23
  "Intended Audience :: Developers",
24
24
  ]
25
25
  name = "hexkit"
26
- version = "7.3.0"
26
+ version = "8.1.0"
27
27
  description = "A Toolkit for Building Microservices using the Hexagonal Architecture"
28
28
  dependencies = [
29
29
  "opentelemetry-api >=1.39, <2",
@@ -45,7 +45,7 @@ s3 = [
45
45
  "botocore >=1.42, <2",
46
46
  ]
47
47
  mongodb = [
48
- "pymongo >=4.15, <5",
48
+ "pymongo >=4.16, <5",
49
49
  ]
50
50
  redis = [
51
51
  "redis >=7.1, <8",
@@ -89,19 +89,19 @@ opentelemetry = [
89
89
  ]
90
90
  test-akafka = [
91
91
  "hexkit[akafka]",
92
- "testcontainers[kafka] >=4.13, <5",
92
+ "testcontainers[kafka] >=4.14, <5",
93
93
  ]
94
94
  test-s3 = [
95
95
  "hexkit[s3]",
96
- "testcontainers >=4.13, <5",
96
+ "testcontainers >=4.14, <5",
97
97
  ]
98
98
  test-mongodb = [
99
99
  "hexkit[mongodb]",
100
- "testcontainers[mongo] >=4.13, <5",
100
+ "testcontainers[mongo] >=4.14, <5",
101
101
  ]
102
102
  test-redis = [
103
103
  "hexkit[redis]",
104
- "testcontainers[redis] >=4.13, <5",
104
+ "testcontainers[redis] >=4.14, <5",
105
105
  ]
106
106
  test = [
107
107
  "hexkit[test-akafka,test-s3,test-mongodb]",
@@ -221,7 +221,7 @@ check_untyped_defs = true
221
221
  no_site_packages = false
222
222
 
223
223
  [tool.pytest.ini_options]
224
- minversion = "8.4"
224
+ minversion = "9.0"
225
225
  asyncio_mode = "strict"
226
226
  asyncio_default_fixture_loop_scope = "function"
227
227
 
@@ -251,6 +251,6 @@ legacy_tox_ini = """
251
251
  deps =
252
252
  --no-deps -r ./lock/requirements-dev.txt
253
253
  commands =
254
- py310: pip install --no-deps backports.asyncio.runner==1.2.0 exceptiongroup==1.3.0 tomli==2.2.1
254
+ py310: pip install --no-deps backports.asyncio.runner==1.2.0 exceptiongroup==1.3.1 tomli==2.2.1
255
255
  pytest {posargs}
256
256
  """
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -29,6 +29,8 @@ from pydantic_settings import (
29
29
 
30
30
  # Default config prefix:
31
31
  DEFAULT_CONFIG_PREFIX: Final = "ghga_services"
32
+ # Default dotenv path:
33
+ DEFAULT_DOTENV_PATH: Final[Path] = Path("/secrets")
32
34
 
33
35
 
34
36
  class ConfigYamlDoesNotExist(RuntimeError):
@@ -82,6 +84,7 @@ def get_default_config_yaml(prefix: str) -> Path | None:
82
84
 
83
85
  def config_from_yaml(
84
86
  prefix: str = DEFAULT_CONFIG_PREFIX,
87
+ dotenv_prefix: Path = DEFAULT_DOTENV_PATH,
85
88
  ) -> Callable:
86
89
  """A factory that returns decorator functions which extends a
87
90
  pydantic BaseSettings class to read in parameters from a config yaml.
@@ -102,6 +105,10 @@ def config_from_yaml(
102
105
  "{prefix}_{actual_variable_name}". Moreover, this prefix is used
103
106
  to derive the default location for the config yaml file
104
107
  ("~/.{prefix}.yaml"). Defaults to "ghga_services".
108
+ dotenv_prefix: (Path, optional):
109
+ When defining parameters via dotenv files, this prefix is used to set the path
110
+ for the dotenv files. The final path used is constructed as
111
+ "{dotenv_prefix}/.env". Defaults to "/secrets".
105
112
  """
106
113
 
107
114
  def decorator(settings) -> Callable:
@@ -141,7 +148,11 @@ def config_from_yaml(
141
148
  class ModSettings(settings):
142
149
  """Modifies the original Settings class provided by the user"""
143
150
 
144
- model_config = SettingsConfigDict(frozen=True, env_prefix=f"{prefix}_")
151
+ model_config = SettingsConfigDict(
152
+ frozen=True,
153
+ env_prefix=f"{prefix}_",
154
+ env_file=(".env", dotenv_prefix / ".env"),
155
+ )
145
156
 
146
157
  @classmethod
147
158
  def settings_customise_sources(
@@ -156,6 +167,7 @@ def config_from_yaml(
156
167
  return (
157
168
  init_settings,
158
169
  env_settings,
170
+ dotenv_settings,
159
171
  file_secret_settings,
160
172
  YamlConfigSettingsSource(settings_cls, config_yaml),
161
173
  )
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -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
 
@@ -152,6 +178,9 @@ class Dao(typing.Protocol[Dto]):
152
178
  Raises:
153
179
  ResourceNotFoundError:
154
180
  when resource with the id specified in the dto was not found
181
+ UniqueConstraintViolationError:
182
+ when updating the dto would violate a unique index constraint over some
183
+ field other than the ID field.
155
184
  """
156
185
  ...
157
186
 
@@ -224,6 +253,9 @@ class Dao(typing.Protocol[Dto]):
224
253
  Raises:
225
254
  ResourceAlreadyExistsError:
226
255
  when a resource with the ID specified in the dto does already exist.
256
+ UniqueConstraintViolationError:
257
+ when inserting the dto would violate a unique index constraint over some
258
+ field other than the ID field.
227
259
  """
228
260
  ...
229
261
 
@@ -234,6 +266,11 @@ class Dao(typing.Protocol[Dto]):
234
266
  dto:
235
267
  Resource content as a pydantic-based data transfer object including the
236
268
  resource ID.
269
+
270
+ Raises:
271
+ UniqueConstraintViolationError:
272
+ when upserting the dto would violate a unique index constraint over some
273
+ field other than the ID field.
237
274
  """
238
275
  ...
239
276
 
@@ -268,16 +305,19 @@ class DaoFactoryBase:
268
305
  cls,
269
306
  *,
270
307
  dto_model: type[Dto],
271
- fields_to_index: Collection[str] | None,
308
+ indexes: Collection[Index] | None,
272
309
  ) -> None:
273
310
  """Checks that all provided fields are present in the dto_model.
274
311
  Raises IndexFieldsInvalidError otherwise.
275
312
  """
276
- if fields_to_index is None:
313
+ if not indexes:
277
314
  return
278
315
 
279
316
  try:
280
- validate_fields_in_model(model=dto_model, fields=fields_to_index)
317
+ for index in indexes:
318
+ validate_fields_in_model(
319
+ model=dto_model, fields=index.list_field_names()
320
+ )
281
321
  except FieldNotInModelError as error:
282
322
  raise cls.IndexFieldsInvalidError(
283
323
  f"Provided index fields are invalid: {error}"
@@ -289,16 +329,14 @@ class DaoFactoryBase:
289
329
  *,
290
330
  dto_model: type[Dto],
291
331
  id_field: str,
292
- fields_to_index: Collection[str] | None,
332
+ indexes: Collection[Index] | None,
293
333
  ) -> None:
294
334
  """Validates the input parameters of the get_dao method."""
295
335
  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
- )
336
+ cls._validate_fields_to_index(dto_model=dto_model, indexes=indexes)
299
337
 
300
338
 
301
- class DaoFactoryProtocol(DaoFactoryBase, ABC):
339
+ class DaoFactoryProtocol(DaoFactoryBase, ABC, Generic[Index]):
302
340
  """A protocol describing a factory to produce Data Access Objects (DAO) objects
303
341
  that are enclosed in transactional scopes.
304
342
  """
@@ -309,7 +347,7 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
309
347
  name: str,
310
348
  dto_model: type[Dto],
311
349
  id_field: str,
312
- fields_to_index: Collection[str] | None = None,
350
+ indexes: Collection[Index] | None = None,
313
351
  ) -> Dao[Dto]:
314
352
  """Constructs a DAO for interacting with resources in a database.
315
353
 
@@ -322,9 +360,9 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
322
360
  id_field:
323
361
  The name of the field of the `dto_model` that serves as resource ID.
324
362
  (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.
363
+ indexes:
364
+ Optionally, provide any indexes that should be created in addition to
365
+ the provider's default index on `id_field`. Defaults to None.
328
366
  Returns:
329
367
  A DAO specific to the provided DTO model.
330
368
 
@@ -334,17 +372,13 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
334
372
  self.IdTypeNotSupportedError:
335
373
  Raised when the id_field of the dto_model has an unexpected type.
336
374
  """
337
- self._validate(
338
- dto_model=dto_model,
339
- id_field=id_field,
340
- fields_to_index=fields_to_index,
341
- )
375
+ self._validate(dto_model=dto_model, id_field=id_field, indexes=indexes)
342
376
 
343
377
  return await self._get_dao(
344
378
  name=name,
345
379
  dto_model=dto_model,
346
380
  id_field=id_field,
347
- fields_to_index=fields_to_index,
381
+ indexes=indexes,
348
382
  )
349
383
 
350
384
  @abstractmethod
@@ -354,7 +388,7 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
354
388
  name: str,
355
389
  dto_model: type[Dto],
356
390
  id_field: str,
357
- fields_to_index: Collection[str] | None,
391
+ indexes: Collection[Index] | None,
358
392
  ) -> Dao[Dto]:
359
393
  """*To be implemented by the provider. Input validation is done outside of this
360
394
  method.*
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -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,
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -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
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,4 +1,4 @@
1
- # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
1
+ # Copyright 2021 - 2026 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
2
  # for the German Human Genome-Phenome Archive (GHGA)
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");