hexkit 8.3.0__tar.gz → 8.4.1__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 (88) hide show
  1. {hexkit-8.3.0/src/hexkit.egg-info → hexkit-8.4.1}/PKG-INFO +1 -1
  2. {hexkit-8.3.0 → hexkit-8.4.1}/pyproject.toml +1 -1
  3. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/log.py +2 -2
  4. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/protocols/dao.py +8 -0
  5. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/config.py +2 -2
  6. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongokafka/provider/daopub.py +14 -2
  7. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/redis/config.py +1 -1
  8. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/config.py +7 -5
  9. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/testutils/_fixtures.py +1 -1
  10. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/testing/dao.py +24 -4
  11. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/vault/config.py +2 -2
  12. {hexkit-8.3.0 → hexkit-8.4.1/src/hexkit.egg-info}/PKG-INFO +1 -1
  13. {hexkit-8.3.0 → hexkit-8.4.1}/LICENSE +0 -0
  14. {hexkit-8.3.0 → hexkit-8.4.1}/MANIFEST.in +0 -0
  15. {hexkit-8.3.0 → hexkit-8.4.1}/README.md +0 -0
  16. {hexkit-8.3.0 → hexkit-8.4.1}/setup.cfg +0 -0
  17. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/__init__.py +0 -0
  18. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/__main__.py +0 -0
  19. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/base.py +0 -0
  20. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/config.py +0 -0
  21. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/correlation.py +0 -0
  22. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/custom_types.py +0 -0
  23. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/opentelemetry.py +0 -0
  24. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/protocols/__init__.py +0 -0
  25. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/protocols/daopub.py +0 -0
  26. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/protocols/daosub.py +0 -0
  27. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/protocols/eventpub.py +0 -0
  28. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/protocols/eventsub.py +0 -0
  29. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/protocols/kvstore.py +0 -0
  30. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/protocols/objstorage.py +0 -0
  31. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/__init__.py +0 -0
  32. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/akafka/__init__.py +0 -0
  33. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/akafka/config.py +0 -0
  34. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/akafka/provider/__init__.py +0 -0
  35. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/akafka/provider/daosub.py +0 -0
  36. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/akafka/provider/eventpub.py +0 -0
  37. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/akafka/provider/eventsub.py +0 -0
  38. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/akafka/provider/utils.py +0 -0
  39. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/akafka/testcontainer.py +0 -0
  40. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/akafka/testutils.py +0 -0
  41. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/__init__.py +0 -0
  42. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/migrations/__init__.py +0 -0
  43. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/migrations/_manager.py +0 -0
  44. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/migrations/_utils.py +0 -0
  45. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/migrations/helpers.py +0 -0
  46. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/provider/__init__.py +0 -0
  47. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/provider/client.py +0 -0
  48. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/provider/dao.py +0 -0
  49. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/provider/kvstore.py +0 -0
  50. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/provider/utils.py +0 -0
  51. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/testutils.py +0 -0
  52. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongokafka/__init__.py +0 -0
  53. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongokafka/config.py +0 -0
  54. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongokafka/provider/__init__.py +0 -0
  55. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongokafka/provider/persistent_pub.py +0 -0
  56. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongokafka/testutils.py +0 -0
  57. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/redis/__init__.py +0 -0
  58. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/redis/provider/__init__.py +0 -0
  59. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/redis/provider/client.py +0 -0
  60. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/redis/provider/kvstore.py +0 -0
  61. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/redis/testutils.py +0 -0
  62. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/__init__.py +0 -0
  63. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/provider/__init__.py +0 -0
  64. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/provider/kvstore.py +0 -0
  65. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/provider/objstorage.py +0 -0
  66. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
  67. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
  68. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
  69. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
  70. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
  71. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/testutils/__init__.py +0 -0
  72. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/testutils/_typical_workflow.py +0 -0
  73. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/testutils/_utils.py +0 -0
  74. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/testing/__init__.py +0 -0
  75. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/testing/eventpub.py +0 -0
  76. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/testing/kvstore.py +0 -0
  77. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/testing/objstorage.py +0 -0
  78. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/vault/__init__.py +0 -0
  79. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/vault/provider/__init__.py +0 -0
  80. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/vault/provider/client.py +0 -0
  81. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/vault/provider/kvstore.py +0 -0
  82. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/vault/testutils.py +0 -0
  83. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/py.typed +0 -0
  84. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/utils.py +0 -0
  85. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit.egg-info/SOURCES.txt +0 -0
  86. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit.egg-info/dependency_links.txt +0 -0
  87. {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit.egg-info/requires.txt +0 -0
  88. {hexkit-8.3.0 → hexkit-8.4.1}/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: 8.3.0
3
+ Version: 8.4.1
4
4
  Summary: A Toolkit for Building Microservices using the Hexagonal Architecture
5
5
  Author-email: "German Human Genome Phenome Archive (GHGA)" <contact@ghga.de>
6
6
  License: Apache 2.0
@@ -23,7 +23,7 @@ classifiers = [
23
23
  "Intended Audience :: Developers",
24
24
  ]
25
25
  name = "hexkit"
26
- version = "8.3.0"
26
+ version = "8.4.1"
27
27
  description = "A Toolkit for Building Microservices using the Hexagonal Architecture"
28
28
  dependencies = [
29
29
  "opentelemetry-api >=1.39, <2",
@@ -49,12 +49,12 @@ class LoggingConfig(BaseSettings):
49
49
  default="INFO", description="The minimum log level to capture."
50
50
  )
51
51
  service_name: str = Field(
52
- ...,
52
+ default=...,
53
53
  examples=["my-cool-special-service"],
54
54
  description="The name of the (micro-)service. This will be included in log messages.",
55
55
  )
56
56
  service_instance_id: str = Field(
57
- ...,
57
+ default=...,
58
58
  examples=["germany-bw-instance-001"],
59
59
  description=(
60
60
  "A string that uniquely identifies this instance across all instances of"
@@ -170,6 +170,14 @@ class FindResult(AsyncIterator[Dto]):
170
170
  self._cached_total = await self._get_total_count()
171
171
  return self._cached_total
172
172
 
173
+ async def to_list(self) -> list[Dto]:
174
+ """Collect all results into a list.
175
+
176
+ Calling this function will consume the iterator. Subsequent cals will return
177
+ an empty list.
178
+ """
179
+ return [item async for item in self]
180
+
173
181
 
174
182
  class Dao(typing.Protocol[Dto]):
175
183
  """A duck type with methods common to all DAOs."""
@@ -29,7 +29,7 @@ class MongoDbConfig(BaseSettings):
29
29
  """
30
30
 
31
31
  mongo_dsn: Secret[MongoDsn] = Field(
32
- ...,
32
+ default=...,
33
33
  examples=["mongodb://localhost:27017"],
34
34
  description=(
35
35
  "MongoDB connection string. Might include credentials."
@@ -38,7 +38,7 @@ class MongoDbConfig(BaseSettings):
38
38
  ),
39
39
  )
40
40
  db_name: str = Field(
41
- ...,
41
+ default=...,
42
42
  examples=["my-database"],
43
43
  description="Name of the database located on the MongoDB server.",
44
44
  )
@@ -420,8 +420,20 @@ class MongoKafkaDaoPublisher(Generic[Dto]):
420
420
  validate_find_mapping(mapping, dto_model=self._dto_model)
421
421
  mapping = replace_id_field_in_find_mapping(mapping, self._id_field)
422
422
 
423
- # Ensure we don't retrieve deleted docs
424
- mapping_without_deleted = {**mapping, "__metadata__.deleted": False}
423
+ # Ensure we don't retrieve deleted docs. Documents lacking outbox metadata are
424
+ # treated as valid (matching update/delete), and the $and wrapper avoids
425
+ # clobbering any caller-supplied $or in the mapping.
426
+ mapping_without_deleted = {
427
+ "$and": [
428
+ dict(mapping),
429
+ {
430
+ "$or": [
431
+ {"__metadata__": {"$exists": False}},
432
+ {"__metadata__.deleted": False},
433
+ ]
434
+ },
435
+ ]
436
+ }
425
437
 
426
438
  # Convert generic sort spec to MongoDB-specific sort spec
427
439
  mongodb_sort: list[tuple[str, int]] = []
@@ -28,7 +28,7 @@ class RedisConfig(BaseSettings):
28
28
  """
29
29
 
30
30
  redis_url: Secret[RedisDsn] = Field(
31
- ...,
31
+ default=...,
32
32
  examples=["redis://localhost:6379/0", "redis://:password@localhost:6379/1"],
33
33
  description=(
34
34
  "Redis connection URL. Might include credentials and database selection."
@@ -31,10 +31,12 @@ class S3Config(BaseSettings):
31
31
  """
32
32
 
33
33
  s3_endpoint_url: str = Field(
34
- ..., examples=["http://localhost:4566"], description="URL to the S3 API."
34
+ default=...,
35
+ examples=["http://localhost:4566"],
36
+ description="URL to the S3 API.",
35
37
  )
36
38
  s3_access_key_id: str = Field(
37
- ...,
39
+ default=...,
38
40
  examples=["my-access-key-id"],
39
41
  description=(
40
42
  "Part of credentials for login into the S3 service. See:"
@@ -42,7 +44,7 @@ class S3Config(BaseSettings):
42
44
  ),
43
45
  )
44
46
  s3_secret_access_key: SecretStr = Field(
45
- ...,
47
+ default=...,
46
48
  examples=["my-secret-access-key"],
47
49
  description=(
48
50
  "Part of credentials for login into the S3 service. See:"
@@ -50,7 +52,7 @@ class S3Config(BaseSettings):
50
52
  ),
51
53
  )
52
54
  s3_session_token: SecretStr | None = Field(
53
- None,
55
+ default=None,
54
56
  examples=["my-session-token"],
55
57
  description=(
56
58
  "Part of credentials for login into the S3 service. See:"
@@ -58,7 +60,7 @@ class S3Config(BaseSettings):
58
60
  ),
59
61
  )
60
62
  aws_config_ini: Path | None = Field(
61
- None,
63
+ default=None,
62
64
  examples=["~/.aws/config"],
63
65
  description=(
64
66
  "Path to a config file for specifying more advanced S3 parameters."
@@ -241,7 +241,7 @@ class S3ContainerFixture(LocalStackContainer):
241
241
  # since IPVv6 can sometimes use a different port mapping,
242
242
  # which can shadow the IPv4 port mapping of a different container.
243
243
  url = url.replace("localhost", "127.0.0.1")
244
- s3_config = S3Config( # type: ignore [call-arg]
244
+ s3_config = S3Config(
245
245
  s3_endpoint_url=url,
246
246
  s3_access_key_id="test",
247
247
  s3_secret_access_key=SecretStr("test"),
@@ -16,10 +16,11 @@
16
16
  """A mock (in-memory) DAO"""
17
17
 
18
18
  from collections.abc import AsyncIterator, Callable, Mapping
19
- from contextlib import suppress
19
+ from contextlib import AbstractAsyncContextManager, suppress
20
20
  from copy import deepcopy
21
+ from functools import partial
21
22
  from typing import Any, Generic, Protocol, TypeVar
22
- from unittest.mock import AsyncMock, Mock
23
+ from unittest.mock import AsyncMock
23
24
 
24
25
  from pydantic import BaseModel
25
26
 
@@ -87,6 +88,18 @@ def _get_nested_value(resource: Document, field_path: str) -> Any:
87
88
  return value
88
89
 
89
90
 
91
+ def _sort_key(resource: Document, *, field_path: str) -> tuple[bool, Any]:
92
+ """Build a sort key for a (possibly nested) field path of a resource.
93
+
94
+ Missing values (None) are grouped together and sorted ahead of present values,
95
+ mirroring MongoDB's handling of missing/null fields. The leading boolean keeps
96
+ None values from being compared against present values (or one another), which
97
+ would otherwise raise a TypeError.
98
+ """
99
+ value = _get_nested_value(resource, field_path)
100
+ return (value is not None, value if value is not None else 0)
101
+
102
+
90
103
  class Predicate(Protocol):
91
104
  def evaluate(self, resource: dict[str, Any]) -> bool:
92
105
  """Determine if the supplied dict value satisfies the predicate."""
@@ -346,7 +359,10 @@ class BaseInMemDao(Generic[DTO]):
346
359
  _deserialize: Callable
347
360
  publish_pending = AsyncMock()
348
361
  republish = AsyncMock()
349
- with_transaction = Mock()
362
+
363
+ @classmethod
364
+ def with_transaction(cls) -> AbstractAsyncContextManager["BaseInMemDao[DTO]"]:
365
+ raise NotImplementedError()
350
366
 
351
367
  def __init__(self) -> None:
352
368
  self.resources: dict[ID, Document] = {}
@@ -437,8 +453,12 @@ class BaseInMemDao(Generic[DTO]):
437
453
  for spec in reversed(sort or []):
438
454
  desc = spec.startswith("-")
439
455
  field = spec.removeprefix("-")
456
+ # Only the exact ID field is translated to the internal "_id" field, matching
457
+ # the MongoDB provider. Other fields (including nested sub-fields of the ID,
458
+ # which MongoDB cannot sort by) are looked up as-is and resolve to None when
459
+ # absent, sorting consistently rather than raising - again like MongoDB.
440
460
  doc_field = "_id" if field == self._id_field else field
441
- matching.sort(key=lambda doc: doc[doc_field], reverse=desc)
461
+ matching.sort(key=partial(_sort_key, field_path=doc_field), reverse=desc)
442
462
 
443
463
  total = len(matching)
444
464
 
@@ -31,12 +31,12 @@ class VaultConfig(BaseSettings):
31
31
  """
32
32
 
33
33
  vault_url: str = Field(
34
- ...,
34
+ default=...,
35
35
  examples=["http://localhost:8200", "https://vault.example.com:8200"],
36
36
  description="URL of the Vault server.",
37
37
  )
38
38
  vault_path: str = Field(
39
- ...,
39
+ default=...,
40
40
  examples=["myapp/secrets", "production/config"],
41
41
  description=(
42
42
  "Path prefix for all keys stored in Vault."
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hexkit
3
- Version: 8.3.0
3
+ Version: 8.4.1
4
4
  Summary: A Toolkit for Building Microservices using the Hexagonal Architecture
5
5
  Author-email: "German Human Genome Phenome Archive (GHGA)" <contact@ghga.de>
6
6
  License: Apache 2.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes