hexkit 3.8.0__tar.gz → 4.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 (57) hide show
  1. {hexkit-3.8.0/src/hexkit.egg-info → hexkit-4.0.0}/PKG-INFO +10 -10
  2. {hexkit-3.8.0 → hexkit-4.0.0}/pyproject.toml +11 -9
  3. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/custom_types.py +23 -5
  4. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/protocols/dao.py +77 -220
  5. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/protocols/daopub.py +4 -3
  6. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/protocols/eventpub.py +34 -11
  7. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/protocols/eventsub.py +75 -0
  8. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/protocols/objstorage.py +1 -2
  9. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/akafka/config.py +40 -5
  10. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/akafka/provider/__init__.py +4 -4
  11. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/akafka/provider/daosub.py +18 -3
  12. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/akafka/provider/eventpub.py +49 -13
  13. hexkit-4.0.0/src/hexkit/providers/akafka/provider/eventsub.py +553 -0
  14. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/akafka/testcontainer.py +29 -65
  15. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/akafka/testutils.py +27 -13
  16. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/mongodb/provider.py +138 -191
  17. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/mongodb/testutils.py +6 -7
  18. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/mongokafka/provider.py +106 -62
  19. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/s3/provider.py +15 -3
  20. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/s3/testutils/_fixtures.py +12 -9
  21. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/testing/eventpub.py +9 -2
  22. {hexkit-3.8.0 → hexkit-4.0.0/src/hexkit.egg-info}/PKG-INFO +10 -10
  23. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit.egg-info/requires.txt +8 -8
  24. hexkit-3.8.0/src/hexkit/providers/akafka/provider/eventsub.py +0 -268
  25. {hexkit-3.8.0 → hexkit-4.0.0}/LICENSE +0 -0
  26. {hexkit-3.8.0 → hexkit-4.0.0}/MANIFEST.in +0 -0
  27. {hexkit-3.8.0 → hexkit-4.0.0}/README.md +0 -0
  28. {hexkit-3.8.0 → hexkit-4.0.0}/setup.cfg +0 -0
  29. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/__init__.py +0 -0
  30. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/__main__.py +0 -0
  31. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/base.py +0 -0
  32. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/config.py +0 -0
  33. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/correlation.py +4 -4
  34. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/log.py +0 -0
  35. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/protocols/__init__.py +0 -0
  36. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/protocols/daosub.py +0 -0
  37. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/__init__.py +0 -0
  38. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/akafka/__init__.py +1 -1
  39. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/akafka/provider/utils.py +0 -0
  40. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/mongodb/__init__.py +0 -0
  41. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/mongokafka/__init__.py +0 -0
  42. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/mongokafka/testutils.py +0 -0
  43. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/s3/__init__.py +0 -0
  44. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
  45. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
  46. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
  47. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
  48. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
  49. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/s3/testutils/__init__.py +13 -13
  50. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/s3/testutils/_typical_workflow.py +0 -0
  51. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/s3/testutils/_utils.py +0 -0
  52. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/providers/testing/__init__.py +0 -0
  53. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/py.typed +0 -0
  54. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit/utils.py +0 -0
  55. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit.egg-info/SOURCES.txt +0 -0
  56. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit.egg-info/dependency_links.txt +0 -0
  57. {hexkit-3.8.0 → hexkit-4.0.0}/src/hexkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: hexkit
3
- Version: 3.8.0
3
+ Version: 4.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
@@ -20,24 +20,24 @@ Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
21
  Requires-Dist: pydantic<3,>=2
22
22
  Requires-Dist: pydantic_settings<3,>=2
23
- Requires-Dist: PyYAML<7,>=6.0
23
+ Requires-Dist: PyYAML<7,>=6
24
24
  Provides-Extra: akafka
25
- Requires-Dist: aiokafka~=0.11.0; extra == "akafka"
26
- Requires-Dist: jsonschema<5,>=4.22; extra == "akafka"
25
+ Requires-Dist: aiokafka~=0.12.0; extra == "akafka"
26
+ Requires-Dist: jsonschema<5,>=4.23; extra == "akafka"
27
27
  Provides-Extra: s3
