Skip to content

Commit 11d4e98

Browse files
committed
feat(firestore): add zero-dependency PyMongo duck-typing write support (PR 1B)
1 parent 5689650 commit 11d4e98

3 files changed

Lines changed: 94 additions & 0 deletions

File tree

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@
4444
import google
4545
from google.cloud import exceptions # type: ignore
4646
from google.cloud.firestore_v1 import transforms, types
47+
from google.cloud.firestore_v1.bson import (
48+
BSONBinary,
49+
BSONDecimal128,
50+
BSONObjectID,
51+
BSONRegex,
52+
)
4753
from google.cloud.firestore_v1.field_path import FieldPath, parse_field_path
4854
from google.cloud.firestore_v1.types import common, document, write
4955
from google.cloud.firestore_v1.types.write import DocumentTransform
@@ -185,6 +191,18 @@ 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(
196+
value, (bytes, bytearray, BSONBinary)
197+
):
198+
return encode_value(BSONObjectID(getattr(value, "binary")))
199+
elif hasattr(value, "to_decimal") and not isinstance(value, BSONDecimal128):
200+
return encode_value(BSONDecimal128(getattr(value, "to_decimal")()))
201+
elif hasattr(value, "pattern") and not isinstance(value, (str, BSONRegex)):
202+
return encode_value(
203+
BSONRegex(getattr(value, "pattern"), str(getattr(value, "flags", "")))
204+
)
205+
188206
# Must come before int since ``bool`` is an integer subtype.
189207
if isinstance(value, bool):
190208
return document.Value(boolean_value=value)

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,40 @@ def test_bson_document_writes(client, cleanup, database):
6666

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

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,45 @@ 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+
360+
from google.cloud.firestore_v1._helpers import encode_value
361+
362+
class DummyPyMongoObjectId:
363+
def __init__(self, raw: bytes):
364+
self.binary = raw
365+
366+
class DummyPyMongoDecimal128:
367+
def __init__(self, d: decimal.Decimal):
368+
self._d = d
369+
370+
def to_decimal(self):
371+
return self._d
372+
373+
class DummyPyMongoRegex:
374+
def __init__(self, pat: str, flags: str):
375+
self.pattern = pat
376+
self.flags = flags
377+
378+
dummy_oid = DummyPyMongoObjectId(bytes.fromhex("507f1f77bcf86cd799439011"))
379+
res_oid = encode_value(dummy_oid)
380+
assert (
381+
res_oid.map_value.fields["__oid__"].string_value == "507f1f77bcf86cd799439011"
382+
)
383+
384+
dummy_dec = DummyPyMongoDecimal128(decimal.Decimal("99.99"))
385+
res_dec = encode_value(dummy_dec)
386+
assert res_dec.map_value.fields["__decimal128__"].string_value == "99.99"
387+
388+
dummy_reg = DummyPyMongoRegex("^test$", "i")
389+
res_reg = encode_value(dummy_reg)
390+
assert (
391+
res_reg.map_value.fields["__regex__"].map_value.fields["pattern"].string_value
392+
== "^test$"
393+
)
394+
395+
357396
def test_reference_value_to_document_w_bad_format():
358397
from google.cloud.firestore_v1._helpers import (
359398
BAD_REFERENCE_ERROR,

0 commit comments

Comments
 (0)