Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 33 additions & 20 deletions .evergreen/scripts/setup_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,29 +353,42 @@ def handle_test_env() -> None:
UV_ARGS.append("--extra zstd")

if test_name in ["encryption", "kms"]:
# Check for libmongocrypt download.
if not (ROOT / "libmongocrypt").exists():
setup_libmongocrypt()
# The "String" algorithm and the GA prefix/suffix/substring query types
# need libmongocrypt 1.19.0+, which is only exercised against MongoDB
# 9.0+. Servers before 9.0 test the preview query types instead, which
# need the "textPreview" algorithm, so pin to the released pymongocrypt
# and use the libmongocrypt bundled in its wheel rather than the
# unreleased master build.
use_released_pymongocrypt = os.environ.get("MONGODB_VERSION", "").startswith("8.")

if not use_released_pymongocrypt:
# Check for libmongocrypt download.
if not (ROOT / "libmongocrypt").exists():
setup_libmongocrypt()

if not opts.test_min_deps:
UV_ARGS.append(
"--with pymongocrypt@git+https://github.com/mongodb/libmongocrypt@master#subdirectory=bindings/python"
)

# Use the nocrypto build to avoid dependency issues with older windows/python versions.
BASE = ROOT / "libmongocrypt/nocrypto"
if PLATFORM == "linux":
if (BASE / "lib/libmongocrypt.so").exists():
PYMONGOCRYPT_LIB = BASE / "lib/libmongocrypt.so"
if use_released_pymongocrypt:
UV_ARGS.append("--with pymongocrypt<1.19")
else:
PYMONGOCRYPT_LIB = BASE / "lib64/libmongocrypt.so"
elif PLATFORM == "darwin":
PYMONGOCRYPT_LIB = BASE / "lib/libmongocrypt.dylib"
else:
PYMONGOCRYPT_LIB = BASE / "bin/mongocrypt.dll"
if not PYMONGOCRYPT_LIB.exists():
raise RuntimeError("Cannot find libmongocrypt shared object file")
write_env("PYMONGOCRYPT_LIB", PYMONGOCRYPT_LIB.as_posix())
UV_ARGS.append(
"--with pymongocrypt@git+https://github.com/mongodb/libmongocrypt@master#subdirectory=bindings/python"
)

if not use_released_pymongocrypt:
# Use the nocrypto build to avoid dependency issues with older windows/python versions.
BASE = ROOT / "libmongocrypt/nocrypto"
if PLATFORM == "linux":
if (BASE / "lib/libmongocrypt.so").exists():
PYMONGOCRYPT_LIB = BASE / "lib/libmongocrypt.so"
else:
PYMONGOCRYPT_LIB = BASE / "lib64/libmongocrypt.so"
elif PLATFORM == "darwin":
PYMONGOCRYPT_LIB = BASE / "lib/libmongocrypt.dylib"
else:
PYMONGOCRYPT_LIB = BASE / "bin/mongocrypt.dll"
if not PYMONGOCRYPT_LIB.exists():
raise RuntimeError("Cannot find libmongocrypt shared object file")
write_env("PYMONGOCRYPT_LIB", PYMONGOCRYPT_LIB.as_posix())
# PATH is updated by configure-env.sh for access to mongocryptd.

if test_name == "encryption":
Expand Down
17 changes: 17 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,23 @@ PyMongo 4.18 brings a number of changes including:
- Fixed a bug on Windows, and on macOS when using PyOpenSSL, where
``SSL_CERT_FILE``/``SSL_CERT_DIR`` were merged with, rather than replacing,
the OS/certifi certificate store.
- Added general availability support for Queryable Encryption prefix, suffix,
and substring queries against MongoDB 9.0+. Prefix and suffix queries require
libmongocrypt 1.19.0 or later; substring queries require libmongocrypt 1.20.0
or later:

- Added :attr:`~pymongo.encryption.Algorithm.STRING` and
:class:`~pymongo.encryption_options.StringOpts`, replacing
``Algorithm.TEXTPREVIEW`` and ``TextOpts``, which are now deprecated.
- Added :attr:`~pymongo.encryption.QueryType.PREFIX`,
:attr:`~pymongo.encryption.QueryType.SUFFIX`, and
:attr:`~pymongo.encryption.QueryType.SUBSTRING`. The corresponding
``PREFIXPREVIEW``, ``SUFFIXPREVIEW``, and ``SUBSTRINGPREVIEW`` query types
remain for experimental use with MongoDB versions before 9.0.
- Added the ``string_opts`` parameter to
:meth:`~pymongo.encryption.ClientEncryption.encrypt` and
:meth:`~pymongo.asynchronous.encryption.AsyncClientEncryption.encrypt`,
deprecating ``text_opts``.

