client-query-cache 0.1.0__py3-none-any.whl

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 (39) hide show
  1. client_query_cache/__init__.py +15 -0
  2. client_query_cache/_core/__init__.py +44 -0
  3. client_query_cache/_core/canonical.py +63 -0
  4. client_query_cache/_core/codec.py +55 -0
  5. client_query_cache/_core/collation.py +22 -0
  6. client_query_cache/_core/collection_metadata.py +65 -0
  7. client_query_cache/_core/entries.py +32 -0
  8. client_query_cache/_core/errors.py +25 -0
  9. client_query_cache/_core/identity_reads.py +64 -0
  10. client_query_cache/_core/keys.py +43 -0
  11. client_query_cache/_core/lifecycle.py +8 -0
  12. client_query_cache/_core/locking.py +40 -0
  13. client_query_cache/_core/lru.py +109 -0
  14. client_query_cache/_core/manager.py +892 -0
  15. client_query_cache/_core/namespace.py +35 -0
  16. client_query_cache/_core/order_sensitive_keys.py +69 -0
  17. client_query_cache/_core/projection.py +59 -0
  18. client_query_cache/_core/read_validation.py +89 -0
  19. client_query_cache/_core/snapshots.py +67 -0
  20. client_query_cache/_core/stream_cost.py +242 -0
  21. client_query_cache/_core/stream_events.py +157 -0
  22. client_query_cache/_core/stream_health.py +47 -0
  23. client_query_cache/_core/stream_options.py +14 -0
  24. client_query_cache/_core/unique_keys.py +119 -0
  25. client_query_cache/asynchronous/__init__.py +16 -0
  26. client_query_cache/asynchronous/collection.py +682 -0
  27. client_query_cache/asynchronous/database.py +59 -0
  28. client_query_cache/asynchronous/manager.py +118 -0
  29. client_query_cache/asynchronous/streams.py +300 -0
  30. client_query_cache/otel.py +206 -0
  31. client_query_cache/py.typed +0 -0
  32. client_query_cache/synchronous/__init__.py +16 -0
  33. client_query_cache/synchronous/collection.py +678 -0
  34. client_query_cache/synchronous/database.py +53 -0
  35. client_query_cache/synchronous/manager.py +118 -0
  36. client_query_cache/synchronous/streams.py +297 -0
  37. client_query_cache-0.1.0.dist-info/METADATA +114 -0
  38. client_query_cache-0.1.0.dist-info/RECORD +39 -0
  39. client_query_cache-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,15 @@
