Move datetime helpers from google.cloud._helpers to google.api_core.datetime_helpers (#4399)

* Move datetime helpers from google.cloud._helpers to google.api_core.datetime_helpers

* Add pragma statements

* Move them around

* Fix test coverage
diff --git a/google/api_core/datetime_helpers.py b/google/api_core/datetime_helpers.py
index b4a4b51..e3f720a 100644
--- a/google/api_core/datetime_helpers.py
+++ b/google/api_core/datetime_helpers.py
@@ -14,9 +14,168 @@
 
 """Helpers for :mod:`datetime`."""
 
+import calendar
 import datetime
+import re
+
+import pytz
+
+
+_UTC_EPOCH = datetime.datetime.utcfromtimestamp(0).replace(tzinfo=pytz.utc)
+_RFC3339_MICROS = '%Y-%m-%dT%H:%M:%S.%fZ'
+_RFC3339_NO_FRACTION = '%Y-%m-%dT%H:%M:%S'
+# datetime.strptime cannot handle nanosecond precision:  parse w/ regex
+_RFC3339_NANOS = re.compile(r"""
+    (?P<no_fraction>
+        \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}  # YYYY-MM-DDTHH:MM:SS
+    )
+    (                                        # Optional decimal part
+     \.                                      # decimal point
+     (?P<nanos>\d{1,9})                      # nanoseconds, maybe truncated
+    )?
+    Z                                        # Zulu
+""", re.VERBOSE)
 
 
 def utcnow():
     """A :meth:`datetime.datetime.utcnow()` alias to allow mocking in tests."""
     return datetime.datetime.utcnow()
+
+
+def to_milliseconds(value):
+    """Convert a zone-aware datetime to milliseconds since the unix epoch.
+
+    Args:
+        value (datetime.datetime): The datetime to covert.
+
+    Returns:
+        int: Milliseconds since the unix epoch.
+    """
+    micros = to_microseconds(value)
+    return micros // 1000
+
+
+def from_microseconds(value):
+    """Convert timestamp in microseconds since the unix epoch to datetime.
+
+    Args:
+        value (float): The timestamp to convert, in microseconds.
+
+    Returns:
+        datetime.datetime: The datetime object equivalent to the timestamp in
+            UTC.
+    """
+    return _UTC_EPOCH + datetime.timedelta(microseconds=value)
+
+
+def to_microseconds(value):
+    """Convert a datetime to microseconds since the unix epoch.
+
+    Args:
+        value (datetime.datetime): The datetime to covert.
+
+    Returns:
+        int: Microseconds since the unix epoch.
+    """
+    if not value.tzinfo:
+        value = value.replace(tzinfo=pytz.utc)
+    # Regardless of what timezone is on the value, convert it to UTC.
+    value = value.astimezone(pytz.utc)
+    # Convert the datetime to a microsecond timestamp.
+    return int(calendar.timegm(value.timetuple()) * 1e6) + value.microsecond
+
+
+def from_iso8601_date(value):
+    """Convert a ISO8601 date string to a date.
+
+    Args:
+        value (str): The ISO8601 date string.
+
+    Returns:
+        datetime.date: A date equivalent to the date string.
+    """
+    return datetime.datetime.strptime(value, '%Y-%m-%d').date()
+
+
+def from_iso8601_time(value):
+    """Convert a zoneless ISO8601 time string to a time.
+
+    Args:
+        value (str): The ISO8601 time string.
+
+    Returns:
+        datetime.time: A time equivalent to the time string.
+    """
+    return datetime.datetime.strptime(value, '%H:%M:%S').time()
+
+
+def from_rfc3339(value):
+    """Convert a microsecond-precision timestamp to datetime.
+
+    Args:
+        value (str): The RFC3339 string to convert.
+
+    Returns:
+        datetime.datetime: The datetime object equivalent to the timestamp in
+            UTC.
+    """
+    return datetime.datetime.strptime(
+        value, _RFC3339_MICROS).replace(tzinfo=pytz.utc)
+
+
+def from_rfc3339_nanos(value):
+    """Convert a nanosecond-precision timestamp to a native datetime.
+
+    .. note::
+        Python datetimes do not support nanosecond precision; this function
+        therefore truncates such values to microseconds.
+
+    Args:
+        value (str): The RFC3339 string to convert.
+
+    Returns:
+        datetime.datetime: The datetime object equivalent to the timestamp in
+            UTC.
+
+    Raises:
+        ValueError: If the timestamp does not match the RFC 3339
+            regular expression.
+    """
+    with_nanos = _RFC3339_NANOS.match(value)
+
+    if with_nanos is None:
+        raise ValueError(
+            'Timestamp: {!r}, does not match pattern: {!r}'.format(
+                value, _RFC3339_NANOS.pattern))
+
+    bare_seconds = datetime.datetime.strptime(
+        with_nanos.group('no_fraction'), _RFC3339_NO_FRACTION)
+    fraction = with_nanos.group('nanos')
+
+    if fraction is None:
+        micros = 0
+    else:
+        scale = 9 - len(fraction)
+        nanos = int(fraction) * (10 ** scale)
+        micros = nanos // 1000
+
+    return bare_seconds.replace(microsecond=micros, tzinfo=pytz.utc)
+
+
+def to_rfc3339(value, ignore_zone=True):
+    """Convert a datetime to an RFC3339 timestamp string.
+
+    Args:
+        value (datetime.datetime):
+            The datetime object to be converted to a string.
+        ignore_zone (bool): If True, then the timezone (if any) of the
+            datetime object is ignored and the datetime is treated as UTC.
+
+    Returns:
+        str: The RFC3339 formated string representing the datetime.
+    """
+    if not ignore_zone and value.tzinfo is not None:
+        # Convert to UTC and remove the time zone info.
+        value = value.replace(tzinfo=None) - value.utcoffset()
+
+    return value.strftime(_RFC3339_MICROS)
diff --git a/setup.py b/setup.py
index 386eb93..d44abf4 100644
--- a/setup.py
+++ b/setup.py
@@ -56,6 +56,9 @@
     'requests >= 2.18.0, < 3.0.0dev',
     'setuptools >= 34.0.0',
     'six >= 1.10.0',
+    # pytz does not adhere to semver and uses a year.month based scheme.
+    # Any valid version of pytz should work for us.
+    'pytz',
 ]
 
 EXTRAS_REQUIREMENTS = {
diff --git a/tests/unit/test_datetime_helpers.py b/tests/unit/test_datetime_helpers.py
index 7e421bf..b25b1d1 100644
--- a/tests/unit/test_datetime_helpers.py
+++ b/tests/unit/test_datetime_helpers.py
@@ -14,9 +14,137 @@
 
 import datetime
 
+import pytest
+import pytz
+
 from google.api_core import datetime_helpers
 
+ONE_MINUTE_IN_MICROSECONDS = 60 * 1e6
+
 
 def test_utcnow():
     result = datetime_helpers.utcnow()
     assert isinstance(result, datetime.datetime)
+
+
+def test_to_milliseconds():
+    dt = datetime.datetime(1970, 1, 1, 0, 0, 1, tzinfo=pytz.utc)
+    assert datetime_helpers.to_milliseconds(dt) == 1000
+
+
+def test_to_microseconds():
+    microseconds = 314159
+    dt = datetime.datetime(
+        1970, 1, 1, 0, 0, 0, microsecond=microseconds)
+    assert datetime_helpers.to_microseconds(dt) == microseconds
+
+
+def test_to_microseconds_non_utc():
+    zone = pytz.FixedOffset(-1)
+    dt = datetime.datetime(1970, 1, 1, 0, 0, 0, tzinfo=zone)
+    assert datetime_helpers.to_microseconds(dt) == ONE_MINUTE_IN_MICROSECONDS
+
+
+def test_to_microseconds_naive():
+    microseconds = 314159
+    dt = datetime.datetime(
+        1970, 1, 1, 0, 0, 0, microsecond=microseconds, tzinfo=None)
+    assert datetime_helpers.to_microseconds(dt) == microseconds
+
+
+def test_from_microseconds():
+    five_mins_from_epoch_in_microseconds = 5 * ONE_MINUTE_IN_MICROSECONDS
+    five_mins_from_epoch_datetime = datetime.datetime(
+        1970, 1, 1, 0, 5, 0, tzinfo=pytz.utc)
+
+    result = datetime_helpers.from_microseconds(
+        five_mins_from_epoch_in_microseconds)
+
+    assert result == five_mins_from_epoch_datetime
+
+
+def test_from_iso8601_date():
+    today = datetime.date.today()
+    iso_8601_today = today.strftime('%Y-%m-%d')
+
+    assert datetime_helpers.from_iso8601_date(iso_8601_today) == today
+
+
+def test_from_iso8601_time():
+    assert (
+        datetime_helpers.from_iso8601_time('12:09:42') ==
+        datetime.time(12, 9, 42))
+
+
+def test_from_rfc3339():
+    value = '2009-12-17T12:44:32.123456Z'
+    assert datetime_helpers.from_rfc3339(value) == datetime.datetime(
+        2009, 12, 17, 12, 44, 32, 123456, pytz.utc)
+
+
+def test_from_rfc3339_with_bad_tz():
+    value = '2009-12-17T12:44:32.123456BAD'
+
+    with pytest.raises(ValueError):
+        datetime_helpers.from_rfc3339(value)
+
+
+def test_from_rfc3339_with_nanos():
+    value = '2009-12-17T12:44:32.123456789Z'
+
+    with pytest.raises(ValueError):
+        datetime_helpers.from_rfc3339(value)
+
+
+def test_from_rfc3339_nanos_without_nanos():
+    value = '2009-12-17T12:44:32Z'
+    assert datetime_helpers.from_rfc3339_nanos(value) == datetime.datetime(
+        2009, 12, 17, 12, 44, 32, 0, pytz.utc)
+
+
+def test_from_rfc3339_nanos_with_bad_tz():
+    value = '2009-12-17T12:44:32.123456789BAD'
+
+    with pytest.raises(ValueError):
+        datetime_helpers.from_rfc3339_nanos(value)
+
+
+@pytest.mark.parametrize('truncated, micros', [
+    ('12345678', 123456),
+    ('1234567', 123456),
+    ('123456', 123456),
+    ('12345', 123450),
+    ('1234', 123400),
+    ('123', 123000),
+    ('12', 120000),
+    ('1', 100000)])
+def test_from_rfc3339_nanos_with_truncated_nanos(truncated, micros):
+    value = '2009-12-17T12:44:32.{}Z'.format(truncated)
+    assert datetime_helpers.from_rfc3339_nanos(value) == datetime.datetime(
+        2009, 12, 17, 12, 44, 32, micros, pytz.utc)
+
+
+def test_to_rfc3339():
+    value = datetime.datetime(2016, 4, 5, 13, 30, 0)
+    expected = '2016-04-05T13:30:00.000000Z'
+    assert datetime_helpers.to_rfc3339(value) == expected
+
+
+def test_to_rfc3339_with_utc():
+    value = datetime.datetime(2016, 4, 5, 13, 30, 0, tzinfo=pytz.utc)
+    expected = '2016-04-05T13:30:00.000000Z'
+    assert datetime_helpers.to_rfc3339(value, ignore_zone=False) == expected
+
+
+def test_to_rfc3339_with_non_utc():
+    zone = pytz.FixedOffset(-60)
+    value = datetime.datetime(2016, 4, 5, 13, 30, 0, tzinfo=zone)
+    expected = '2016-04-05T14:30:00.000000Z'
+    assert datetime_helpers.to_rfc3339(value, ignore_zone=False) == expected
+
+
+def test_to_rfc3339_with_non_utc_ignore_zone():
+    zone = pytz.FixedOffset(-60)
+    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