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.
- {hexkit-8.3.0/src/hexkit.egg-info → hexkit-8.4.1}/PKG-INFO +1 -1
- {hexkit-8.3.0 → hexkit-8.4.1}/pyproject.toml +1 -1
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/log.py +2 -2
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/protocols/dao.py +8 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/config.py +2 -2
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongokafka/provider/daopub.py +14 -2
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/redis/config.py +1 -1
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/config.py +7 -5
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/testutils/_fixtures.py +1 -1
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/testing/dao.py +24 -4
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/vault/config.py +2 -2
- {hexkit-8.3.0 → hexkit-8.4.1/src/hexkit.egg-info}/PKG-INFO +1 -1
- {hexkit-8.3.0 → hexkit-8.4.1}/LICENSE +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/MANIFEST.in +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/README.md +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/setup.cfg +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/__init__.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/__main__.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/base.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/config.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/correlation.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/custom_types.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/opentelemetry.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/protocols/__init__.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/protocols/daopub.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/protocols/daosub.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/protocols/eventpub.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/protocols/eventsub.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/protocols/kvstore.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/protocols/objstorage.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/__init__.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/akafka/__init__.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/akafka/config.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/akafka/provider/__init__.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/akafka/provider/daosub.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/akafka/provider/eventpub.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/akafka/provider/eventsub.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/akafka/provider/utils.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/akafka/testcontainer.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/akafka/testutils.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/__init__.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/migrations/__init__.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/migrations/_manager.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/migrations/_utils.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/migrations/helpers.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/provider/__init__.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/provider/client.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/provider/dao.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/provider/kvstore.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/provider/utils.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongodb/testutils.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongokafka/__init__.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongokafka/config.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongokafka/provider/__init__.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongokafka/provider/persistent_pub.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/mongokafka/testutils.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/redis/__init__.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/redis/provider/__init__.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/redis/provider/client.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/redis/provider/kvstore.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/redis/testutils.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/__init__.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/provider/__init__.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/provider/kvstore.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/provider/objstorage.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/testutils/__init__.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/testutils/_typical_workflow.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/s3/testutils/_utils.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/testing/__init__.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/testing/eventpub.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/testing/kvstore.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/testing/objstorage.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/vault/__init__.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/vault/provider/__init__.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/vault/provider/client.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/vault/provider/kvstore.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/providers/vault/testutils.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/py.typed +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit/utils.py +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit.egg-info/SOURCES.txt +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit.egg-info/dependency_links.txt +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit.egg-info/requires.txt +0 -0
- {hexkit-8.3.0 → hexkit-8.4.1}/src/hexkit.egg-info/top_level.txt +0 -0
|
@@ -23,7 +23,7 @@ classifiers = [
|
|
|
23
23
|
"Intended Audience :: Developers",
|
|
24
24
|
]
|
|
25
25
|
name = "hexkit"
|
|
26
|
-
version = "8.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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=
|
|
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."
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|