28
- Requires-Dist: boto3<2,>=1.34.140; extra == "s3"
29
- Requires-Dist: botocore<2,>=1.34.140; extra == "s3"
28
+ Requires-Dist: boto3<2,>=1.35.80; extra == "s3"
29
+ Requires-Dist: botocore<2,>=1.35.80; extra == "s3"
30
30
  Provides-Extra: mongodb
31
31
  Requires-Dist: motor<4,>=3.5.0; extra == "mongodb"
32
32
  Provides-Extra: test-akafka
33
33
  Requires-Dist: hexkit[akafka]; extra == "test-akafka"
34
- Requires-Dist: testcontainers[kafka]<5,>=4.7.1; extra == "test-akafka"
34
+ Requires-Dist: testcontainers[kafka]<5,>=4.9.0; extra == "test-akafka"
35
35
  Provides-Extra: test-s3
36
36
  Requires-Dist: hexkit[s3]; extra == "test-s3"
37
- Requires-Dist: testcontainers<5,>=4.7.1; extra == "test-s3"
37
+ Requires-Dist: testcontainers<5,>=4.9.0; extra == "test-s3"
38
38
  Provides-Extra: test-mongodb
39
39
  Requires-Dist: hexkit[mongodb]; extra == "test-mongodb"
40
- Requires-Dist: testcontainers[mongo]<5,>=4.7.1; extra == "test-mongodb"
40
+ Requires-Dist: testcontainers[mongo]<5,>=4.9.0; extra == "test-mongodb"
41
41
  Provides-Extra: test
42
42
  Requires-Dist: hexkit[test-akafka,test-mongodb,test-s3]; extra == "test"
43
43
  Provides-Extra: all
@@ -23,12 +23,12 @@ classifiers = [
23
23
  "Intended Audience :: Developers",
24
24
  ]
25
25
  name = "hexkit"
26
- version = "3.8.0"
26
+ version = "4.0.0"
27
27
  description = "A Toolkit for Building Microservices using the Hexagonal Architecture"
28
28
  dependencies = [
29
29
  "pydantic >=2, <3",
30
30
  "pydantic_settings >=2, <3",
31
- "PyYAML >=6.0, <7",
31
+ "PyYAML >=6, <7",
32
32
  ]
33
33
 
34
34
  [project.license]
@@ -36,27 +36,27 @@ text = "Apache 2.0"
36
36
 
37
37
  [project.optional-dependencies]
38
38
  akafka = [
39
- "aiokafka~=0.11.0",
40
- "jsonschema >=4.22, <5",
39
+ "aiokafka~=0.12.0",
40
+ "jsonschema >=4.23, <5",
41
41
  ]
42
42
  s3 = [
43
- "boto3 >=1.34.140, <2",
44
- "botocore >=1.34.140, <2",
43
+ "boto3 >=1.35.80, <2",
44
+ "botocore >=1.35.80, <2",
45
45
  ]
46
46
  mongodb = [
47
47
  "motor >=3.5.0, <4",
48
48
  ]
49
49
  test-akafka = [
50
50
  "hexkit[akafka]",
51
- "testcontainers[kafka] >=4.7.1, <5",
51
+ "testcontainers[kafka] >=4.9.0, <5",
52
52
  ]
53
53
  test-s3 = [
54
54
  "hexkit[s3]",
55
- "testcontainers >=4.7.1, <5",
55
+ "testcontainers >=4.9.0, <5",
56
56
  ]
57
57
  test-mongodb = [
58
58
  "hexkit[mongodb]",
59
- "testcontainers[mongo] >=4.7.1, <5",
59
+ "testcontainers[mongo] >=4.9.0, <5",
60
60
  ]
