Skip to content

Commit 10898cf

Browse files
committed
feat(firestore): add zero-dependency PyMongo duck-typing write support (PR 1B)
1 parent 7c7622e commit 10898cf

3 files changed

Lines changed: 82 additions & 0 deletions

File tree

packages/google-cloud-firestore/google/cloud/firestore_v1/_helpers.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@
4747
from google.cloud.firestore_v1.field_path import FieldPath, parse_field_path
4848
from google.cloud.firestore_v1.types import common, document, write
4949
from google.cloud.firestore_v1.types.write import DocumentTransform
50+
from google.cloud.firestore_v1.bson import (
51+
BSONBinary,
52+
BSONDecimal128,
53+
BSONObjectID,
54+
BSONRegex,
55+
)
5056
from google.cloud.firestore_v1.vector import Vector
5157

5258
if TYPE_CHECKING: # pragma: NO COVER
@@ -185,6 +191,14 @@ def encode_value(value) -> types.document.Value:
185191
if hasattr(value, "to_map_value"):
186192
return encode_value(value.to_map_value())
187193

194+
# Duck-typing input bridge for external PyMongo / bson package objects (zero dependency)
195+
if hasattr(value, "binary") and not isinstance(value, (bytes, bytearray, BSONBinary)):
196+
return encode_value(BSONObjectID(getattr(value, "binary")))
197+
elif hasattr(value, "to_decimal") and not isinstance(value, BSONDecimal128):
198+
return encode_value(BSONDecimal128(getattr(value, "to_decimal")()))
199+
elif hasattr(value, "pattern") and not isinstance(value, (str, BSONRegex)):
200+
return encode_value(BSONRegex(getattr(value, "pattern"), str(getattr(value, "flags", ""))))
201+
188202
# Must come before int since ``bool`` is an integer subtype.
189203
if isinstance(value, bool):
190204
return document.Value(boolean_value=value)

packages/google-cloud-firestore/tests/system/test_bson.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,39 @@ def test_bson_document_writes(client, cleanup, database):
6565

6666
snapshot = doc_ref.get()
6767
assert snapshot.exists
68+
69+
70+
@pytest.mark.parametrize("database", [FIRESTORE_ENTERPRISE_DB], indirect=True)
71+
def test_pymongo_document_writes(client, cleanup, database):
72+
"""Test write operations using native duck-typed PyMongo objects on Enterprise DB."""
73+
import decimal
74+
75+
class DummyPyMongoObjectId:
76+
def __init__(self, raw: bytes):
77+
self.binary = raw
78+
79+
class DummyPyMongoDecimal128:
80+
def __init__(self, d: decimal.Decimal):
81+
self._d = d
82+
def to_decimal(self):
83+
return self._d
84+
85+
class DummyPyMongoRegex:
86+
def __init__(self, pat: str, flags: str):
87+
self.pattern = pat
88+
self.flags = flags
89+
90+
collection_id = "pymongo_docs_write_" + UNIQUE_RESOURCE_ID
91+
doc_ref = client.collection(collection_id).document("pymongo_doc")
92+
cleanup(doc_ref.delete)
93+
94+
payload = {
95+
"_id": DummyPyMongoObjectId(bytes.fromhex("507f191e810c19729de860ea")),
96+
"price": DummyPyMongoDecimal128(decimal.Decimal("99.99")),
97+
"pattern": DummyPyMongoRegex("^test.*", "i"),
98+
}
99+
100+
doc_ref.set(payload)
101+
102+
snapshot = doc_ref.get()
103+
assert snapshot.exists

packages/google-cloud-firestore/tests/unit/v1/test__helpers.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,38 @@ def test_encode_dict_w_many_types():
354354
assert encoded_dict == expected_dict
355355

356356

357+
def test_encode_value_duck_typed_pymongo():
358+
import decimal
359+
from google.cloud.firestore_v1._helpers import encode_value
360+
361+
class DummyPyMongoObjectId:
362+
def __init__(self, raw: bytes):
363+
self.binary = raw
364+
365+
class DummyPyMongoDecimal128:
366+
def __init__(self, d: decimal.Decimal):
367+
self._d = d
368+
def to_decimal(self):
369+
return self._d
370+
371+
class DummyPyMongoRegex:
372+
def __init__(self, pat: str, flags: str):
373+
self.pattern = pat
374+
self.flags = flags
375+
376+
dummy_oid = DummyPyMongoObjectId(bytes.fromhex("507f1f77bcf86cd799439011"))
377+
res_oid = encode_value(dummy_oid)
378+
assert res_oid.map_value.fields["__oid__"].string_value == "507f1f77bcf86cd799439011"
379+
380+
dummy_dec = DummyPyMongoDecimal128(decimal.Decimal("99.99"))
381+
res_dec = encode_value(dummy_dec)
382+
assert res_dec.map_value.fields["__decimal128__"].string_value == "99.99"
383+
384+
dummy_reg = DummyPyMongoRegex("^test$", "i")
385+
res_reg = encode_value(dummy_reg)
386+
assert res_reg.map_value.fields["__regex__"].map_value.fields["pattern"].string_value == "^test$"
387+
388+
357389
def test_reference_value_to_document_w_bad_format():
358390
from google.cloud.firestore_v1._helpers import (
359391
BAD_REFERENCE_ERROR,

0 commit comments

Comments
 (0)