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.
Files changed (78) hide show
  1. client_query_cache-0.9.0/PKG-INFO +104 -0
  2. client_query_cache-0.9.0/README.md +78 -0
  3. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/pyproject.toml +2 -2
  4. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/codec.py +9 -0
  5. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/collection_metadata.py +8 -20
  6. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/cursor_capture.py +4 -6
  7. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/find_reads.py +24 -4
  8. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/manager.py +425 -68
  9. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/namespace.py +2 -0
  10. client_query_cache-0.9.0/src/client_query_cache/_core/namespace_metadata.py +84 -0
  11. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/residency.py +5 -0
  12. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/snapshots.py +1 -0
  13. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/stream_events.py +25 -4
  14. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/unique_keys.py +7 -13
  15. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_shared/adapters.py +773 -144
  16. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_shared/attachment.py +68 -9
  17. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_shared/coordinator.py +102 -28
  18. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_shared/shared_index.py +80 -26
  19. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_shared/wire.py +51 -3
  20. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/asynchronous/collection.py +15 -15
  21. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/asynchronous/cursors.py +8 -9
  22. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/asynchronous/manager.py +41 -13
  23. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/asynchronous/shared.py +0 -7
  24. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/asynchronous/streams.py +16 -1
  25. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/shared.py +2 -0
  26. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/synchronous/collection.py +15 -15
  27. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/synchronous/cursors.py +8 -9
  28. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/synchronous/manager.py +41 -13
  29. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/synchronous/shared.py +0 -7
  30. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/synchronous/streams.py +16 -1
  31. client_query_cache-0.8.1/PKG-INFO +0 -94
  32. client_query_cache-0.8.1/README.md +0 -68
  33. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/LICENSE +0 -0
  34. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/Cargo.lock +0 -0
  35. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/Cargo.toml +0 -0
  36. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/src/clock.rs +0 -0
  37. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/src/echo.rs +0 -0
  38. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/src/frames.rs +0 -0
  39. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/src/hash.rs +0 -0
  40. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/src/index.rs +0 -0
  41. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/src/lib.rs +0 -0
  42. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/src/memory.rs +0 -0
  43. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/src/python.rs +0 -0
  44. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/src/relay.rs +0 -0
  45. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/src/ring.rs +0 -0
  46. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/native/shared-index/src/sync.rs +0 -0
  47. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/__init__.py +0 -0
  48. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/__init__.py +0 -0
  49. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/canonical.py +0 -0
  50. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/collation.py +0 -0
  51. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/count_reads.py +0 -0
  52. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/entries.py +0 -0
  53. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/errors.py +0 -0
  54. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/find_one_reads.py +0 -0
  55. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/identity_reads.py +0 -0
  56. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/keys.py +0 -0
  57. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/lifecycle.py +0 -0
  58. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/locking.py +0 -0
  59. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/lru.py +0 -0
  60. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/order_sensitive_keys.py +0 -0
  61. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/projection.py +0 -0
  62. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/query_filters.py +0 -0
  63. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/read_classification.py +0 -0
  64. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/read_validation.py +0 -0
  65. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/stream_activation.py +0 -0
  66. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/stream_cost.py +0 -0
  67. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/stream_health.py +0 -0
  68. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/stream_options.py +0 -0
  69. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_core/traversal.py +0 -0
  70. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_shared/__init__.py +0 -0
  71. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_shared_index.pyi +0 -0
  72. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/_types.py +0 -0
  73. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/asynchronous/__init__.py +0 -0
  74. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/asynchronous/database.py +0 -0
  75. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/otel.py +0 -0
  76. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/py.typed +0 -0
  77. {client_query_cache-0.8.1 → client_query_cache-0.9.0}/src/client_query_cache/synchronous/__init__.py +0 -0
  78. {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
+ [![PyPI](https://img.shields.io/pypi/v/client-query-cache?color=lightgrey)](https://pypi.org/project/client-query-cache/)
29
+ [![Python](https://img.shields.io/pypi/pyversions/client-query-cache)](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/)
30
+ [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/alessio-locatelli/client-query-cache/actions/workflows/test.yml)
31
+ [![Typing](https://img.shields.io/pypi/types/client-query-cache)](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
+ ![Illustrative process-local read latency on a logarithmic scale: direct local MongoDB read, about 120 microseconds; direct Atlas M0 read, 79.4 milliseconds; cached reads in either deployment, about 61 microseconds.](docs/user/assets/benchmark-latency-light.svg)
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
+ [![PyPI](https://img.shields.io/pypi/v/client-query-cache?color=lightgrey)](https://pypi.org/project/client-query-cache/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/client-query-cache)](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/)
5
+ [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/alessio-locatelli/client-query-cache/actions/workflows/test.yml)
6
+ [![Typing](https://img.shields.io/pypi/types/client-query-cache)](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
+ ![Illustrative process-local read latency on a logarithmic scale: direct local MongoDB read, about 120 microseconds; direct Atlas M0 read, 79.4 milliseconds; cached reads in either deployment, about 61 microseconds.](docs/user/assets/benchmark-latency-light.svg)
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.8.1"
4
- description = "Client-side caching for PyMongo, kept coherent using MongoDB change streams."
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__ = ("_entries", "_lock")
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 put(self, namespace: NamespaceId, metadata: CollectionMetadata) -> None:
46
- with self._lock:
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)
@@ -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 encode_value
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.snapshot().max_entry_bytes
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 = encode_value(document, self._codec_options)
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
- # Access the envelope without replaying application codec transformations.
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:
@@ -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
- if isinstance(self.limit, bool) or self.limit < 0:
35
- return None
36
- return FindSource(hash(canonicalize(self.family)), self.limit)
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(