client-query-cache 0.2.0__tar.gz → 0.4.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/PKG-INFO +59 -0
- client_query_cache-0.4.0/README.md +45 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/pyproject.toml +20 -7
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/pyproject.toml.orig +23 -7
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/collation.py +12 -0
- client_query_cache-0.4.0/src/client_query_cache/_core/count_reads.py +32 -0
- client_query_cache-0.4.0/src/client_query_cache/_core/cursor_capture.py +93 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/entries.py +2 -0
- client_query_cache-0.4.0/src/client_query_cache/_core/find_reads.py +69 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/manager.py +96 -7
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/namespace.py +9 -1
- client_query_cache-0.4.0/src/client_query_cache/_core/query_filters.py +27 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/read_classification.py +9 -1
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/read_validation.py +3 -7
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/asynchronous/collection.py +72 -174
- client_query_cache-0.4.0/src/client_query_cache/asynchronous/cursors.py +315 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/asynchronous/manager.py +8 -15
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/synchronous/collection.py +78 -188
- client_query_cache-0.4.0/src/client_query_cache/synchronous/cursors.py +312 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/synchronous/manager.py +8 -13
- client_query_cache-0.2.0/PKG-INFO +0 -141
- client_query_cache-0.2.0/README.md +0 -127
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/__init__.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/__init__.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/canonical.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/codec.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/collection_metadata.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/errors.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/find_one_reads.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/identity_reads.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/keys.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/lifecycle.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/locking.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/lru.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/order_sensitive_keys.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/projection.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/snapshots.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/stream_cost.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/stream_events.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/stream_health.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/stream_options.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/traversal.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/unique_keys.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/asynchronous/__init__.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/asynchronous/database.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/asynchronous/streams.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/otel.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/py.typed +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/synchronous/__init__.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/synchronous/database.py +0 -0
- {client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/synchronous/streams.py +0 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: client-query-cache
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Client-side caching for PyMongo, kept coherent using MongoDB change streams.
|
|
5
|
+
Author: Alessio Locatelli
|
|
6
|
+
Author-email: Alessio Locatelli <<software.development@secure.mailbox.org>>
|
|
7
|
+
Requires-Dist: pymongo>=4.18.1
|
|
8
|
+
Requires-Dist: opentelemetry-api>=1.45.0 ; extra == 'otel'
|
|
9
|
+
Requires-Python: >=3.14.6
|
|
10
|
+
Project-URL: Repository, https://github.com/alessio-locatelli/client-query-cache
|
|
11
|
+
Project-URL: Documentation, https://alessio-locatelli.github.io/client-query-cache/
|
|
12
|
+
Provides-Extra: otel
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Coherent client-side caching for PyMongo
|
|
16
|
+
|
|
17
|
+
[](https://www.python.org/downloads/)
|
|
18
|
+
|
|
19
|
+
`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
|
+
|
|
21
|
+
**Built for production:** Designed for high-traffic, read-heavy applications, with cache hits served from memory to reduce latency and database load. Quality is backed by a 100% test-coverage requirement, real MongoDB integration and concurrency stress tests, and automated performance and memory regression checks.
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
Results vary by workload and deployment. [Measurements and methodology](https://alessio-locatelli.github.io/client-query-cache/benchmarks/).
|
|
26
|
+
|
|
27
|
+
Caching requires [MongoDB 8.0+ on a replica set or sharded cluster](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/#requirements). Reads on standalone servers and older MongoDB versions run through PyMongo without caching.
|
|
28
|
+
|
|
29
|
+
Invalidation is asynchronous: a cached read can return a preceding value until the write's change-stream event is processed. Use PyMongo directly when a read must immediately observe a preceding write.
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uv add client-query-cache
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or use `pip install client-query-cache`.
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from pymongo import MongoClient
|
|
41
|
+
|
|
42
|
+
from client_query_cache import CacheManager
|
|
43
|
+
|
|
44
|
+
with (
|
|
45
|
+
MongoClient("mongodb://localhost:27017") as client,
|
|
46
|
+
CacheManager(client) as cache,
|
|
47
|
+
):
|
|
48
|
+
users = cache.get_cached_collection(client["my_database"]["users"])
|
|
49
|
+
users.find_one({"_id": "alice"}) # Reads from MongoDB.
|
|
50
|
+
users.find_one({"_id": "alice"}) # Repeated reads can use the cache.
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## References
|
|
54
|
+
|
|
55
|
+
[Getting started](https://alessio-locatelli.github.io/client-query-cache/getting-started/synchronous/) · [Benchmarks](https://alessio-locatelli.github.io/client-query-cache/benchmarks/) · [Examples](https://alessio-locatelli.github.io/client-query-cache/examples/)
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
> MongoDB is a registered trademark of MongoDB, Inc. This project is independent and is not affiliated with or endorsed by MongoDB, Inc.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Coherent client-side caching for PyMongo
|
|
2
|
+
|
|
3
|
+
[](https://www.python.org/downloads/)
|
|
4
|
+
|
|
5
|
+
`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
|
+
|
|
7
|
+
**Built for production:** Designed for high-traffic, read-heavy applications, with cache hits served from memory to reduce latency and database load. Quality is backed by a 100% test-coverage requirement, real MongoDB integration and concurrency stress tests, and automated performance and memory regression checks.
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
Results vary by workload and deployment. [Measurements and methodology](https://alessio-locatelli.github.io/client-query-cache/benchmarks/).
|
|
12
|
+
|
|
13
|
+
Caching requires [MongoDB 8.0+ on a replica set or sharded cluster](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/#requirements). Reads on standalone servers and older MongoDB versions run through PyMongo without caching.
|
|
14
|
+
|
|
15
|
+
Invalidation is asynchronous: a cached read can return a preceding value until the write's change-stream event is processed. Use PyMongo directly when a read must immediately observe a preceding write.
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv add client-query-cache
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or use `pip install client-query-cache`.
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from pymongo import MongoClient
|
|
27
|
+
|
|
28
|
+
from client_query_cache import CacheManager
|
|
29
|
+
|
|
30
|
+
with (
|
|
31
|
+
MongoClient("mongodb://localhost:27017") as client,
|
|
32
|
+
CacheManager(client) as cache,
|
|
33
|
+
):
|
|
34
|
+
users = cache.get_cached_collection(client["my_database"]["users"])
|
|
35
|
+
users.find_one({"_id": "alice"}) # Reads from MongoDB.
|
|
36
|
+
users.find_one({"_id": "alice"}) # Repeated reads can use the cache.
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## References
|
|
40
|
+
|
|
41
|
+
[Getting started](https://alessio-locatelli.github.io/client-query-cache/getting-started/synchronous/) · [Benchmarks](https://alessio-locatelli.github.io/client-query-cache/benchmarks/) · [Examples](https://alessio-locatelli.github.io/client-query-cache/examples/)
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
> MongoDB is a registered trademark of MongoDB, Inc. This project is independent and is not affiliated with or endorsed by MongoDB, Inc.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "client-query-cache"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "Client-side caching for PyMongo, kept coherent using MongoDB change streams."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14.6"
|
|
@@ -27,7 +27,6 @@ dev = [
|
|
|
27
27
|
"types-pygments>=2.21.0.20260819",
|
|
28
28
|
"types-pysocks>=1.7.1.20260518",
|
|
29
29
|
"types-simplejson>=4.1.0.20260724",
|
|
30
|
-
"coverage>=7.16.1",
|
|
31
30
|
"faker>=40.38.0",
|
|
32
31
|
"hypothesis>=6.168.0",
|
|
33
32
|
"pytest>=9.1.1",
|
|
@@ -45,19 +44,30 @@ dev = [
|
|
|
45
44
|
"types-psutil>=7.2.2.20260906",
|
|
46
45
|
"dnspython>=2.8.0",
|
|
47
46
|
"python-snappy>=0.7.3",
|
|
47
|
+
"pytest-xdist[psutil]>=3.8.0",
|
|
48
|
+
"pytest-cov>=7.1.0",
|
|
49
|
+
"pyyaml>=6.0.3",
|
|
50
|
+
"types-pyyaml>=6.0.12.20260906",
|
|
48
51
|
]
|
|
49
|
-
docs = [
|
|
52
|
+
docs = [
|
|
53
|
+
"mike",
|
|
54
|
+
"tomli-w>=1.2.0",
|
|
55
|
+
"zensical>=0.0.68",
|
|
56
|
+
]
|
|
57
|
+
memory = ["pytest-memray>=1.11.0"]
|
|
50
58
|
|
|
51
59
|
[build-system]
|
|
52
60
|
requires = ["uv_build>=0.12.18,<0.13.0"]
|
|
53
61
|
build-backend = "uv_build"
|
|
54
62
|
|
|
55
|
-
[tool.uv
|
|
56
|
-
|
|
63
|
+
[tool.uv]
|
|
64
|
+
environments = ["sys_platform == 'linux'"]
|
|
65
|
+
|
|
66
|
+
[tool.uv.sources.mike]
|
|
67
|
+
git = "https://github.com/squidfunk/mike.git"
|
|
68
|
+
rev = "2d4ad799442f4592db8ad53b179bfb33db8c69ac"
|
|
57
69
|
|
|
58
70
|
[tool.slotscheck]
|
|
59
|
-
strict-imports = true
|
|
60
|
-
require-superclass = true
|
|
61
71
|
require-subclass = true
|
|
62
72
|
exclude-modules = """
|
|
63
73
|
(
|
|
@@ -90,3 +100,6 @@ level = "aggressive"
|
|
|
90
100
|
|
|
91
101
|
[tool.ruff-extra-rules.redundant-dict-get]
|
|
92
102
|
level = "aggressive"
|
|
103
|
+
|
|
104
|
+
[tool.ruff-extra-rules.defined-far-from-use]
|
|
105
|
+
level = "aggressive"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "client-query-cache"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.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>" },
|
|
@@ -26,7 +26,6 @@ dev = [
|
|
|
26
26
|
"types-pygments>=2.21.0.20260819",
|
|
27
27
|
"types-pysocks>=1.7.1.20260518",
|
|
28
28
|
"types-simplejson>=4.1.0.20260724",
|
|
29
|
-
"coverage>=7.16.1",
|
|
30
29
|
"faker>=40.38.0",
|
|
31
30
|
"hypothesis>=6.168.0",
|
|
32
31
|
"pytest>=9.1.1",
|
|
@@ -44,19 +43,29 @@ dev = [
|
|
|
44
43
|
"types-psutil>=7.2.2.20260906",
|
|
45
44
|
"dnspython>=2.8.0",
|
|
46
45
|
"python-snappy>=0.7.3",
|
|
46
|
+
"pytest-xdist[psutil]>=3.8.0",
|
|
47
|
+
"pytest-cov>=7.1.0",
|
|
48
|
+
"pyyaml>=6.0.3",
|
|
49
|
+
"types-pyyaml>=6.0.12.20260906",
|
|
47
50
|
]
|
|
48
|
-
docs = ["zensical>=0.0.
|
|
51
|
+
docs = ["mike", "tomli-w>=1.2.0", "zensical>=0.0.68"]
|
|
52
|
+
memory = ["pytest-memray>=1.11.0"]
|
|
49
53
|
|
|
50
54
|
[build-system]
|
|
51
55
|
requires = ["uv_build>=0.12.18,<0.13.0"]
|
|
52
56
|
build-backend = "uv_build"
|
|
53
57
|
|
|
54
|
-
[tool.uv
|
|
55
|
-
|
|
58
|
+
[tool.uv]
|
|
59
|
+
# uv 0.12.9 / lock context: https://docs.astral.sh/uv/reference/settings/#environments
|
|
60
|
+
# The default is all environments. We override it because development dependencies and their lock resolution target Linux.
|
|
61
|
+
environments = ["sys_platform == 'linux'"]
|
|
62
|
+
|
|
63
|
+
[tool.uv.sources]
|
|
64
|
+
mike = { git = "https://github.com/squidfunk/mike.git", rev = "2d4ad799442f4592db8ad53b179bfb33db8c69ac" } # pragma: allowlist secret
|
|
56
65
|
|
|
57
66
|
[tool.slotscheck]
|
|
58
|
-
|
|
59
|
-
require-
|
|
67
|
+
# slotscheck 0.21.0: https://github.com/ariebovenberg/slotscheck/blob/v0.21.0/src/slotscheck/config.py
|
|
68
|
+
# require-subclass: The default is false. We override it because subclasses must preserve their slotted parent's memory contract.
|
|
60
69
|
require-subclass = true
|
|
61
70
|
exclude-modules = '''
|
|
62
71
|
(
|
|
@@ -69,10 +78,15 @@ exclude-modules = '''
|
|
|
69
78
|
paths = ["src", "tests", "benchmarks", ".vulture_whitelist"]
|
|
70
79
|
|
|
71
80
|
[tool.ruff-extra-rules]
|
|
81
|
+
# ruff-extra-rules v0.5.0: https://github.com/alessio-locatelli/ruff-extra-rules/blob/v0.5.0/README.md
|
|
82
|
+
# The default is false. We override it because hook runs should apply available repairs.
|
|
72
83
|
fix = true
|
|
73
84
|
exclude = ["specifications"]
|
|
74
85
|
extend-select = ["unused-pytriage"]
|
|
75
86
|
[tool.ruff-extra-rules.meaningless-vars]
|
|
87
|
+
# ruff-extra-rules v0.5.0: https://github.com/alessio-locatelli/ruff-extra-rules/tree/v0.5.0/src/pre_commit_hooks/ast_checks
|
|
88
|
+
# For meaningless-vars, redundant-assignment, redundant-type-conversion, redundant-dict-get, and defined-far-from-use level:
|
|
89
|
+
# The default is conservative. We override it because repository quality checks must include the broader supported simplifications and naming diagnostics.
|
|
76
90
|
level = "aggressive"
|
|
77
91
|
[tool.ruff-extra-rules.redundant-assignment]
|
|
78
92
|
level = "aggressive"
|
|
@@ -80,3 +94,5 @@ level = "aggressive"
|
|
|
80
94
|
level = "aggressive"
|
|
81
95
|
[tool.ruff-extra-rules.redundant-dict-get]
|
|
82
96
|
level = "aggressive"
|
|
97
|
+
[tool.ruff-extra-rules.defined-far-from-use]
|
|
98
|
+
level = "aggressive"
|
{client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/collation.py
RENAMED
|
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING, Any
|
|
4
4
|
|
|
5
|
+
from pymongo.collation import Collation
|
|
6
|
+
|
|
5
7
|
if TYPE_CHECKING:
|
|
6
8
|
from collections.abc import Mapping
|
|
7
9
|
|
|
@@ -20,3 +22,13 @@ def normalize_collation(
|
|
|
20
22
|
if locale == _SIMPLE_LOCALE:
|
|
21
23
|
return None
|
|
22
24
|
return collation
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def collation_document(
|
|
28
|
+
collation: Collation | Mapping[str, Any] | None,
|
|
29
|
+
) -> Mapping[str, Any] | None:
|
|
30
|
+
if collation is None:
|
|
31
|
+
return None
|
|
32
|
+
if isinstance(collation, Collation):
|
|
33
|
+
return collation.document
|
|
34
|
+
return dict(collation)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, TypedDict
|
|
4
|
+
|
|
5
|
+
from pymongo.collation import Collation
|
|
6
|
+
|
|
7
|
+
from client_query_cache._core.collation import collation_document
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
|
|
12
|
+
_OMITTED = object()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CountReadOptions(TypedDict):
|
|
16
|
+
cache_options: tuple[object, object, object, object]
|
|
17
|
+
extra_options: dict[str, object] # Can be empty for cacheable reads.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def count_read_options(kwargs: Mapping[str, object]) -> CountReadOptions:
|
|
21
|
+
extra_options = dict(kwargs)
|
|
22
|
+
skip = extra_options.pop("skip", 0)
|
|
23
|
+
limit = extra_options.pop("limit", _OMITTED)
|
|
24
|
+
collation = extra_options.pop("collation", None)
|
|
25
|
+
hint = extra_options.pop("hint", _OMITTED)
|
|
26
|
+
if isinstance(collation, Collation):
|
|
27
|
+
collation = collation_document(collation)
|
|
28
|
+
cache_options = (skip, limit, collation, hint)
|
|
29
|
+
return {
|
|
30
|
+
"cache_options": cache_options,
|
|
31
|
+
"extra_options": extra_options,
|
|
32
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from bson.raw_bson import DEFAULT_RAW_BSON_OPTIONS, RawBSONDocument
|
|
6
|
+
|
|
7
|
+
from client_query_cache._core.codec import encode_value
|
|
8
|
+
from client_query_cache._core.errors import CacheClosedError
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
|
|
13
|
+
from bson.codec_options import CodecOptions
|
|
14
|
+
|
|
15
|
+
from client_query_cache._core.entries import AdmissionOutcome
|
|
16
|
+
from client_query_cache._core.find_reads import FindSource
|
|
17
|
+
from client_query_cache._core.manager import CacheCore, NamespaceCapture
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CursorCapture:
|
|
21
|
+
"""Own immutable document snapshots until consumption completes."""
|
|
22
|
+
|
|
23
|
+
__slots__ = (
|
|
24
|
+
"_active",
|
|
25
|
+
"_capture",
|
|
26
|
+
"_codec_options",
|
|
27
|
+
"_core",
|
|
28
|
+
"_discriminator",
|
|
29
|
+
"_documents",
|
|
30
|
+
"_find_source",
|
|
31
|
+
"_max_entry_bytes",
|
|
32
|
+
"retained_bytes",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
core: CacheCore,
|
|
38
|
+
capture: NamespaceCapture,
|
|
39
|
+
discriminator: object,
|
|
40
|
+
codec_options: CodecOptions[Mapping[str, Any]],
|
|
41
|
+
*,
|
|
42
|
+
find_source: FindSource | None = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
self._core = core
|
|
45
|
+
self._capture = capture
|
|
46
|
+
self._discriminator = discriminator
|
|
47
|
+
self._codec_options = codec_options
|
|
48
|
+
self._find_source = find_source
|
|
49
|
+
self._max_entry_bytes = core.snapshot().max_entry_bytes
|
|
50
|
+
self._documents: list[RawBSONDocument] = [] # An empty result is cacheable.
|
|
51
|
+
self.retained_bytes = 0 # Empty and abandoned captures retain no payload.
|
|
52
|
+
self._active = True
|
|
53
|
+
|
|
54
|
+
def append(self, document: Mapping[str, Any]) -> None:
|
|
55
|
+
if not self._active:
|
|
56
|
+
return
|
|
57
|
+
try:
|
|
58
|
+
encoded = encode_value(document, self._codec_options)
|
|
59
|
+
except Exception: # noqa: BLE001 - Application BSON encoders can raise arbitrary errors.
|
|
60
|
+
self.abandon()
|
|
61
|
+
return
|
|
62
|
+
if self.retained_bytes + len(encoded) > self._max_entry_bytes:
|
|
63
|
+
self._core.record_oversized_bypass()
|
|
64
|
+
self.abandon()
|
|
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"])
|
|
69
|
+
self.retained_bytes += len(encoded)
|
|
70
|
+
|
|
71
|
+
def abandon(self) -> None:
|
|
72
|
+
self._active = False
|
|
73
|
+
self._documents.clear()
|
|
74
|
+
self.retained_bytes = 0
|
|
75
|
+
|
|
76
|
+
def finish(self) -> AdmissionOutcome | None:
|
|
77
|
+
if not self._active:
|
|
78
|
+
return None
|
|
79
|
+
self._active = False
|
|
80
|
+
try:
|
|
81
|
+
return self._core.admit_namespace(
|
|
82
|
+
self._capture,
|
|
83
|
+
self._discriminator,
|
|
84
|
+
self._documents,
|
|
85
|
+
codec_options=self._codec_options,
|
|
86
|
+
find_source=self._find_source,
|
|
87
|
+
)
|
|
88
|
+
except CacheClosedError:
|
|
89
|
+
# The cursor owns its native resources independently of the manager.
|
|
90
|
+
return None
|
|
91
|
+
finally:
|
|
92
|
+
self._documents.clear()
|
|
93
|
+
self.retained_bytes = 0
|
{client_query_cache-0.2.0 → client_query_cache-0.4.0}/src/client_query_cache/_core/entries.py
RENAMED
|
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
6
6
|
|
|
7
7
|
if TYPE_CHECKING:
|
|
8
8
|
from client_query_cache._core.canonical import Canonical
|
|
9
|
+
from client_query_cache._core.find_reads import FindSource
|
|
9
10
|
from client_query_cache._core.keys import NamespaceId
|
|
10
11
|
|
|
11
12
|
|
|
@@ -24,6 +25,7 @@ class CacheEntry:
|
|
|
24
25
|
value: bytes
|
|
25
26
|
namespace: NamespaceId
|
|
26
27
|
identity: Canonical | None
|
|
28
|
+
find_source: FindSource | None = None
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
@dataclass(frozen=True, slots=True)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from client_query_cache._core.canonical import canonicalize
|
|
7
|
+
from client_query_cache._core.order_sensitive_keys import (
|
|
8
|
+
order_sensitive_discriminator_key,
|
|
9
|
+
)
|
|
10
|
+
from client_query_cache._core.query_filters import find_filter_key
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from client_query_cache._core.canonical import Canonical
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class FindSource:
|
|
18
|
+
family: int # A compact hash, possibly zero; collisions require key equality.
|
|
19
|
+
limit: int # Zero denotes unlimited; negative and boolean limits are excluded.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class FindReadShape:
|
|
24
|
+
family: object
|
|
25
|
+
limit: int # Zero and negative limits retain exact-only lookup.
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def discriminator(self) -> object:
|
|
29
|
+
return (self.family, self.limit)
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def source(self) -> FindSource | None:
|
|
33
|
+
if isinstance(self.limit, bool) or self.limit < 0:
|
|
34
|
+
return None
|
|
35
|
+
return FindSource(hash(canonicalize(self.family)), self.limit)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def find_discriminator(
|
|
39
|
+
family: Canonical,
|
|
40
|
+
limit: int, # Zero and negative limits retain exact identity.
|
|
41
|
+
) -> Canonical:
|
|
42
|
+
# Reuse the already canonical family without copying its query tree.
|
|
43
|
+
return canonicalize((family, limit))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def find_read_shape(
|
|
47
|
+
filter_document: object,
|
|
48
|
+
projection: object,
|
|
49
|
+
ordering: object,
|
|
50
|
+
skip: int, # Zero means no skipped documents.
|
|
51
|
+
limit: int, # Zero and negative limits use native semantics.
|
|
52
|
+
*,
|
|
53
|
+
collation: object,
|
|
54
|
+
codec: object,
|
|
55
|
+
) -> FindReadShape:
|
|
56
|
+
return FindReadShape(
|
|
57
|
+
family=order_sensitive_discriminator_key(
|
|
58
|
+
(
|
|
59
|
+
"find",
|
|
60
|
+
find_filter_key(filter_document),
|
|
61
|
+
projection,
|
|
62
|
+
ordering,
|
|
63
|
+
skip,
|
|
64
|
+
collation,
|
|
65
|
+
codec,
|
|
66
|
+
)
|
|
67
|
+
),
|
|
68
|
+
limit=limit,
|
|
69
|
+
)
|