client-query-cache 0.6.0__tar.gz → 0.7.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 (52) hide show
  1. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/PKG-INFO +26 -9
  2. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/README.md +24 -8
  3. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/pyproject.toml +6 -2
  4. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/pyproject.toml.orig +3 -2
  5. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/canonical.py +17 -8
  6. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/collection_metadata.py +2 -1
  7. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/count_reads.py +1 -1
  8. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/cursor_capture.py +1 -1
  9. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/entries.py +4 -2
  10. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/find_one_reads.py +5 -2
  11. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/find_reads.py +4 -3
  12. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/identity_reads.py +11 -9
  13. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/lru.py +8 -6
  14. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/manager.py +136 -44
  15. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/namespace.py +24 -9
  16. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/order_sensitive_keys.py +22 -24
  17. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/query_filters.py +2 -6
  18. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/read_classification.py +23 -16
  19. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/read_validation.py +27 -22
  20. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/snapshots.py +23 -12
  21. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/stream_activation.py +3 -2
  22. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/stream_cost.py +33 -18
  23. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/stream_events.py +5 -4
  24. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/stream_health.py +13 -8
  25. client_query_cache-0.7.0/src/client_query_cache/_core/stream_options.py +33 -0
  26. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/unique_keys.py +2 -1
  27. client_query_cache-0.7.0/src/client_query_cache/_types.py +21 -0
  28. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/asynchronous/collection.py +54 -42
  29. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/asynchronous/cursors.py +31 -30
  30. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/asynchronous/manager.py +2 -1
  31. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/asynchronous/streams.py +10 -17
  32. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/otel.py +5 -4
  33. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/synchronous/collection.py +52 -40
  34. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/synchronous/cursors.py +31 -30
  35. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/synchronous/manager.py +2 -1
  36. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/synchronous/streams.py +9 -16
  37. client_query_cache-0.6.0/src/client_query_cache/_core/stream_options.py +0 -14
  38. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/__init__.py +0 -0
  39. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/__init__.py +0 -0
  40. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/codec.py +0 -0
  41. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/collation.py +0 -0
  42. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/errors.py +0 -0
  43. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/keys.py +0 -0
  44. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/lifecycle.py +0 -0
  45. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/locking.py +0 -0
  46. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/projection.py +0 -0
  47. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/_core/traversal.py +0 -0
  48. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/asynchronous/__init__.py +0 -0
  49. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/asynchronous/database.py +0 -0
  50. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/py.typed +0 -0
  51. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/synchronous/__init__.py +0 -0
  52. {client_query_cache-0.6.0 → client_query_cache-0.7.0}/src/client_query_cache/synchronous/database.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: client-query-cache
3
- Version: 0.6.0
3
+ Version: 0.7.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>>
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.15
12
12
  Classifier: Topic :: Database
13
13
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
14
  Classifier: Typing :: Typed
15
+ Requires-Dist: annotated-types>=0.8.0
15
16
  Requires-Dist: pymongo>=4.18.1
16
17
  Requires-Dist: opentelemetry-api>=1.45.0 ; extra == 'otel'
17
18
  Requires-Python: >=3.14
@@ -20,16 +21,26 @@ Project-URL: Documentation, https://alessio-locatelli.github.io/client-query-cac
20
21
  Provides-Extra: otel
21
22
  Description-Content-Type: text/markdown
22
23
 
23
- # Coherent client-side caching for PyMongo
24
+ # Client-side query caching for PyMongo
24
25
 
