Skip to content

Commit 5e121ce

Browse files
authored
Api_core: Convert 'DatetimeWithNanos' to / from 'google.protobuf.timestamp_pb2.Timestamp' (#6919)
Toward #6547.
1 parent 9b789b2 commit 5e121ce

File tree

2 files changed

+193
-87
lines changed

2 files changed

+193
-87
lines changed

google/api_core/datetime_helpers.py

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

2121
import pytz
2222

23+
from google.protobuf import timestamp_pb2
24+
2325

2426
_UTC_EPOCH = datetime.datetime.utcfromtimestamp(0).replace(tzinfo=pytz.utc)
2527
_RFC3339_MICROS = "%Y-%m-%dT%H:%M:%S.%fZ"
@@ -263,3 +265,39 @@ def from_rfc3339(cls, stamp):
263265
nanosecond=nanos,
264266
tzinfo=pytz.UTC,
265267
)
268+
269+
def timestamp_pb(self):
270+
"""Return a timestamp message.
271+
272+
Returns:
273+
(:class:`~google.protobuf.timestamp_pb2.Timestamp`): Timestamp message
274+
"""
275+
inst = self if self.tzinfo is not None else self.replace(tzinfo=pytz.UTC)
276+
delta = inst - _UTC_EPOCH
277+
seconds = int(delta.total_seconds())
278+
nanos = self._nanosecond or self.microsecond * 1000
279+
return timestamp_pb2.Timestamp(seconds=seconds, nanos=nanos)
280+
281+
@classmethod
282+
def from_timestamp_pb(cls, stamp):
283+
"""Parse RFC 3339-compliant timestamp, preserving nanoseconds.
284+
285+
Args:
286+
stamp (:class:`~google.protobuf.timestamp_pb2.Timestamp`): timestamp message
287+
288+
Returns:
289+
:class:`DatetimeWithNanoseconds`:
290+
an instance matching the timestamp message
291+
"""
292+
microseconds = int(stamp.seconds * 1e6)
293+
bare = from_microseconds(microseconds)
294+
return cls(
295+
bare.year,
296+
bare.month,
297+
bare.day,
298+
bare.hour,
299+
bare.minute,
300+
bare.second,
301+
nanosecond=stamp.nanos,
302+
tzinfo=pytz.UTC,
303+
)

tests/unit/test_datetime_helpers.py

Lines changed: 155 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import calendar
1516
import datetime
1617

1718
import pytest
1819
import pytz
1920

2021
from google.api_core import datetime_helpers
22+
from google.protobuf import timestamp_pb2
2123

2224

2325
ONE_MINUTE_IN_MICROSECONDS = 60 * 1e6
@@ -154,93 +156,159 @@ def test_to_rfc3339_with_non_utc_ignore_zone():
154156
assert datetime_helpers.to_rfc3339(value, ignore_zone=True) == expected
155157

156158

