client-query-cache 0.8.1__tar.gz → 0.9.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.9.0/PKG-INFO +104 -0
- client_query_cache-0.9.0/README.md +78 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/pyproject.toml +2 -2
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/codec.py +9 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/collection_metadata.py +8 -20
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/cursor_capture.py +4 -6
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/find_reads.py +24 -4
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/manager.py +425 -68
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/namespace.py +2 -0
- client_query_cache-0.9.0/src/client_query_cache/_core/namespace_metadata.py +84 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/residency.py +5 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/snapshots.py +1 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/stream_events.py +25 -4
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/unique_keys.py +7 -13
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_shared/adapters.py +773 -144
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_shared/attachment.py +68 -9
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_shared/coordinator.py +102 -28
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_shared/shared_index.py +80 -26
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_shared/wire.py +51 -3
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/asynchronous/collection.py +15 -15
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/asynchronous/cursors.py +8 -9
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/asynchronous/manager.py +41 -13
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/asynchronous/shared.py +0 -7
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/asynchronous/streams.py +16 -1
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/shared.py +2 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/synchronous/collection.py +15 -15
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/synchronous/cursors.py +8 -9
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/synchronous/manager.py +41 -13
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/synchronous/shared.py +0 -7
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/synchronous/streams.py +16 -1
- client_query_cache-0.8.1/PKG-INFO +0 -94
- client_query_cache-0.8.1/README.md +0 -68
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/LICENSE +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/Cargo.lock +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/Cargo.toml +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/src/clock.rs +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/src/echo.rs +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/src/frames.rs +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/src/hash.rs +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/src/index.rs +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/src/lib.rs +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/src/memory.rs +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/src/python.rs +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/src/relay.rs +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/src/ring.rs +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/src/sync.rs +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/__init__.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/__init__.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/canonical.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/collation.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/count_reads.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/entries.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/errors.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/find_one_reads.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/identity_reads.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/keys.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/lifecycle.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/locking.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/lru.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/order_sensitive_keys.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/projection.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/query_filters.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/read_classification.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/read_validation.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/stream_activation.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/stream_cost.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/stream_health.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/stream_options.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/traversal.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_shared/__init__.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_shared_index.pyi +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_types.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/asynchronous/__init__.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/asynchronous/database.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/otel.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/py.typed +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/synchronous/__init__.py +0 -0
- {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/synchronous/database.py +0 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: client-query-cache
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Classifier: Framework :: AsyncIO
|
|
5
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
6
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
10
|
+
Classifier: Topic :: Database
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
12
|
+
Classifier: Typing :: Typed
|
|
13
|
+
Requires-Dist: annotated-types>=0.8.0
|
|
14
|
+
Requires-Dist: pymongo>=4.18.1
|
|
15
|
+
Requires-Dist: opentelemetry-api>=1.45.0 ; extra == 'otel'
|
|
16
|
+
Provides-Extra: otel
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Summary: Sync and asyncio PyMongo query caching with change-stream invalidation and Rust-backed sharing across workers.
|
|
19
|
+
Author-email: Alessio Locatelli <<software.development@secure.mailbox.org>>
|
|
20
|
+
License-Expression: Apache-2.0
|
|
21
|
+
Requires-Python: >=3.14
|
|
22
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
23
|
+
Project-URL: Documentation, https://alessio-locatelli.github.io/client-query-cache/
|
|
24
|
+
Project-URL: Repository, https://github.com/alessio-locatelli/client-query-cache
|
|
25
|
+
|
|
26
|
+
# Client-side query caching for PyMongo
|
|
27
|
+
|
|
28
|
+
[](https://pypi.org/project/client-query-cache/)
|
|
29
|
+
[](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/)
|
|
30
|
+
[](https://github.com/alessio-locatelli/client-query-cache/actions/workflows/test.yml)
|
|
31
|
+
[](https://pypi.org/project/client-query-cache/)
|
|
32
|
+
|
|
33
|
+
**Speed up read-heavy, high-throughput Python applications.** `client-query-cache` serves eligible repeated PyMongo reads from memory, reducing read latency and avoiding MongoDB round trips on cache hits.
|
|
34
|
+
|
|
35
|
+
Add caching through an explicit [cached collection view](https://alessio-locatelli.github.io/client-query-cache/usage/cached-reads/). Keep your existing MongoDB queries and use PyMongo for writes.
|
|
36
|
+
|
|
37
|
+
- **[Synchronous](https://alessio-locatelli.github.io/client-query-cache/getting-started/synchronous/) and [asyncio](https://alessio-locatelli.github.io/client-query-cache/getting-started/asyncio/) APIs** for long-lived Python applications.
|
|
38
|
+
- **[MongoDB change streams](https://alessio-locatelli.github.io/client-query-cache/usage/consistency/)** invalidate affected results, including writes from other applications.
|
|
39
|
+
- **[Shared caching across application workers](https://alessio-locatelli.github.io/client-query-cache/operations/shared-cache/)** on one [supported Linux host](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/#requirements) reduces duplicate cached documents and uses one change stream per active database.
|
|
40
|
+
- **[A Rust-backed shared cache](https://alessio-locatelli.github.io/client-query-cache/benchmarks/shared-cache/)** serves eligible warm reads from native shared memory without a MongoDB round trip.
|
|
41
|
+
|
|
42
|
+
Choose `CacheManager` for caching within one process, or `SharedCacheManager` to reuse eligible collection reads across trusted same-host workers. Both support sync and asyncio. See [which manager to choose](https://alessio-locatelli.github.io/client-query-cache/getting-started/choose-a-manager/).
|
|
43
|
+
|
|
44
|
+
With shared caching, workers look up the same cache and use their own PyMongo clients for misses and bypasses:
|
|
45
|
+
|
|
46
|
+
```mermaid
|
|
47
|
+
flowchart TB
|
|
48
|
+
subgraph Application["Python application"]
|
|
49
|
+
Worker1["Python worker 1"] -->|"Cache lookup"| Cache["Rust-backed<br/>shared cache"]
|
|
50
|
+
Worker2["Python worker 2"] -->|"Cache lookup"| Cache
|
|
51
|
+
end
|
|
52
|
+
Worker1 -->|"Miss / bypass"| DB[("MongoDB")]
|
|
53
|
+
Worker2 -->|"Miss / bypass"| DB
|
|
54
|
+
DB -.->|"Change streams<br/>Async invalidation"| Cache
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
[Benchmarks](https://alessio-locatelli.github.io/client-query-cache/benchmarks/shared-cache/) compare direct reads, independent caches and shared caching. Sharing avoids duplicated values and streams, but its owner process and cache misses have costs. Rust does not guarantee a faster application or lower total memory for every workload.
|
|
58
|
+
|
|
59
|
+

|
|
60
|
+
|
|
61
|
+
The graphic shows [process-local measurements from local MongoDB and Atlas deployments](https://alessio-locatelli.github.io/client-query-cache/benchmarks/#illustrative-read-latency); it is separate from the shared-cache comparison above.
|
|
62
|
+
|
|
63
|
+
## Quick start
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uv add client-query-cache
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or use `pip install client-query-cache`.
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from pymongo import MongoClient
|
|
73
|
+
|
|
74
|
+
from client_query_cache import CacheManager
|
|
75
|
+
|
|
76
|
+
with (
|
|
77
|
+
MongoClient("mongodb://localhost:27017") as client,
|
|
78
|
+
CacheManager(client) as cache_manager,
|
|
79
|
+
):
|
|
80
|
+
collection = client["client_query_cache_tutorial"]["items"]
|
|
81
|
+
cached_collection = cache_manager.get_cached_collection(collection)
|
|
82
|
+
collection.replace_one(
|
|
83
|
+
{"_id": "example"}, {"_id": "example", "value": 42}, upsert=True
|
|
84
|
+
)
|
|
85
|
+
cached_collection.find_one({"_id": "example"})
|
|
86
|
+
cached_collection.find_one({"_id": "example"}) # Repeated reads can hit the cache.
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Check the [platform and MongoDB requirements](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/#requirements) before installing. [Read eligibility](https://alessio-locatelli.github.io/client-query-cache/reference/api/#bypass-conditions) determines which requests can use the cache.
|
|
90
|
+
|
|
91
|
+
Invalidation is asynchronous: cached reads can briefly return a preceding value after a write. Use direct PyMongo reads with suitable consistency controls for read-after-write and current authorization; see [consistency](https://alessio-locatelli.github.io/client-query-cache/usage/consistency/).
|
|
92
|
+
|
|
93
|
+
## Run an application
|
|
94
|
+
|
|
95
|
+
Start with the [synchronous](https://alessio-locatelli.github.io/client-query-cache/getting-started/synchronous/) or [asyncio](https://alessio-locatelli.github.io/client-query-cache/getting-started/asyncio/) tutorial. The [shared FastAPI example](https://alessio-locatelli.github.io/client-query-cache/examples/fastapi-shared/) runs real Granian workers and verifies cross-process hits, invalidation and graceful shutdown. The [compact FastAPI example](https://alessio-locatelli.github.io/client-query-cache/examples/fastapi-minimal/) demonstrates process-local caching.
|
|
96
|
+
|
|
97
|
+
[Deployment](https://alessio-locatelli.github.io/client-query-cache/operations/) · [API reference](https://alessio-locatelli.github.io/client-query-cache/reference/api/) · [Examples](https://alessio-locatelli.github.io/client-query-cache/examples/)
|
|
98
|
+
|
|
99
|
+
The library is tested against real MongoDB replica sets, including [concurrency stress tests](https://github.com/alessio-locatelli/client-query-cache/blob/main/docs/development/concurrency-stress-tests.md). Separate checks guard against [performance](https://github.com/alessio-locatelli/client-query-cache/blob/main/docs/development/performance-regression-guard.md) and [memory](https://github.com/alessio-locatelli/client-query-cache/blob/main/docs/development/memory-regression-tests.md) regressions.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
> MongoDB is a registered trademark of MongoDB, Inc. This project is independent and is not affiliated with or endorsed by MongoDB, Inc.
|
|
104
|
+
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Client-side query caching for PyMongo
|
|
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/)
|
|
7
|
+
|
|
8
|
+
**Speed up read-heavy, high-throughput Python applications.** `client-query-cache` serves eligible repeated PyMongo reads from memory, reducing read latency and avoiding MongoDB round trips on cache hits.
|
|
9
|
+
|
|
10
|
+
Add caching through an explicit [cached collection view](https://alessio-locatelli.github.io/client-query-cache/usage/cached-reads/). Keep your existing MongoDB queries and use PyMongo for writes.
|
|
11
|
+
|
|
12
|
+
- **[Synchronous](https://alessio-locatelli.github.io/client-query-cache/getting-started/synchronous/) and [asyncio](https://alessio-locatelli.github.io/client-query-cache/getting-started/asyncio/) APIs** for long-lived Python applications.
|
|
13
|
+
- **[MongoDB change streams](https://alessio-locatelli.github.io/client-query-cache/usage/consistency/)** invalidate affected results, including writes from other applications.
|
|
14
|
+
- **[Shared caching across application workers](https://alessio-locatelli.github.io/client-query-cache/operations/shared-cache/)** on one [supported Linux host](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/#requirements) reduces duplicate cached documents and uses one change stream per active database.
|
|
15
|
+
- **[A Rust-backed shared cache](https://alessio-locatelli.github.io/client-query-cache/benchmarks/shared-cache/)** serves eligible warm reads from native shared memory without a MongoDB round trip.
|
|
16
|
+
|
|
17
|
+
Choose `CacheManager` for caching within one process, or `SharedCacheManager` to reuse eligible collection reads across trusted same-host workers. Both support sync and asyncio. See [which manager to choose](https://alessio-locatelli.github.io/client-query-cache/getting-started/choose-a-manager/).
|
|
18
|
+
|
|
19
|
+
With shared caching, workers look up the same cache and use their own PyMongo clients for misses and bypasses:
|
|
20
|
+
|
|
21
|
+
```mermaid
|
|
22
|
+
flowchart TB
|
|
23
|
+
subgraph Application["Python application"]
|
|
24
|
+
Worker1["Python worker 1"] -->|"Cache lookup"| Cache["Rust-backed<br/>shared cache"]
|
|
25
|
+
Worker2["Python worker 2"] -->|"Cache lookup"| Cache
|
|
26
|
+
end
|
|
27
|
+
Worker1 -->|"Miss / bypass"| DB[("MongoDB")]
|
|
28
|
+
Worker2 -->|"Miss / bypass"| DB
|
|
29
|
+
DB -.->|"Change streams<br/>Async invalidation"| Cache
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
[Benchmarks](https://alessio-locatelli.github.io/client-query-cache/benchmarks/shared-cache/) compare direct reads, independent caches and shared caching. Sharing avoids duplicated values and streams, but its owner process and cache misses have costs. Rust does not guarantee a faster application or lower total memory for every workload.
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
The graphic shows [process-local measurements from local MongoDB and Atlas deployments](https://alessio-locatelli.github.io/client-query-cache/benchmarks/#illustrative-read-latency); it is separate from the shared-cache comparison above.
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv add client-query-cache
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or use `pip install client-query-cache`.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from pymongo import MongoClient
|
|
48
|
+
|
|
49
|
+
from client_query_cache import CacheManager
|
|
50
|
+
|
|
51
|
+
with (
|
|
52
|
+
MongoClient("mongodb://localhost:27017") as client,
|
|
53
|
+
CacheManager(client) as cache_manager,
|
|
54
|
+
):
|
|
55
|
+
collection = client["client_query_cache_tutorial"]["items"]
|
|
56
|
+
cached_collection = cache_manager.get_cached_collection(collection)
|
|
57
|
+
collection.replace_one(
|
|
58
|
+
{"_id": "example"}, {"_id": "example", "value": 42}, upsert=True
|
|
59
|
+
)
|
|
60
|
+
cached_collection.find_one({"_id": "example"})
|
|
61
|
+
cached_collection.find_one({"_id": "example"}) # Repeated reads can hit the cache.
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Check the [platform and MongoDB requirements](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/#requirements) before installing. [Read eligibility](https://alessio-locatelli.github.io/client-query-cache/reference/api/#bypass-conditions) determines which requests can use the cache.
|
|
65
|
+
|
|
66
|
+
Invalidation is asynchronous: cached reads can briefly return a preceding value after a write. Use direct PyMongo reads with suitable consistency controls for read-after-write and current authorization; see [consistency](https://alessio-locatelli.github.io/client-query-cache/usage/consistency/).
|
|
67
|
+
|
|
68
|
+
## Run an application
|
|
69
|
+
|
|
70
|
+
Start with the [synchronous](https://alessio-locatelli.github.io/client-query-cache/getting-started/synchronous/) or [asyncio](https://alessio-locatelli.github.io/client-query-cache/getting-started/asyncio/) tutorial. The [shared FastAPI example](https://alessio-locatelli.github.io/client-query-cache/examples/fastapi-shared/) runs real Granian workers and verifies cross-process hits, invalidation and graceful shutdown. The [compact FastAPI example](https://alessio-locatelli.github.io/client-query-cache/examples/fastapi-minimal/) demonstrates process-local caching.
|
|
71
|
+
|
|
72
|
+
[Deployment](https://alessio-locatelli.github.io/client-query-cache/operations/) · [API reference](https://alessio-locatelli.github.io/client-query-cache/reference/api/) · [Examples](https://alessio-locatelli.github.io/client-query-cache/examples/)
|
|
73
|
+
|
|
74
|
+
The library is tested against real MongoDB replica sets, including [concurrency stress tests](https://github.com/alessio-locatelli/client-query-cache/blob/main/docs/development/concurrency-stress-tests.md). Separate checks guard against [performance](https://github.com/alessio-locatelli/client-query-cache/blob/main/docs/development/performance-regression-guard.md) and [memory](https://github.com/alessio-locatelli/client-query-cache/blob/main/docs/development/memory-regression-tests.md) regressions.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
> MongoDB is a registered trademark of MongoDB, Inc. This project is independent and is not affiliated with or endorsed by MongoDB, Inc.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "client-query-cache"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "
|
|
3
|
+
version = "0.9.0"
|
|
4
|
+
description = "Sync and asyncio PyMongo query caching with change-stream invalidation and Rust-backed sharing across workers."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Alessio Locatelli", email = "<software.development@secure.mailbox.org>" },
|
|
7
7
|
]
|
|
@@ -11,6 +11,8 @@ if TYPE_CHECKING:
|
|
|
11
11
|
from bson.codec_options import TypeRegistry
|
|
12
12
|
|
|
13
13
|
_ENVELOPE_FIELD = "v"
|
|
14
|
+
# BSON length, document tag, UTF-8 field name, and its NUL terminator.
|
|
15
|
+
_DOCUMENT_OFFSET = 4 + 1 + len(_ENVELOPE_FIELD.encode()) + 1
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
class _TypeRegistryIdentity:
|
|
@@ -37,6 +39,13 @@ def encode_value(
|
|
|
37
39
|
)
|
|
38
40
|
|
|
39
41
|
|
|
42
|
+
def encode_document(
|
|
43
|
+
document: Mapping[str, Any], codec_options: CodecOptions[Mapping[str, Any]]
|
|
44
|
+
) -> bytes:
|
|
45
|
+
# Embedded encoding preserves field order; top-level encoding promotes _id.
|
|
46
|
+
return encode_value(document, codec_options)[_DOCUMENT_OFFSET:-1]
|
|
47
|
+
|
|
48
|
+
|
|
40
49
|
def decode_value(
|
|
41
50
|
encoded: bytes, codec_options: CodecOptions[Mapping[str, Any]] | None = None
|
|
42
51
|
) -> object:
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import threading
|
|
4
3
|
from dataclasses import dataclass
|
|
5
4
|
from typing import TYPE_CHECKING, Any
|
|
6
5
|
|
|
7
6
|
from client_query_cache._core.collation import normalize_collation
|
|
7
|
+
from client_query_cache._core.namespace_metadata import NamespaceMetadataCache
|
|
8
8
|
from client_query_cache._core.snapshots import BypassReason
|
|
9
|
-
from client_query_cache._types import NonNegativeInt
|
|
9
|
+
from client_query_cache._types import NonNegativeFloat, NonNegativeInt, PositiveInt
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
12
|
from collections.abc import Mapping
|
|
13
13
|
|
|
14
|
-
from client_query_cache._core.keys import NamespaceId
|
|
15
|
-
|
|
16
14
|
|
|
17
15
|
_COLLECTION_REASONS = {
|
|
18
16
|
"collection": None,
|
|
@@ -26,25 +24,15 @@ class CollectionMetadata:
|
|
|
26
24
|
checked_epoch: NonNegativeInt
|
|
27
25
|
bypass_reason: BypassReason | None
|
|
28
26
|
default_collation: Mapping[str, Any] | None
|
|
27
|
+
epoch_bound: bool = True
|
|
28
|
+
expires_at: NonNegativeFloat | None = None
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
class CollectionMetadataCache:
|
|
32
|
-
__slots__ = (
|
|
33
|
-
|
|
34
|
-
def __init__(self) -> None:
|
|
35
|
-
self._entries: dict[NamespaceId, CollectionMetadata] = {}
|
|
36
|
-
self._lock = threading.Lock()
|
|
37
|
-
|
|
38
|
-
def get(self, namespace: NamespaceId) -> CollectionMetadata | None:
|
|
39
|
-
with self._lock:
|
|
40
|
-
try:
|
|
41
|
-
return self._entries[namespace]
|
|
42
|
-
except KeyError:
|
|
43
|
-
return None
|
|
31
|
+
class CollectionMetadataCache(NamespaceMetadataCache[CollectionMetadata]):
|
|
32
|
+
__slots__ = ()
|
|
44
33
|
|
|
45
|
-
def
|
|
46
|
-
|
|
47
|
-
self._entries[namespace] = metadata
|
|
34
|
+
def __init__(self, max_namespaces: PositiveInt = 1024) -> None:
|
|
35
|
+
super().__init__(max_namespaces)
|
|
48
36
|
|
|
49
37
|
|
|
50
38
|
@dataclass(frozen=True, slots=True)
|
{client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/cursor_capture.py
RENAMED
|
@@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
4
4
|
|
|
5
5
|
from bson.raw_bson import DEFAULT_RAW_BSON_OPTIONS, RawBSONDocument
|
|
6
6
|
|
|
7
|
-
from client_query_cache._core.codec import
|
|
7
|
+
from client_query_cache._core.codec import encode_document
|
|
8
8
|
from client_query_cache._core.errors import CacheClosedError
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
@@ -46,7 +46,7 @@ class CursorCapture:
|
|
|
46
46
|
self._discriminator = discriminator
|
|
47
47
|
self._codec_options = codec_options
|
|
48
48
|
self._find_source = find_source
|
|
49
|
-
self._max_entry_bytes = core.
|
|
49
|
+
self._max_entry_bytes = core.max_entry_bytes
|
|
50
50
|
self._documents: list[RawBSONDocument] = [] # An empty result is cacheable.
|
|
51
51
|
self.retained_bytes = 0
|
|
52
52
|
self._active = True
|
|
@@ -55,7 +55,7 @@ class CursorCapture:
|
|
|
55
55
|
if not self._active:
|
|
56
56
|
return
|
|
57
57
|
try:
|
|
58
|
-
encoded =
|
|
58
|
+
encoded = encode_document(document, self._codec_options)
|
|
59
59
|
except Exception: # noqa: BLE001 - Application BSON encoders can raise arbitrary errors.
|
|
60
60
|
self.abandon()
|
|
61
61
|
return
|
|
@@ -63,9 +63,7 @@ class CursorCapture:
|
|
|
63
63
|
self._core.record_oversized_bypass()
|
|
64
64
|
self.abandon()
|
|
65
65
|
return
|
|
66
|
-
|
|
67
|
-
envelope = RawBSONDocument(encoded, DEFAULT_RAW_BSON_OPTIONS)
|
|
68
|
-
self._documents.append(envelope["v"])
|
|
66
|
+
self._documents.append(RawBSONDocument(encoded, DEFAULT_RAW_BSON_OPTIONS))
|
|
69
67
|
self.retained_bytes += len(encoded)
|
|
70
68
|
|
|
71
69
|
def abandon(self) -> None:
|
{client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/find_reads.py
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from dataclasses import dataclass
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
4
|
from typing import TYPE_CHECKING
|
|
5
5
|
|
|
6
6
|
from client_query_cache._core.canonical import canonicalize
|
|
@@ -31,9 +31,29 @@ class FindReadShape:
|
|
|
31
31
|
|
|
32
32
|
@property
|
|
33
33
|
def source(self) -> FindSource | None:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
return _find_source(self.family, self.limit)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class PreparedFindRead:
|
|
39
|
+
family: Canonical
|
|
40
|
+
limit: int # Negative and boolean limits retain exact identity.
|
|
41
|
+
discriminator: Canonical = field(init=False)
|
|
42
|
+
|
|
43
|
+
def __post_init__(self) -> None:
|
|
44
|
+
object.__setattr__(
|
|
45
|
+
self, "discriminator", find_discriminator(self.family, self.limit)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def source(self) -> FindSource | None:
|
|
50
|
+
return _find_source(self.family, self.limit)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _find_source(family: object, limit: int) -> FindSource | None:
|
|
54
|
+
if isinstance(limit, bool) or limit < 0:
|
|
55
|
+
return None
|
|
56
|
+
return FindSource(hash(canonicalize(family)), limit)
|
|
37
57
|
|
|
38
58
|
|
|
39
59
|
def find_discriminator(
|