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