Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit d7316af

Browse files
PYTHON-5328 CRUD Support in Driver for Prefix/Suffix/Substring Indexes (#2521)
1 parent 7580309 commit d7316af

File tree

9 files changed

+710
-7
lines changed

9 files changed

+710
-7
lines changed

‎doc/changelog.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ Changes in Version 4.15.0 (XXXX/XX/XX)
44
--------------------------------------
55
PyMongo 4.15 brings a number of changes including:
66

7+
- Added :class:`~pymongo.encryption_options.TextOpts`,
8+
:attr:`~pymongo.encryption.Algorithm.TEXTPREVIEW`,
9+
:attr:`~pymongo.encryption.QueryType.PREFIXPREVIEW`,
10+
:attr:`~pymongo.encryption.QueryType.SUFFIXPREVIEW`,
11+
:attr:`~pymongo.encryption.QueryType.SUBSTRINGPREVIEW`,
12+
as part of the experimental Queryable Encryption text queries beta.
13+
``pymongocrypt>=1.16`` is required for text query support.
714
- Added :class:`bson.decimal128.DecimalEncoder` and :class:`bson.decimal128.DecimalDecoder`
815
to support encoding and decoding of BSON Decimal128 values to decimal.Decimal values using the TypeRegistry API.
916

‎pymongo/asynchronous/encryption.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
from pymongo.asynchronous.pool import AsyncBaseConnection
6868
from pymongo.common import CONNECT_TIMEOUT
6969
from pymongo.daemon import _spawn_daemon
70-
from pymongo.encryption_options import AutoEncryptionOpts, RangeOpts
70+
from pymongo.encryption_options import AutoEncryptionOpts, RangeOpts, TextOpts
7171
from pymongo.errors import (
7272
ConfigurationError,
7373
EncryptedCollectionError,
@@ -516,6 +516,11 @@ class Algorithm(str, enum.Enum):
516516
517517
.. versionadded:: 4.4
518518
"""
519+
TEXTPREVIEW = "TextPreview"
520+
"""**BETA** - TextPreview.
521+
522+
.. versionadded:: 4.15
523+
"""
519524

520525

521526
class QueryType(str, enum.Enum):
@@ -541,6 +546,24 @@ class QueryType(str, enum.Enum):
541546
.. versionadded:: 4.4
542547
"""
543548

549+
PREFIXPREVIEW = "prefixPreview"
550+
"""**BETA** - Used to encrypt a value for a prefixPreview query.
551+
552+
.. versionadded:: 4.15
553+
"""
554+
555+
SUFFIXPREVIEW = "suffixPreview"
556+
"""**BETA** - Used to encrypt a value for a suffixPreview query.
557+
558+
.. versionadded:: 4.15
559+
"""
560+
561+
SUBSTRINGPREVIEW = "substringPreview"
562+
"""**BETA** - Used to encrypt a value for a substringPreview query.
563+
564+
.. versionadded:: 4.15
565+
"""
566+
544567

545568
def _create_mongocrypt_options(**kwargs: Any) -> MongoCryptOptions:
546569
# For compat with pymongocrypt <1.13, avoid setting the default key_expiration_ms.
@@ -876,6 +899,7 @@ async def _encrypt_helper(
876899
contention_factor: Optional[int] = None,
877900
range_opts: Optional[RangeOpts] = None,
878901
is_expression: bool = False,
902+
text_opts: Optional[TextOpts] = None,
879903
) -> Any:
880904
self._check_closed()
881905
if isinstance(key_id, uuid.UUID):
@@ -895,6 +919,12 @@ async def _encrypt_helper(
895919
range_opts.document,
896920
codec_options=self._codec_options,
897921
)
922+
text_opts_bytes = None
923+
if text_opts:
924+
text_opts_bytes = encode(
925+
text_opts.document,
926+
codec_options=self._codec_options,
927+
)
898928
with _wrap_encryption_errors():
899929
encrypted_doc = await self._encryption.encrypt(
900930
value=doc,
@@ -905,6 +935,7 @@ async def _encrypt_helper(
905935
contention_factor=contention_factor,
906936
range_opts=range_opts_bytes,
907937
is_expression=is_expression,
938+
text_opts=text_opts_bytes,
908939
)
909940
return decode(encrypted_doc)["v"]
910941

@@ -917,6 +948,7 @@ async def encrypt(
917948
query_type: Optional[str] = None,
918949
contention_factor: Optional[int] = None,
919950
range_opts: Optional[RangeOpts] = None,
951+
text_opts: Optional[TextOpts] = None,
920952
) -> Binary:
921953
"""Encrypt a BSON value with a given key and algorithm.
922954
@@ -937,9 +969,14 @@ async def encrypt(
937969
used.
938970
:param range_opts: Index options for `range` queries. See
939971
:class:`RangeOpts` for some valid options.
972+
:param text_opts: Index options for `textPreview` queries. See
973+
:class:`TextOpts` for some valid options.
940974
941975
:return: The encrypted value, a :class:`~bson.binary.Binary` with subtype 6.
942976
977+
.. versionchanged:: 4.9
978+
Added the `text_opts` parameter.
979+
943980
.. versionchanged:: 4.9
944981
Added the `range_opts` parameter.
945982
@@ -960,6 +997,7 @@ async def encrypt(
960997
contention_factor=contention_factor,
961998
range_opts=range_opts,
962999
is_expression=False,
1000+
text_opts=text_opts,
9631001
),
9641002
)
9651003

‎pymongo/encryption_options.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"""
1919
from __future__ import annotations
2020

21-
from typing import TYPE_CHECKING, Any, Mapping, Optional
21+
from typing import TYPE_CHECKING, Any, Mapping, Optional, TypedDict
2222

2323
from pymongo.uri_parser_shared import _parse_kms_tls_options
2424

@@ -295,3 +295,85 @@ def document(self) -> dict[str, Any]:
295295
if v is not None:
296296
doc[k] = v
297297
return doc
298+
299+
300+
class TextOpts:
301+
"""**BETA** Options to configure encrypted queries using the text algorithm.
302+
303+
TextOpts is currently unstable API and subject to backwards breaking changes."""
304+
305+
def __init__(
306+
self,
307+
substring: Optional[SubstringOpts] = None,
308+
prefix: Optional[PrefixOpts] = None,
309+
suffix: Optional[SuffixOpts] = None,
310+
case_sensitive: Optional[bool] = None,
311+
diacritic_sensitive: Optional[bool] = None,
312+
) -> None:
313+
"""Options to configure encrypted queries using the text algorithm.
314+
315+
:param substring: Further options to support substring queries.
316+
:param prefix: Further options to support prefix queries.
317+
:param suffix: Further options to support suffix queries.
318+
:param case_sensitive: Whether text indexes for this field are case sensitive.
319+
:param diacritic_sensitive: Whether text indexes for this field are diacritic sensitive.
320+
321+
.. versionadded:: 4.15
322+
"""
323+
self.substring = substring
324+
self.prefix = prefix
325+
self.suffix = suffix
326+
self.case_sensitive = case_sensitive
327+
self.diacritic_sensitive = diacritic_sensitive
328+
329+
@property
330+
def document(self) -> dict[str, Any]:
331+
doc = {}
332+
for k, v in [
333+
("substring", self.substring),
334+
("prefix", self.prefix),
335+
("suffix", self.suffix),
336+
("caseSensitive", self.case_sensitive),
337+
("diacriticSensitive", self.diacritic_sensitive),
338+
]:
339+
if v is not None:
340+
doc[k] = v
341+
return doc
342+
343+
344+
class SubstringOpts(TypedDict):
345+
"""**BETA** Options for substring text queries.
346+
347+
SubstringOpts is currently unstable API and subject to backwards breaking changes.
348+
"""
349+
350+
# strMaxLength is the maximum allowed length to insert. Inserting longer strings will error.
351+
strMaxLength: int
352+
# strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
353+
strMinQueryLength: int
354+
# strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
355+
strMaxQueryLength: int
356+
357+
358+
class PrefixOpts(TypedDict):
359+
"""**BETA** Options for prefix text queries.
360+
361+
PrefixOpts is currently unstable API and subject to backwards breaking changes.
362+
"""
363+
364+
# strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
365+
strMinQueryLength: int
366+
# strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
367+
strMaxQueryLength: int
368+
369+
370+
class SuffixOpts(TypedDict):
371+
"""**BETA** Options for suffix text queries.
372+
373+
SuffixOpts is currently unstable API and subject to backwards breaking changes.
374+
"""
375+
376+
# strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
377+
strMinQueryLength: int
378+
# strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
379+
strMaxQueryLength: int

‎pymongo/synchronous/encryption.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
from pymongo import _csot
6262
from pymongo.common import CONNECT_TIMEOUT
6363
from pymongo.daemon import _spawn_daemon
64-
from pymongo.encryption_options import AutoEncryptionOpts, RangeOpts
64+
from pymongo.encryption_options import AutoEncryptionOpts, RangeOpts, TextOpts
6565
from pymongo.errors import (
6666
ConfigurationError,
6767
EncryptedCollectionError,
@@ -513,6 +513,11 @@ class Algorithm(str, enum.Enum):
513513
514514
.. versionadded:: 4.4
515515
"""
516+
TEXTPREVIEW = "TextPreview"
517+
"""**BETA** - TextPreview.
518+
519+
.. versionadded:: 4.15
520+
"""
516521

517522

518523
class QueryType(str, enum.Enum):
@@ -538,6 +543,24 @@ class QueryType(str, enum.Enum):
538543
.. versionadded:: 4.4
539544
"""
540545

546+
PREFIXPREVIEW = "prefixPreview"
547+
"""**BETA** - Used to encrypt a value for a prefixPreview query.
548+
549+
.. versionadded:: 4.15
550+
"""
551+
552+
SUFFIXPREVIEW = "suffixPreview"
553+
"""**BETA** - Used to encrypt a value for a suffixPreview query.
554+
555+
.. versionadded:: 4.15
556+
"""
557+
558+
SUBSTRINGPREVIEW = "substringPreview"
559+
"""**BETA** - Used to encrypt a value for a substringPreview query.
560+
561+
.. versionadded:: 4.15
562+
"""
563+
541564

542565
def _create_mongocrypt_options(**kwargs: Any) -> MongoCryptOptions:
543566
# For compat with pymongocrypt <1.13, avoid setting the default key_expiration_ms.
@@ -869,6 +892,7 @@ def _encrypt_helper(
869892
contention_factor: Optional[int] = None,
870893
range_opts: Optional[RangeOpts] = None,
871894
is_expression: bool = False,
895+
text_opts: Optional[TextOpts] = None,
872896
) -> Any:
873897
self._check_closed()
874898
if isinstance(key_id, uuid.UUID):
@@ -888,6 +912,12 @@ def _encrypt_helper(
888912
range_opts.document,
889913
codec_options=self._codec_options,
890914
)
915+
text_opts_bytes = None
916+
if text_opts:
917+
text_opts_bytes = encode(
918+
text_opts.document,
919+
codec_options=self._codec_options,
920+
)
891921
with _wrap_encryption_errors():
892922
encrypted_doc = self._encryption.encrypt(
893923
value=doc,
@@ -898,6 +928,7 @@ def _encrypt_helper(
898928
contention_factor=contention_factor,
899929
range_opts=range_opts_bytes,
900930
is_expression=is_expression,
931+
text_opts=text_opts_bytes,
901932
)
902933
return decode(encrypted_doc)["v"]
903934

@@ -910,6 +941,7 @@ def encrypt(
910941
query_type: Optional[str] = None,
911942
contention_factor: Optional[int] = None,
912943
range_opts: Optional[RangeOpts] = None,
944+
text_opts: Optional[TextOpts] = None,
913945
) -> Binary:
914946
"""Encrypt a BSON value with a given key and algorithm.
915947
@@ -930,9 +962,14 @@ def encrypt(
930962
used.
931963
:param range_opts: Index options for `range` queries. See
932964
:class:`RangeOpts` for some valid options.
965+
:param text_opts: Index options for `textPreview` queries. See
966+
:class:`TextOpts` for some valid options.
933967
934968
:return: The encrypted value, a :class:`~bson.binary.Binary` with subtype 6.
935969
970+
.. versionchanged:: 4.9
971+
Added the `text_opts` parameter.
972+
936973
.. versionchanged:: 4.9
937974
Added the `range_opts` parameter.
938975
@@ -953,6 +990,7 @@ def encrypt(
953990
contention_factor=contention_factor,
954991
range_opts=range_opts,
955992
is_expression=False,
993+
text_opts=text_opts,
956994
),
957995
)
958996

‎test/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import warnings
3333
from inspect import iscoroutinefunction
3434

35+
from pymongo.encryption_options import _HAVE_PYMONGOCRYPT
3536
from pymongo.errors import AutoReconnect
3637
from pymongo.synchronous.uri_parser import parse_uri
3738

@@ -524,6 +525,19 @@ def require_version_max(self, *ver):
524525
"Server version must be at most %s" % str(other_version),
525526
)
526527

528+
def require_libmongocrypt_min(self, *ver):
529+
other_version = Version(*ver)
530+
if not _HAVE_PYMONGOCRYPT:
531+
version = Version.from_string("0.0.0")
532+
else:
533+
from pymongocrypt import libmongocrypt_version
534+
535+
version = Version.from_string(libmongocrypt_version())
536+
return self._require(
537+
lambda: version >= other_version,
538+
"Libmongocrypt version must be at least %s" % str(other_version),
539+
)
540+
527541
def require_auth(self, func):
528542
"""Run a test only if the server is running with auth enabled."""
529543
return self._require(

‎test/asynchronous/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from inspect import iscoroutinefunction
3434

3535
from pymongo.asynchronous.uri_parser import parse_uri
36+
from pymongo.encryption_options import _HAVE_PYMONGOCRYPT
3637
from pymongo.errors import AutoReconnect
3738

3839
try:
@@ -524,6 +525,19 @@ def require_version_max(self, *ver):
524525
"Server version must be at most %s" % str(other_version),
525526
)
526527

528+
def require_libmongocrypt_min(self, *ver):
529+
other_version = Version(*ver)
530+
if not _HAVE_PYMONGOCRYPT:
531+
version = Version.from_string("0.0.0")
532+
else:
533+
from pymongocrypt import libmongocrypt_version
534+
535+
version = Version.from_string(libmongocrypt_version())
536+
return self._require(
537+
lambda: version >= other_version,
538+
"Libmongocrypt version must be at least %s" % str(other_version),
539+
)
540+
527541
def require_auth(self, func):
528542
"""Run a test only if the server is running with auth enabled."""
529543
return self._require(

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /