Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ replacements:
BSONMaxKey,
BSONMinKey,
BSONObjectId,
BSONTimestamp,
)
from google.cloud.firestore_v1.client import Client
from google.cloud.firestore_v1.collection import CollectionReference
Expand Down Expand Up @@ -182,6 +183,7 @@ replacements:
"BSONMaxKey",
"BSONMinKey",
"BSONObjectId",
"BSONTimestamp",
"Client",
"CountAggregation",
"CollectionGroup",
Expand Down Expand Up @@ -259,6 +261,7 @@ replacements:
BSONMaxKey,
BSONMinKey,
BSONObjectId,
BSONTimestamp,
Client,
CollectionGroup,
CollectionReference,
Expand Down Expand Up @@ -321,6 +324,7 @@ replacements:
"BSONMaxKey",
"BSONMinKey",
"BSONObjectId",
"BSONTimestamp",
"Client",
"CountAggregation",
"CollectionGroup",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
BSONMaxKey,
BSONMinKey,
BSONObjectId,
BSONTimestamp,
Client,
CollectionGroup,
CollectionReference,
Expand Down Expand Up @@ -102,6 +103,7 @@
"BSONMaxKey",
"BSONMinKey",
"BSONObjectId",
"BSONTimestamp",
"Client",
"CountAggregation",
"CollectionGroup",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
BSONMaxKey,
BSONMinKey,
BSONObjectId,
BSONTimestamp,
)
from google.cloud.firestore_v1.client import Client
from google.cloud.firestore_v1.collection import CollectionReference
Expand Down Expand Up @@ -159,6 +160,7 @@
"BSONMaxKey",
"BSONMinKey",
"BSONObjectId",
"BSONTimestamp",
"Client",
"CountAggregation",
"CollectionGroup",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"BSONMaxKey",
"BSONInt32",
"BSONBinary",
"BSONTimestamp",
]

_OBJECT_ID_BYTES_LEN = 12
Expand Down Expand Up @@ -280,3 +281,64 @@ def __eq__(self, other: Any) -> bool:

def __hash__(self) -> int:
return hash((type(self), self._data, self._subtype))


class BSONTimestamp(_BSONType):
"""Container for BSON Timestamp values.

Args:
seconds (int): Seconds count.
increment (int): Increment/ordinal.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it's not clear to me what increment means here. But I tried looking at other docs, and it doesn't seem well defined there either. Maybe this is more clear to the intended user


Raises:
TypeError: If seconds or increment is not an int or is a bool.

Example:
>>> ts = BSONTimestamp(1700000000, 1)
>>> ts.seconds
1700000000
>>> ts.increment
1
"""

__slots__ = ("_seconds", "_increment")

def __init__(self, seconds: int, increment: int):
if isinstance(seconds, bool) or not isinstance(seconds, int):
raise TypeError("BSONTimestamp seconds must be an int.")
if isinstance(increment, bool) or not isinstance(increment, int):
raise TypeError("BSONTimestamp increment must be an int.")
self._seconds: int = seconds
self._increment: int = increment

@property
def seconds(self) -> int:
"""int: The seconds value."""
return self._seconds

@property
def increment(self) -> int:
"""int: The increment value."""
return self._increment

def _to_map_value(self) -> Dict[str, Dict[str, int]]:
"""Returns map dictionary representation for wire serialization."""
return {
"__request_timestamp__": {
"seconds": self._seconds,
"increment": self._increment,
}
}

def __repr__(self) -> str:
return f"BSONTimestamp(seconds={self._seconds}, increment={self._increment})"

def __eq__(self, other: Any) -> bool:
if isinstance(other, BSONTimestamp):
return (
self._seconds == other._seconds and self._increment == other._increment
)
return NotImplemented

def __hash__(self) -> int:
return hash((type(self), self._seconds, self._increment))
8 changes: 8 additions & 0 deletions packages/google-cloud-firestore/tests/system/test_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
BSONMaxKey,
BSONMinKey,
BSONObjectId,
BSONTimestamp,
)
from google.cloud.firestore_v1.vector import Vector

