Skip to content

Commit

Permalink
add nanoseconds timestamp to api core
Browse files Browse the repository at this point in the history
  • Loading branch information
chemelnucfin committed Mar 1, 2018
1 parent 857da54 commit d386fd0
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 142 deletions.
68 changes: 68 additions & 0 deletions api_core/google/api_core/datetime_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
102 changes: 102 additions & 0 deletions api_core/tests/unit/test_datetime_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
54 changes: 6 additions & 48 deletions core/google/cloud/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import re
from threading import local as Local

import pytz
import six
from six.moves import http_client

Expand Down Expand Up @@ -64,6 +65,7 @@
'gcloud', 'configurations', 'config_default')
_GCLOUD_CONFIG_SECTION = 'core'
_GCLOUD_CONFIG_KEY = 'project'
_EPOCH = datetime.datetime.utcfromtimestamp(0).replace(tzinfo=pytz.UTC)


class _LocalStack(Local):
Expand Down Expand Up @@ -106,41 +108,6 @@ def top(self):
return self._stack[-1]


class _UTC(datetime.tzinfo):
"""Basic UTC implementation.
Implementing a small surface area to avoid depending on ``pytz``.
"""

_dst = datetime.timedelta(0)
_tzname = 'UTC'
_utcoffset = _dst

def dst(self, dt): # pylint: disable=unused-argument
"""Daylight savings time offset."""
return self._dst

def fromutc(self, dt):
"""Convert a timestamp from (naive) UTC to this timezone."""
if dt.tzinfo is None:
return dt.replace(tzinfo=self)
return super(_UTC, self).fromutc(dt)

def tzname(self, dt): # pylint: disable=unused-argument
"""Get the name of this timezone."""
return self._tzname

def utcoffset(self, dt): # pylint: disable=unused-argument
"""UTC offset of this timezone."""
return self._utcoffset

def __repr__(self):
return '<%s>' % (self._tzname,)

def __str__(self):
return self._tzname


def _ensure_tuple_or_list(arg_name, tuple_or_list):
"""Ensures an input is a tuple or list.
Expand Down Expand Up @@ -215,9 +182,9 @@ def _microseconds_from_datetime(value):
:returns: The timestamp, in microseconds.
"""
if not value.tzinfo:
value = value.replace(tzinfo=UTC)
value = value.replace(tzinfo=pytz.UTC)
# Regardless of what timezone is on the value, convert it to UTC.
value = value.astimezone(UTC)
value = value.astimezone(pytz.UTC)
# Convert the datetime to a microsecond timestamp.
return int(calendar.timegm(value.timetuple()) * 1e6) + value.microsecond

Expand Down Expand Up @@ -271,7 +238,7 @@ def _rfc3339_to_datetime(dt_str):
:returns: The datetime object created from the string.
"""
return datetime.datetime.strptime(
dt_str, _RFC3339_MICROS).replace(tzinfo=UTC)
dt_str, _RFC3339_MICROS).replace(tzinfo=pytz.UTC)


def _rfc3339_nanos_to_datetime(dt_str):
Expand Down Expand Up @@ -304,7 +271,7 @@ def _rfc3339_nanos_to_datetime(dt_str):
scale = 9 - len(fraction)
nanos = int(fraction) * (10 ** scale)
micros = nanos // 1000
return bare_seconds.replace(microsecond=micros, tzinfo=UTC)
return bare_seconds.replace(microsecond=micros, tzinfo=pytz.UTC)


def _datetime_to_rfc3339(value, ignore_zone=True):
Expand Down Expand Up @@ -616,12 +583,3 @@ def make_insecure_stub(stub_class, host, port=None):
target = '%s:%d' % (host, port)
channel = grpc.insecure_channel(target)
return stub_class(channel)


try:
from pytz import UTC # pylint: disable=unused-import,wrong-import-order
except ImportError: # pragma: NO COVER
UTC = _UTC() # Singleton instance to be used throughout.

# Need to define _EPOCH at the end of module since it relies on UTC.
_EPOCH = datetime.datetime.utcfromtimestamp(0).replace(tzinfo=UTC)
Loading

0 comments on commit d386fd0

Please sign in to comment.