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,682 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import logging
5
+ from collections.abc import Mapping, Sequence
6
+ from typing import TYPE_CHECKING, Any, cast
7
+
8
+ from pymongo import ReadPreference
9
+ from pymongo.asynchronous.collection import AsyncCollection
10
+ from pymongo.collation import Collation
11
+ from pymongo.cursor import CursorType
12
+ from pymongo.errors import PyMongoError
13
+ from pymongo.read_concern import ReadConcern
14
+
15
+ from client_query_cache._core.canonical import is_canonicalizable
16
+ from client_query_cache._core.codec import codec_fingerprint
17
+ from client_query_cache._core.collection_metadata import (
18
+ interpret_list_collections_entry,
19
+ )
20
+ from client_query_cache._core.entries import AdmissionOutcome
21
+ from client_query_cache._core.errors import UnsupportedCacheRequestError
22
+ from client_query_cache._core.identity_reads import (
23
+ NO_IDENTITY,
24
+ extract_id_identity,
25
+ normalize_identity_for_cache_key,
26
+ )
27
+ from client_query_cache._core.keys import NamespaceId, canonical_alias_key
28
+ from client_query_cache._core.order_sensitive_keys import (
29
+ order_sensitive_discriminator_key,
30
+ )
31
+ from client_query_cache._core.projection import (
32
+ ensure_id_present_for_resolution,
33
+ without_id,
34
+ )
35
+ from client_query_cache._core.read_validation import (
36
+ is_filter_cacheable,
37
+ is_pipeline_cacheable,
38
+ is_projection_cacheable,
39
+ pipeline_blocks_full_materialization,
40
+ )
41
+ from client_query_cache._core.unique_keys import match_unique_key
42
+
43
+ if TYPE_CHECKING:
44
+ from pymongo.asynchronous.client_session import AsyncClientSession
45
+ from pymongo.asynchronous.database import AsyncDatabase
46
+
47
+ from client_query_cache._core.collection_metadata import CollectionProbeResult
48
+ from client_query_cache._core.keys import AliasKey
49
+ from client_query_cache._core.unique_keys import UniqueKeyDefinition
50
+ from client_query_cache.asynchronous.database import CachedDatabase
51
+
52
+ _FORCED_READ_CONCERN = ReadConcern("majority")
53
+ _ACCEPTABLE_READ_CONCERN_LEVELS = (None, "majority")
54
+
55
+ logger = logging.getLogger(__name__)
56
+
57
+ type _CollationIn = Collation | Mapping[str, Any]
58
+
59
+
60
+ def _collation_document(collation: _CollationIn | None) -> Mapping[str, Any] | None:
61
+ if collation is None:
62
+ return None
63
+ if isinstance(collation, Collation):
64
+ return collation.document
65
+ return dict(collation)
66
+
67
+
68
+ def _blocks_full_materialization(kwargs: Mapping[str, object]) -> bool:
69
+ try:
70
+ cursor_type = kwargs["cursor_type"]
71
+ except KeyError:
72
+ cursor_type = CursorType.NON_TAILABLE
73
+ if cursor_type != CursorType.NON_TAILABLE:
74
+ return True
75
+ try:
76
+ allow_partial_results = kwargs["allow_partial_results"]
77
+ except KeyError:
78
+ allow_partial_results = False
79
+ return bool(allow_partial_results)
80
+
81
+
82
+ def _count_documents_kwargs(
83
+ skip: int,
84
+ limit: int,
85
+ collation: _CollationIn | None,
86
+ hint: str | Sequence[tuple[str, int]] | None,
87
+ ) -> dict[str, Any]:
88
+ kwargs: dict[str, Any] = {}
89
+ if skip:
90
+ kwargs["skip"] = skip
91
+ if limit:
92
+ kwargs["limit"] = limit
93
+ if collation is not None:
94
+ kwargs["collation"] = collation
95
+ if hint is not None:
96
+ kwargs["hint"] = hint
97
+ return kwargs
98
+
99
+
100
+ class CachedCollection[DocumentType: Mapping[str, Any]]:
101
+ __slots__ = ("_collection", "_database", "_forced_collection", "_forced_database")
102
+
103
+ def __init__(
104
+ self,
105
+ database: CachedDatabase[DocumentType],
106
+ collection: AsyncCollection[DocumentType],
107
+ ) -> None:
108
+ self._database = database
109
+ self._collection = collection
110
+ self._forced_collection: AsyncCollection[DocumentType] | None = None
111
+ self._forced_database: AsyncDatabase[DocumentType] | None = None
112
+
113
+ @property
114
+ def database(self) -> CachedDatabase[DocumentType]:
115
+ return self._database
116
+
117
+ @property
118
+ def name(self) -> str:
119
+ return self._collection.name
120
+
121
+ @property
122
+ def raw(self) -> AsyncCollection[DocumentType]:
123
+ return self._collection
124
+
125
+ def __getattr__(self, name: str) -> Any: # noqa: ANN401
126
+ return self._wrap_delegated(getattr(self._collection, name))
127
+
128
+ def _wrap_delegated(self, value: Any) -> Any: # noqa: ANN401
129
+ if isinstance(value, AsyncCollection):
130
+ return CachedCollection(self._database, value)
131
+ if inspect.iscoroutine(value):
132
+ return self._await_and_wrap(value)
133
+ if not callable(value):
134
+ return value
135
+
136
+ def _delegate(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401
137
+ return self._wrap_delegated(value(*args, **kwargs))
138
+
139
+ return _delegate
140
+
141
+ async def _await_and_wrap(self, coroutine: Any) -> Any: # noqa: ANN401
142
+ return self._wrap_delegated(await coroutine)
143
+
144
+ async def find_one(
145
+ self,
146
+ filter: object = None, # noqa: A002
147
+ projection: Mapping[str, Any] | Sequence[str] | None = None,
148
+ *,
149
+ session: AsyncClientSession | None = None,
150
+ **kwargs: object,
151
+ ) -> DocumentType | None:
152
+ identity = extract_id_identity(filter)
153
+ codec_options = self._collection.codec_options
154
+ read_shape = order_sensitive_discriminator_key(
155
+ ("find_one", projection, codec_fingerprint(codec_options))
156
+ )
157
+ if (
158
+ self._wants_bypass(session=session, kwargs=kwargs)
159
+ or not is_projection_cacheable(projection)
160
+ or not is_canonicalizable(read_shape)
161
+ or not await self._is_cache_eligible()
162
+ ):
163
+ self._record_bypass()
164
+ return await self._collection.find_one(
165
+ filter, projection, session=session, **kwargs
166
+ )
167
+ if identity is not NO_IDENTITY:
168
+ if not is_canonicalizable(identity):
169
+ self._record_bypass()
170
+ return await self._collection.find_one(
171
+ filter, projection, session=session, **kwargs
172
+ )
173
+ return await self._find_one_by_id(identity, projection, read_shape)
174
+ unique_key_match = await self._match_unique_key(filter)
175
+ if unique_key_match is None:
176
+ self._record_bypass()
177
+ return await self._collection.find_one(
178
+ filter, projection, session=session, **kwargs
179
+ )
180
+ key_definition, key_values = unique_key_match
181
+ return await self._find_one_by_unique_key(
182
+ key_definition,
183
+ key_values,
184
+ cast("Mapping[str, Any]", filter),
185
+ projection,
186
+ read_shape,
187
+ )
188
+
189
+ async def find(
190
+ self,
191
+ filter: Mapping[str, Any] | None = None, # noqa: A002
192
+ projection: Mapping[str, Any] | Sequence[str] | None = None,
193
+ *,
194
+ sort: Sequence[tuple[str, int]] | None = None,
195
+ skip: int = 0,
196
+ limit: int = 0,
197
+ collation: _CollationIn | None = None,
198
+ session: AsyncClientSession | None = None,
199
+ **kwargs: object,
200
+ ) -> list[DocumentType]:
201
+ if kwargs and _blocks_full_materialization(kwargs):
202
+ message = (
203
+ "find() always fully materializes its result and cannot support a "
204
+ "tailable, exhaust, or partial-result cursor; use .raw.find() instead"
205
+ )
206
+ raise UnsupportedCacheRequestError(message)
207
+ codec_options = self._collection.codec_options
208
+ discriminator = order_sensitive_discriminator_key(
209
+ (
210
+ "find",
211
+ filter,
212
+ projection,
213
+ sort,
214
+ skip,
215
+ limit,
216
+ _collation_document(collation),
217
+ codec_fingerprint(codec_options),
218
+ )
219
+ )
220
+ if (
221
+ self._wants_bypass(session=session, kwargs=kwargs)
222
+ or not is_filter_cacheable(filter)
223
+ or not is_projection_cacheable(projection)
224
+ or not is_canonicalizable(discriminator)
225
+ or not await self._is_cache_eligible()
226
+ ):
227
+ self._record_bypass()
228
+ cursor = self._collection.find(
229
+ filter,
230
+ projection,
231
+ skip=skip,
232
+ limit=limit,
233
+ sort=sort,
234
+ collation=collation,
235
+ session=session,
236
+ **kwargs,
237
+ )
238
+ return await cursor.to_list()
239
+ namespace = self._namespace()
240
+ cache = self._database.manager.cache_core
241
+ lookup_result = cache.lookup_namespace(
242
+ namespace, discriminator, codec_options=codec_options
243
+ )
244
+ if lookup_result.hit:
245
+ return cast("list[DocumentType]", lookup_result.value)
246
+ if not cache.is_database_available(namespace.database):
247
+ cursor = self._collection.find(
248
+ filter,
249
+ projection,
250
+ skip=skip,
251
+ limit=limit,
252
+ sort=sort,
253
+ collation=collation,
254
+ session=session,
255
+ **kwargs,
256
+ )
257
+ return await cursor.to_list()
258
+ capture = cache.capture_namespace_generation(namespace)
259
+ cursor = self._forced_collection_handle().find(
260
+ filter,
261
+ projection,
262
+ skip=skip,
263
+ limit=limit,
264
+ sort=sort,
265
+ collation=collation,
266
+ )
267
+ documents = await cursor.to_list()
268
+ cache.admit_namespace(
269
+ capture, discriminator, documents, codec_options=codec_options
270
+ )
271
+ return documents
272
+
273
+ async def aggregate(
274
+ self,
275
+ pipeline: Sequence[Mapping[str, Any]],
276
+ *,
277
+ collation: _CollationIn | None = None,
278
+ session: AsyncClientSession | None = None,
279
+ **kwargs: object,
280
+ ) -> list[DocumentType]:
281
+ if pipeline_blocks_full_materialization(pipeline):
282
+ message = (
283
+ "aggregate() always fully materializes its result and cannot "
284
+ "support a $changeStream pipeline; use .raw.aggregate() instead"
285
+ )
286
+ raise UnsupportedCacheRequestError(message)
287
+ codec_options = self._collection.codec_options
288
+ discriminator = order_sensitive_discriminator_key(
289
+ (
290
+ "aggregate",
291
+ pipeline,
292
+ _collation_document(collation),
293
+ codec_fingerprint(codec_options),
294
+ )
295
+ )
296
+ if (
297
+ self._wants_bypass(session=session, kwargs=kwargs)
298
+ or not is_pipeline_cacheable(pipeline)
299
+ or not is_canonicalizable(discriminator)
300
+ or not await self._is_cache_eligible()
301
+ ):
302
+ self._record_bypass()
303
+ cursor = await self._collection.aggregate(
304
+ pipeline,
305
+ collation=collation,
306
+ session=session,
307
+ **cast("dict[str, Any]", kwargs),
308
+ )
309
+ return await cursor.to_list()
310
+ namespace = self._namespace()
311
+ cache = self._database.manager.cache_core
312
+ lookup_result = cache.lookup_namespace(
313
+ namespace, discriminator, codec_options=codec_options
314
+ )
315
+ if lookup_result.hit:
316
+ return cast("list[DocumentType]", lookup_result.value)
317
+ if not cache.is_database_available(namespace.database):
318
+ cursor = await self._collection.aggregate(
319
+ pipeline,
320
+ collation=collation,
321
+ session=session,
322
+ **cast("dict[str, Any]", kwargs),
323
+ )
324
+ return await cursor.to_list()
325
+ capture = cache.capture_namespace_generation(namespace)
326
+ cursor = await self._forced_collection_handle().aggregate(
327
+ pipeline, collation=collation
328
+ )
329
+ documents = await cursor.to_list()
330
+ cache.admit_namespace(
331
+ capture, discriminator, documents, codec_options=codec_options
332
+ )
333
+ return documents
334
+
335
+ async def count_documents(
336
+ self,
337
+ filter: Mapping[str, Any], # noqa: A002
338
+ *,
339
+ skip: int = 0,
340
+ limit: int = 0,
341
+ collation: _CollationIn | None = None,
342
+ hint: str | Sequence[tuple[str, int]] | None = None,
343
+ session: AsyncClientSession | None = None,
344
+ **kwargs: object,
345
+ ) -> int:
346
+ merged_kwargs = _count_documents_kwargs(skip, limit, collation, hint) | kwargs
347
+ codec_options = self._collection.codec_options
348
+ discriminator = order_sensitive_discriminator_key(
349
+ (
350
+ "count_documents",
351
+ filter,
352
+ skip,
353
+ limit,
354
+ _collation_document(collation),
355
+ hint,
356
+ codec_fingerprint(codec_options),
357
+ )
358
+ )
359
+ if (
360
+ self._wants_bypass(session=session, kwargs=kwargs)
361
+ or not is_filter_cacheable(filter)
362
+ or not is_canonicalizable(discriminator)
363
+ or not await self._is_cache_eligible()
364
+ ):
365
+ self._record_bypass()
366
+ return await self._collection.count_documents(
367
+ filter, session=session, **merged_kwargs
368
+ )
369
+ namespace = self._namespace()
370
+ cache = self._database.manager.cache_core
371
+ lookup_result = cache.lookup_namespace(
372
+ namespace, discriminator, codec_options=codec_options
373
+ )
374
+ if lookup_result.hit:
375
+ return cast("int", lookup_result.value)
376
+ if not cache.is_database_available(namespace.database):
377
+ return await self._collection.count_documents(
378
+ filter, session=session, **merged_kwargs
379
+ )
380
+ capture = cache.capture_namespace_generation(namespace)
381
+ count = await self._forced_collection_handle().count_documents(
382
+ filter, **merged_kwargs
383
+ )
384
+ cache.admit_namespace(
385
+ capture, discriminator, count, codec_options=codec_options
386
+ )
387
+ return count
388
+
389
+ async def estimated_document_count(self, **kwargs: object) -> int:
390
+ if (
391
+ kwargs
392
+ or not self._is_primary_majority()
393
+ or not await self._is_cache_eligible()
394
+ ):
395
+ self._record_bypass()
396
+ return await self._collection.estimated_document_count(**kwargs)
397
+ namespace = self._namespace()
398
+ discriminator = ("estimated_document_count",)
399
+ cache = self._database.manager.cache_core
400
+ codec_options = self._collection.codec_options
401
+ lookup_result = cache.lookup_namespace(
402
+ namespace, discriminator, codec_options=codec_options
403
+ )
404
+ if lookup_result.hit:
405
+ return cast("int", lookup_result.value)
406
+ if not cache.is_database_available(namespace.database):
407
+ return await self._collection.estimated_document_count()
408
+ capture = cache.capture_namespace_generation(namespace)
409
+ count = await self._forced_collection_handle().estimated_document_count()
410
+ cache.admit_namespace(
411
+ capture, discriminator, count, codec_options=codec_options
412
+ )
413
+ return count
414
+
415
+ async def distinct(
416
+ self,
417
+ key: str,
418
+ filter: Mapping[str, Any] | None = None, # noqa: A002
419
+ *,
420
+ collation: _CollationIn | None = None,
421
+ session: AsyncClientSession | None = None,
422
+ **kwargs: object,
423
+ ) -> list[Any]:
424
+ codec_options = self._collection.codec_options
425
+ discriminator = order_sensitive_discriminator_key(
426
+ (
427
+ "distinct",
428
+ key,
429
+ filter,
430
+ _collation_document(collation),
431
+ codec_fingerprint(codec_options),
432
+ )
433
+ )
434
+ if (
435
+ self._wants_bypass(session=session, kwargs=kwargs)
436
+ or not is_filter_cacheable(filter)
437
+ or not is_canonicalizable(discriminator)
438
+ or not await self._is_cache_eligible()
439
+ ):
440
+ self._record_bypass()
441
+ return await self._collection.distinct(
442
+ key,
443
+ filter,
444
+ collation=collation,
445
+ session=session,
446
+ **cast("dict[str, Any]", kwargs),
447
+ )
448
+ namespace = self._namespace()
449
+ cache = self._database.manager.cache_core
450
+ lookup_result = cache.lookup_namespace(
451
+ namespace, discriminator, codec_options=codec_options
452
+ )
453
+ if lookup_result.hit:
454
+ return cast("list[Any]", lookup_result.value)
455
+ if not cache.is_database_available(namespace.database):
456
+ return await self._collection.distinct(
457
+ key,
458
+ filter,
459
+ collation=collation,
460
+ session=session,
461
+ **cast("dict[str, Any]", kwargs),
462
+ )
463
+ capture = cache.capture_namespace_generation(namespace)
464
+ values = await self._forced_collection_handle().distinct(
465
+ key, filter, collation=collation
466
+ )
467
+ cache.admit_namespace(
468
+ capture, discriminator, values, codec_options=codec_options
469
+ )
470
+ return values
471
+
472
+ async def _find_one_by_id(
473
+ self,
474
+ identity: object,
475
+ projection: Mapping[str, Any] | Sequence[str] | None,
476
+ read_shape: object,
477
+ ) -> DocumentType | None:
478
+ namespace = self._namespace()
479
+ cache = self._database.manager.cache_core
480
+ codec_options = self._collection.codec_options
481
+ cache_identity = normalize_identity_for_cache_key(
482
+ identity, codec_options, self._database.manager.client.codec_options
483
+ )
484
+ if not is_canonicalizable(cache_identity):
485
+ self._record_bypass()
486
+ return await self._collection.find_one({"_id": identity}, projection)
487
+ lookup_result = cache.lookup_identity(
488
+ namespace, cache_identity, read_shape, codec_options=codec_options
489
+ )
490
+ if lookup_result.hit:
491
+ return cast("DocumentType | None", lookup_result.value)
492
+ if not cache.is_database_available(namespace.database):
493
+ return await self._collection.find_one({"_id": identity}, projection)
494
+ capture = cache.begin_identity_admission(namespace, cache_identity)
495
+ try:
496
+ document = await self._forced_collection_handle().find_one(
497
+ {"_id": identity}, projection
498
+ )
499
+ except BaseException:
500
+ cache.discard_identity_admission(capture)
501
+ raise
502
+ cache.admit_identity(capture, read_shape, document, codec_options=codec_options)
503
+ return document
504
+
505
+ async def _match_unique_key(
506
+ self, filter_query: object
507
+ ) -> tuple[UniqueKeyDefinition, tuple[Any, ...]] | None:
508
+ namespace = self._namespace()
509
+ manager = self._database.manager
510
+ keys = await manager.unique_keys_for(namespace, self._list_indexes_probe)
511
+ if not keys:
512
+ return None
513
+ default_collation = manager.default_collation_for(namespace)
514
+ return match_unique_key(filter_query, keys, default_collation)
515
+
516
+ async def _find_one_by_unique_key(
517
+ self,
518
+ key_definition: UniqueKeyDefinition,
519
+ key_values: tuple[Any, ...],
520
+ original_filter: Mapping[str, Any],
521
+ projection: Mapping[str, Any] | Sequence[str] | None,
522
+ read_shape: object,
523
+ ) -> DocumentType | None:
524
+ namespace = self._namespace()
525
+ cache = self._database.manager.cache_core
526
+ codec_options = self._collection.codec_options
527
+ alias = canonical_alias_key(
528
+ key_definition.fields, key_values, key_definition.collation
529
+ )
530
+ discriminator = (alias, read_shape)
531
+
532
+ resolved_identity = cache.resolve_alias(
533
+ namespace, key_definition.fields, key_values, key_definition.collation
534
+ )
535
+ if resolved_identity is not None:
536
+ lookup_result = cache.lookup_identity(
537
+ namespace, resolved_identity, read_shape, codec_options=codec_options
538
+ )
539
+ if lookup_result.hit:
540
+ return cast("DocumentType | None", lookup_result.value)
541
+ if not cache.is_database_available(namespace.database):
542
+ return await self._collection.find_one(original_filter, projection)
543
+ return await self._resolve_unique_key_read(
544
+ alias,
545
+ discriminator,
546
+ original_filter,
547
+ projection,
548
+ read_shape,
549
+ previous_identity=resolved_identity,
550
+ )
551
+
552
+ lookup_result = cache.lookup_namespace(
553
+ namespace, discriminator, codec_options=codec_options
554
+ )
555
+ if lookup_result.hit:
556
+ return cast("DocumentType | None", lookup_result.value)
557
+ if not cache.is_database_available(namespace.database):
558
+ return await self._collection.find_one(original_filter, projection)
559
+ return await self._resolve_unique_key_read(
560
+ alias, discriminator, original_filter, projection, read_shape
561
+ )
562
+
563
+ async def _resolve_unique_key_read(
564
+ self,
565
+ alias: AliasKey,
566
+ discriminator: object,
567
+ original_filter: Mapping[str, Any],
568
+ projection: Mapping[str, Any] | Sequence[str] | None,
569
+ read_shape: object,
570
+ *,
571
+ previous_identity: object | None = None,
572
+ ) -> DocumentType | None:
573
+ namespace = self._namespace()
574
+ cache = self._database.manager.cache_core
575
+ codec_options = self._collection.codec_options
576
+ capture = cache.capture_namespace_generation(namespace)
577
+ server_projection, exclude_id = ensure_id_present_for_resolution(projection)
578
+ document = await self._forced_collection_handle().find_one(
579
+ original_filter, server_projection
580
+ )
581
+ if document is None:
582
+ if previous_identity is not None:
583
+ cache.discard_stale_alias(namespace, alias, previous_identity)
584
+ cache.discard_namespace_entry(namespace, discriminator, capture.generation)
585
+ cache.admit_namespace(
586
+ capture, discriminator, None, codec_options=codec_options
587
+ )
588
+ return None
589
+ raw_identity = document["_id"]
590
+ if exclude_id:
591
+ document = cast("DocumentType", without_id(document, codec_options))
592
+ cache_identity = normalize_identity_for_cache_key(
593
+ raw_identity, codec_options, self._database.manager.client.codec_options
594
+ )
595
+ if cache_identity is not None and is_canonicalizable(cache_identity):
596
+ cache.discard_namespace_entry(namespace, discriminator, capture.generation)
597
+ outcome = cache.admit_unique_key_match(
598
+ capture,
599
+ discriminator,
600
+ cache_identity,
601
+ read_shape,
602
+ document,
603
+ alias=alias,
604
+ codec_options=codec_options,
605
+ )
606
+ if (
607
+ outcome is not AdmissionOutcome.ADMITTED
608
+ and previous_identity is not None
609
+ ):
610
+ cache.discard_stale_alias(namespace, alias, previous_identity)
611
+ elif previous_identity is not None:
612
+ cache.discard_stale_alias(namespace, alias, previous_identity)
613
+ cache.discard_namespace_entry(namespace, discriminator, capture.generation)
614
+ return document
615
+
616
+ def _namespace(self) -> NamespaceId:
617
+ return NamespaceId(self._database.name, self.name)
618
+
619
+ def _is_primary_majority(self) -> bool:
620
+ read_concern = self._collection.read_concern
621
+ return (
622
+ self._collection.read_preference == ReadPreference.PRIMARY
623
+ and read_concern.level in _ACCEPTABLE_READ_CONCERN_LEVELS
624
+ )
625
+
626
+ def _wants_bypass(
627
+ self, *, session: AsyncClientSession | None, kwargs: Mapping[str, object]
628
+ ) -> bool:
629
+ return session is not None or bool(kwargs) or not self._is_primary_majority()
630
+
631
+ def _record_bypass(self) -> None:
632
+ self._database.manager.cache_core.record_bypass()
633
+
634
+ def _forced_collection_handle(self) -> AsyncCollection[DocumentType]:
635
+ if self._forced_collection is None:
636
+ self._forced_collection = self._collection.with_options(
637
+ read_preference=ReadPreference.PRIMARY,
638
+ read_concern=_FORCED_READ_CONCERN,
639
+ )
640
+ return self._forced_collection
641
+
642
+ def _forced_database_handle(self) -> AsyncDatabase[DocumentType]:
643
+ if self._forced_database is None:
644
+ self._forced_database = self._database.raw.with_options(
645
+ read_preference=ReadPreference.PRIMARY,
646
+ read_concern=_FORCED_READ_CONCERN,
647
+ )
648
+ return self._forced_database
649
+
650
+ async def _is_cache_eligible(self) -> bool:
651
+ return await self._database.manager.ensure_cache_eligible(
652
+ self._namespace(), self._probe_collection
653
+ )
654
+
655
+ async def _probe_collection(self) -> CollectionProbeResult | None:
656
+ try:
657
+ cursor = await self._forced_database_handle().list_collections(
658
+ filter={"name": self.name}
659
+ )
660
+ entries = await cursor.to_list(length=1)
661
+ except PyMongoError:
662
+ logger.warning(
663
+ "collection-type probe failed; this read bypasses the cache",
664
+ extra={"database": self._database.name, "collection": self.name},
665
+ exc_info=True,
666
+ )
667
+ return None
668
+ entry = entries[0] if entries else None
669
+ return interpret_list_collections_entry(entry)
670
+
671
+ async def _list_indexes_probe(self) -> list[Mapping[str, Any]] | None:
672
+ try:
673
+ cursor = await self._forced_collection_handle().list_indexes()
674
+ return cast("list[Mapping[str, Any]]", await cursor.to_list())
675
+ except PyMongoError:
676
+ logger.warning(
677
+ "index metadata probe failed; unique-key discovery is skipped for "
678
+ "this read",
679
+ extra={"database": self._database.name, "collection": self.name},
680
+ exc_info=True,
681
+ )
682
+ return None