Skip to content

Commit fa821ac

Browse files
committed
Merge branch 'master' of github.com:mongodb/mongo-python-driver
2 parents 67a2e17 + e08284b commit fa821ac

11 files changed

+1777
-50
lines changed

bson/decimal128.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@
2020

2121
import decimal
2222
import struct
23+
from decimal import Decimal
2324
from typing import Any, Sequence, Tuple, Type, Union
2425

26+
from bson.codec_options import TypeDecoder, TypeEncoder
27+
2528
_PACK_64 = struct.Struct("<Q").pack
2629
_UNPACK_64 = struct.Struct("<Q").unpack
2730

@@ -58,6 +61,42 @@
5861
_VALUE_OPTIONS = Union[decimal.Decimal, float, str, Tuple[int, Sequence[int], int]]
5962

6063

64+
class DecimalEncoder(TypeEncoder):
65+
"""Converts Python :class:`decimal.Decimal` to BSON :class:`Decimal128`.
66+
67+
For example::
68+
opts = CodecOptions(type_registry=TypeRegistry([DecimalEncoder()]))
69+
bson.encode({"d": decimal.Decimal('1.0')}, codec_options=opts)
70+
71+
.. versionadded:: 4.15
72+
"""
73+
74+
@property
75+
def python_type(self) -> Type[Decimal]:
76+
return Decimal
77+
78+
def transform_python(self, value: Any) -> Decimal128:
79+
return Decimal128(value)
80+
81+
82+
class DecimalDecoder(TypeDecoder):
83+
"""Converts BSON :class:`Decimal128` to Python :class:`decimal.Decimal`.
84+
85+
For example::
86+
opts = CodecOptions(type_registry=TypeRegistry([DecimalDecoder()]))
87+
bson.decode(data, codec_options=opts)
88+
89+
.. versionadded:: 4.15
90+
"""
91+
92+
@property
93+
def bson_type(self) -> Type[Decimal128]:
94+
return Decimal128
95+
96+
def transform_bson(self, value: Any) -> decimal.Decimal:
97+
return value.to_decimal()
98+
99+
61100
def create_decimal128_context() -> decimal.Context:
62101
"""Returns an instance of :class:`decimal.Context` appropriate
63102
for working with IEEE-754 128-bit decimal floating point values.

doc/changelog.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
Changelog
22
=========
3+
Changes in Version 4.15.0 (XXXX/XX/XX)
4+
--------------------------------------
5+
PyMongo 4.15 brings a number of changes including:
6+
7+
- Added :class:`bson.decimal128.DecimalEncoder` and :class:`bson.decimal128.DecimalDecoder`
8+
to support encoding and decoding of BSON Decimal128 values to decimal.Decimal values using the TypeRegistry API.
9+
310
Changes in Version 4.14.1 (2025/08/19)
411
--------------------------------------
512

test/asynchronous/test_custom_types.py

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from random import random
2424
from typing import Any, Tuple, Type, no_type_check
2525

26+
from bson.decimal128 import DecimalDecoder, DecimalEncoder
2627
from gridfs.asynchronous.grid_file import AsyncGridIn, AsyncGridOut
2728

2829
sys.path[0:0] = [""]
@@ -59,29 +60,7 @@
5960
_IS_SYNC = False
6061

6162

62-
class DecimalEncoder(TypeEncoder):
63-
@property
64-
def python_type(self):
65-
return Decimal
66-
67-
def transform_python(self, value):
68-
return Decimal128(value)
69-
70-
71-
class DecimalDecoder(TypeDecoder):
72-
@property
73-
def bson_type(self):
74-
return Decimal128
75-
76-
def transform_bson(self, value):
77-
return value.to_decimal()
78-
79-
80-
class DecimalCodec(DecimalDecoder, DecimalEncoder):
81-
pass
82-
83-
84-
DECIMAL_CODECOPTS = CodecOptions(type_registry=TypeRegistry([DecimalCodec()]))
63+
DECIMAL_CODECOPTS = CodecOptions(type_registry=TypeRegistry([DecimalEncoder(), DecimalDecoder()]))
8564

8665

8766
class UndecipherableInt64Type:

test/asynchronous/unified_format.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,14 @@ async def is_run_on_requirement_satisfied(requirement):
159159
if req_csfle is True:
160160
min_version_satisfied = Version.from_string("4.2") <= server_version
161161
csfle_satisfied = _HAVE_PYMONGOCRYPT and min_version_satisfied
162+
elif isinstance(req_csfle, dict) and "minLibmongocryptVersion" in req_csfle:
163+
csfle_satisfied = False
164+
req_version = req_csfle["minLibmongocryptVersion"]
165+
if _HAVE_PYMONGOCRYPT:
166+
from pymongocrypt import libmongocrypt_version
167+
168+
if Version.from_string(libmongocrypt_version()) >= Version.from_string(req_version):
169+
csfle_satisfied = True
162170

163171
return (
164172
topology_satisfied
@@ -450,7 +458,7 @@ class UnifiedSpecTestMixinV1(AsyncIntegrationTest):
450458
a class attribute ``TEST_SPEC``.
451459
"""
452460