Expand Down Expand Up @@ -1294,6 +1295,7 @@ def test_bson_document_writes(client, cleanup, database):
"max_key": BSONMaxKey(),
"int32_val": BSONInt32(42),
"binary_val_sub128": BSONBinary(b"world", subtype=128),
"timestamp_val": BSONTimestamp(1700000000, 1),
}

doc_ref.set(bson_payload)
Expand All @@ -1306,6 +1308,12 @@ def test_bson_document_writes(client, cleanup, database):
"max_key": {"__max__": None},
"int32_val": {"__int__": 42},
"binary_val_sub128": {"__binary__": b"\x80world"},
"timestamp_val": {
"__request_timestamp__": {
"seconds": 1700000000,
"increment": 1,
}
},
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
BSONMaxKey,
BSONMinKey,
BSONObjectId,
BSONTimestamp,
)
from google.cloud.firestore_v1.query_profile import (
ExecutionStats,
Expand Down Expand Up @@ -1267,6 +1268,7 @@ async def test_async_bson_document_writes(client, cleanup, database):
"max_key": BSONMaxKey(),
"int32_val": BSONInt32(42),
"binary_val_sub128": BSONBinary(b"world", subtype=128),
"timestamp_val": BSONTimestamp(1700000000, 1),
}

await doc_ref.set(bson_payload)
Expand All @@ -1279,6 +1281,12 @@ async def test_async_bson_document_writes(client, cleanup, database):
"max_key": {"__max__": None},
"int32_val": {"__int__": 42},
"binary_val_sub128": {"__binary__": b"\x80world"},
"timestamp_val": {
"__request_timestamp__": {
"seconds": 1700000000,
"increment": 1,
}
},
}


Expand Down
66 changes: 66 additions & 0 deletions packages/google-cloud-firestore/tests/unit/v1/test_bson.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
BSONMaxKey,
BSONMinKey,
BSONObjectId,
BSONTimestamp,
_BSONType,
)

Expand Down Expand Up @@ -345,3 +346,68 @@ def test_bson_binary_copy():
def test_bson_binary_pickle():
val = BSONBinary(b"hello", subtype=5)
assert pickle.loads(pickle.dumps(val)) == val


def test_bson_timestamp_valid():
ts = BSONTimestamp(1700000000, 42)
assert ts.seconds == 1700000000
assert ts.increment == 42
assert ts._to_map_value() == {
"__request_timestamp__": {
"seconds": 1700000000,
"increment": 42,
}
}
assert repr(ts) == "BSONTimestamp(seconds=1700000000, increment=42)"


def test_bson_timestamp_boundaries():
ts_min = BSONTimestamp(0, 0)
ts_max = BSONTimestamp(4294967295, 4294967295)
assert ts_min.seconds == 0
assert ts_min.increment == 0
assert ts_max.seconds == 4294967295
assert ts_max.increment == 4294967295


@pytest.mark.parametrize(
"sec_input, inc_input, exc_type, match_msg",
[
(True, 0, TypeError, "seconds must be an int"),
(0, False, TypeError, "increment must be an int"),
("1700000000", 0, TypeError, "seconds must be an int"),
(0, 1.5, TypeError, "increment must be an int"),
],
)
def test_bson_timestamp_invalid_inputs(sec_input, inc_input, exc_type, match_msg):
with pytest.raises(exc_type, match=match_msg):
BSONTimestamp(sec_input, inc_input)


def test_bson_timestamp_equality():
ts1 = BSONTimestamp(100, 1)
ts2 = BSONTimestamp(100, 1)
ts3 = BSONTimestamp(100, 2)
ts4 = BSONTimestamp(200, 1)
assert ts1 == ts2
assert ts1 != ts3
assert ts1 != ts4
assert ts1 != 100


def test_bson_timestamp_hash_and_dict_key():
ts1 = BSONTimestamp(100, 1)
ts2 = BSONTimestamp(100, 1)
assert hash(ts1) == hash(ts2)
assert len({ts1, ts2}) == 1


def test_bson_timestamp_copy():
ts = BSONTimestamp(100, 1)
assert copy.copy(ts) == ts
assert copy.deepcopy(ts) == ts


def test_bson_timestamp_pickle():
ts = BSONTimestamp(100, 1)
assert pickle.loads(pickle.dumps(ts)) == ts
Loading