Changes in Version 4.17.0 (2026/04/20)
--------------------------------------
Expand Down
93 changes: 81 additions & 12 deletions pymongo/asynchronous/encryption.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import socket
import time as time # noqa: PLC0414 # needed in sync version
import uuid
import warnings
import weakref
from collections.abc import AsyncGenerator, Iterator, Mapping, MutableMapping, Sequence
from copy import deepcopy
Expand Down Expand Up @@ -65,7 +66,10 @@
from pymongo.encryption_options import (
AutoEncryptionOpts,
RangeOpts,
TextOpts,
StringOpts,
# Re-exported for backwards compatibility: TextOpts is deprecated but must
# remain importable from this module until it is removed.
TextOpts, # noqa: F401
check_min_pymongocrypt,
)
from pymongo.errors import (
Expand Down Expand Up @@ -529,8 +533,15 @@ class Algorithm(str, enum.Enum):

.. versionadded:: 4.4
"""
STRING = "String"
"""String.

.. versionadded:: 4.18
"""
TEXTPREVIEW = "TextPreview"
"""**BETA** - TextPreview.
"""**DEPRECATED** - TextPreview.

.. note:: Support for TextPreview is deprecated. Use :attr:`Algorithm.STRING` instead.

.. versionadded:: 4.15
"""
Expand Down Expand Up @@ -559,25 +570,77 @@ class QueryType(str, enum.Enum):
.. versionadded:: 4.4
"""

PREFIX = "prefix"
"""Used to encrypt a value for a prefix query.

Used for the ``$encStrStartsWith`` operator. Requires MongoDB 9.0+.

.. versionadded:: 4.18
"""

SUFFIX = "suffix"
"""Used to encrypt a value for a suffix query.

Used for the ``$encStrEndsWith`` operator. Requires MongoDB 9.0+.

.. versionadded:: 4.18
"""

SUBSTRING = "substring"
"""Used to encrypt a value for a substring query.

Used for the ``$encStrContains`` operator. Requires MongoDB 9.0+.

.. versionadded:: 4.18
"""

PREFIXPREVIEW = "prefixPreview"
"""**BETA** - Used to encrypt a value for a prefixPreview query.

.. note:: The preview query types are for experimental workloads only and
are only supported by MongoDB versions before 9.0. Use
:attr:`QueryType.PREFIX` instead.

.. versionadded:: 4.15
"""

SUFFIXPREVIEW = "suffixPreview"
"""**BETA** - Used to encrypt a value for a suffixPreview query.

.. note:: The preview query types are for experimental workloads only and
are only supported by MongoDB versions before 9.0. Use
:attr:`QueryType.SUFFIX` instead.

.. versionadded:: 4.15
"""

SUBSTRINGPREVIEW = "substringPreview"
"""**BETA** - Used to encrypt a value for a substringPreview query.

.. note:: The preview query types are for experimental workloads only and
are only supported by MongoDB versions before 9.0. Use
:attr:`QueryType.SUBSTRING` instead.

.. versionadded:: 4.15
"""


def _resolve_string_opts(
string_opts: Optional[StringOpts], text_opts: Optional[StringOpts]
) -> Optional[StringOpts]:
"""Resolve the deprecated text_opts alias for string_opts."""
if text_opts is None:
return string_opts
if string_opts is not None:
raise ConfigurationError("Cannot set both string_opts and text_opts")
warnings.warn(
"The text_opts parameter is deprecated. Use string_opts instead.",
DeprecationWarning,
stacklevel=3,
)
return text_opts


def _create_mongocrypt_options(**kwargs: Any) -> MongoCryptOptions:
# For compat with pymongocrypt <1.13, avoid setting the default key_expiration_ms.
if kwargs.get("key_expiration_ms") is None:
Expand Down Expand Up @@ -917,7 +980,7 @@ async def _encrypt_helper(
contention_factor: Optional[int] = None,
range_opts: Optional[RangeOpts] = None,
is_expression: bool = False,
text_opts: Optional[TextOpts] = None,
string_opts: Optional[StringOpts] = None,
) -> Any:
self._check_closed()
if isinstance(key_id, uuid.UUID):
Expand All @@ -937,10 +1000,10 @@ async def _encrypt_helper(
range_opts.document,
codec_options=self._codec_options,
)
text_opts_bytes = None
if text_opts:
text_opts_bytes = encode(
text_opts.document,
string_opts_bytes = None
if string_opts:
string_opts_bytes = encode(
string_opts.document,
codec_options=self._codec_options,
)
with _wrap_encryption_errors():
Expand All @@ -953,8 +1016,9 @@ async def _encrypt_helper(
contention_factor=contention_factor,
range_opts=range_opts_bytes,
is_expression=is_expression,
# pymongocrypt still names this parameter text_opts.
# For compatibility with pymongocrypt < 1.16:
**{"text_opts": text_opts_bytes} if text_opts_bytes else {},
**{"text_opts": string_opts_bytes} if string_opts_bytes else {},
)
return decode(encrypted_doc)["v"]

Expand All @@ -967,7 +1031,8 @@ async def encrypt(
query_type: Optional[str] = None,
contention_factor: Optional[int] = None,
range_opts: Optional[RangeOpts] = None,
text_opts: Optional[TextOpts] = None,
string_opts: Optional[StringOpts] = None,
text_opts: Optional[StringOpts] = None,
) -> Binary:
"""Encrypt a BSON value with a given key and algorithm.

Expand All @@ -988,11 +1053,15 @@ async def encrypt(
used.
:param range_opts: Index options for `range` queries. See
:class:`RangeOpts` for some valid options.
:param text_opts: Index options for `textPreview` queries. See
:class:`TextOpts` for some valid options.
:param string_opts: Index options for `prefix`, `suffix`, and
`substring` queries. See :class:`StringOpts` for some valid options.
:param text_opts: **DEPRECATED** - Alias for `string_opts`.

:return: The encrypted value, a :class:`~bson.binary.Binary` with subtype 6.

.. versionchanged:: 4.18
Added the `string_opts` parameter and deprecated `text_opts`.

.. versionchanged:: 4.9
Added the `text_opts` parameter.

Expand All @@ -1016,7 +1085,7 @@ async def encrypt(
contention_factor=contention_factor,
range_opts=range_opts,
is_expression=False,
text_opts=text_opts,
string_opts=_resolve_string_opts(string_opts, text_opts),
),
)

Expand Down
48 changes: 34 additions & 14 deletions pymongo/encryption_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

from __future__ import annotations

import warnings
from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, Optional, TypedDict

Expand Down Expand Up @@ -312,10 +313,8 @@ def document(self) -> dict[str, Any]:
return doc


class TextOpts:
"""**BETA** Options to configure encrypted queries using the text algorithm.

TextOpts is currently unstable API and subject to backwards breaking changes."""
class StringOpts:
"""Options to configure encrypted queries using the string algorithm."""

def __init__(
self,
Expand All @@ -325,15 +324,16 @@ def __init__(
case_sensitive: Optional[bool] = None,
diacritic_sensitive: Optional[bool] = None,
) -> None:
"""Options to configure encrypted queries using the text algorithm.
"""Options to configure encrypted queries using the string algorithm.

:param substring: Further options to support substring queries.
:param prefix: Further options to support prefix queries.
:param suffix: Further options to support suffix queries.
:param case_sensitive: Whether text indexes for this field are case sensitive.
:param diacritic_sensitive: Whether text indexes for this field are diacritic sensitive.
:param case_sensitive: Whether string indexes for this field are case sensitive.
:param diacritic_sensitive: Whether string indexes for this field are diacritic sensitive.

.. versionadded:: 4.15
.. versionadded:: 4.18
``StringOpts`` replaces ``TextOpts``, which is deprecated.
"""
self.substring = substring
self.prefix = prefix
Expand All @@ -357,9 +357,9 @@ def document(self) -> dict[str, Any]:


class SubstringOpts(TypedDict):
"""**BETA** Options for substring text queries.
"""Options for substring string queries.

SubstringOpts is currently unstable API and subject to backwards breaking changes.
.. versionadded:: 4.15
"""

# strMaxLength is the maximum allowed length to insert. Inserting longer strings will error.
Expand All @@ -371,9 +371,9 @@ class SubstringOpts(TypedDict):


class PrefixOpts(TypedDict):
"""**BETA** Options for prefix text queries.
"""Options for prefix string queries.

PrefixOpts is currently unstable API and subject to backwards breaking changes.
.. versionadded:: 4.15
"""

# strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
Expand All @@ -383,12 +383,32 @@ class PrefixOpts(TypedDict):


class SuffixOpts(TypedDict):
"""**BETA** Options for suffix text queries.
"""Options for suffix string queries.

SuffixOpts is currently unstable API and subject to backwards breaking changes.
.. versionadded:: 4.15
"""

# strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
strMinQueryLength: int
# strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
strMaxQueryLength: int


class TextOpts(StringOpts):
"""**DEPRECATED** Options to configure encrypted queries using the text algorithm.

.. note:: ``TextOpts`` is deprecated. Use :class:`StringOpts` instead.

.. versionadded:: 4.15

.. versionchanged:: 4.18
Deprecated in favor of :class:`StringOpts`.
"""

def __init__(self, *args: Any, **kwargs: Any) -> None:
warnings.warn(
"TextOpts is deprecated. Use StringOpts instead.",
DeprecationWarning,
stacklevel=2,
)
super().__init__(*args, **kwargs)
Loading
Loading