157-
def test_datetimewithnanos_ctor_wo_nanos():
158-
stamp = datetime_helpers.DatetimeWithNanoseconds(2016, 12, 20, 21, 13, 47, 123456)
159-
assert stamp.year == 2016
160-
assert stamp.month == 12
161-
assert stamp.day == 20
162-
assert stamp.hour == 21
163-
assert stamp.minute == 13
164-
assert stamp.second == 47
165-
assert stamp.microsecond == 123456
166-
assert stamp.nanosecond == 0
167-
168-
169-
def test_datetimewithnanos_ctor_w_nanos():
170-
stamp = datetime_helpers.DatetimeWithNanoseconds(
171-
2016, 12, 20, 21, 13, 47, nanosecond=123456789
172-
)
173-
assert stamp.year == 2016
174-
assert stamp.month == 12
175-
assert stamp.day == 20
176-
assert stamp.hour == 21
177-
assert stamp.minute == 13
178-
assert stamp.second == 47
179-
assert stamp.microsecond == 123456
180-
assert stamp.nanosecond == 123456789
181-
182-
183-
def test_datetimewithnanos_ctor_w_micros_positional_and_nanos():
184-
with pytest.raises(TypeError):
185-
datetime_helpers.DatetimeWithNanoseconds(
186-
2016, 12, 20, 21, 13, 47, 123456, nanosecond=123456789
159+
class Test_DateTimeWithNanos(object):
160+
161+
@staticmethod
162+
def test_ctor_wo_nanos():
163+
stamp = datetime_helpers.DatetimeWithNanoseconds(2016, 12, 20, 21, 13, 47, 123456)
164+
assert stamp.year == 2016
165+
assert stamp.month == 12
166+
assert stamp.day == 20
167+
assert stamp.hour == 21
168+
assert stamp.minute == 13
169+
assert stamp.second == 47
170+
assert stamp.microsecond == 123456
171+
assert stamp.nanosecond == 0
172+
173+
@staticmethod
174+
def test_ctor_w_nanos():
175+
stamp = datetime_helpers.DatetimeWithNanoseconds(
176+
2016, 12, 20, 21, 13, 47, nanosecond=123456789
187177
)
188-
189-
190-
def test_datetimewithnanos_ctor_w_micros_keyword_and_nanos():
191-
with pytest.raises(TypeError):
192-
datetime_helpers.DatetimeWithNanoseconds(
193-
2016, 12, 20, 21, 13, 47, microsecond=123456, nanosecond=123456789
178+
assert stamp.year == 2016
179+
assert stamp.month == 12
180+
assert stamp.day == 20
181+
assert stamp.hour == 21
182+
assert stamp.minute == 13
183+
assert stamp.second == 47
184+
assert stamp.microsecond == 123456
185+
assert stamp.nanosecond == 123456789
186+
187+
@staticmethod
188+
def test_ctor_w_micros_positional_and_nanos():
189+
with pytest.raises(TypeError):
190+
datetime_helpers.DatetimeWithNanoseconds(
191+
2016, 12, 20, 21, 13, 47, 123456, nanosecond=123456789
192+
)
193+
194+
@staticmethod
195+
def test_ctor_w_micros_keyword_and_nanos():
196+
with pytest.raises(TypeError):
197+
datetime_helpers.DatetimeWithNanoseconds(
198+
2016, 12, 20, 21, 13, 47, microsecond=123456, nanosecond=123456789
199+
)
200+
201+
@staticmethod
202+
def test_rfc3339_wo_nanos():
203+
stamp = datetime_helpers.DatetimeWithNanoseconds(2016, 12, 20, 21, 13, 47, 123456)
204+
assert stamp.rfc3339() == "2016-12-20T21:13:47.123456Z"
205+
206+
@staticmethod
207+
def test_rfc3339_w_nanos():
208+
stamp = datetime_helpers.DatetimeWithNanoseconds(
209+
2016, 12, 20, 21, 13, 47, nanosecond=123456789
194210
)
211+
assert stamp.rfc3339() == "2016-12-20T21:13:47.123456789Z"
195212

196-
197-
def test_datetimewithnanos_rfc339_wo_nanos():
198-
stamp = datetime_helpers.DatetimeWithNanoseconds(2016, 12, 20, 21, 13, 47, 123456)
199-
assert stamp.rfc3339() == "2016-12-20T21:13:47.123456Z"
200-
201-
202-
def test_datetimewithnanos_rfc339_w_nanos():
203-
stamp = datetime_helpers.DatetimeWithNanoseconds(
204-
2016, 12, 20, 21, 13, 47, nanosecond=123456789
205-
)
206-
assert stamp.rfc3339() == "2016-12-20T21:13:47.123456789Z"
207-
208-
209-
def test_datetimewithnanos_rfc339_w_nanos_no_trailing_zeroes():
210-
stamp = datetime_helpers.DatetimeWithNanoseconds(
211-
2016, 12, 20, 21, 13, 47, nanosecond=100000000
212-
)
213-
assert stamp.rfc3339() == "2016-12-20T21:13:47.1Z"
214-
215-
216-
def test_datetimewithnanos_from_rfc3339_w_invalid():
217-
stamp = "2016-12-20T21:13:47"
218-
with pytest.raises(ValueError):
219-
datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(stamp)
220-
221-
222-
def test_datetimewithnanos_from_rfc3339_wo_fraction():
223-
timestamp = "2016-12-20T21:13:47Z"
224-
expected = datetime_helpers.DatetimeWithNanoseconds(
225-
2016, 12, 20, 21, 13, 47, tzinfo=pytz.UTC
226-
)
227-
stamp = datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(timestamp)
228-
assert stamp == expected
229-
230-
231-
def test_datetimewithnanos_from_rfc3339_w_partial_precision():
232-
timestamp = "2016-12-20T21:13:47.1Z"
233-
expected = datetime_helpers.DatetimeWithNanoseconds(
234-
2016, 12, 20, 21, 13, 47, microsecond=100000, tzinfo=pytz.UTC
235-
)
236-
stamp = datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(timestamp)
237-
assert stamp == expected
238-
239-
240-
def test_datetimewithnanos_from_rfc3339_w_full_precision():
241-
timestamp = "2016-12-20T21:13:47.123456789Z"
242-
expected = datetime_helpers.DatetimeWithNanoseconds(
243-
2016, 12, 20, 21, 13, 47, nanosecond=123456789, tzinfo=pytz.UTC
244-
)
245-
stamp = datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(timestamp)
246-
assert stamp == expected
213+
@staticmethod
214+
def test_rfc3339_w_nanos_no_trailing_zeroes():
215+
stamp = datetime_helpers.DatetimeWithNanoseconds(
216+
2016, 12, 20, 21, 13, 47, nanosecond=100000000
217+
)
218+
assert stamp.rfc3339() == "2016-12-20T21:13:47.1Z"
219+
220+
@staticmethod
221+
def test_from_rfc3339_w_invalid():
222+
stamp = "2016-12-20T21:13:47"
223+
with pytest.raises(ValueError):
224+
datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(stamp)
225+
226+
@staticmethod
227+
def test_from_rfc3339_wo_fraction():
228+
timestamp = "2016-12-20T21:13:47Z"
229+
expected = datetime_helpers.DatetimeWithNanoseconds(
230+
2016, 12, 20, 21, 13, 47, tzinfo=pytz.UTC
231+
)
232+
stamp = datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(timestamp)
233+
assert stamp == expected
234+
235+
@staticmethod
236+
def test_from_rfc3339_w_partial_precision():
237+
timestamp = "2016-12-20T21:13:47.1Z"
238+
expected = datetime_helpers.DatetimeWithNanoseconds(
239+
2016, 12, 20, 21, 13, 47, microsecond=100000, tzinfo=pytz.UTC
240+
)
241+
stamp = datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(timestamp)
242+
assert stamp == expected
243+
244+
@staticmethod
245+
def test_from_rfc3339_w_full_precision():
246+
timestamp = "2016-12-20T21:13:47.123456789Z"
247+
expected = datetime_helpers.DatetimeWithNanoseconds(
248+
2016, 12, 20, 21, 13, 47, nanosecond=123456789, tzinfo=pytz.UTC
249+
)
250+
stamp = datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(timestamp)
251+
assert stamp == expected
252+
253+
@staticmethod
254+
def test_timestamp_pb_wo_nanos_naive():
255+
stamp = datetime_helpers.DatetimeWithNanoseconds(
256+
2016, 12, 20, 21, 13, 47, 123456)
257+
delta = stamp.replace(tzinfo=pytz.UTC) - datetime_helpers._UTC_EPOCH
258+
seconds = int(delta.total_seconds())
259+
nanos = 123456000
260+
timestamp = timestamp_pb2.Timestamp(seconds=seconds, nanos=nanos)
261+
assert stamp.timestamp_pb() == timestamp
262+
263+
@staticmethod
264+
def test_timestamp_pb_w_nanos():
265+
stamp = datetime_helpers.DatetimeWithNanoseconds(
266+
2016, 12, 20, 21, 13, 47, nanosecond=123456789, tzinfo=pytz.UTC
267+
)
268+
delta = stamp - datetime_helpers._UTC_EPOCH
269+
timestamp = timestamp_pb2.Timestamp(
270+
seconds=int(delta.total_seconds()), nanos=123456789)
271+
assert stamp.timestamp_pb() == timestamp
272+
273+
@staticmethod
274+
def test_from_timestamp_pb_wo_nanos():
275+
when = datetime.datetime(2016, 12, 20, 21, 13, 47, 123456, tzinfo=pytz.UTC)
276+
delta = when - datetime_helpers._UTC_EPOCH
277+
seconds = int(delta.total_seconds())
278+
timestamp = timestamp_pb2.Timestamp(seconds=seconds)
279+
280+
stamp = datetime_helpers.DatetimeWithNanoseconds.from_timestamp_pb(
281+
timestamp)
282+
283+
assert _to_seconds(when) == _to_seconds(stamp)
284+
assert stamp.microsecond == 0
285+
assert stamp.nanosecond == 0
286+
assert stamp.tzinfo == pytz.UTC
287+
288+
@staticmethod
289+
def test_from_timestamp_pb_w_nanos():
290+
when = datetime.datetime(2016, 12, 20, 21, 13, 47, 123456, tzinfo=pytz.UTC)
291+
delta = when - datetime_helpers._UTC_EPOCH
292+
seconds = int(delta.total_seconds())
293+
timestamp = timestamp_pb2.Timestamp(seconds=seconds, nanos=123456789)
294+
295+
stamp = datetime_helpers.DatetimeWithNanoseconds.from_timestamp_pb(
296+
timestamp)
297+
298+
assert _to_seconds(when) == _to_seconds(stamp)
299+
assert stamp.microsecond == 123456
300+
assert stamp.nanosecond == 123456789
301+
assert stamp.tzinfo == pytz.UTC
302+
303+
304+
def _to_seconds(value):
305+
"""Convert a datetime to seconds since the unix epoch.
306+
307+
Args:
308+
value (datetime.datetime): The datetime to covert.
309+
310+
Returns:
311+
int: Microseconds since the unix epoch.
312+
"""
313+
assert value.tzinfo is pytz.UTC
314+
return calendar.timegm(value.timetuple())

0 commit comments

Comments
 (0)