453-
SCHEMA_VERSION = Version.from_string("1.23")
461+
SCHEMA_VERSION = Version.from_string("1.25")
454462
RUN_ON_LOAD_BALANCER = True
455463
TEST_SPEC: Any
456464
TEST_PATH = "" # This gets filled in by generate_test_classes
@@ -1545,7 +1553,7 @@ class SpecTestBase(with_metaclass(UnifiedSpecTestMeta)): # type: ignore
15451553

15461554
# Add "encryption" marker if the "csfle" runOnRequirement is set.
15471555
for req in test_spec.get("runOnRequirements", []):
1548-
if req.get("csfle", False):
1556+
if "csfle" in req:
15491557
base = pytest.mark.encryption(base)
15501558

15511559
return base
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
{
2+
"description": "QE-Text-cleanupStructuredEncryptionData",
3+
"schemaVersion": "1.25",
4+
"runOnRequirements": [
5+
{
6+
"minServerVersion": "8.2.0",
7+
"topologies": [
8+
"replicaset",
9+
"sharded",
10+
"load-balanced"
11+
],
12+
"csfle": {
13+
"minLibmongocryptVersion": "1.15.0"
14+
}
15+
}
16+
],
17+
"createEntities": [
18+
{
19+
"client": {
20+
"id": "client",
21+
"autoEncryptOpts": {
22+
"keyVaultNamespace": "keyvault.datakeys",
23+
"kmsProviders": {
24+
"local": {
25+
"key": "Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk"
26+
}
27+
}
28+
},
29+
"observeEvents": [
30+
"commandStartedEvent"
31+
]
32+
}
33+
},
34+
{
35+
"database": {
36+
"id": "db",
37+
"client": "client",
38+
"databaseName": "db"
39+
}
40+
},
41+
{
42+
"collection": {
43+
"id": "coll",
44+
"database": "db",
45+
"collectionName": "coll"
46+
}
47+
}
48+
],
49+
"initialData": [
50+
{
51+
"databaseName": "keyvault",
52+
"collectionName": "datakeys",
53+
"documents": [
54+
{
55+
"_id": {
56+
"$binary": {
57+
"base64": "q83vqxI0mHYSNBI0VniQEg==",
58+
"subType": "04"
59+
}
60+
},
61+
"keyMaterial": {
62+
"$binary": {
63+
"base64": "HBk9BWihXExNDvTp1lUxOuxuZK2Pe2ZdVdlsxPEBkiO1bS4mG5NNDsQ7zVxJAH8BtdOYp72Ku4Y3nwc0BUpIKsvAKX4eYXtlhv5zUQxWdeNFhg9qK7qb8nqhnnLeT0f25jFSqzWJoT379hfwDeu0bebJHr35QrJ8myZdPMTEDYF08QYQ48ShRBli0S+QzBHHAQiM2iJNr4svg2WR8JSeWQ==",
64+
"subType": "00"
65+
}
66+
},
67+
"creationDate": {
68+
"$date": {
69+
"$numberLong": "1648914851981"
70+
}
71+
},
72+
"updateDate": {
73+
"$date": {
74+
"$numberLong": "1648914851981"
75+
}
76+
},
77+
"status": {
78+
"$numberInt": "0"
79+
},
80+
"masterKey": {
81+
"provider": "local"
82+
}
83+
}
84+
]
85+
},
86+
{
87+
"databaseName": "db",
88+
"collectionName": "coll",
89+
"documents": [],
90+
"createOptions": {
91+
"encryptedFields": {
92+
"fields": [
93+
{
94+
"keyId": {
95+
"$binary": {
96+
"base64": "q83vqxI0mHYSNBI0VniQEg==",
97+
"subType": "04"
98+
}
99+
},
100+
"path": "encryptedText",
101+
"bsonType": "string",
102+
"queries": [
103+
{
104+
"queryType": "suffixPreview",
105+
"contention": {
106+
"$numberLong": "0"
107+
},
108+
"strMinQueryLength": {
109+
"$numberLong": "3"
110+
},
111+
"strMaxQueryLength": {
112+
"$numberLong": "30"
113+
},
114+
"caseSensitive": true,
115+
"diacriticSensitive": true
116+
}
117+
]
118+
}
119+
]
120+
}
121+
}
122+
}
123+
],
124+
"tests": [
125+
{
126+
"description": "QE Text cleanupStructuredEncryptionData works",
127+
"operations": [
128+
{
129+
"name": "runCommand",
130+
"object": "db",
131+
"arguments": {
132+
"command": {
133+
"cleanupStructuredEncryptionData": "coll"
134+
},
135+
"commandName": "cleanupStructuredEncryptionData"
136+
},
137+
"expectResult": {
138+
"ok": 1
139+
}
140+
}
141+
],
142+
"expectEvents": [
143+
{
144+
"client": "client",
145+
"events": [
146+
{
147+
"commandStartedEvent": {
148+
"command": {
149+
"listCollections": 1,
150+
"filter": {
151+
"name": "coll"
152+
}
153+
},
154+
"commandName": "listCollections"
155+
}
156+
},
157+
{
158+
"commandStartedEvent": {
159+
"command": {
160+
"find": "datakeys",
161+
"filter": {
162+
"$or": [
163+
{
164+
"_id": {
165+
"$in": [
166+
{
167+
"$binary": {
168+
"base64": "q83vqxI0mHYSNBI0VniQEg==",
169+
"subType": "04"
170+
}
171+
}
172+
]
173+
}
174+
},
175+
{
176+
"keyAltNames": {
177+
"$in": []
178+
}
179+
}
180+
]
181+
},
182+
"$db": "keyvault",
183+
"readConcern": {
184+
"level": "majority"
185+
}
186+
},
187+
"commandName": "find"
188+
}
189+
},
190+
{
191+
"commandStartedEvent": {
192+
"command": {
193+
"cleanupStructuredEncryptionData": "coll",
194+
"cleanupTokens": {
195+
"encryptedText": {
196+
"ecoc": {
197+
"$binary": {
198+
"base64": "SWO8WEoZ2r2Kx/muQKb7+COizy85nIIUFiHh4K9kcvA=",
199+
"subType": "00"
200+
}
201+
},
202+
"anchorPaddingToken": {
203+
"$binary": {
204+
"base64": "YAiF7Iwhqq1UyfxPvm70xfQJtrIRPrjfD2yRLG1+saQ=",
205+
"subType": "00"
206+
}
207+
}
208+
}
209+
}
210+
},
211+
"commandName": "cleanupStructuredEncryptionData"
212+
}
213+
}
214+
]
215+
}
216+
]
217+
}
218+
]
219+
}

0 commit comments

Comments
 (0)