From 63fc6be427d394ab2a10c29b6d04b260fe7fd0a2 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Wed, 28 Feb 2018 21:56:18 -0800 Subject: [PATCH 1/3] add UTC and TimestampwithNanoSeconds to api_core --- api_core/google/api_core/datetime_helpers.py | 68 +++++++++++++ api_core/tests/unit/test_datetime_helpers.py | 102 +++++++++++++++++++ 2 files changed, 170 insertions(+) diff --git a/api_core/google/api_core/datetime_helpers.py b/api_core/google/api_core/datetime_helpers.py index e3f720a140bb..ccd5d6ae1aa6 100644 --- a/api_core/google/api_core/datetime_helpers.py +++ b/api_core/google/api_core/datetime_helpers.py @@ -179,3 +179,71 @@ def to_rfc3339(value, ignore_zone=True): value = value.replace(tzinfo=None) - value.utcoffset() return value.strftime(_RFC3339_MICROS) + + +class TimestampWithNanoseconds(datetime.datetime): + """Track nanosecond in addition to normal datetime attrs. + + Nanosecond can be passed only as a keyword argument. + """ + __slots__ = ('_nanosecond',) + + # pylint: disable=arguments-differ + def __new__(cls, *args, **kw): + nanos = kw.pop('nanosecond', 0) + if nanos > 0: + if 'microsecond' in kw: + raise TypeError( + "Specify only one of 'microsecond' or 'nanosecond'") + kw['microsecond'] = nanos // 1000 + inst = datetime.datetime.__new__(cls, *args, **kw) + inst._nanosecond = nanos or 0 + return inst + # pylint: disable=arguments-differ + + @property + def nanosecond(self): + """Read-only: nanosecond precision.""" + return self._nanosecond + + def rfc3339(self): + """Return an RFC 3339-compliant timestamp. + + Returns: + (str): Timestamp string according to RFC 3339 spec. + """ + if self._nanosecond == 0: + return to_rfc3339(self) + nanos = str(self._nanosecond).rstrip('0') + return '{}.{}Z'.format(self.strftime(_RFC3339_NO_FRACTION), nanos) + + @classmethod + def from_rfc3339(cls, stamp): + """Parse RFC 3339-compliant timestamp, preserving nanoseconds. + + Args: + stamp (str): RFC 3339 stamp, with up to nanosecond precision + + Returns: + :class:`TimestampWithNanoseconds`: + an instance matching the timestamp string + + Raises: + ValueError: if `stamp` does not match the expected format + """ + with_nanos = _RFC3339_NANOS.match(stamp) + if with_nanos is None: + raise ValueError( + 'Timestamp: {}, does not match pattern: {}'.format( + stamp, _RFC3339_NANOS.pattern)) + bare = datetime.datetime.strptime( + with_nanos.group('no_fraction'), _RFC3339_NO_FRACTION) + fraction = with_nanos.group('nanos') + if fraction is None: + nanos = 0 + else: + scale = 9 - len(fraction) + nanos = int(fraction) * (10 ** scale) + return cls(bare.year, bare.month, bare.day, + bare.hour, bare.minute, bare.second, + nanosecond=nanos, tzinfo=pytz.UTC) diff --git a/api_core/tests/unit/test_datetime_helpers.py b/api_core/tests/unit/test_datetime_helpers.py index b25b1d15a502..29e0f3afec28 100644 --- a/api_core/tests/unit/test_datetime_helpers.py +++ b/api_core/tests/unit/test_datetime_helpers.py @@ -148,3 +148,105 @@ def test_to_rfc3339_with_non_utc_ignore_zone(): value = datetime.datetime(2016, 4, 5, 13, 30, 0, tzinfo=zone) expected = '2016-04-05T13:30:00.000000Z' assert datetime_helpers.to_rfc3339(value, ignore_zone=True) == expected + + +def test_timestampwithnanos_ctor_wo_nanos(): + stamp = datetime_helpers.TimestampWithNanoseconds( + 2016, 12, 20, 21, 13, 47, 123456) + assert stamp.year == 2016 + assert stamp.month == 12 + assert stamp.day == 20 + assert stamp.hour == 21 + assert stamp.minute == 13 + assert stamp.second == 47 + assert stamp.microsecond == 123456 + assert stamp.nanosecond == 0 + + +def test_timestampwithnanos_ctor_w_nanos(): + stamp = datetime_helpers.TimestampWithNanoseconds( + 2016, 12, 20, 21, 13, 47, nanosecond=123456789) + assert stamp.year == 2016 + assert stamp.month == 12 + assert stamp.day == 20 + assert stamp.hour == 21 + assert stamp.minute == 13 + assert stamp.second == 47 + assert stamp.microsecond == 123456 + assert stamp.nanosecond == 123456789 + + +def test_timestampwithnanos_ctor_w_micros_positional_and_nanos(): + with pytest.raises(TypeError): + datetime_helpers.TimestampWithNanoseconds( + 2016, 12, 20, 21, 13, 47, 123456, nanosecond=123456789) + + +def test_timestampwithnanos_ctor_w_micros_keyword_and_nanos(): + with pytest.raises(TypeError): + datetime_helpers.TimestampWithNanoseconds( + 2016, 12, 20, 21, 13, 47, + microsecond=123456, nanosecond=123456789) + + +def test_timestampwithnanos_rfc339_wo_nanos(): + stamp = datetime_helpers.TimestampWithNanoseconds( + 2016, 12, 20, 21, 13, 47, 123456) + assert stamp.rfc3339() == '2016-12-20T21:13:47.123456Z' + + +def test_timestampwithnanos_rfc339_w_nanos(): + stamp = datetime_helpers.TimestampWithNanoseconds( + 2016, 12, 20, 21, 13, 47, nanosecond=123456789) + assert stamp.rfc3339() == '2016-12-20T21:13:47.123456789Z' + + +def test_timestampwithnanos_rfc339_w_nanos_no_trailing_zeroes(): + stamp = datetime_helpers.TimestampWithNanoseconds( + 2016, 12, 20, 21, 13, 47, nanosecond=100000000) + assert stamp.rfc3339() == '2016-12-20T21:13:47.1Z' + + +def test_timestampwithnanos_from_rfc3339_w_invalid(): + klass = datetime_helpers.TimestampWithNanoseconds + STAMP = '2016-12-20T21:13:47' + with pytest.raises(ValueError): + klass.from_rfc3339(STAMP) + + +def test_timestampwithnanos_from_rfc3339_wo_fraction(): + from google.api_core import datetime_helpers + + klass = datetime_helpers.TimestampWithNanoseconds + timestamp = '2016-12-20T21:13:47Z' + expected = datetime_helpers.TimestampWithNanoseconds( + 2016, 12, 20, 21, 13, 47, + tzinfo=pytz.UTC) + stamp = klass.from_rfc3339(timestamp) + assert (stamp == expected) + + +def test_timestampwithnanos_from_rfc3339_w_partial_precision(): + from google.api_core import datetime_helpers + + klass = datetime_helpers.TimestampWithNanoseconds + timestamp = '2016-12-20T21:13:47.1Z' + expected = datetime_helpers.TimestampWithNanoseconds( + 2016, 12, 20, 21, 13, 47, + microsecond=100000, + tzinfo=pytz.UTC) + stamp = klass.from_rfc3339(timestamp) + assert stamp == expected + + +def test_timestampwithnanos_from_rfc3339_w_full_precision(): + from google.api_core import datetime_helpers + + klass = datetime_helpers.TimestampWithNanoseconds + timestamp = '2016-12-20T21:13:47.123456789Z' + expected = datetime_helpers.TimestampWithNanoseconds( + 2016, 12, 20, 21, 13, 47, + nanosecond=123456789, + tzinfo=pytz.UTC) + stamp = klass.from_rfc3339(timestamp) + assert stamp == expected From 770aecd8d8f6fc0364b55a6d8e7b2dca874c23cd Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Fri, 9 Mar 2018 13:02:33 -0800 Subject: [PATCH 2/3] Review Changes --- api_core/tests/unit/test_datetime_helpers.py | 21 ++++++-------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/api_core/tests/unit/test_datetime_helpers.py b/api_core/tests/unit/test_datetime_helpers.py index 29e0f3afec28..664d1f0c2fcd 100644 --- a/api_core/tests/unit/test_datetime_helpers.py +++ b/api_core/tests/unit/test_datetime_helpers.py @@ -19,6 +19,7 @@ from google.api_core import datetime_helpers + ONE_MINUTE_IN_MICROSECONDS = 60 * 1e6 @@ -208,45 +209,35 @@ def test_timestampwithnanos_rfc339_w_nanos_no_trailing_zeroes(): def test_timestampwithnanos_from_rfc3339_w_invalid(): - klass = datetime_helpers.TimestampWithNanoseconds - STAMP = '2016-12-20T21:13:47' + stamp = '2016-12-20T21:13:47' with pytest.raises(ValueError): - klass.from_rfc3339(STAMP) + datetime_helpers.TimestampWithNanoseconds.from_rfc3339(stamp) def test_timestampwithnanos_from_rfc3339_wo_fraction(): - from google.api_core import datetime_helpers - - klass = datetime_helpers.TimestampWithNanoseconds timestamp = '2016-12-20T21:13:47Z' expected = datetime_helpers.TimestampWithNanoseconds( 2016, 12, 20, 21, 13, 47, tzinfo=pytz.UTC) - stamp = klass.from_rfc3339(timestamp) + stamp = datetime_helpers.TimestampWithNanoseconds.from_rfc3339(timestamp) assert (stamp == expected) def test_timestampwithnanos_from_rfc3339_w_partial_precision(): - from google.api_core import datetime_helpers - - klass = datetime_helpers.TimestampWithNanoseconds timestamp = '2016-12-20T21:13:47.1Z' expected = datetime_helpers.TimestampWithNanoseconds( 2016, 12, 20, 21, 13, 47, microsecond=100000, tzinfo=pytz.UTC) - stamp = klass.from_rfc3339(timestamp) + stamp = datetime_helpers.TimestampWithNanoseconds.from_rfc3339(timestamp) assert stamp == expected def test_timestampwithnanos_from_rfc3339_w_full_precision(): - from google.api_core import datetime_helpers - - klass = datetime_helpers.TimestampWithNanoseconds timestamp = '2016-12-20T21:13:47.123456789Z' expected = datetime_helpers.TimestampWithNanoseconds( 2016, 12, 20, 21, 13, 47, nanosecond=123456789, tzinfo=pytz.UTC) - stamp = klass.from_rfc3339(timestamp) + stamp = datetime_helpers.TimestampWithNanoseconds.from_rfc3339(timestamp) assert stamp == expected From d7435aff2a4b40adf080816e357ec2ac1b08ba50 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Fri, 9 Mar 2018 14:16:11 -0800 Subject: [PATCH 3/3] Api Core: TimestampWithNanos to DatetimeWithNanos --- api_core/google/api_core/datetime_helpers.py | 4 +- api_core/tests/unit/test_datetime_helpers.py | 50 ++++++++++---------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/api_core/google/api_core/datetime_helpers.py b/api_core/google/api_core/datetime_helpers.py index ccd5d6ae1aa6..393d2d6c612a 100644 --- a/api_core/google/api_core/datetime_helpers.py +++ b/api_core/google/api_core/datetime_helpers.py @@ -181,7 +181,7 @@ def to_rfc3339(value, ignore_zone=True): return value.strftime(_RFC3339_MICROS) -class TimestampWithNanoseconds(datetime.datetime): +class DatetimeWithNanoseconds(datetime.datetime): """Track nanosecond in addition to normal datetime attrs. Nanosecond can be passed only as a keyword argument. @@ -225,7 +225,7 @@ def from_rfc3339(cls, stamp): stamp (str): RFC 3339 stamp, with up to nanosecond precision Returns: - :class:`TimestampWithNanoseconds`: + :class:`DatetimeWithNanoseconds`: an instance matching the timestamp string Raises: diff --git a/api_core/tests/unit/test_datetime_helpers.py b/api_core/tests/unit/test_datetime_helpers.py index 664d1f0c2fcd..03b9477a7d2f 100644 --- a/api_core/tests/unit/test_datetime_helpers.py +++ b/api_core/tests/unit/test_datetime_helpers.py @@ -151,8 +151,8 @@ def test_to_rfc3339_with_non_utc_ignore_zone(): assert datetime_helpers.to_rfc3339(value, ignore_zone=True) == expected -def test_timestampwithnanos_ctor_wo_nanos(): - stamp = datetime_helpers.TimestampWithNanoseconds( +def test_datetimewithnanos_ctor_wo_nanos(): + stamp = datetime_helpers.DatetimeWithNanoseconds( 2016, 12, 20, 21, 13, 47, 123456) assert stamp.year == 2016 assert stamp.month == 12 @@ -164,8 +164,8 @@ def test_timestampwithnanos_ctor_wo_nanos(): assert stamp.nanosecond == 0 -def test_timestampwithnanos_ctor_w_nanos(): - stamp = datetime_helpers.TimestampWithNanoseconds( +def test_datetimewithnanos_ctor_w_nanos(): + stamp = datetime_helpers.DatetimeWithNanoseconds( 2016, 12, 20, 21, 13, 47, nanosecond=123456789) assert stamp.year == 2016 assert stamp.month == 12 @@ -177,67 +177,67 @@ def test_timestampwithnanos_ctor_w_nanos(): assert stamp.nanosecond == 123456789 -def test_timestampwithnanos_ctor_w_micros_positional_and_nanos(): +def test_datetimewithnanos_ctor_w_micros_positional_and_nanos(): with pytest.raises(TypeError): - datetime_helpers.TimestampWithNanoseconds( + datetime_helpers.DatetimeWithNanoseconds( 2016, 12, 20, 21, 13, 47, 123456, nanosecond=123456789) -def test_timestampwithnanos_ctor_w_micros_keyword_and_nanos(): +def test_datetimewithnanos_ctor_w_micros_keyword_and_nanos(): with pytest.raises(TypeError): - datetime_helpers.TimestampWithNanoseconds( + datetime_helpers.DatetimeWithNanoseconds( 2016, 12, 20, 21, 13, 47, microsecond=123456, nanosecond=123456789) -def test_timestampwithnanos_rfc339_wo_nanos(): - stamp = datetime_helpers.TimestampWithNanoseconds( +def test_datetimewithnanos_rfc339_wo_nanos(): + stamp = datetime_helpers.DatetimeWithNanoseconds( 2016, 12, 20, 21, 13, 47, 123456) assert stamp.rfc3339() == '2016-12-20T21:13:47.123456Z' -def test_timestampwithnanos_rfc339_w_nanos(): - stamp = datetime_helpers.TimestampWithNanoseconds( +def test_datetimewithnanos_rfc339_w_nanos(): + stamp = datetime_helpers.DatetimeWithNanoseconds( 2016, 12, 20, 21, 13, 47, nanosecond=123456789) assert stamp.rfc3339() == '2016-12-20T21:13:47.123456789Z' -def test_timestampwithnanos_rfc339_w_nanos_no_trailing_zeroes(): - stamp = datetime_helpers.TimestampWithNanoseconds( +def test_datetimewithnanos_rfc339_w_nanos_no_trailing_zeroes(): + stamp = datetime_helpers.DatetimeWithNanoseconds( 2016, 12, 20, 21, 13, 47, nanosecond=100000000) assert stamp.rfc3339() == '2016-12-20T21:13:47.1Z' -def test_timestampwithnanos_from_rfc3339_w_invalid(): +def test_datetimewithnanos_from_rfc3339_w_invalid(): stamp = '2016-12-20T21:13:47' with pytest.raises(ValueError): - datetime_helpers.TimestampWithNanoseconds.from_rfc3339(stamp) + datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(stamp) -def test_timestampwithnanos_from_rfc3339_wo_fraction(): +def test_datetimewithnanos_from_rfc3339_wo_fraction(): timestamp = '2016-12-20T21:13:47Z' - expected = datetime_helpers.TimestampWithNanoseconds( + expected = datetime_helpers.DatetimeWithNanoseconds( 2016, 12, 20, 21, 13, 47, tzinfo=pytz.UTC) - stamp = datetime_helpers.TimestampWithNanoseconds.from_rfc3339(timestamp) + stamp = datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(timestamp) assert (stamp == expected) -def test_timestampwithnanos_from_rfc3339_w_partial_precision(): +def test_datetimewithnanos_from_rfc3339_w_partial_precision(): timestamp = '2016-12-20T21:13:47.1Z' - expected = datetime_helpers.TimestampWithNanoseconds( + expected = datetime_helpers.DatetimeWithNanoseconds( 2016, 12, 20, 21, 13, 47, microsecond=100000, tzinfo=pytz.UTC) - stamp = datetime_helpers.TimestampWithNanoseconds.from_rfc3339(timestamp) + stamp = datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(timestamp) assert stamp == expected -def test_timestampwithnanos_from_rfc3339_w_full_precision(): +def test_datetimewithnanos_from_rfc3339_w_full_precision(): timestamp = '2016-12-20T21:13:47.123456789Z' - expected = datetime_helpers.TimestampWithNanoseconds( + expected = datetime_helpers.DatetimeWithNanoseconds( 2016, 12, 20, 21, 13, 47, nanosecond=123456789, tzinfo=pytz.UTC) - stamp = datetime_helpers.TimestampWithNanoseconds.from_rfc3339(timestamp) + stamp = datetime_helpers.DatetimeWithNanoseconds.from_rfc3339(timestamp) assert stamp == expected