25
26
  [![PyPI](https://img.shields.io/pypi/v/client-query-cache?color=lightgrey)](https://pypi.org/project/client-query-cache/)
26
27
  [![Python](https://img.shields.io/pypi/pyversions/client-query-cache)](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/)
27
28
  [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/alessio-locatelli/client-query-cache/actions/workflows/test.yml)
28
29
  [![Typing](https://img.shields.io/pypi/types/client-query-cache)](https://pypi.org/project/client-query-cache/)
29
30
 
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.
31
+ `client-query-cache` uses MongoDB change streams to asynchronously invalidate cached query results. It supports synchronous and asyncio applications without requiring a separate cache server.
31
32
 
32
- **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.
33
+ ```mermaid
34
+ %%{init: {"flowchart": {"diagramPadding": 75}}}%%
35
+ flowchart LR
36
+ App["Python<br/>application"] -->|"Reads"| Cache["Client-side<br/>caching"]
37
+ Cache -->|"Miss / bypass"| DB[("MongoDB")]
38
+ DB -.->|"Change streams<br/>Async invalidation"| Cache
39
+ ```
40
+
41
+ Cache hits are served from client-side memory, reducing read latency and database load in read-heavy applications.
42
+
43
+ The library is tested against real MongoDB replica sets, including concurrency stress tests, and automated checks guard its performance and memory use against regressions.
33
44
 
34
45
  ![Illustrative read latency on a logarithmic scale: direct local MongoDB read, 120 microseconds; direct Atlas M0 read, 79.4 milliseconds; cached read in either deployment, about 61 microseconds.](docs/user/assets/benchmark-latency-light.svg)
35
46
 
@@ -37,7 +48,9 @@ Results vary by workload and deployment. [Measurements and methodology](https://
37
48
 
38
49
  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.
39
50
 
40
- 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.
51
+ 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; see [consistency](https://alessio-locatelli.github.io/client-query-cache/usage/consistency/).
52
+
53
+ Eligible cache misses read from the primary with `majority` read concern, even when the collection leaves read concern unspecified, so only majority-committed results enter the cache. Uncached PyMongo reads use the deployment's default read concern, normally `local`. The two usually perform alike, but while replication lags or data-bearing members are unavailable, a miss can return older data or fail where an uncached read would not. Majority admission does not make cache hits current: invalidation remains asynchronous. See [bypass conditions](https://alessio-locatelli.github.io/client-query-cache/reference/api/#bypass-conditions).
41
54
 
42
55
  ## Quick start
43
56
 
@@ -54,11 +67,15 @@ from client_query_cache import CacheManager
54
67
 
55
68
  with (
56
69
  MongoClient("mongodb://localhost:27017") as client,
57
- CacheManager(client) as cache,
70
+ CacheManager(client) as cache_manager,
58
71
  ):
59
- users = cache.get_cached_collection(client["my_database"]["users"])
60
- users.find_one({"_id": "alice"}) # Reads from MongoDB.
61
- users.find_one({"_id": "alice"}) # Repeated reads can use the cache.
72
+ collection = client["client_query_cache_tutorial"]["items"]
73
+ cached_collection = cache_manager.get_cached_collection(collection)
74
+ collection.replace_one(
75
+ {"_id": "example"}, {"_id": "example", "value": 42}, upsert=True
76
+ )
77
+ cached_collection.find_one({"_id": "example"}) # Reads from MongoDB.
78
+ cached_collection.find_one({"_id": "example"}) # Repeated reads can use the cache.
62
79
  ```
63
80
 
64
81
  ## References
@@ -1,13 +1,23 @@
1
- # Coherent client-side caching for PyMongo
1
+ # Client-side query caching for PyMongo
2
2
 
3
3
  [![PyPI](https://img.shields.io/pypi/v/client-query-cache?color=lightgrey)](https://pypi.org/project/client-query-cache/)
4
4
  [![Python](https://img.shields.io/pypi/pyversions/client-query-cache)](https://alessio-locatelli.github.io/client-query-cache/getting-started/installation/)
5
5
  [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/alessio-locatelli/client-query-cache/actions/workflows/test.yml)
6
6
  [![Typing](https://img.shields.io/pypi/types/client-query-cache)](https://pypi.org/project/client-query-cache/)
7
7
 
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.
8
+ `client-query-cache` uses MongoDB change streams to asynchronously invalidate cached query results. It supports synchronous and asyncio applications without requiring a separate cache server.
9
9
 
10
- **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.
10
+ ```mermaid
11
+ %%{init: {"flowchart": {"diagramPadding": 75}}}%%
12
+ flowchart LR
13
+ App["Python<br/>application"] -->|"Reads"| Cache["Client-side<br/>caching"]
14
+ Cache -->|"Miss / bypass"| DB[("MongoDB")]
15
+ DB -.->|"Change streams<br/>Async invalidation"| Cache
16
+ ```
17
+
18
+ Cache hits are served from client-side memory, reducing read latency and database load in read-heavy applications.
19
+
20
+ The library is tested against real MongoDB replica sets, including concurrency stress tests, and automated checks guard its performance and memory use against regressions.
11
21
 
12
22
  ![Illustrative read latency on a logarithmic scale: direct local MongoDB read, 120 microseconds; direct Atlas M0 read, 79.4 milliseconds; cached read in either deployment, about 61 microseconds.](docs/user/assets/benchmark-latency-light.svg)
13
23
 
@@ -15,7 +25,9 @@ Results vary by workload and deployment. [Measurements and methodology](https://
15
25
 
16
26
  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.
17
27
 
18
- 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.
28
+ 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; see [consistency](https://alessio-locatelli.github.io/client-query-cache/usage/consistency/).
29
+
30
+ Eligible cache misses read from the primary with `majority` read concern, even when the collection leaves read concern unspecified, so only majority-committed results enter the cache. Uncached PyMongo reads use the deployment's default read concern, normally `local`. The two usually perform alike, but while replication lags or data-bearing members are unavailable, a miss can return older data or fail where an uncached read would not. Majority admission does not make cache hits current: invalidation remains asynchronous. See [bypass conditions](https://alessio-locatelli.github.io/client-query-cache/reference/api/#bypass-conditions).
19
31
 
20
32
  ## Quick start
21
33
 
@@ -32,11 +44,15 @@ from client_query_cache import CacheManager
32
44
 
33
45
  with (
34
46
  MongoClient("mongodb://localhost:27017") as client,
35
- CacheManager(client) as cache,
47
+ CacheManager(client) as cache_manager,
36
48
  ):
37
- users = cache.get_cached_collection(client["my_database"]["users"])
38
- users.find_one({"_id": "alice"}) # Reads from MongoDB.
39
- users.find_one({"_id": "alice"}) # Repeated reads can use the cache.
49
+ collection = client["client_query_cache_tutorial"]["items"]
50
+ cached_collection = cache_manager.get_cached_collection(collection)
51
+ collection.replace_one(
52
+ {"_id": "example"}, {"_id": "example", "value": 42}, upsert=True
53
+ )
54
+ cached_collection.find_one({"_id": "example"}) # Reads from MongoDB.
55
+ cached_collection.find_one({"_id": "example"}) # Repeated reads can use the cache.
40
56
  ```
41
57
 
42
58
  ## References
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "client-query-cache"
3
- version = "0.6.0"
3
+ version = "0.7.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"
@@ -14,7 +14,10 @@ classifiers = [
14
14
  "Topic :: Software Development :: Libraries :: Python Modules",
15
15
  "Typing :: Typed",
16
16
  ]
17
- dependencies = ["pymongo>=4.18.1"]
17
+ dependencies = [
18
+ "annotated-types>=0.8.0",
19
+ "pymongo>=4.18.1",
20
+ ]
18
21
 
19
22
  [[project.authors]]
20
23
  name = "Alessio Locatelli"
@@ -59,6 +62,7 @@ dev = [
59
62
  "pytest-cov>=7.1.0",
60
63
  "pyyaml>=6.0.3",
61
64
  "types-pyyaml>=6.0.12.20260906",
65
+ "filelock>=4.1.0",
62
66
  ]
63
67
  docs = [
64
68
  "mike",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "client-query-cache"
3
- version = "0.6.0"
3
+ version = "0.7.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>" },
@@ -17,7 +17,7 @@ classifiers = [
17
17
  "Topic :: Software Development :: Libraries :: Python Modules",
18
18
  "Typing :: Typed",
19
19
  ]
20
- dependencies = ["pymongo>=4.18.1"]
20
+ dependencies = ["annotated-types>=0.8.0", "pymongo>=4.18.1"]
21
21
 
22
22
  [project.urls]
23
23
  Repository = "https://github.com/alessio-locatelli/client-query-cache"
@@ -58,6 +58,7 @@ dev = [
58
58
  "pytest-cov>=7.1.0",
59
59
  "pyyaml>=6.0.3",
60
60
  "types-pyyaml>=6.0.12.20260906",
61
+ "filelock>=4.1.0",
61
62
  ]
62
63
  docs = ["mike", "tomli-w>=1.2.0", "zensical>=0.0.68"]
63
64
  memory = ["pytest-memray>=1.11.0"]
@@ -17,26 +17,35 @@ class _CanonicalTag:
17
17
  return f"<canonical:{self._name}>"
18
18
 
19
19
 
20
+ class _PendingTag:
21
+ __slots__ = ("canonical", "items")
22
+
23
+ def __init__(self, name: str, *, items: bool) -> None:
24
+ self.canonical = _CanonicalTag(name)
25
+ self.items = items
26
+
27
+
20
28
  _MAPPING_TAG = _CanonicalTag("map")
21
29
  _SEQUENCE_TAG = _CanonicalTag("seq")
22
30
  _BOOL_TAG = _CanonicalTag("bool")
23
- _OWN_TAGS = (_MAPPING_TAG, _SEQUENCE_TAG, _BOOL_TAG)
24
31
  _TAGGED_TUPLE_SIZE = 2
25
32
 
26
33
 
27
34
  def canonicalize(value: object) -> Canonical:
28
- if (
29
- isinstance(value, tuple)
30
- and len(value) == _TAGGED_TUPLE_SIZE
31
- and any(value[0] is tag for tag in _OWN_TAGS)
32
- ):
33
- return value
35
+ if isinstance(value, tuple) and len(value) == _TAGGED_TUPLE_SIZE:
36
+ tag, payload = value
37
+ if type(tag) is _CanonicalTag:
38
+ return value
39
+ if type(tag) is _PendingTag:
40
+ if tag.items:
41
+ return (tag.canonical, tuple([canonicalize(item) for item in payload]))
42
+ return (tag.canonical, canonicalize(payload))
34
43
  if isinstance(value, Mapping):
35
44
  pairs = [(canonicalize(key), canonicalize(item)) for key, item in value.items()]
36
45
  items = tuple(sorted(pairs, key=lambda pair: repr(pair[0])))
37
46
  return (_MAPPING_TAG, items)
38
47
  if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
39
- return (_SEQUENCE_TAG, tuple(canonicalize(item) for item in value))
48
+ return (_SEQUENCE_TAG, tuple([canonicalize(item) for item in value]))
40
49
  if isinstance(value, bool):
41
50
  return (_BOOL_TAG, value)
42
51
  try:
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any
6
6
 
7
7
  from client_query_cache._core.collation import normalize_collation
8
8
  from client_query_cache._core.snapshots import BypassReason
9
+ from client_query_cache._types import NonNegativeInt
9
10
 
10
11
  if TYPE_CHECKING:
11
12
  from collections.abc import Mapping
@@ -22,7 +23,7 @@ _COLLECTION_REASONS = {
22
23
 
23
24
  @dataclass(frozen=True, slots=True)
24
25
  class CollectionMetadata:
25
- checked_epoch: int
26
+ checked_epoch: NonNegativeInt
26
27
  bypass_reason: BypassReason | None
27
28
  default_collation: Mapping[str, Any] | None
28
29
 
@@ -14,7 +14,7 @@ _OMITTED = object()
14
14
 
15
15
  class CountReadOptions(TypedDict):
16
16
  cache_options: tuple[object, object, object, object]
17
- extra_options: dict[str, object] # Can be empty for cacheable reads.
17
+ extra_options: dict[str, object]
18
18
 
19
19
 
20
20
  def count_read_options(kwargs: Mapping[str, object]) -> CountReadOptions:
@@ -48,7 +48,7 @@ class CursorCapture:
48
48
  self._find_source = find_source
49
49
  self._max_entry_bytes = core.snapshot().max_entry_bytes
50
50
  self._documents: list[RawBSONDocument] = [] # An empty result is cacheable.
51
- self.retained_bytes = 0 # Empty and abandoned captures retain no payload.
51
+ self.retained_bytes = 0
52
52
  self._active = True
53
53
 
54
54
  def append(self, document: Mapping[str, Any]) -> None:
@@ -4,6 +4,8 @@ import enum
4
4
  from dataclasses import dataclass
5
5
  from typing import TYPE_CHECKING, Any
6
6
 
7
+ from client_query_cache._types import NonNegativeInt
8
+
7
9
  if TYPE_CHECKING:
8
10
  from client_query_cache._core.canonical import Canonical
9
11
  from client_query_cache._core.find_reads import FindSource
@@ -20,8 +22,8 @@ class AdmissionOutcome(enum.Enum):
20
22
 
21
23
  @dataclass(eq=False, slots=True)
22
24
  class CacheEntry:
23
- generation_key: tuple[int, ...]
24
- weight: int
25
+ generation_key: tuple[NonNegativeInt, ...]
26
+ weight: NonNegativeInt
25
27
  value: bytes
26
28
  namespace: NamespaceId
27
29
  identity: Canonical | None
@@ -9,6 +9,7 @@ from client_query_cache._core.collation import normalize_collation
9
9
  from client_query_cache._core.order_sensitive_keys import (
10
10
  order_sensitive_discriminator_key,
11
11
  )
12
+ from client_query_cache._types import NonNegativeInt
12
13
 
13
14
  type CollationInput = Collation | Mapping[str, Any]
14
15
 
@@ -92,16 +93,18 @@ def find_one_read_shape(
92
93
  sort: Sequence[tuple[str, int]] | None,
93
94
  effective_collation: Mapping[str, Any] | None,
94
95
  codec_identity: object,
96
+ remainder: Mapping[str, Any] | None = None,
95
97
  ) -> object:
98
+ shape = ("find_one", projection, sort, effective_collation, codec_identity)
96
99
  return order_sensitive_discriminator_key(
97
- ("find_one", projection, sort, effective_collation, codec_identity)
100
+ (*shape, remainder) if remainder else shape
98
101
  )
99
102
 
100
103
 
101
104
  def generic_find_one_discriminator(
102
105
  filter_query: object,
103
106
  read_shape: object,
104
- index_generation: int, # Can be zero.
107
+ index_generation: NonNegativeInt,
105
108
  ) -> object:
106
109
  return order_sensitive_discriminator_key(
107
110
  ("find_one_generic", filter_query, read_shape, index_generation)
@@ -8,6 +8,7 @@ from client_query_cache._core.order_sensitive_keys import (
8
8
  order_sensitive_discriminator_key,
9
9
  )
10
10
  from client_query_cache._core.query_filters import find_filter_key
11
+ from client_query_cache._types import NonNegativeInt
11
12
 
12
13
  if TYPE_CHECKING:
13
14
  from client_query_cache._core.canonical import Canonical
@@ -15,8 +16,8 @@ if TYPE_CHECKING:
15
16
 
16
17
  @dataclass(frozen=True, slots=True)
17
18
  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.
19
+ family: int # A compact hash; collisions require key equality.
20
+ limit: NonNegativeInt # Zero denotes unlimited; boolean limits are excluded.
20
21
 
21
22
 
22
23
  @dataclass(frozen=True, slots=True)
@@ -47,7 +48,7 @@ def find_read_shape(
47
48
  filter_document: object,
48
49
  projection: object,
49
50
  ordering: object,
50
- skip: int, # Zero means no skipped documents.
51
+ skip: NonNegativeInt, # Zero means no skipped documents.
51
52
  limit: int, # Zero and negative limits use native semantics.
52
53
  *,
53
54
  collation: object,
@@ -40,16 +40,18 @@ def extract_equality_value(value: object) -> object:
40
40
  return value
41
41
 
42
42
 
43
- def extract_id_identity(filter_query: object) -> object:
44
- if filter_query is None:
45
- return NO_IDENTITY
43
+ def extract_id_anchor(filter_query: object) -> tuple[object, Mapping[str, Any]]:
46
44
  if not isinstance(filter_query, Mapping):
47
- if _is_regex(filter_query):
48
- return NO_IDENTITY
49
- return filter_query
50
- if set(filter_query) != {_ID_FIELD}:
51
- return NO_IDENTITY
52
- return extract_equality_value(filter_query[_ID_FIELD])
45
+ return extract_equality_value(filter_query), {}
46
+ try:
47
+ identity = extract_equality_value(filter_query[_ID_FIELD])
48
+ except KeyError:
49
+ return NO_IDENTITY, {}
50
+ if identity is NO_IDENTITY:
51
+ return NO_IDENTITY, {}
52
+ return identity, {
53
+ key: value for key, value in filter_query.items() if key != _ID_FIELD
54
+ }
53
55
 
54
56
 
55
57
  def normalize_identity_for_cache_key(
@@ -4,6 +4,8 @@ import threading
4
4
  from collections import OrderedDict
5
5
  from typing import TYPE_CHECKING
6
6
 
7
+ from client_query_cache._types import NonNegativeInt, PositiveInt
8
+
7
9
  if TYPE_CHECKING:
8
10
  from client_query_cache._core.entries import CacheEntry
9
11
  from client_query_cache._core.keys import CacheKey
@@ -23,8 +25,8 @@ class WeightedLru:
23
25
  def __init__(
24
26
  self,
25
27
  *,
26
- shared_budget_bytes: int,
27
- max_entry_bytes: int,
28
+ shared_budget_bytes: PositiveInt,
29
+ max_entry_bytes: PositiveInt,
28
30
  guard: LockOrderGuard,
29
31
  ) -> None:
30
32
  self._shared_budget_bytes = shared_budget_bytes
@@ -35,17 +37,17 @@ class WeightedLru:
35
37
  self._lock = threading.Lock()
36
38
 
37
39
  @property
38
- def max_entry_bytes(self) -> int:
40
+ def max_entry_bytes(self) -> PositiveInt:
39
41
  return self._max_entry_bytes
40
42
 
41
43
  @property
42
- def shared_budget_bytes(self) -> int:
44
+ def shared_budget_bytes(self) -> PositiveInt:
43
45
  return self._shared_budget_bytes
44
46
 
45
- def is_oversize(self, weight: int) -> bool:
47
+ def is_oversize(self, weight: NonNegativeInt) -> bool:
46
48
  return weight > self._max_entry_bytes
47
49
 
48
- def snapshot_usage(self) -> tuple[int, int]:
50
+ def snapshot_usage(self) -> tuple[NonNegativeInt, NonNegativeInt]:
49
51
  with self._guard.lru_section(), self._lock:
50
52
  return self._used_bytes, len(self._order)
51
53