client-query-cache 0.4.0__tar.gz → 0.6.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.
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/PKG-INFO +15 -4
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/README.md +4 -1
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/pyproject.toml +13 -2
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/pyproject.toml.orig +13 -2
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/entries.py +1 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/manager.py +14 -4
- client_query_cache-0.6.0/src/client_query_cache/_core/stream_activation.py +31 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/stream_health.py +4 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/asynchronous/collection.py +8 -2
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/asynchronous/manager.py +6 -3
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/asynchronous/streams.py +118 -42
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/synchronous/collection.py +8 -2
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/synchronous/manager.py +2 -1
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/synchronous/streams.py +118 -50
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/__init__.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/__init__.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/canonical.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/codec.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/collation.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/collection_metadata.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/count_reads.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/cursor_capture.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/errors.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/find_one_reads.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/find_reads.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/identity_reads.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/keys.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/lifecycle.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/locking.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/lru.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/namespace.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/order_sensitive_keys.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/projection.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/query_filters.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/read_classification.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/read_validation.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/snapshots.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/stream_cost.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/stream_events.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/stream_options.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/traversal.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/unique_keys.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/asynchronous/__init__.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/asynchronous/cursors.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/asynchronous/database.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/otel.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/py.typed +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/synchronous/__init__.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/synchronous/cursors.py +0 -0
- {client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/synchronous/database.py +0 -0
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: client-query-cache
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Client-side caching for PyMongo, kept coherent using MongoDB change streams.
|
|
5
5
|
Author: Alessio Locatelli
|
|
6
6
|
Author-email: Alessio Locatelli <<software.development@secure.mailbox.org>>
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Classifier: Framework :: AsyncIO
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
12
|
+
Classifier: Topic :: Database
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Classifier: Typing :: Typed
|
|
7
15
|
Requires-Dist: pymongo>=4.18.1
|
|
8
16
|
Requires-Dist: opentelemetry-api>=1.45.0 ; extra == 'otel'
|
|
9
|
-
Requires-Python: >=3.14
|
|
17
|
+
Requires-Python: >=3.14
|
|
10
18
|
Project-URL: Repository, https://github.com/alessio-locatelli/client-query-cache
|
|
11
19
|
Project-URL: Documentation, https://alessio-locatelli.github.io/client-query-cache/
|
|
12
20
|
Provides-Extra: otel
|
|
@@ -14,7 +22,10 @@ Description-Content-Type: text/markdown
|
|
|
14
22
|
|
|
15
23
|
# Coherent client-side caching for PyMongo
|
|
16
24
|
|
|
17
|
-
[](https://pypi.org/project/client-query-cache/)
|
|
26
|
+
[](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/)
|
|
27
|
+
[](https://github.com/alessio-locatelli/client-query-cache/actions/workflows/test.yml)
|
|
28
|
+
[](https://pypi.org/project/client-query-cache/)
|
|
18
29
|
|
|
19
30
|
`client-query-cache` uses MongoDB change streams to keep cached reads coherent. It supports synchronous and asyncio applications without requiring a separate cache server.
|
|
20
31
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# Coherent client-side caching for PyMongo
|
|
2
2
|
|
|
3
|
-
[](https://pypi.org/project/client-query-cache/)
|
|
4
|
+
[](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/)
|
|
5
|
+
[](https://github.com/alessio-locatelli/client-query-cache/actions/workflows/test.yml)
|
|
6
|
+
[](https://pypi.org/project/client-query-cache/)
|
|
4
7
|
|
|
5
8
|
`client-query-cache` uses MongoDB change streams to keep cached reads coherent. It supports synchronous and asyncio applications without requiring a separate cache server.
|
|
6
9
|
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "client-query-cache"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "Client-side caching for PyMongo, kept coherent using MongoDB change streams."
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.14
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Framework :: AsyncIO",
|
|
10
|
+
"Intended Audience :: Developers",
|
|
11
|
+
"Programming Language :: Python :: 3.14",
|
|
12
|
+
"Programming Language :: Python :: 3.15",
|
|
13
|
+
"Topic :: Database",
|
|
14
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
15
|
+
"Typing :: Typed",
|
|
16
|
+
]
|
|
7
17
|
dependencies = ["pymongo>=4.18.1"]
|
|
8
18
|
|
|
9
19
|
[[project.authors]]
|
|
@@ -42,6 +52,7 @@ dev = [
|
|
|
42
52
|
"opentelemetry-api>=1.45.0",
|
|
43
53
|
"opentelemetry-sdk>=1.45.0",
|
|
44
54
|
"types-psutil>=7.2.2.20260906",
|
|
55
|
+
"psutil>=7.2.2",
|
|
45
56
|
"dnspython>=2.8.0",
|
|
46
57
|
"python-snappy>=0.7.3",
|
|
47
58
|
"pytest-xdist[psutil]>=3.8.0",
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "client-query-cache"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "Client-side caching for PyMongo, kept coherent using MongoDB change streams."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Alessio Locatelli", email = "<software.development@secure.mailbox.org>" },
|
|
7
7
|
]
|
|
8
8
|
readme = "README.md"
|
|
9
|
-
requires-python = ">=3.14
|
|
9
|
+
requires-python = ">=3.14"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Framework :: AsyncIO",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Programming Language :: Python :: 3.14",
|
|
15
|
+
"Programming Language :: Python :: 3.15",
|
|
16
|
+
"Topic :: Database",
|
|
17
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
18
|
+
"Typing :: Typed",
|
|
19
|
+
]
|
|
10
20
|
dependencies = ["pymongo>=4.18.1"]
|
|
11
21
|
|
|
12
22
|
[project.urls]
|
|
@@ -41,6 +51,7 @@ dev = [
|
|
|
41
51
|
"opentelemetry-api>=1.45.0",
|
|
42
52
|
"opentelemetry-sdk>=1.45.0",
|
|
43
53
|
"types-psutil>=7.2.2.20260906",
|
|
54
|
+
"psutil>=7.2.2",
|
|
44
55
|
"dnspython>=2.8.0",
|
|
45
56
|
"python-snappy>=0.7.3",
|
|
46
57
|
"pytest-xdist[psutil]>=3.8.0",
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/manager.py
RENAMED
|
@@ -54,6 +54,8 @@ DEFAULT_MAX_ENTRY_BYTES = 1 * 1024 * 1024
|
|
|
54
54
|
|
|
55
55
|
_NOT_INDEXED = object()
|
|
56
56
|
_DEFAULT_DATABASE_AVAILABILITY = (True, 0)
|
|
57
|
+
_MISS = LookupResult(hit=False)
|
|
58
|
+
_DEFERRED_MISS = LookupResult(hit=False, deferred_miss=True)
|
|
57
59
|
|
|
58
60
|
logger = logging.getLogger(__name__)
|
|
59
61
|
|
|
@@ -313,6 +315,9 @@ class _CacheCoreLifecycle(_CacheCoreBase):
|
|
|
313
315
|
self._database_namespaces.clear()
|
|
314
316
|
logger.info("cache manager closed")
|
|
315
317
|
|
|
318
|
+
def record_miss(self) -> None:
|
|
319
|
+
self._statistics.record_miss()
|
|
320
|
+
|
|
316
321
|
def record_bypass(self, reason: BypassReason = BypassReason.UNSPECIFIED) -> None:
|
|
317
322
|
self._statistics.record_bypass(reason)
|
|
318
323
|
|
|
@@ -908,6 +913,7 @@ class _CacheCoreLookup(_CacheCoreBase):
|
|
|
908
913
|
discriminator: object,
|
|
909
914
|
*,
|
|
910
915
|
codec_options: CodecOptions[Any] | None = None,
|
|
916
|
+
defer_miss: bool = False,
|
|
911
917
|
) -> LookupResult:
|
|
912
918
|
self._ensure_active()
|
|
913
919
|
if not self._is_database_available(namespace.database):
|
|
@@ -917,18 +923,22 @@ class _CacheCoreLookup(_CacheCoreBase):
|
|
|
917
923
|
key = NamespaceCacheKey(namespace, canonical_discriminator)
|
|
918
924
|
entry = self._lru.peek(key)
|
|
919
925
|
if entry is None:
|
|
920
|
-
self.
|
|
921
|
-
return LookupResult(hit=False)
|
|
926
|
+
return self._namespace_miss(defer_miss=defer_miss)
|
|
922
927
|
state = self._namespace(namespace)
|
|
923
928
|
with self._namespace_section(state):
|
|
924
929
|
valid = (state.generation,) == entry.generation_key
|
|
925
930
|
if not valid:
|
|
926
|
-
self.
|
|
927
|
-
return LookupResult(hit=False)
|
|
931
|
+
return self._namespace_miss(defer_miss=defer_miss)
|
|
928
932
|
self._lru.touch(key)
|
|
929
933
|
self._statistics.record_hit()
|
|
930
934
|
return LookupResult(hit=True, value=decode_value(entry.value, codec_options))
|
|
931
935
|
|
|
936
|
+
def _namespace_miss(self, *, defer_miss: bool) -> LookupResult:
|
|
937
|
+
if defer_miss:
|
|
938
|
+
return _DEFERRED_MISS
|
|
939
|
+
self._statistics.record_miss()
|
|
940
|
+
return _MISS
|
|
941
|
+
|
|
932
942
|
def resolve_alias(
|
|
933
943
|
self,
|
|
934
944
|
namespace: NamespaceId,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from time import monotonic
|
|
6
|
+
|
|
7
|
+
from client_query_cache._core.stream_health import RetryBackoff
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _startup_jitter(_low: float, cap: float) -> float:
|
|
11
|
+
return random.uniform(cap / 2, cap)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(slots=True)
|
|
15
|
+
class StartupRetry:
|
|
16
|
+
deadline: float = 0.0 # Zero permits the first attempt.
|
|
17
|
+
backoff: RetryBackoff = field(default_factory=RetryBackoff)
|
|
18
|
+
|
|
19
|
+
def ready(self) -> bool:
|
|
20
|
+
return monotonic() >= self.deadline
|
|
21
|
+
|
|
22
|
+
def failed(self) -> None:
|
|
23
|
+
self.deadline = monotonic() + self.backoff.next_delay(_startup_jitter)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(slots=True)
|
|
27
|
+
class StreamActivation[Supervisor, Completion]:
|
|
28
|
+
supervisor: Supervisor
|
|
29
|
+
completion: Completion
|
|
30
|
+
pending: bool = True
|
|
31
|
+
retry: StartupRetry = field(default_factory=StartupRetry)
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/stream_health.py
RENAMED
|
@@ -63,6 +63,10 @@ class StreamHealthRegistry:
|
|
|
63
63
|
str, StreamHealthStatus | Callable[[], StreamHealthStatus]
|
|
64
64
|
] = {} # Can be empty.
|
|
65
65
|
|
|
66
|
+
def record_connecting(self, database_name: str) -> None:
|
|
67
|
+
with self._lock:
|
|
68
|
+
self._states[database_name] = StreamHealthStatus.CONNECTING
|
|
69
|
+
|
|
66
70
|
def record_starting(
|
|
67
71
|
self, database_name: str, health: Callable[[], StreamHealthStatus]
|
|
68
72
|
) -> None:
|
|
@@ -209,15 +209,21 @@ class CachedCollection[DocumentType: Mapping[str, Any]]:
|
|
|
209
209
|
cache.current_index_generation(namespace),
|
|
210
210
|
)
|
|
211
211
|
)
|
|
212
|
+
discovers_unique_keys = identity is NO_IDENTITY and known_keys is None
|
|
212
213
|
lookup_result = cache.lookup_namespace(
|
|
213
|
-
namespace,
|
|
214
|
+
namespace,
|
|
215
|
+
discriminator,
|
|
216
|
+
codec_options=codec_options,
|
|
217
|
+
defer_miss=discovers_unique_keys,
|
|
214
218
|
)
|
|
215
219
|
if lookup_result.hit:
|
|
216
220
|
return cast("DocumentType | None", lookup_result.value)
|
|
217
|
-
if
|
|
221
|
+
if discovers_unique_keys:
|
|
218
222
|
unique_key_match = await self._match_unique_key(
|
|
219
223
|
filter_query, effective_collation
|
|
220
224
|
)
|
|
225
|
+
if unique_key_match is None and lookup_result.deferred_miss:
|
|
226
|
+
cache.record_miss()
|
|
221
227
|
if unique_key_match is not None:
|
|
222
228
|
key_definition, key_values = unique_key_match
|
|
223
229
|
return await self._find_one_by_unique_key(
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/asynchronous/manager.py
RENAMED
|
@@ -81,7 +81,8 @@ class CacheManager[DocumentType: Mapping[str, Any]]:
|
|
|
81
81
|
namespace: NamespaceId,
|
|
82
82
|
collection_probe: Callable[[], Awaitable[CollectionProbeResult | None]],
|
|
83
83
|
) -> BypassReason | None:
|
|
84
|
-
await self._coordinator.activate_database(namespace.database)
|
|
84
|
+
if await self._coordinator.activate_database(namespace.database) is None:
|
|
85
|
+
return BypassReason.STREAM_UNAVAILABLE
|
|
85
86
|
if not self._cache.is_database_available(namespace.database):
|
|
86
87
|
return BypassReason.STREAM_UNAVAILABLE
|
|
87
88
|
current_epoch = self._cache.current_epoch(namespace)
|
|
@@ -159,8 +160,10 @@ class CacheManager[DocumentType: Mapping[str, Any]]:
|
|
|
159
160
|
return CachedCollection(CachedDatabase(self, collection.database), collection)
|
|
160
161
|
|
|
161
162
|
async def close(self) -> None:
|
|
162
|
-
|
|
163
|
-
|
|
163
|
+
try:
|
|
164
|
+
await self._coordinator.close()
|
|
165
|
+
finally:
|
|
166
|
+
self._cache.close()
|
|
164
167
|
|
|
165
168
|
async def __aenter__(self) -> Self:
|
|
166
169
|
return self
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/asynchronous/streams.py
RENAMED
|
@@ -9,6 +9,7 @@ from bson.errors import BSONError
|
|
|
9
9
|
from pymongo.errors import OperationFailure, PyMongoError
|
|
10
10
|
|
|
11
11
|
from client_query_cache._core.errors import StreamLifecycleError, StreamStartupError
|
|
12
|
+
from client_query_cache._core.stream_activation import StreamActivation
|
|
12
13
|
from client_query_cache._core.stream_events import (
|
|
13
14
|
build_change_stream_pipeline,
|
|
14
15
|
is_unresumable_change_stream_error,
|
|
@@ -38,14 +39,28 @@ MINIMUM_SERVER_VERSION = (8, 0)
|
|
|
38
39
|
logger = logging.getLogger(__name__)
|
|
39
40
|
|
|
40
41
|
|
|
42
|
+
async def _join_cleanup(task: asyncio.Task[None]) -> None:
|
|
43
|
+
cancelled = False
|
|
44
|
+
while not task.done():
|
|
45
|
+
try:
|
|
46
|
+
await asyncio.shield(task)
|
|
47
|
+
except asyncio.CancelledError:
|
|
48
|
+
cancelled = True
|
|
49
|
+
task.result()
|
|
50
|
+
if cancelled:
|
|
51
|
+
raise asyncio.CancelledError
|
|
52
|
+
|
|
53
|
+
|
|
41
54
|
class DatabaseStreamSupervisor:
|
|
42
55
|
__slots__ = (
|
|
43
56
|
"_backoff",
|
|
44
57
|
"_cache",
|
|
58
|
+
"_closed_stream",
|
|
45
59
|
"_database",
|
|
46
60
|
"_health",
|
|
47
61
|
"_max_await_time_ms",
|
|
48
62
|
"_resume_token",
|
|
63
|
+
"_shutdown_task",
|
|
49
64
|
"_stop_event",
|
|
50
65
|
"_stream",
|
|
51
66
|
"_task",
|
|
@@ -65,9 +80,11 @@ class DatabaseStreamSupervisor:
|
|
|
65
80
|
self._max_await_time_ms = max_await_time_ms
|
|
66
81
|
self._health = StreamHealth.STARTING
|
|
67
82
|
self._stream: AsyncDatabaseChangeStream[Any] | None = None
|
|
83
|
+
self._closed_stream: AsyncDatabaseChangeStream[Any] | None = None
|
|
68
84
|
self._resume_token: Mapping[str, Any] | None = None
|
|
69
85
|
self._stop_event = asyncio.Event()
|
|
70
86
|
self._task: asyncio.Task[None] | None = None
|
|
87
|
+
self._shutdown_task: asyncio.Task[None] | None = None
|
|
71
88
|
self._cache.set_database_available(self._database.name, available=False)
|
|
72
89
|
|
|
73
90
|
@property
|
|
@@ -96,29 +113,30 @@ class DatabaseStreamSupervisor:
|
|
|
96
113
|
)
|
|
97
114
|
raise StreamStartupError(message) from exc
|
|
98
115
|
except asyncio.CancelledError:
|
|
99
|
-
self.
|
|
100
|
-
stream = self._stream
|
|
101
|
-
if stream is not None:
|
|
102
|
-
try:
|
|
103
|
-
await stream.close()
|
|
104
|
-
except PyMongoError:
|
|
105
|
-
pass
|
|
116
|
+
await self.stop()
|
|
106
117
|
raise
|
|
107
118
|
if self._stop_event.is_set():
|
|
108
|
-
|
|
109
|
-
try:
|
|
110
|
-
await self._stream.close()
|
|
111
|
-
except PyMongoError:
|
|
112
|
-
pass
|
|
113
|
-
self._set_health(StreamHealth.CLOSED)
|
|
119
|
+
await self.stop()
|
|
114
120
|
message = "stop() was called while start() was still connecting"
|
|
115
121
|
raise StreamLifecycleError(message)
|
|
116
122
|
self._set_health(StreamHealth.HEALTHY)
|
|
117
123
|
self._task = asyncio.ensure_future(self._run())
|
|
118
124
|
|
|
119
|
-
|
|
125
|
+
def request_stop(self) -> None:
|
|
120
126
|
self._stop_event.set()
|
|
121
127
|
self._set_health(StreamHealth.CLOSED)
|
|
128
|
+
if self._task is not None:
|
|
129
|
+
self._task.cancel()
|
|
130
|
+
|
|
131
|
+
async def stop(self) -> None:
|
|
132
|
+
self.request_stop()
|
|
133
|
+
if self._shutdown_task is None or (
|
|
134
|
+
self._shutdown_task.done() and self._stream is not self._closed_stream
|
|
135
|
+
):
|
|
136
|
+
self._shutdown_task = asyncio.create_task(self._finish_stop())
|
|
137
|
+
await _join_cleanup(self._shutdown_task)
|
|
138
|
+
|
|
139
|
+
async def _finish_stop(self) -> None:
|
|
122
140
|
task = self._task
|
|
123
141
|
if task is not None:
|
|
124
142
|
task.cancel()
|
|
@@ -136,6 +154,7 @@ class DatabaseStreamSupervisor:
|
|
|
136
154
|
extra={"database": self._database.name},
|
|
137
155
|
exc_info=True,
|
|
138
156
|
)
|
|
157
|
+
self._closed_stream = stream
|
|
139
158
|
self._set_health(StreamHealth.CLOSED)
|
|
140
159
|
|
|
141
160
|
def _set_health(self, health: StreamHealth) -> None:
|
|
@@ -177,11 +196,6 @@ class DatabaseStreamSupervisor:
|
|
|
177
196
|
await previous_stream.close()
|
|
178
197
|
except PyMongoError:
|
|
179
198
|
pass
|
|
180
|
-
if self._stop_event.is_set():
|
|
181
|
-
try:
|
|
182
|
-
await self._stream.close()
|
|
183
|
-
except PyMongoError:
|
|
184
|
-
pass
|
|
185
199
|
|
|
186
200
|
async def _interruptible_sleep(self, delay: float) -> bool:
|
|
187
201
|
try:
|
|
@@ -268,12 +282,14 @@ class DatabaseStreamSupervisor:
|
|
|
268
282
|
|
|
269
283
|
class ChangeStreamCoordinator:
|
|
270
284
|
__slots__ = (
|
|
285
|
+
"_activations",
|
|
271
286
|
"_cache",
|
|
272
287
|
"_client",
|
|
273
288
|
"_closed",
|
|
274
289
|
"_health_registry",
|
|
275
290
|
"_lock",
|
|
276
291
|
"_max_await_time_ms",
|
|
292
|
+
"_shutdown_task",
|
|
277
293
|
"_supervisors",
|
|
278
294
|
)
|
|
279
295
|
|
|
@@ -287,49 +303,109 @@ class ChangeStreamCoordinator:
|
|
|
287
303
|
self._client = client
|
|
288
304
|
self._max_await_time_ms = max_await_time_ms
|
|
289
305
|
self._cache = cache
|
|
306
|
+
self._activations: dict[
|
|
307
|
+
str, StreamActivation[DatabaseStreamSupervisor, asyncio.Event]
|
|
308
|
+
] = {} # Can be empty.
|
|
290
309
|
self._supervisors: dict[str, DatabaseStreamSupervisor] = {}
|
|
291
310
|
self._closed = False
|
|
292
311
|
self._health_registry = StreamHealthRegistry()
|
|
293
312
|
self._lock = asyncio.Lock()
|
|
313
|
+
self._shutdown_task: asyncio.Task[None] | None = None
|
|
294
314
|
|
|
295
315
|
async def activate_database(self, name: str) -> DatabaseStreamSupervisor | None:
|
|
296
316
|
async with self._lock:
|
|
297
317
|
if self._closed:
|
|
298
318
|
raise StreamLifecycleError("coordinator is closed")
|
|
299
319
|
try:
|
|
300
|
-
|
|
320
|
+
return self._supervisors[name]
|
|
301
321
|
except KeyError:
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
322
|
+
pass
|
|
323
|
+
try:
|
|
324
|
+
activation = self._activations[name]
|
|
325
|
+
except KeyError:
|
|
326
|
+
activation = None
|
|
327
|
+
if activation is not None and (
|
|
328
|
+
activation.pending or not activation.retry.ready()
|
|
329
|
+
):
|
|
330
|
+
return None
|
|
331
|
+
supervisor = DatabaseStreamSupervisor(
|
|
332
|
+
self._client[name],
|
|
333
|
+
self._cache,
|
|
334
|
+
max_await_time_ms=self._max_await_time_ms,
|
|
335
|
+
)
|
|
336
|
+
if activation is None:
|
|
337
|
+
activation = StreamActivation(supervisor, asyncio.Event())
|
|
338
|
+
self._activations[name] = activation
|
|
339
|
+
else:
|
|
340
|
+
activation.supervisor = supervisor
|
|
341
|
+
activation.pending = True
|
|
342
|
+
activation.completion.clear()
|
|
343
|
+
self._health_registry.record_connecting(name)
|
|
344
|
+
published = False
|
|
345
|
+
try:
|
|
346
|
+
try:
|
|
347
|
+
await supervisor.start()
|
|
348
|
+
except StreamStartupError:
|
|
349
|
+
async with self._lock:
|
|
350
|
+
if not self._closed:
|
|
351
|
+
activation.retry.failed()
|
|
352
|
+
self._health_registry.record_startup_failure(name)
|
|
353
|
+
logger.warning(
|
|
354
|
+
"change stream startup failed for database %r; reads for "
|
|
355
|
+
"this database will bypass the cache",
|
|
356
|
+
name,
|
|
357
|
+
exc_info=True,
|
|
306
358
|
)
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
self._health_registry.record_startup_failure(name)
|
|
312
|
-
logger.warning(
|
|
313
|
-
"change stream startup failed for database %r; reads for "
|
|
314
|
-
"this database will bypass the cache",
|
|
315
|
-
name,
|
|
316
|
-
exc_info=True,
|
|
317
|
-
)
|
|
318
|
-
return None
|
|
319
|
-
except asyncio.CancelledError:
|
|
320
|
-
self._health_registry.record_startup_failure(name)
|
|
321
|
-
raise
|
|
359
|
+
return None
|
|
360
|
+
async with self._lock:
|
|
361
|
+
if self._closed:
|
|
362
|
+
raise StreamLifecycleError("coordinator is closed")
|
|
322
363
|
self._supervisors[name] = supervisor
|
|
364
|
+
del self._activations[name]
|
|
365
|
+
self._health_registry.record_starting(name, supervisor.health_status)
|
|
366
|
+
published = True
|
|
323
367
|
return supervisor
|
|
368
|
+
finally:
|
|
369
|
+
if not published:
|
|
370
|
+
try:
|
|
371
|
+
await supervisor.stop()
|
|
372
|
+
finally:
|
|
373
|
+
async with self._lock:
|
|
374
|
+
if not self._closed:
|
|
375
|
+
self._health_registry.record_startup_failure(name)
|
|
376
|
+
activation.pending = False
|
|
377
|
+
activation.completion.set()
|
|
378
|
+
else:
|
|
379
|
+
activation.completion.set()
|
|
324
380
|
|
|
325
381
|
def stream_health_snapshot(self, database_name: str) -> StreamHealthSnapshot:
|
|
326
382
|
return self._health_registry.snapshot(database_name)
|
|
327
383
|
|
|
328
384
|
async def close(self) -> None:
|
|
329
|
-
|
|
385
|
+
if self._shutdown_task is None:
|
|
330
386
|
self._closed = True
|
|
331
387
|
self._health_registry.close()
|
|
332
|
-
supervisors =
|
|
333
|
-
self.
|
|
388
|
+
supervisors = tuple(self._supervisors.values())
|
|
389
|
+
activations = tuple(self._activations.values())
|
|
390
|
+
for supervisor in supervisors:
|
|
391
|
+
supervisor.request_stop()
|
|
392
|
+
for activation in activations:
|
|
393
|
+
activation.supervisor.request_stop()
|
|
394
|
+
self._shutdown_task = asyncio.create_task(
|
|
395
|
+
self._finish_close(supervisors, activations)
|
|
396
|
+
)
|
|
397
|
+
await _join_cleanup(self._shutdown_task)
|
|
398
|
+
|
|
399
|
+
async def _finish_close(
|
|
400
|
+
self,
|
|
401
|
+
supervisors: tuple[DatabaseStreamSupervisor, ...],
|
|
402
|
+
activations: tuple[
|
|
403
|
+
StreamActivation[DatabaseStreamSupervisor, asyncio.Event], ...
|
|
404
|
+
],
|
|
405
|
+
) -> None:
|
|
406
|
+
for activation in activations:
|
|
407
|
+
await activation.completion.wait()
|
|
334
408
|
for supervisor in supervisors:
|
|
335
409
|
await supervisor.stop()
|
|
410
|
+
self._activations.clear()
|
|
411
|
+
self._supervisors.clear()
|
|
@@ -209,15 +209,21 @@ class CachedCollection[DocumentType: Mapping[str, Any]]:
|
|
|
209
209
|
cache.current_index_generation(namespace),
|
|
210
210
|
)
|
|
211
211
|
)
|
|
212
|
+
discovers_unique_keys = identity is NO_IDENTITY and known_keys is None
|
|
212
213
|
lookup_result = cache.lookup_namespace(
|
|
213
|
-
namespace,
|
|
214
|
+
namespace,
|
|
215
|
+
discriminator,
|
|
216
|
+
codec_options=codec_options,
|
|
217
|
+
defer_miss=discovers_unique_keys,
|
|
214
218
|
)
|
|
215
219
|
if lookup_result.hit:
|
|
216
220
|
return cast("DocumentType | None", lookup_result.value)
|
|
217
|
-
if
|
|
221
|
+
if discovers_unique_keys:
|
|
218
222
|
unique_key_match = self._match_unique_key(
|
|
219
223
|
filter_query, effective_collation
|
|
220
224
|
)
|
|
225
|
+
if unique_key_match is None and lookup_result.deferred_miss:
|
|
226
|
+
cache.record_miss()
|
|
221
227
|
if unique_key_match is not None:
|
|
222
228
|
key_definition, key_values = unique_key_match
|
|
223
229
|
return self._find_one_by_unique_key(
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/synchronous/manager.py
RENAMED
|
@@ -81,7 +81,8 @@ class CacheManager[DocumentType: Mapping[str, Any]]:
|
|
|
81
81
|
namespace: NamespaceId,
|
|
82
82
|
collection_probe: Callable[[], CollectionProbeResult | None],
|
|
83
83
|
) -> BypassReason | None:
|
|
84
|
-
self._coordinator.activate_database(namespace.database)
|
|
84
|
+
if self._coordinator.activate_database(namespace.database) is None:
|
|
85
|
+
return BypassReason.STREAM_UNAVAILABLE
|
|
85
86
|
if not self._cache.is_database_available(namespace.database):
|
|
86
87
|
return BypassReason.STREAM_UNAVAILABLE
|
|
87
88
|
current_epoch = self._cache.current_epoch(namespace)
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/synchronous/streams.py
RENAMED
|
@@ -9,6 +9,7 @@ from bson.errors import BSONError
|
|
|
9
9
|
from pymongo.errors import OperationFailure, PyMongoError
|
|
10
10
|
|
|
11
11
|
from client_query_cache._core.errors import StreamLifecycleError, StreamStartupError
|
|
12
|
+
from client_query_cache._core.stream_activation import StreamActivation
|
|
12
13
|
from client_query_cache._core.stream_events import (
|
|
13
14
|
build_change_stream_pipeline,
|
|
14
15
|
is_unresumable_change_stream_error,
|
|
@@ -103,41 +104,45 @@ class DatabaseStreamSupervisor:
|
|
|
103
104
|
)
|
|
104
105
|
raise StreamStartupError(message) from exc
|
|
105
106
|
with self._lifecycle_lock:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
self.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
daemon=True,
|
|
120
|
-
)
|
|
121
|
-
self._thread.start()
|
|
107
|
+
stopped = self._stop_event.is_set()
|
|
108
|
+
if not stopped:
|
|
109
|
+
self._set_health(StreamHealth.HEALTHY)
|
|
110
|
+
self._thread = threading.Thread(
|
|
111
|
+
target=self._run,
|
|
112
|
+
name=f"client-query-cache-stream-{self._database.name}",
|
|
113
|
+
daemon=True,
|
|
114
|
+
)
|
|
115
|
+
self._thread.start()
|
|
116
|
+
if stopped:
|
|
117
|
+
self.stop()
|
|
118
|
+
message = "stop() was called while start() was still connecting"
|
|
119
|
+
raise StreamLifecycleError(message)
|
|
122
120
|
|
|
123
|
-
def
|
|
121
|
+
def request_stop(self) -> None:
|
|
124
122
|
with self._lifecycle_lock:
|
|
125
123
|
self._stop_event.set()
|
|
126
124
|
self._set_health(StreamHealth.CLOSED)
|
|
125
|
+
|
|
126
|
+
def stop(self) -> None:
|
|
127
|
+
self.request_stop()
|
|
128
|
+
with self._lifecycle_lock:
|
|
127
129
|
stream = self._stream
|
|
128
|
-
if stream is not None:
|
|
129
|
-
try:
|
|
130
|
-
stream.close()
|
|
131
|
-
except PyMongoError:
|
|
132
|
-
logger.warning(
|
|
133
|
-
"change stream close failed during shutdown",
|
|
134
|
-
extra={"database": self._database.name},
|
|
135
|
-
exc_info=True,
|
|
136
|
-
)
|
|
137
130
|
thread = self._thread
|
|
131
|
+
if stream is not None:
|
|
132
|
+
try:
|
|
133
|
+
stream.close()
|
|
134
|
+
except PyMongoError:
|
|
135
|
+
logger.warning(
|
|
136
|
+
"change stream close failed during shutdown",
|
|
137
|
+
extra={"database": self._database.name},
|
|
138
|
+
exc_info=True,
|
|
139
|
+
)
|
|
138
140
|
if thread is not None:
|
|
139
141
|
thread.join()
|
|
140
|
-
self.
|
|
142
|
+
with self._lifecycle_lock:
|
|
143
|
+
if self._stream is stream:
|
|
144
|
+
self._stream = None
|
|
145
|
+
self._set_health(StreamHealth.CLOSED)
|
|
141
146
|
|
|
142
147
|
def _ensure_server_supports_expanded_events(self) -> None:
|
|
143
148
|
server_info = self._database.client.server_info()
|
|
@@ -168,11 +173,13 @@ class DatabaseStreamSupervisor:
|
|
|
168
173
|
previous_stream.close()
|
|
169
174
|
except PyMongoError:
|
|
170
175
|
pass
|
|
176
|
+
|
|
171
177
|
if self._stop_event.is_set():
|
|
172
178
|
try:
|
|
173
179
|
self._stream.close()
|
|
174
180
|
except PyMongoError:
|
|
175
181
|
pass
|
|
182
|
+
self._stream = None
|
|
176
183
|
|
|
177
184
|
def _set_health(self, health: StreamHealth) -> None:
|
|
178
185
|
with self._lifecycle_lock:
|
|
@@ -262,12 +269,14 @@ class DatabaseStreamSupervisor:
|
|
|
262
269
|
|
|
263
270
|
class ChangeStreamCoordinator:
|
|
264
271
|
__slots__ = (
|
|
272
|
+
"_activations",
|
|
265
273
|
"_cache",
|
|
266
274
|
"_client",
|
|
267
275
|
"_closed",
|
|
268
276
|
"_health_registry",
|
|
269
277
|
"_lock",
|
|
270
278
|
"_max_await_time_ms",
|
|
279
|
+
"_shutdown_complete",
|
|
271
280
|
"_supervisors",
|
|
272
281
|
)
|
|
273
282
|
|
|
@@ -281,46 +290,105 @@ class ChangeStreamCoordinator:
|
|
|
281
290
|
self._client = client
|
|
282
291
|
self._max_await_time_ms = max_await_time_ms
|
|
283
292
|
self._cache = cache
|
|
293
|
+
self._activations: dict[
|
|
294
|
+
str, StreamActivation[DatabaseStreamSupervisor, threading.Event]
|
|
295
|
+
] = {} # Can be empty.
|
|
284
296
|
self._supervisors: dict[str, DatabaseStreamSupervisor] = {}
|
|
285
297
|
self._closed = False
|
|
286
298
|
self._health_registry = StreamHealthRegistry()
|
|
287
299
|
self._lock = threading.Lock()
|
|
300
|
+
self._shutdown_complete = threading.Event()
|
|
288
301
|
|
|
289
302
|
def activate_database(self, name: str) -> DatabaseStreamSupervisor | None:
|
|
290
303
|
with self._lock:
|
|
291
304
|
if self._closed:
|
|
292
305
|
raise StreamLifecycleError("coordinator is closed")
|
|
293
306
|
try:
|
|
294
|
-
|
|
307
|
+
return self._supervisors[name]
|
|
295
308
|
except KeyError:
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
309
|
+
pass
|
|
310
|
+
try:
|
|
311
|
+
activation = self._activations[name]
|
|
312
|
+
except KeyError:
|
|
313
|
+
activation = None
|
|
314
|
+
if activation is not None and (
|
|
315
|
+
activation.pending or not activation.retry.ready()
|
|
316
|
+
):
|
|
317
|
+
return None
|
|
318
|
+
supervisor = DatabaseStreamSupervisor(
|
|
319
|
+
self._client[name],
|
|
320
|
+
self._cache,
|
|
321
|
+
max_await_time_ms=self._max_await_time_ms,
|
|
322
|
+
)
|
|
323
|
+
if activation is None:
|
|
324
|
+
activation = StreamActivation(supervisor, threading.Event())
|
|
325
|
+
self._activations[name] = activation
|
|
326
|
+
else:
|
|
327
|
+
activation.supervisor = supervisor
|
|
328
|
+
activation.pending = True
|
|
329
|
+
activation.completion.clear()
|
|
330
|
+
self._health_registry.record_connecting(name)
|
|
331
|
+
published = False
|
|
332
|
+
try:
|
|
333
|
+
try:
|
|
334
|
+
supervisor.start()
|
|
335
|
+
except StreamStartupError:
|
|
336
|
+
with self._lock:
|
|
337
|
+
if not self._closed:
|
|
338
|
+
activation.retry.failed()
|
|
339
|
+
self._health_registry.record_startup_failure(name)
|
|
340
|
+
logger.warning(
|
|
341
|
+
"change stream startup failed for database %r; reads for "
|
|
342
|
+
"this database will bypass the cache",
|
|
343
|
+
name,
|
|
344
|
+
exc_info=True,
|
|
300
345
|
)
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
self._health_registry.record_startup_failure(name)
|
|
306
|
-
logger.warning(
|
|
307
|
-
"change stream startup failed for database %r; reads for "
|
|
308
|
-
"this database will bypass the cache",
|
|
309
|
-
name,
|
|
310
|
-
exc_info=True,
|
|
311
|
-
)
|
|
312
|
-
return None
|
|
346
|
+
return None
|
|
347
|
+
with self._lock:
|
|
348
|
+
if self._closed:
|
|
349
|
+
raise StreamLifecycleError("coordinator is closed")
|
|
313
350
|
self._supervisors[name] = supervisor
|
|
351
|
+
del self._activations[name]
|
|
352
|
+
self._health_registry.record_starting(name, supervisor.health_status)
|
|
353
|
+
published = True
|
|
314
354
|
return supervisor
|
|
355
|
+
finally:
|
|
356
|
+
if not published:
|
|
357
|
+
try:
|
|
358
|
+
supervisor.stop()
|
|
359
|
+
finally:
|
|
360
|
+
with self._lock:
|
|
361
|
+
if not self._closed:
|
|
362
|
+
self._health_registry.record_startup_failure(name)
|
|
363
|
+
activation.pending = False
|
|
364
|
+
activation.completion.set()
|
|
365
|
+
else:
|
|
366
|
+
activation.completion.set()
|
|
315
367
|
|
|
316
368
|
def stream_health_snapshot(self, database_name: str) -> StreamHealthSnapshot:
|
|
317
369
|
return self._health_registry.snapshot(database_name)
|
|
318
370
|
|
|
319
371
|
def close(self) -> None:
|
|
320
372
|
with self._lock:
|
|
321
|
-
self._closed
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
373
|
+
owns_cleanup = not self._closed
|
|
374
|
+
if owns_cleanup:
|
|
375
|
+
self._closed = True
|
|
376
|
+
self._health_registry.close()
|
|
377
|
+
supervisors = tuple(self._supervisors.values())
|
|
378
|
+
activations = tuple(self._activations.values())
|
|
379
|
+
for supervisor in supervisors:
|
|
380
|
+
supervisor.request_stop()
|
|
381
|
+
for activation in activations:
|
|
382
|
+
activation.supervisor.request_stop()
|
|
383
|
+
self._supervisors.clear()
|
|
384
|
+
self._activations.clear()
|
|
385
|
+
if not owns_cleanup:
|
|
386
|
+
self._shutdown_complete.wait()
|
|
387
|
+
return
|
|
388
|
+
try:
|
|
389
|
+
for activation in activations:
|
|
390
|
+
activation.completion.wait()
|
|
391
|
+
for supervisor in supervisors:
|
|
392
|
+
supervisor.stop()
|
|
393
|
+
finally:
|
|
394
|
+
self._shutdown_complete.set()
|
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/__init__.py
RENAMED
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/canonical.py
RENAMED
|
File without changes
|
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/collation.py
RENAMED
|
File without changes
|
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/count_reads.py
RENAMED
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/cursor_capture.py
RENAMED
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/errors.py
RENAMED
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/find_one_reads.py
RENAMED
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/find_reads.py
RENAMED
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/identity_reads.py
RENAMED
|
File without changes
|
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/lifecycle.py
RENAMED
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/locking.py
RENAMED
|
File without changes
|
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/namespace.py
RENAMED
|
File without changes
|
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/projection.py
RENAMED
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/query_filters.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/snapshots.py
RENAMED
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/stream_cost.py
RENAMED
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/stream_events.py
RENAMED
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/stream_options.py
RENAMED
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/traversal.py
RENAMED
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/_core/unique_keys.py
RENAMED
|
File without changes
|
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/asynchronous/cursors.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/synchronous/__init__.py
RENAMED
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/synchronous/cursors.py
RENAMED
|
File without changes
|
{client_query_cache-0.4.0 → client_query_cache-0.6.0}/src/client_query_cache/synchronous/database.py
RENAMED
|
File without changes
|