1
+ from .synchronous import (
2
+ CacheCore,
3
+ CacheCoreConfig,
4
+ CachedCollection,
5
+ CachedDatabase,
6
+ CacheManager,
7
+ )
8
+
9
+ __all__ = [
10
+ "CacheCore",
11
+ "CacheCoreConfig",
12
+ "CacheManager",
13
+ "CachedCollection",
14
+ "CachedDatabase",
15
+ ]
@@ -0,0 +1,44 @@
1
+ from client_query_cache._core.entries import AdmissionOutcome, LookupResult
2
+ from client_query_cache._core.errors import (
3
+ CacheClosedError,
4
+ CacheConfigurationError,
5
+ CacheError,
6
+ StreamLifecycleError,
7
+ StreamStartupError,
8
+ UnsupportedCacheRequestError,
9
+ )
10
+ from client_query_cache._core.keys import NamespaceId
11
+ from client_query_cache._core.lifecycle import CacheLifecycleState
12
+ from client_query_cache._core.manager import (
13
+ CacheCore,
14
+ CacheCoreConfig,
15
+ IdentityCapture,
16
+ NamespaceCapture,
17
+ )
18
+ from client_query_cache._core.snapshots import CacheSnapshot
19
+ from client_query_cache._core.stream_cost import (
20
+ LagCaptureWindowConfig,
21
+ StreamCostSnapshot,
22
+ )
23
+ from client_query_cache._core.stream_health import StreamHealth
24
+
25
+ __all__ = [
26
+ "AdmissionOutcome",
27
+ "CacheClosedError",
28
+ "CacheConfigurationError",
29
+ "CacheCore",
30
+ "CacheCoreConfig",
31
+ "CacheError",
32
+ "CacheLifecycleState",
33
+ "CacheSnapshot",
34
+ "IdentityCapture",
35
+ "LagCaptureWindowConfig",
36
+ "LookupResult",
37
+ "NamespaceCapture",
38
+ "NamespaceId",
39
+ "StreamCostSnapshot",
40
+ "StreamHealth",
41
+ "StreamLifecycleError",
42
+ "StreamStartupError",
43
+ "UnsupportedCacheRequestError",
44
+ ]
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Hashable, Mapping, Sequence
4
+
5
+ from client_query_cache._core.errors import UnsupportedCacheRequestError
6
+
7
+ type Canonical = Hashable
8
+
9
+
10
+ class _CanonicalTag:
11
+ __slots__ = ("_name",)
12
+
13
+ def __init__(self, name: str) -> None:
14
+ self._name = name
15
+
16
+ def __repr__(self) -> str:
17
+ return f"<canonical:{self._name}>"
18
+
19
+
20
+ _MAPPING_TAG = _CanonicalTag("map")
21
+ _SEQUENCE_TAG = _CanonicalTag("seq")
22
+ _BOOL_TAG = _CanonicalTag("bool")
23
+ _OWN_TAGS = (_MAPPING_TAG, _SEQUENCE_TAG, _BOOL_TAG)
24
+ _TAGGED_TUPLE_SIZE = 2
25
+
26
+
27
+ 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
34
+ if isinstance(value, Mapping):
35
+ pairs = [(canonicalize(key), canonicalize(item)) for key, item in value.items()]
36
+ items = tuple(sorted(pairs, key=lambda pair: repr(pair[0])))
37
+ return (_MAPPING_TAG, items)
38
+ if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
39
+ return (_SEQUENCE_TAG, tuple(canonicalize(item) for item in value))
40
+ if isinstance(value, bool):
41
+ return (_BOOL_TAG, value)
42
+ try:
43
+ hash(value)
44
+ except TypeError:
45
+ type_name = type(value).__name__
46
+ message = f"cannot canonicalize value of type {type_name!r} for a cache key"
47
+ raise UnsupportedCacheRequestError(message) from None
48
+ if value != value: # noqa: PLR0124 (deliberate self-inequality check for NaN-like values)
49
+ type_name = type(value).__name__
50
+ message = (
51
+ f"cannot canonicalize a non-reflexive value of type {type_name!r} "
52
+ "for a cache key"
53
+ )
54
+ raise UnsupportedCacheRequestError(message)
55
+ return value
56
+
57
+
58
+ def is_canonicalizable(value: object) -> bool:
59
+ try:
60
+ canonicalize(value)
61
+ except UnsupportedCacheRequestError:
62
+ return False
63
+ return True
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ import bson
6
+ from bson.codec_options import CodecOptions
7
+
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Mapping
10
+
11
+ from bson.codec_options import TypeRegistry
12
+
13
+ _ENVELOPE_FIELD = "v"
14
+
15
+
16
+ class _TypeRegistryIdentity:
17
+ __slots__ = ("_registry",)
18
+
19
+ def __init__(self, registry: TypeRegistry) -> None:
20
+ self._registry = registry
21
+
22
+ def __eq__(self, other: object) -> bool:
23
+ return (
24
+ isinstance(other, _TypeRegistryIdentity)
25
+ and self._registry is other._registry
26
+ )
27
+
28
+ def __hash__(self) -> int:
29
+ return id(self._registry)
30
+
31
+
32
+ def encode_value(
33
+ value: object, codec_options: CodecOptions[Mapping[str, Any]] | None = None
34
+ ) -> bytes:
35
+ return bson.encode(
36
+ {_ENVELOPE_FIELD: value}, codec_options=codec_options or CodecOptions()
37
+ )
38
+
39
+
40
+ def decode_value(
41
+ encoded: bytes, codec_options: CodecOptions[Mapping[str, Any]] | None = None
42
+ ) -> object:
43
+ return bson.decode(encoded, codec_options=codec_options)[_ENVELOPE_FIELD]
44
+
45
+
46
+ def codec_fingerprint(codec_options: CodecOptions[Mapping[str, Any]]) -> object:
47
+ return (
48
+ codec_options.document_class,
49
+ codec_options.tz_aware,
50
+ codec_options.uuid_representation,
51
+ codec_options.unicode_decode_error_handler,
52
+ codec_options.tzinfo,
53
+ codec_options.datetime_conversion,
54
+ _TypeRegistryIdentity(codec_options.type_registry),
55
+ )
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ if TYPE_CHECKING:
6
+ from collections.abc import Mapping
7
+
8
+ _SIMPLE_LOCALE = "simple"
9
+
10
+
11
+ def normalize_collation(
12
+ collation: Mapping[str, Any] | None,
13
+ ) -> Mapping[str, Any] | None:
14
+ if collation is None:
15
+ return None
16
+ try:
17
+ locale = collation["locale"]
18
+ except KeyError:
19
+ locale = None
20
+ if locale == _SIMPLE_LOCALE:
21
+ return None
22
+ return collation
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from client_query_cache._core.collation import normalize_collation
8
+
9
+ if TYPE_CHECKING:
10
+ from collections.abc import Mapping
11
+
12
+ from client_query_cache._core.keys import NamespaceId
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class CollectionMetadata:
17
+ checked_epoch: int
18
+ is_cacheable: bool
19
+ default_collation: Mapping[str, Any] | None
20
+
21
+
22
+ class CollectionMetadataCache:
23
+ __slots__ = ("_entries", "_lock")
24
+
25
+ def __init__(self) -> None:
26
+ self._entries: dict[NamespaceId, CollectionMetadata] = {}
27
+ self._lock = threading.Lock()
28
+
29
+ def get(self, namespace: NamespaceId) -> CollectionMetadata | None:
30
+ with self._lock:
31
+ try:
32
+ return self._entries[namespace]
33
+ except KeyError:
34
+ return None
35
+
36
+ def put(self, namespace: NamespaceId, metadata: CollectionMetadata) -> None:
37
+ with self._lock:
38
+ self._entries[namespace] = metadata
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class CollectionProbeResult:
43
+ is_cacheable: bool
44
+ default_collation: Mapping[str, Any] | None
45
+
46
+
47
+ def interpret_list_collections_entry(
48
+ entry: Mapping[str, Any] | None,
49
+ ) -> CollectionProbeResult | None:
50
+ if entry is None:
51
+ return None
52
+ collection_type = entry["type"]
53
+ try:
54
+ options = entry["options"]
55
+ except KeyError:
56
+ options = {}
57
+ try:
58
+ collation = options["collation"]
59
+ except KeyError:
60
+ collation = None
61
+ default_collation = normalize_collation(collation)
62
+ return CollectionProbeResult(
63
+ is_cacheable=collection_type == "collection",
64
+ default_collation=default_collation,
65
+ )
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ if TYPE_CHECKING:
8
+ from client_query_cache._core.canonical import Canonical
9
+ from client_query_cache._core.keys import NamespaceId
10
+
11
+
12
+ class AdmissionOutcome(enum.Enum):
13
+ ADMITTED = "admitted"
14
+ DECLINED_OVERSIZE = "declined_oversize"
15
+ DECLINED_STALE = "declined_stale"
16
+ DECLINED_UNAVAILABLE = "declined_unavailable"
17
+ DECLINED_UNENCODABLE = "declined_unencodable"
18
+
19
+
20
+ @dataclass(eq=False, slots=True)
21
+ class CacheEntry:
22
+ generation_key: tuple[int, ...]
23
+ weight: int
24
+ value: bytes
25
+ namespace: NamespaceId
26
+ identity: Canonical | None
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class LookupResult:
31
+ hit: bool
32
+ value: Any = None
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class CacheError(Exception):
5
+ pass
6
+
7
+
8
+ class CacheConfigurationError(CacheError, ValueError):
9
+ pass
10
+
11
+
12
+ class UnsupportedCacheRequestError(CacheError, TypeError):
13
+ pass
14
+
15
+
16
+ class CacheClosedError(CacheError):
17
+ pass
18
+
19
+
20
+ class StreamStartupError(CacheError):
21
+ pass
22
+
23
+
24
+ class StreamLifecycleError(CacheError):
25
+ pass
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from collections.abc import Mapping
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from bson.errors import BSONError
8
+ from bson.regex import Regex
9
+
10
+ from client_query_cache._core.codec import decode_value, encode_value
11
+
12
+ if TYPE_CHECKING:
13
+ from bson.codec_options import CodecOptions
14
+
15
+ _ID_FIELD = "_id"
16
+
17
+
18
+ class _NoIdentity:
19
+ __slots__ = ()
20
+
21
+
22
+ NO_IDENTITY = _NoIdentity()
23
+
24
+
25
+ def _is_query_operator_mapping(value: Mapping[str, Any]) -> bool:
26
+ return any(key.startswith("$") for key in value)
27
+
28
+
29
+ def _is_regex(value: object) -> bool:
30
+ return isinstance(value, re.Pattern | Regex)
31
+
32
+
33
+ def extract_equality_value(value: object) -> object:
34
+ if value is None:
35
+ return NO_IDENTITY
36
+ if isinstance(value, Mapping) and _is_query_operator_mapping(value):
37
+ return NO_IDENTITY
38
+ if _is_regex(value):
39
+ return NO_IDENTITY
40
+ return value
41
+
42
+
43
+ def extract_id_identity(filter_query: object) -> object:
44
+ if filter_query is None:
45
+ return NO_IDENTITY
46
+ 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])
53
+
54
+
55
+ def normalize_identity_for_cache_key(
56
+ identity: object,
57
+ read_codec_options: CodecOptions[Any],
58
+ stream_codec_options: CodecOptions[Any],
59
+ ) -> object:
60
+ try:
61
+ encoded = encode_value(identity, read_codec_options)
62
+ return decode_value(encoded, stream_codec_options)
63
+ except BSONError:
64
+ return identity
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, NamedTuple
5
+
6
+ from client_query_cache._core.canonical import canonicalize
7
+ from client_query_cache._core.order_sensitive_keys import order_sensitive_key
8
+
9
+ if TYPE_CHECKING:
10
+ from client_query_cache._core.canonical import Canonical
11
+
12
+
13
+ class NamespaceId(NamedTuple):
14
+ database: str
15
+ collection: str
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class IdentityCacheKey:
20
+ namespace: NamespaceId
21
+ identity: Canonical
22
+ read_shape: Canonical
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class NamespaceCacheKey:
27
+ namespace: NamespaceId
28
+ discriminator: Canonical
29
+
30
+
31
+ type CacheKey = IdentityCacheKey | NamespaceCacheKey
32
+
33
+ type AliasKey = tuple[Canonical, Canonical, Canonical]
34
+
35
+
36
+ def canonical_alias_key(
37
+ definition: object, value: object, collation: object
38
+ ) -> AliasKey:
39
+ return (
40
+ canonicalize(definition),
41
+ canonicalize(order_sensitive_key(value)),
42
+ canonicalize(collation),
43
+ )
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+
5
+
6
+ class CacheLifecycleState(enum.Enum):
7
+ ACTIVE = "active"
8
+ CLOSED = "closed"
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ from contextlib import contextmanager
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from collections.abc import Iterator
9
+
10
+
11
+ class LockOrderViolationError(RuntimeError):
12
+ pass
13
+
14
+
15
+ class LockOrderGuard:
16
+ __slots__ = ("_state",)
17
+
18
+ def __init__(self) -> None:
19
+ self._state = threading.local()
20
+
21
+ @contextmanager
22
+ def namespace_section(self) -> Iterator[None]:
23
+ with self._section(entering="in_namespace", forbidden="in_lru"):
24
+ yield
25
+
26
+ @contextmanager
27
+ def lru_section(self) -> Iterator[None]:
28
+ with self._section(entering="in_lru", forbidden="in_namespace"):
29
+ yield
30
+
31
+ @contextmanager
32
+ def _section(self, *, entering: str, forbidden: str) -> Iterator[None]:
33
+ if getattr(self._state, forbidden, False):
34
+ message = f"cannot enter {entering!r} section while holding {forbidden!r}"
35
+ raise LockOrderViolationError(message)
36
+ setattr(self._state, entering, True)
37
+ try:
38
+ yield
39
+ finally:
40
+ setattr(self._state, entering, False)
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ from collections import OrderedDict
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from client_query_cache._core.entries import CacheEntry
9
+ from client_query_cache._core.keys import CacheKey
10
+ from client_query_cache._core.locking import LockOrderGuard
11
+
12
+
13
+ class WeightedLru:
14
+ __slots__ = (
15
+ "_guard",
16
+ "_lock",
17
+ "_max_entry_bytes",
18
+ "_order",
19
+ "_shared_budget_bytes",
20
+ "_used_bytes",
21
+ )
22
+
23
+ def __init__(
24
+ self,
25
+ *,
26
+ shared_budget_bytes: int,
27
+ max_entry_bytes: int,
28
+ guard: LockOrderGuard,
29
+ ) -> None:
30
+ self._shared_budget_bytes = shared_budget_bytes
31
+ self._max_entry_bytes = max_entry_bytes
32
+ self._guard = guard
33
+ self._order: OrderedDict[CacheKey, CacheEntry] = OrderedDict()
34
+ self._used_bytes = 0
35
+ self._lock = threading.Lock()
36
+
37
+ @property
38
+ def max_entry_bytes(self) -> int:
39
+ return self._max_entry_bytes
40
+
41
+ @property
42
+ def shared_budget_bytes(self) -> int:
43
+ return self._shared_budget_bytes
44
+
45
+ def is_oversize(self, weight: int) -> bool:
46
+ return weight > self._max_entry_bytes
47
+
48
+ def snapshot_usage(self) -> tuple[int, int]:
49
+ with self._guard.lru_section(), self._lock:
50
+ return self._used_bytes, len(self._order)
51
+
52
+ def peek(self, key: CacheKey) -> CacheEntry | None:
53
+ with self._guard.lru_section(), self._lock:
54
+ try:
55
+ return self._order[key]
56
+ except KeyError:
57
+ return None
58
+
59
+ def touch(self, key: CacheKey) -> None:
60
+ with self._guard.lru_section(), self._lock:
61
+ if key in self._order:
62
+ self._order.move_to_end(key)
63
+
64
+ def conditional_put(
65
+ self, key: CacheKey, entry: CacheEntry
66
+ ) -> tuple[bool, CacheEntry | None, list[CacheEntry]]:
67
+ evicted: list[CacheEntry] = []
68
+ with self._guard.lru_section(), self._lock:
69
+ try:
70
+ current = self._order[key]
71
+ except KeyError:
72
+ current = None
73
+ if current is not None and current.generation_key >= entry.generation_key:
74
+ return False, None, evicted
75
+ if current is not None:
76
+ self._used_bytes -= current.weight
77
+ del self._order[key]
78
+ self._order[key] = entry
79
+ self._used_bytes += entry.weight
80
+ while self._used_bytes > self._shared_budget_bytes:
81
+ oldest_key, oldest_entry = next(iter(self._order.items()))
82
+ del self._order[oldest_key]
83
+ self._used_bytes -= oldest_entry.weight
84
+ evicted.append(oldest_entry)
85
+ return True, current, evicted
86
+
87
+ def contains_exact(self, key: CacheKey, entry: CacheEntry) -> bool:
88
+ with self._guard.lru_section(), self._lock:
89
+ try:
90
+ return self._order[key] is entry
91
+ except KeyError:
92
+ return False
93
+
94
+ def remove_exact(self, key: CacheKey, entry: CacheEntry) -> bool:
95
+ with self._guard.lru_section(), self._lock:
96
+ try:
97
+ current = self._order[key]
98
+ except KeyError:
99
+ return False
100
+ if current is entry:
101
+ del self._order[key]
102
+ self._used_bytes -= entry.weight
103
+ return True
104
+ return False
105
+
106
+ def clear_all(self) -> None:
107
+ with self._guard.lru_section(), self._lock:
108
+ self._order.clear()
109
+ self._used_bytes = 0