Skip to content

Commit 49ddc9d

Browse files
authored
Move DatetimeWithNanoSeconds to api_core (#4979)
* Move DatetimewithNanoSeconds to api_core
1 parent e197685 commit 49ddc9d

File tree

2 files changed

+161
-0
lines changed

2 files changed

+161
-0
lines changed

google/api_core/datetime_helpers.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,71 @@ def to_rfc3339(value, ignore_zone=True):
179179
value = value.replace(tzinfo=None) - value.utcoffset()
180180

181181
return value.strftime(_RFC3339_MICROS)
182+
183+
184+
class DatetimeWithNanoseconds(datetime.datetime):
185+
"""Track nanosecond in addition to normal datetime attrs.
186+
187+
Nanosecond can be passed only as a keyword argument.
188+
"""
189+
__slots__ = ('_nanosecond',)
190+
191+
# pylint: disable=arguments-differ
192+
def __new__(cls, *args, **kw):
193+
nanos = kw.pop('nanosecond', 0)
194+
if nanos > 0:
195+
if 'microsecond' in kw:
196+
raise TypeError(
197+
"Specify only one of 'microsecond' or 'nanosecond'")
198+
kw['microsecond'] = nanos // 1000
199+
inst = datetime.datetime.__new__(cls, *args, **kw)
200+
inst._nanosecond = nanos or 0
201+
return inst
202+
# pylint: disable=arguments-differ
203+
204+
@property
205+
def nanosecond(self):
206+
"""Read-only: nanosecond precision."""
207+
return self._nanosecond
208+
209+
def rfc3339(self):
210+
"""Return an RFC 3339-compliant timestamp.
211+
212+
Returns:
213+
(str): Timestamp string according to RFC 3339 spec.
214+
"""
215+
if self._nanosecond == 0:
216+
return to_rfc3339(self)
217+
nanos = str(self._nanosecond).rstrip('0')
218+
return '{}.{}Z'.format(self.strftime(_RFC3339_NO_FRACTION), nanos)
219+
220+
@classmethod
221+
def from_rfc3339(cls, stamp):
222+
"""Parse RFC 3339-compliant timestamp, preserving nanoseconds.
223+
224+
Args:
225+
stamp (str): RFC 3339 stamp, with up to nanosecond precision
226+
227+
Returns:
228+
:class:`DatetimeWithNanoseconds`:
229+
an instance matching the timestamp string
230+
231+
Raises:
232+
ValueError: if `stamp` does not match the expected format
233+
"""
234+
with_nanos = _RFC3339_NANOS.match(stamp)
235+
if with_nanos is None:
236+
raise ValueError(
237+
'Timestamp: {}, does not match pattern: {}'.format(
238+
stamp, _RFC3339_NANOS.pattern))
239+
bare = datetime.datetime.strptime(
240+
with_nanos.group('no_fraction'), _RFC3339_NO_FRACTION)
241+
fraction = with_nanos.group('nanos')
242+
if fraction is None:
243+
nanos = 0
244+
else:
245+
scale = 9 - len(fraction)
246+
nanos = int(fraction) * (10 ** scale)
247+
return cls(bare.year, bare.month, bare.day,
248+
bare.hour, bare.minute, bare.second,
249+
nanosecond=nanos, tzinfo=pytz.UTC)

tests/unit/test_datetime_helpers.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from google.api_core import datetime_helpers
2121

22+
2223
ONE_MINUTE_IN_MICROSECONDS = 60 * 1e6
2324

2425

@@ -148,3 +149,95 @@ def test_to_rfc3339_with_non_utc_ignore_zone():
148149
value = datetime.datetime(2016, 4, 5, 13, 30, 0, tzinfo=zone)
149150
expected = '2016-04-05T13:30:00.000000Z'
150151
assert datetime_helpers.to_rfc3339(value, ignore_zone=True) == expected
152+
153+
154+
def test_datetimewithnanos_ctor_wo_nanos():
155+
stamp = datetime_helpers.DatetimeWithNanoseconds(
156+
2016, 12, 20, 21, 13, 47, 123456)
157+
assert stamp.year == 2016
158+
assert stamp.month == 12
159+
assert stamp.day == 20
160+
assert stamp.hour == 21
161+
assert stamp.minute == 13
162+
assert stamp.second == 47
163+
assert stamp.microsecond == 123456
164+
assert stamp.nanosecond == 0
165+
166+
167+
def test_datetimewithnanos_ctor_w_nanos():
168+
stamp = datetime_helpers.DatetimeWithNanoseconds(
169+
2016, 12, 20, 21, 13, 47, nanosecond=123456789)
170+
assert stamp.year == 2016
171+
assert stamp.month == 12
172+
assert stamp.day == 20
173+
assert stamp.hour == 21
174+
assert stamp.minute == 13
175+
assert stamp.second == 47
176+
assert stamp.microsecond == 123456
177+
assert stamp.nanosecond == 123456789
178+
179+
180+
def test_datetimewithnanos_ctor_w_micros_positional_and_nanos():
181+
with pytest.raises(TypeError):
182+
datetime_helpers.DatetimeWithNanoseconds(
183+
2016, 12, 20, 21, 13, 47, 123456, nanosecond=123456789)
184+
185+
186+
def test_datetimewithnanos_ctor_w_micros_keyword_and_nanos():
187+
with pytest.raises(TypeError):
188+
datetime_helpers.DatetimeWithNanoseconds(
189+
2016, 12, 20, 21, 13, 47,
190+
microsecond=123456, nanosecond=123456789)
191+
192+
193+
def test_datetimewithnanos_rfc339_wo_nanos():
194+
stamp = datetime_helpers.DatetimeWithNanoseconds(
195+
2016, 12, 20, 21, 13, 47, 123456)
196+
assert stamp.rfc3339() == '2016-12-20T21:13:47.123456Z'
197+
198+
199+
def test_datetimewithnanos_rfc339_w_nanos():
200+
stamp = datetime_helpers.DatetimeWithNanoseconds(
201+
2016, 12, 20, 21, 13, 47, nanosecond=123456789)
202+
assert stamp.rfc3339() == '2016-12-20T21:13:47.123456789Z'
203+
204+
205+
def test_datetimewithnanos_rfc339_w_nanos_no_trailing_zeroes():
206+
stamp = datetime_helpers.DatetimeWithNanoseconds(
207+
2016, 12, 20, 21, 13, 47, nanosecond=100000000)
208+
assert stamp.rfc3339() == '2016-12-20T21:13:47.1Z'
209+
210+
211+
def test_datetimewithnanos_from_rfc3339_w_invalid():
212+
stamp = '2016-12-20T21:13:47'
213+
with pytest.raises(ValueError):
214+
datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(stamp)
215+
216+
217+
def test_datetimewithnanos_from_rfc3339_wo_fraction():
218+
timestamp = '2016-12-20T21:13:47Z'
219+
expected = datetime_helpers.DatetimeWithNanoseconds(
220+
2016, 12, 20, 21, 13, 47,
221+
tzinfo=pytz.UTC)
222+
stamp = datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(timestamp)
223+
assert (stamp == expected)
224+
225+
226+
def test_datetimewithnanos_from_rfc3339_w_partial_precision():
227+
timestamp = '2016-12-20T21:13:47.1Z'
228+
expected = datetime_helpers.DatetimeWithNanoseconds(
229+
2016, 12, 20, 21, 13, 47,
230+
microsecond=100000,
231+
tzinfo=pytz.UTC)
232+
stamp = datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(timestamp)
233+
assert stamp == expected
234+
235+
236+
def test_datetimewithnanos_from_rfc3339_w_full_precision():
237+
timestamp = '2016-12-20T21:13:47.123456789Z'
238+
expected = datetime_helpers.DatetimeWithNanoseconds(
239+
2016, 12, 20, 21, 13, 47,
240+
nanosecond=123456789,
241+
tzinfo=pytz.UTC)
242+
stamp = datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(timestamp)
243+
assert stamp == expected

0 commit comments

Comments
 (0)