61
61
  test = [
62
62
  "hexkit[test-akafka,test-s3,test-mongodb]",
@@ -95,6 +95,7 @@ fixable = [
95
95
  "UP",
96
96
  "I",
97
97
  "D",
98
+ "RUF022",
98
99
  ]
99
100
  ignore = [
100
101
  "E111",
@@ -114,6 +115,7 @@ ignore = [
114
115
  "D206",
115
116
  "D300",
116
117
  "UP040",
118
+ "PLC0206",
117
119
  ]
118
120
  select = [
119
121
  "C90",
@@ -17,22 +17,40 @@
17
17
  """Module collecting custom types."""
18
18
 
19
19
  from collections.abc import Mapping, Sequence
20
+ from datetime import date, datetime
20
21
  from typing import Any, Literal, Union
22
+ from uuid import UUID
21
23
 
22
- # This is intended to type objects that could be a potential output of `json.loads`.
23
- # (Scalar types as well as arrays are excluded from the above assumption.)
24
+ # A type for fields that can be used as identifiers (primary keys).
25
+ ID = Union[int, str, UUID]
26
+
27
+ # This is intended to type objects that are assumed to be JSON serializable.
28
+ # (Scalar types as well as arrays are excluded from the above assumption,
29
+ # and we serialize date, datetime and UUID objects to JSON as strings.)
24
30
  JsonObject = Mapping[
25
- str, Union[int, float, str, bool, Sequence[Any], Mapping[str, Any], None]
31
+ str,
32
+ Union[
33
+ int,
34
+ float,
35
+ str,
36
+ bool,
37
+ date,
38
+ datetime,
39
+ UUID,
40
+ Sequence[Any],
41
+ Mapping[str, Any],
42
+ None,
43
+ ],
26
44
  ]
27
45
 
28
46
 
29
47
  # A type indicating that a string should be ascii-compatible.
30
- # Technically it is an alias for `str` so it only serves documention purposes.
48
+ # Technically it is an alias for `str` so it only serves documentation purposes.
31
49
  Ascii = str
32
50
 
33
51
 
34
52
  # A AsyncConstructable is a class with a async (class-)method `construct` that is used when
35
- # asynchronous constuction/instantiation logic is needed (which cannot be handeled in
53
+ # asynchronous construction/instantiation logic is needed (which cannot be handled in
36
54
  # a synchronous __init__ method).
37
55
  # With the current typing features of Python, it seems not possible to correctly type
38
56
  # a class with that signature.
@@ -21,53 +21,53 @@ with the database.
21
21
  # ruff: noqa: PLR0913
22
22
 
23
23
  import typing
24
- import warnings
25
24
  from abc import ABC, abstractmethod
26
- from collections.abc import AsyncGenerator, AsyncIterator, Collection, Mapping
25
+ from collections.abc import AsyncIterator, Collection, Mapping
27
26
  from contextlib import AbstractAsyncContextManager
28
- from copy import copy
29
- from typing import Any, Optional, TypeVar, Union, overload
27
+ from functools import partial
28
+ from typing import Any, Optional, TypeVar
30
29
  from uuid import uuid4
31
30
 
32
- from pydantic import BaseModel
33
- from typing_extensions import deprecated
31
+ from pydantic import BaseModel, Field
34
32
 
33
+ from hexkit.custom_types import ID
35
34
  from hexkit.utils import FieldNotInModelError, validate_fields_in_model
36
35
 
37
36
  __all__ = [
38
- "ResourceNotFoundError",
39
- "ResourceAlreadyExistsError",
37
+ "Dao",
38
+ "DaoFactoryProtocol",
40
39
  "FindError",
41
40
  "MultipleHitsFoundError",
42
- "DaoNaturalId",
43
- "DaoSurrogateId",
44
- "DaoFactoryProtocol",
45
- "uuid4_id_generator",
41
+ "ResourceAlreadyExistsError",
42
+ "ResourceNotFoundError",
43
+ "UUID4Field",
46
44
  ]
47
45
 
48
- # Type variables for handling Data Transfer Objects:
46
+ # Type variable for handling Data Transfer Objects:
49
47
  Dto = TypeVar("Dto", bound=BaseModel)
50
- DtoCreation = TypeVar("DtoCreation", bound=BaseModel)
51
- DtoCreation_contra = TypeVar("DtoCreation_contra", bound=BaseModel, contravariant=True)
52
48
 
53
49
 
54
- class ResourceNotFoundError(RuntimeError):
50
+ class DaoError(RuntimeError):
51
+ """Base for all errors related to DAO operations."""
52
+
53
+
54
+ class ResourceNotFoundError(DaoError):
55
55
  """Raised when a requested resource did not exist."""
56
56
 
57
- def __init__(self, *, id_: str):
57
+ def __init__(self, *, id_: ID):
58
58
  message = f'The resource with the id "{id_}" does not exist.'
59
59
  super().__init__(message)
60
60
 
61
61
 
62
- class ResourceAlreadyExistsError(RuntimeError):
62
+ class ResourceAlreadyExistsError(DaoError):
63
63
  """Raised when a resource did unexpectedly exist."""
64
64
 
65
- def __init__(self, *, id_: str):
65
+ def __init__(self, *, id_: ID):
66
66
  message = f'The resource with the id "{id_}" already exists.'
67
67
  super().__init__(message)
68
68
 
69
69
 
70
- class FindError(RuntimeError):
70
+ class FindError(DaoError):
71
71
  """Base for all error related to DAO find operations."""
72
72
 
73
73
 
@@ -101,12 +101,35 @@ class NoHitsFoundError(FindError):
101
101
  super().__init__(message)
102
102
 
103
103
 
104
- class DaoCommons(typing.Protocol[Dto]):
105
- """A duck type with methods common to all DAOs. This shall be used as base class for
106
- other DAO duck types.
107
- """
104
+ class DbTimeoutError(DaoError):
105
+ """Raised when a database operation timed out."""
106
+
107
+ def __init__(self, details: str):
108
+ message = f"The database operation timed out: {details}"
109
+ super().__init__(message)
110
+
108
111
 
109
- async def get_by_id(self, id_: str) -> Dto:
112
+ # provide standardized default factory for UUID4 fields
113
+ UUID4Field = partial(Field, default_factory=uuid4)
114
+
115
+
116
+ class Dao(typing.Protocol[Dto]):
117
+ """A duck type with methods common to all DAOs."""
118
+
119
+ @classmethod
120
+ def with_transaction(cls) -> AbstractAsyncContextManager["Dao[Dto]"]:
121
+ """Creates a transaction manager that uses an async context manager interface:
122
+
123
+ Upon __aenter__, pens a new transactional scope. Returns a transaction-scoped
124
+ DAO.
125
+
126
+ Upon __aexit__, closes the transactional scope. A full rollback of the
127
+ transaction is performed in case of an exception. Otherwise, the changes to the
128
+ database are committed and flushed.
129
+ """
130
+ ...
131
+
132
+ async def get_by_id(self, id_: ID) -> Dto:
110
133
  """Get a resource by providing its ID.
111
134
 
112
135
  Args:
@@ -134,7 +157,7 @@ class DaoCommons(typing.Protocol[Dto]):
134
157
  """
135
158
  ...
136
159
 
137
- async def delete(self, id_: str) -> None:
160
+ async def delete(self, id_: ID) -> None:
138
161
  """Delete a resource by providing its ID.
139
162
 
140
163
  Args:
@@ -146,9 +169,15 @@ class DaoCommons(typing.Protocol[Dto]):
146
169
  ...
147
170
 
148
171
  async def find_one(self, *, mapping: Mapping[str, Any]) -> Dto:
149
- """Find the resource that matches the specified mapping. It is expected that
150
- at most one resource matches the constraints. An exception is raised if no or
151
- multiple hits are found.
172
+ """Find the resource that matches the specified mapping.
173
+
174
+ It is expected that at most one resource matches the constraints.
175
+ An exception is raised if no or multiple hits are found.
176
+
177
+ The values in the mapping are used to filter the resources, these are
178
+ assumed to be standard JSON scalar types. Particularly, UUIDs and datetimes
179
+ must be represented as strings. The behavior for non-scalars types depends
180
+ on the specific provider.
152
181
 
153
182
  Args:
154
183
  mapping:
@@ -170,6 +199,11 @@ class DaoCommons(typing.Protocol[Dto]):
170
199
  def find_all(self, *, mapping: Mapping[str, Any]) -> AsyncIterator[Dto]:
171
200
  """Find all resources that match the specified mapping.
172
201
 
202
+ The values in the mapping are used to filter the resources, these are
203
+ assumed to be standard JSON scalar types. Particularly, UUIDs and datetimes
204
+ must be represented as strings. The behavior for non-scalar types depends
205
+ on the specific provider.
206
+
173
207
  Args:
174
208
  mapping:
175
209
  A mapping where the keys correspond to the names of resource fields
@@ -181,61 +215,6 @@ class DaoCommons(typing.Protocol[Dto]):
181
215
  """
182
216
  ...
183
217
 
184
-
185
- class DaoSurrogateId(DaoCommons[Dto], typing.Protocol[Dto, DtoCreation_contra]):
186
- """A duck type of a DAO that generates an internal/surrogate key for
187
- identifying resources in the database. ID/keys cannot be defined by the client of
188
- the DAO. Thus, both a standard DTO model (first type parameter), which includes
189
- the key field, as well as special DTO model (second type parameter), which is
190
- identical to the first one, but does not include the ID field and is dedicated for
191
- creation of new resources.
192
- """
193
-
194
- @classmethod
195
- def with_transaction(
196
- cls,
197
- ) -> AbstractAsyncContextManager["DaoSurrogateId[Dto, DtoCreation_contra]"]:
198
- """Creates a transaction manager that uses an async context manager interface:
199
-
200
- Upon __aenter__, pens a new transactional scope. Returns a transaction-scoped
201
- DAO.
202
-
203
- Upon __aexit__, closes the transactional scope. A full rollback of the
204
- transaction is performed in case of an exception. Otherwise, the changes to the
205
- database are committed and flushed.
206
- """
207
- ...
208
-
209
- async def insert(self, dto: DtoCreation_contra) -> Dto:
210
- """Create a new resource.
211
-
212
- Args:
213
- dto:
214
- Resource content as a pydantic-based data transfer object without the
215
- resource ID (which will be set automatically).
216
-
217
- Returns:
218
- Returns a copy of the newly inserted resource including its assigned ID.
219
- """
220
- ...
221
-
222
-
223
- class DaoNaturalId(DaoCommons[Dto], typing.Protocol[Dto]):
224
- """A duck type of a DAO that uses a natural resource ID provided by the client."""
225
-
226
- @classmethod
227
- def with_transaction(cls) -> AbstractAsyncContextManager["DaoNaturalId[Dto]"]:
228
- """Creates a transaction manager that uses an async context manager interface:
229
-
230
- Upon __aenter__, pens a new transactional scope. Returns a transaction-scoped
231
- DAO.
232
-
233
- Upon __aexit__, closes the transactional scope. A full rollback of the
234
- transaction is performed in case of an exception. Otherwise, the changes to the
235
- database are committed and flushed.
236
- """
237
- ...
238
-
239
218
  async def insert(self, dto: Dto) -> None:
240
219
  """Create a new resource.
241
220
 
@@ -261,28 +240,14 @@ class DaoNaturalId(DaoCommons[Dto], typing.Protocol[Dto]):
261
240
  ...
262
241
 
263
242
 
264
- @deprecated(
265
- "uuid4_id_generator is deprecated as of v3.6 and will be removed in hexkit v4."
266
- )
267
- async def uuid4_id_generator() -> AsyncGenerator[str, None]:
268
- """Generates a new ID using the UUID4 algorithm.
269
- This is an AsyncGenerator to be compliant with the id_generator requirements of the
270
- DaoFactoryProtocol.
271
- """
272
- while True:
273
- yield str(uuid4())
274
-
275
-
276
243
  class DaoFactoryBase:
277
244
  """A base for Data Access Objects (DAO) Factory protocols."""
278
245
 
279
- class IdFieldNotFoundError(ValueError):
246
+ class IdFieldNotFoundError(TypeError):
280
247
  """Raised when the dto_model did not contain the expected id_field."""
281
248
 
282
- class CreationModelInvalidError(ValueError):
283
- """Raised when the DtoCreationModel was invalid in relation to the main
284
- DTO model.
285
- """
249
+ class IdTypeNotSupportedError(TypeError):
250
+ """Raised when the id_field of the dto_model has an unexpected Type."""
286
251
 
287
252
  class IndexFieldsInvalidError(ValueError):
288
253
  """Raised when providing an invalid list of fields to index."""
@@ -292,31 +257,13 @@ class DaoFactoryBase:
292
257
  """Checks whether the dto_model contains the expected id_field.
293
258
  Raises IdFieldNotFoundError otherwise.
294
259
  """
295
- if id_field not in dto_model.model_json_schema()["properties"]:
260
+ properties = dto_model.model_json_schema()["properties"]
261
+ schema = properties.get(id_field)
262
+ if schema is None:
296
263
  raise cls.IdFieldNotFoundError()
297
-
298
- @classmethod
299
- def _validate_dto_creation_model(
300
- cls,
301
- *,
302
- dto_model: type[Dto],
303
- dto_creation_model: Optional[type[DtoCreation]],
304
- id_field: str,
305
- ) -> None:
306
- """Checks that the dto_creation_model has the same fields as the dto_model
307
- except missing the ID. Raises CreationModelInvalidError otherwise.
308
- """
309
- if dto_creation_model is None:
310
- return
311
-
312
- expected_properties = copy(dto_model.model_json_schema()["properties"])
313
- # (the schema method returns an attribute of the class, making a copy to not
314
- # alter the class)
315
- del expected_properties[id_field]
316
- observed_properties = dto_creation_model.model_json_schema()["properties"]
317
-
318
- if observed_properties != expected_properties:
319
- raise cls.CreationModelInvalidError()
264
+ id_type = schema.get("type")
265
+ if id_type not in ("integer", "string"):
266
+ raise cls.IdTypeNotSupportedError()
320
267
 
321
268
  @classmethod
322
269
  def _validate_fields_to_index(
@@ -344,16 +291,10 @@ class DaoFactoryBase:
344
291
  *,
345
292
  dto_model: type[Dto],
346
293
  id_field: str,
347
- dto_creation_model: Optional[type[DtoCreation]],
348
294
  fields_to_index: Optional[Collection[str]],
349
295
  ) -> None:
350
296
  """Validates the input parameters of the get_dao method."""
351
297
  cls._validate_dto_model_id(dto_model=dto_model, id_field=id_field)
352
- cls._validate_dto_creation_model(
353
- dto_model=dto_model,
354
- dto_creation_model=dto_creation_model,
355
- id_field=id_field,
356
- )
357
298
  cls._validate_fields_to_index(
358
299
  dto_model=dto_model, fields_to_index=fields_to_index
359
300
  )
@@ -364,7 +305,6 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
364
305
  that are enclosed in transactional scopes.
365
306
  """
366
307
 
367
- @overload
368
308
  async def get_dao(
369
309
  self,
370
310
  *,
@@ -372,31 +312,7 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
372
312
  dto_model: type[Dto],
373
313
  id_field: str,
374
314
  fields_to_index: Optional[Collection[str]] = None,
375
- id_generator: Optional[AsyncGenerator[str, None]] = None,
376
- ) -> DaoNaturalId[Dto]: ...
377
-
378
- @overload
379
- async def get_dao(
380
- self,
381
- *,
382
- name: str,
383
- dto_model: type[Dto],
384
- id_field: str,
385
- dto_creation_model: type[DtoCreation],
386
- fields_to_index: Optional[Collection[str]] = None,
387
- id_generator: Optional[AsyncGenerator[str, None]] = None,
388
- ) -> DaoSurrogateId[Dto, DtoCreation]: ...
389
-
390
- async def get_dao(
391
- self,
392
- *,
393
- name: str,
394
- dto_model: type[Dto],
395
- id_field: str,
396
- dto_creation_model: Optional[type[DtoCreation]] = None,
397
- fields_to_index: Optional[Collection[str]] = None,
398
- id_generator: Optional[AsyncGenerator[str, None]] = None,
399
- ) -> Union[DaoSurrogateId[Dto, DtoCreation], DaoNaturalId[Dto]]:
315
+ ) -> Dao[Dto]:
400
316
  """Constructs a DAO for interacting with resources in a database.
401
317
 
402
318
  Args:
@@ -411,85 +327,28 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
411
327
  fields_to_index:
412
328
  Optionally, provide any fields that should be indexed in addition to the
413
329
  `id_field`. Defaults to None.
414
- dto_creation_model:
415
- An optional DTO model specific for creation of a new resource. This
416
- model has to be identical to the `dto_model` except that it has to miss
417
- the `id_field`. If specified, the resource ID will be generated by the DAO
418
- implementation upon resource creation. Otherwise (if set to None), resource IDs
419
- have to be specified upon resource creation. Defaults to None.
420
- id_generator:
421
- A generator that yields strings that will be used as IDs when creating
422
- new resources. Please note, each ID should be unique. Moreover, the
423
- generator should never exhaust.
424
- By default a UUID4-based generator is used.
425
330
  Returns:
426
- If a dedicated `dto_creation_model` is specified, a DAO of type
427
- DaoSurrogateID, which autogenerates IDs upon resource creation, is returned.
428
- Otherwise, returns a DAO of type DaoNaturalId, which require ID
429
- specification upon resource creation.
331
+ A DAO specific to the provided DTO model.
430
332
 
431
333
  Raises:
432
- self.CreationModelInvalidError:
433
- Raised when the DtoCreationModel was invalid in relation to the main
434
- DTO model.
435
334
  self.IdFieldNotFoundError:
436
335
  Raised when the dto_model did not contain the expected id_field.
336
+ self.IdTypeNotSupportedError:
337
+ Raised when the id_field of the dto_model has an unexpected type.
437
338
  """
438
339
  self._validate(
439
340
  dto_model=dto_model,
440
341
  id_field=id_field,
441
- dto_creation_model=dto_creation_model,
442
342
  fields_to_index=fields_to_index,
443
343
  )
444
344
 
445
- if id_generator is None:
446
- # instantiate the default ID generator:
447
- id_generator = uuid4_id_generator()
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
-
458
345
  return await self._get_dao(
459
346
  name=name,
460
347
  dto_model=dto_model,
461
348
  id_field=id_field,
462
349
  fields_to_index=fields_to_index,
463
- dto_creation_model=dto_creation_model,
464
- id_generator=id_generator,
465
350
  )
466
351
 
467
- @overload
468
- @abstractmethod
469
- async def _get_dao(
470
- self,
471
- *,
472
- name: str,
473
- dto_model: type[Dto],
474
- id_field: str,
475
- dto_creation_model: None,
476
- fields_to_index: Optional[Collection[str]],
477
- id_generator: AsyncGenerator[str, None],
478
- ) -> DaoNaturalId[Dto]: ...
479
-
480
- @overload
481
- @abstractmethod
482
- async def _get_dao(
483
- self,
484
- *,
485
- name: str,
486
- dto_model: type[Dto],
487
- id_field: str,
488
- dto_creation_model: type[DtoCreation],
489
- fields_to_index: Optional[Collection[str]],
490
- id_generator: AsyncGenerator[str, None],
491
- ) -> DaoSurrogateId[Dto, DtoCreation]: ...
492
-
493
352
  @abstractmethod
494
353
  async def _get_dao(
495
354
  self,
@@ -497,10 +356,8 @@ class DaoFactoryProtocol(DaoFactoryBase, ABC):
497
356
  name: str,
498
357
  dto_model: type[Dto],
499
358
  id_field: str,
500
- dto_creation_model: Optional[type[DtoCreation]],
501
359
  fields_to_index: Optional[Collection[str]],
502
- id_generator: AsyncGenerator[str, None],
503
- ) -> Union[DaoSurrogateId[Dto, DtoCreation], DaoNaturalId[Dto]]:
360
+ ) -> Dao[Dto]:
504
361
  """*To be implemented by the provider. Input validation is done outside of this
505
362
  method.*
506
363
  """
@@ -28,13 +28,13 @@ from typing import Callable, Optional
28
28
 
29
29
  from hexkit.custom_types import JsonObject
30
30
  from hexkit.protocols.dao import (
31
+ Dao,
31
32
  DaoFactoryBase,
32
- DaoNaturalId,
33
33
  Dto,
34
34
  )
35
35
 
36
36
 
37
- class DaoPublisher(DaoNaturalId[Dto], typing.Protocol[Dto]):
37
+ class DaoPublisher(Dao[Dto], typing.Protocol[Dto]):
38
38
  """A Data Access Object (DAO) that automatically publishes changes according to the
39
39
  outbox pattern.
40
40
  """
@@ -95,11 +95,12 @@ class DaoPublisherFactoryProtocol(DaoFactoryBase, ABC):
95
95
  Raises:
96
96
  self.IdFieldNotFoundError:
97
97
  Raised when the dto_model did not contain the expected id_field.
98
+ self.IdTypeNotSupportedError:
99
+ Raised when the id_field of the dto_model has an unexpected type.
98
100
  """
99
101
  self._validate(
100
102
  dto_model=dto_model,
101
103
  id_field=id_field,
102
- dto_creation_model=None,
103
104
  fields_to_index=fields_to_index,
104
105
  )
105
106