hexkit 8.2.1__tar.gz → 8.3.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.
- {hexkit-8.2.1/src/hexkit.egg-info → hexkit-8.3.0}/PKG-INFO +1 -1
- {hexkit-8.2.1 → hexkit-8.3.0}/pyproject.toml +1 -1
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/protocols/dao.py +62 -4
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/mongodb/provider/dao.py +68 -8
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/mongokafka/provider/daopub.py +75 -11
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/testing/dao.py +65 -12
- {hexkit-8.2.1 → hexkit-8.3.0/src/hexkit.egg-info}/PKG-INFO +1 -1
- {hexkit-8.2.1 → hexkit-8.3.0}/LICENSE +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/MANIFEST.in +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/README.md +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/setup.cfg +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/__init__.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/__main__.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/base.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/config.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/correlation.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/custom_types.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/log.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/opentelemetry.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/protocols/__init__.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/protocols/daopub.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/protocols/daosub.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/protocols/eventpub.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/protocols/eventsub.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/protocols/kvstore.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/protocols/objstorage.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/__init__.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/akafka/__init__.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/akafka/config.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/akafka/provider/__init__.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/akafka/provider/daosub.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/akafka/provider/eventpub.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/akafka/provider/eventsub.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/akafka/provider/utils.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/akafka/testcontainer.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/akafka/testutils.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/mongodb/__init__.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/mongodb/config.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/mongodb/migrations/__init__.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/mongodb/migrations/_manager.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/mongodb/migrations/_utils.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/mongodb/migrations/helpers.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/mongodb/provider/__init__.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/mongodb/provider/client.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/mongodb/provider/kvstore.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/mongodb/provider/utils.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/mongodb/testutils.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/mongokafka/__init__.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/mongokafka/config.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/mongokafka/provider/__init__.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/mongokafka/provider/persistent_pub.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/mongokafka/testutils.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/redis/__init__.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/redis/config.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/redis/provider/__init__.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/redis/provider/client.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/redis/provider/kvstore.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/redis/testutils.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/s3/__init__.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/s3/config.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/s3/provider/__init__.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/s3/provider/kvstore.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/s3/provider/objstorage.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/s3/testutils/__init__.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/s3/testutils/_fixtures.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/s3/testutils/_typical_workflow.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/s3/testutils/_utils.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/testing/__init__.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/testing/eventpub.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/testing/kvstore.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/testing/objstorage.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/vault/__init__.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/vault/config.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/vault/provider/__init__.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/vault/provider/client.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/vault/provider/kvstore.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/providers/vault/testutils.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/py.typed +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit/utils.py +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit.egg-info/SOURCES.txt +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit.egg-info/dependency_links.txt +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/src/hexkit.egg-info/requires.txt +0 -0
- {hexkit-8.2.1 → hexkit-8.3.0}/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.3.0"
|
|
27
27
|
description = "A Toolkit for Building Microservices using the Hexagonal Architecture"
|
|
28
28
|
dependencies = [
|
|
29
29
|
"opentelemetry-api >=1.39, <2",
|
|
@@ -20,7 +20,7 @@ with the database.
|
|
|
20
20
|
|
|
21
21
|
import typing
|
|
22
22
|
from abc import ABC, abstractmethod
|
|
23
|
-
from collections.abc import AsyncIterator, Collection, Mapping
|
|
23
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Collection, Mapping
|
|
24
24
|
from contextlib import AbstractAsyncContextManager
|
|
25
25
|
from functools import partial
|
|
26
26
|
from typing import Any, Generic, TypeVar
|
|
@@ -35,6 +35,7 @@ __all__ = [
|
|
|
35
35
|
"Dao",
|
|
36
36
|
"DaoFactoryProtocol",
|
|
37
37
|
"FindError",
|
|
38
|
+
"FindResult",
|
|
38
39
|
"MultipleHitsFoundError",
|
|
39
40
|
"ResourceAlreadyExistsError",
|
|
40
41
|
"ResourceNotFoundError",
|
|
@@ -137,6 +138,39 @@ class DbTimeoutError(DaoError):
|
|
|
137
138
|
UUID4Field = partial(Field, default_factory=uuid4)
|
|
138
139
|
|
|
139
140
|
|
|
141
|
+
class FindResult(AsyncIterator[Dto]):
|
|
142
|
+
"""An async-iterable result from find_all() that also exposes pagination metadata.
|
|
143
|
+
|
|
144
|
+
The `total_count()` method performs an additional count query on first call (lazily)
|
|
145
|
+
and caches the result for subsequent calls.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(
|
|
149
|
+
self,
|
|
150
|
+
*,
|
|
151
|
+
results_iterator: AsyncIterator[Dto],
|
|
152
|
+
get_total_count: Callable[[], Awaitable[int]],
|
|
153
|
+
) -> None:
|
|
154
|
+
self._iterator = results_iterator
|
|
155
|
+
self._get_total_count = get_total_count
|
|
156
|
+
self._cached_total: int | None = None
|
|
157
|
+
|
|
158
|
+
def __aiter__(self) -> "FindResult[Dto]": # noqa: D105
|
|
159
|
+
return self
|
|
160
|
+
|
|
161
|
+
async def __anext__(self) -> Dto: # noqa: D105
|
|
162
|
+
return await self._iterator.__anext__()
|
|
163
|
+
|
|
164
|
+
async def total_count(self) -> int:
|
|
165
|
+
"""Total number of matching documents, ignoring skip and limit.
|
|
166
|
+
|
|
167
|
+
The result is cached after the first call.
|
|
168
|
+
"""
|
|
169
|
+
if self._cached_total is None:
|
|
170
|
+
self._cached_total = await self._get_total_count()
|
|
171
|
+
return self._cached_total
|
|
172
|
+
|
|
173
|
+
|
|
140
174
|
class Dao(typing.Protocol[Dto]):
|
|
141
175
|
"""A duck type with methods common to all DAOs."""
|
|
142
176
|
|
|
@@ -223,7 +257,14 @@ class Dao(typing.Protocol[Dto]):
|
|
|
223
257
|
"""
|
|
224
258
|
...
|
|
225
259
|
|
|
226
|
-
def find_all(
|
|
260
|
+
def find_all(
|
|
261
|
+
self,
|
|
262
|
+
*,
|
|
263
|
+
mapping: Mapping[str, Any],
|
|
264
|
+
skip: int | None = None,
|
|
265
|
+
limit: int | None = None,
|
|
266
|
+
sort: list[str] | None = None,
|
|
267
|
+
) -> "FindResult[Dto]":
|
|
227
268
|
"""Find all resources that match the specified mapping.
|
|
228
269
|
|
|
229
270
|
The values in the mapping are used to filter the resources, these are
|
|
@@ -235,10 +276,27 @@ class Dao(typing.Protocol[Dto]):
|
|
|
235
276
|
mapping:
|
|
236
277
|
A mapping where the keys correspond to the names of resource fields
|
|
237
278
|
and the values correspond to the actual values of the resource fields.
|
|
279
|
+
skip:
|
|
280
|
+
Number of matching resources to skip before yielding results.
|
|
281
|
+
Defaults to None (no skipping).
|
|
282
|
+
limit:
|
|
283
|
+
Maximum number of resources to yield. Defaults to None (no limit).
|
|
284
|
+
Specifying 0 is interpreted literally and returns no results.
|
|
285
|
+
sort:
|
|
286
|
+
A list of field names defining the sort order, where field names
|
|
287
|
+
prefixed with "-" indicate *descending* order. For example, if sort is
|
|
288
|
+
specified as `["name", "-age"]`, the results would be sorted first by
|
|
289
|
+
"name" in ascending order, then by "age" in descending order. When
|
|
290
|
+
paginating with skip/limit, providing a sort order is strongly
|
|
291
|
+
recommended to ensure consistent, deterministic results. Defaults to
|
|
292
|
+
None (no sort).
|
|
238
293
|
|
|
239
294
|
Returns:
|
|
240
|
-
|
|
241
|
-
|
|
295
|
+
A FindResult that is async-iterable and also provides total_count().
|
|
296
|
+
|
|
297
|
+
Raises:
|
|
298
|
+
InvalidMappingError: If `mapping` doesn't pass validation.
|
|
299
|
+
ValueError: if `skip` or `limit` are less than 0.
|
|
242
300
|
"""
|
|
243
301
|
...
|
|
244
302
|
|
|
@@ -33,6 +33,7 @@ from hexkit.protocols.dao import (
|
|
|
33
33
|
Dao,
|
|
34
34
|
DaoFactoryProtocol,
|
|
35
35
|
Dto,
|
|
36
|
+
FindResult,
|
|
36
37
|
IndexBase,
|
|
37
38
|
InvalidFindMappingError,
|
|
38
39
|
MultipleHitsFoundError,
|
|
@@ -65,7 +66,7 @@ def validate_find_mapping(mapping: Mapping[str, Any], *, dto_model: type[Dto]):
|
|
|
65
66
|
by checking if the mapping keys exist as model fields.
|
|
66
67
|
|
|
67
68
|
Raises:
|
|
68
|
-
InvalidMappingError: If validation
|
|
69
|
+
InvalidMappingError: If `mapping` doesn't pass validation.
|
|
69
70
|
"""
|
|
70
71
|
try:
|
|
71
72
|
validate_fields_in_model(model=dto_model, fields=set(mapping))
|
|
@@ -310,7 +311,14 @@ class MongoDbDao(Generic[Dto]):
|
|
|
310
311
|
hits = self.find_all(mapping=mapping)
|
|
311
312
|
return await get_single_hit(hits=hits, mapping=mapping)
|
|
312
313
|
|
|
313
|
-
|
|
314
|
+
def find_all( # noqa: C901
|
|
315
|
+
self,
|
|
316
|
+
*,
|
|
317
|
+
mapping: Mapping[str, Any],
|
|
318
|
+
skip: int | None = None,
|
|
319
|
+
limit: int | None = None,
|
|
320
|
+
sort: list[str] | None = None,
|
|
321
|
+
) -> FindResult[Dto]:
|
|
314
322
|
"""Find all resources that match the specified mapping.
|
|
315
323
|
|
|
316
324
|
The values in the mapping are used to filter the resources, these are
|
|
@@ -322,19 +330,71 @@ class MongoDbDao(Generic[Dto]):
|
|
|
322
330
|
mapping:
|
|
323
331
|
A mapping where the keys correspond to the names of resource fields
|
|
324
332
|
and the values correspond to the actual values of the resource fields.
|
|
333
|
+
skip:
|
|
334
|
+
Number of matching resources to skip before yielding results.
|
|
335
|
+
Defaults to None (no skipping).
|
|
336
|
+
limit:
|
|
337
|
+
Maximum number of resources to yield. Defaults to None (no limit).
|
|
338
|
+
Specifying 0 is interpreted literally and returns no results.
|
|
339
|
+
sort:
|
|
340
|
+
A list of field names defining the sort order, where field names
|
|
341
|
+
prefixed with "-" indicate *descending* order. For example, if sort is
|
|
342
|
+
specified as `["name", "-age"]`, the results would be sorted first by
|
|
343
|
+
"name" in ascending order, then by "age" in descending order. When
|
|
344
|
+
paginating with skip/limit, providing a sort order is strongly
|
|
345
|
+
recommended to ensure consistent, deterministic results. Defaults to
|
|
346
|
+
None (no sort).
|
|
325
347
|
|
|
326
348
|
Returns:
|
|
327
|
-
|
|
328
|
-
model.
|
|
349
|
+
A FindResult that is async-iterable and also provides total_count().
|
|
329
350
|
"""
|
|
351
|
+
skip = skip or 0
|
|
352
|
+
|
|
353
|
+
if skip < 0:
|
|
354
|
+
raise ValueError("skip must be >= 0")
|
|
355
|
+
if limit is not None and limit < 0:
|
|
356
|
+
raise ValueError("limit must be >= 0")
|
|
357
|
+
|
|
330
358
|
validate_find_mapping(mapping, dto_model=self._dto_model)
|
|
331
359
|
mapping = replace_id_field_in_find_mapping(mapping, self._id_field)
|
|
332
360
|
|
|
333
|
-
|
|
334
|
-
|
|
361
|
+
# Convert generic sort spec to MongoDB-specific sort spec
|
|
362
|
+
mongodb_sort: list[tuple[str, int]] = []
|
|
363
|
+
for spec in sort or []:
|
|
364
|
+
field = spec.removeprefix("-")
|
|
365
|
+
order = 1 if field == spec else -1
|
|
366
|
+
if field == self._id_field:
|
|
367
|
+
field = "_id"
|
|
368
|
+
mongodb_sort.append((field, order))
|
|
369
|
+
|
|
370
|
+
collection = self._collection
|
|
371
|
+
|
|
372
|
+
async def _total_count() -> int:
|
|
373
|
+
with translate_pymongo_errors():
|
|
374
|
+
return await collection.count_documents(filter=mapping)
|
|
375
|
+
|
|
376
|
+
if limit == 0:
|
|
377
|
+
|
|
378
|
+
async def _empty_iter() -> AsyncIterator[Dto]:
|
|
379
|
+
return
|
|
380
|
+
yield # turns this into an async generator
|
|
381
|
+
|
|
382
|
+
return FindResult(
|
|
383
|
+
results_iterator=_empty_iter(), get_total_count=_total_count
|
|
384
|
+
)
|
|
335
385
|
|
|
336
|
-
|
|
337
|
-
|
|
386
|
+
async def _iter() -> AsyncIterator[Dto]:
|
|
387
|
+
with translate_pymongo_errors():
|
|
388
|
+
cursor = collection.find(filter=mapping)
|
|
389
|
+
if sort:
|
|
390
|
+
cursor = cursor.sort(mongodb_sort)
|
|
391
|
+
cursor = cursor.skip(skip)
|
|
392
|
+
if limit:
|
|
393
|
+
cursor = cursor.limit(limit)
|
|
394
|
+
async for document in cursor:
|
|
395
|
+
yield self._document_to_dto(document)
|
|
396
|
+
|
|
397
|
+
return FindResult(results_iterator=_iter(), get_total_count=_total_count)
|
|
338
398
|
|
|
339
399
|
@classmethod
|
|
340
400
|
def with_transaction(cls) -> AbstractAsyncContextManager["Dao[Dto]"]:
|
|
@@ -42,6 +42,7 @@ from hexkit.custom_types import ID, JsonObject
|
|
|
42
42
|
from hexkit.protocols.dao import (
|
|
43
43
|
Dao,
|
|
44
44
|
Dto,
|
|
45
|
+
FindResult,
|
|
45
46
|
ResourceNotFoundError,
|
|
46
47
|
UniqueConstraintViolationError,
|
|
47
48
|
)
|
|
@@ -369,7 +370,14 @@ class MongoKafkaDaoPublisher(Generic[Dto]):
|
|
|
369
370
|
hits = self.find_all(mapping=mapping)
|
|
370
371
|
return await get_single_hit(hits=hits, mapping=mapping)
|
|
371
372
|
|
|
372
|
-
|
|
373
|
+
def find_all( # noqa: C901
|
|
374
|
+
self,
|
|
375
|
+
*,
|
|
376
|
+
mapping: Mapping[str, Any],
|
|
377
|
+
skip: int | None = None,
|
|
378
|
+
limit: int | None = None,
|
|
379
|
+
sort: list[str] | None = None,
|
|
380
|
+
) -> FindResult[Dto]:
|
|
373
381
|
"""Find all resources that match the specified mapping.
|
|
374
382
|
|
|
375
383
|
The values in the mapping are used to filter the resources, these are
|
|
@@ -381,23 +389,79 @@ class MongoKafkaDaoPublisher(Generic[Dto]):
|
|
|
381
389
|
mapping:
|
|
382
390
|
A mapping where the keys correspond to the names of resource fields
|
|
383
391
|
and the values correspond to the actual values of the resource fields.
|
|
392
|
+
skip:
|
|
393
|
+
Number of matching resources to skip before yielding results.
|
|
394
|
+
Defaults to None (no skipping).
|
|
395
|
+
limit:
|
|
396
|
+
Maximum number of resources to yield. Defaults to None (no limit).
|
|
397
|
+
Specifying 0 is interpreted literally and returns no results.
|
|
398
|
+
sort:
|
|
399
|
+
A list of field names defining the sort order, where field names
|
|
400
|
+
prefixed with "-" indicate *descending* order. For example, if sort is
|
|
401
|
+
specified as `["name", "-age"]`, the results would be sorted first by
|
|
402
|
+
"name" in ascending order, then by "age" in descending order. When
|
|
403
|
+
paginating with skip/limit, providing a sort order is strongly
|
|
404
|
+
recommended to ensure consistent, deterministic results. Defaults to
|
|
405
|
+
None (no sort).
|
|
384
406
|
|
|
385
407
|
Returns:
|
|
386
|
-
|
|
387
|
-
|
|
408
|
+
A FindResult that is async-iterable and also provides total_count().
|
|
409
|
+
|
|
410
|
+
Raises:
|
|
411
|
+
InvalidMappingError: If `mapping` doesn't pass validation.
|
|
412
|
+
ValueError: if `skip` or `limit` are less than 0.
|
|
388
413
|
"""
|
|
414
|
+
skip = skip or 0
|
|
415
|
+
if skip < 0:
|
|
416
|
+
raise ValueError("skip must be >= 0")
|
|
417
|
+
if limit is not None and limit < 0:
|
|
418
|
+
raise ValueError("limit must be >= 0")
|
|
419
|
+
|
|
389
420
|
validate_find_mapping(mapping, dto_model=self._dto_model)
|
|
390
421
|
mapping = replace_id_field_in_find_mapping(mapping, self._id_field)
|
|
391
422
|
|
|
392
|
-
|
|
393
|
-
|
|
423
|
+
# Ensure we don't retrieve deleted docs
|
|
424
|
+
mapping_without_deleted = {**mapping, "__metadata__.deleted": False}
|
|
394
425
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
426
|
+
# Convert generic sort spec to MongoDB-specific sort spec
|
|
427
|
+
mongodb_sort: list[tuple[str, int]] = []
|
|
428
|
+
for spec in sort or []:
|
|
429
|
+
field = spec.removeprefix("-")
|
|
430
|
+
order = 1 if field == spec else -1
|
|
431
|
+
if field == self._id_field:
|
|
432
|
+
field = "_id"
|
|
433
|
+
mongodb_sort.append((field, order))
|
|
434
|
+
|
|
435
|
+
collection = self._collection
|
|
436
|
+
|
|
437
|
+
async def _total_count() -> int:
|
|
438
|
+
with translate_pymongo_errors():
|
|
439
|
+
return await collection.count_documents(filter=mapping_without_deleted)
|
|
440
|
+
|
|
441
|
+
if limit == 0:
|
|
442
|
+
|
|
443
|
+
async def _empty_iter() -> AsyncIterator[Dto]:
|
|
444
|
+
return
|
|
445
|
+
yield # turns this into an async generator
|
|
446
|
+
|
|
447
|
+
return FindResult(
|
|
448
|
+
results_iterator=_empty_iter(), get_total_count=_total_count
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
async def _iter() -> AsyncIterator[Dto]:
|
|
452
|
+
with translate_pymongo_errors():
|
|
453
|
+
cursor = collection.find(filter=mapping_without_deleted)
|
|
454
|
+
if sort:
|
|
455
|
+
cursor = cursor.sort(mongodb_sort)
|
|
456
|
+
cursor = cursor.skip(skip)
|
|
457
|
+
if limit:
|
|
458
|
+
cursor = cursor.limit(limit)
|
|
459
|
+
async for document in cursor:
|
|
460
|
+
yield document_to_dto(
|
|
461
|
+
document, id_field=self._id_field, dto_model=self._dto_model
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
return FindResult(results_iterator=_iter(), get_total_count=_total_count)
|
|
401
465
|
|
|
402
466
|
async def insert(self, dto: Dto) -> None:
|
|
403
467
|
"""Create a new resource.
|
|
@@ -25,6 +25,7 @@ from pydantic import BaseModel
|
|
|
25
25
|
|
|
26
26
|
from hexkit.custom_types import ID
|
|
27
27
|
from hexkit.protocols.dao import (
|
|
28
|
+
FindResult,
|
|
28
29
|
MultipleHitsFoundError,
|
|
29
30
|
NoHitsFoundError,
|
|
30
31
|
ResourceAlreadyExistsError,
|
|
@@ -391,27 +392,79 @@ class BaseInMemDao(Generic[DTO]):
|
|
|
391
392
|
|
|
392
393
|
raise MultipleHitsFoundError(mapping=mapping)
|
|
393
394
|
|
|
394
|
-
|
|
395
|
+
def _resource_matches(
|
|
396
|
+
self,
|
|
397
|
+
resource: Document,
|
|
398
|
+
mapping: Mapping[str, Any],
|
|
399
|
+
predicates: list["Predicate"],
|
|
400
|
+
) -> bool:
|
|
401
|
+
"""Return whether a resource matches the given mapping or predicates."""
|
|
402
|
+
if self._handle_mql:
|
|
403
|
+
return all(p.evaluate(resource=resource) for p in predicates)
|
|
404
|
+
for key, expected_value in mapping.items():
|
|
405
|
+
if _get_nested_value(resource, key) != expected_value:
|
|
406
|
+
return False
|
|
407
|
+
return True
|
|
408
|
+
|
|
409
|
+
def find_all(
|
|
410
|
+
self,
|
|
411
|
+
*,
|
|
412
|
+
mapping: Mapping[str, Any],
|
|
413
|
+
skip: int | None = None,
|
|
414
|
+
limit: int | None = None,
|
|
415
|
+
sort: list[str] | None = None,
|
|
416
|
+
) -> "FindResult[DTO]":
|
|
395
417
|
"""Find all resources that match the specified mapping."""
|
|
418
|
+
skip = skip or 0
|
|
419
|
+
if skip < 0:
|
|
420
|
+
raise ValueError("skip must be >= 0")
|
|
421
|
+
if limit is not None and limit < 0:
|
|
422
|
+
raise ValueError("limit must be >= 0")
|
|
423
|
+
|
|
396
424
|
if "" in mapping:
|
|
397
425
|
raise KeyError("Query mappings can't contain empty-string keys")
|
|
398
426
|
|
|
399
427
|
_mapping = replace_id_field_in_find_mapping(mapping, self._id_field)
|
|
400
428
|
predicates = build_predicates(_mapping) if self._handle_mql else []
|
|
401
429
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
if
|
|
430
|
+
matching = [
|
|
431
|
+
resource
|
|
432
|
+
for resource in self.resources.values()
|
|
433
|
+
if self._resource_matches(resource, mapping, predicates)
|
|
434
|
+
]
|
|
435
|
+
|
|
436
|
+
# Interpret the sorting specification and sort our resources accordingly
|
|
437
|
+
for spec in reversed(sort or []):
|
|
438
|
+
desc = spec.startswith("-")
|
|
439
|
+
field = spec.removeprefix("-")
|
|
440
|
+
doc_field = "_id" if field == self._id_field else field
|
|
441
|
+
matching.sort(key=lambda doc: doc[doc_field], reverse=desc)
|
|
442
|
+
|
|
443
|
+
total = len(matching)
|
|
444
|
+
|
|
445
|
+
async def _total_count() -> int:
|
|
446
|
+
return total
|
|
447
|
+
|
|
448
|
+
if limit == 0:
|
|
449
|
+
|
|
450
|
+
async def _empty_iter() -> AsyncIterator[DTO]:
|
|
451
|
+
return
|
|
452
|
+
yield # makes this an async generator
|
|
453
|
+
|
|
454
|
+
return FindResult(
|
|
455
|
+
results_iterator=_empty_iter(), get_total_count=_total_count
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
# Manually apply pagination params
|
|
459
|
+
end = (skip + limit) if limit is not None else None
|
|
460
|
+
page = matching[skip:end]
|
|
461
|
+
|
|
462
|
+
async def _iter() -> AsyncIterator[DTO]:
|
|
463
|
+
for resource in page:
|
|
413
464
|
yield self._deserialize(resource)
|
|
414
465
|
|
|
466
|
+
return FindResult(results_iterator=_iter(), get_total_count=_total_count)
|
|
467
|
+
|
|
415
468
|
async def insert(self, dto: DTO) -> None:
|
|
416
469
|
"""Insert a resource.
|
|
417
470
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|