Skip to content

Commit

Permalink
Merge pull request #1044 from dhermes/remove-pytz-dependency
Browse files Browse the repository at this point in the history
Removing use of pytz module / implementing our own UTC.
  • Loading branch information
dhermes committed Aug 10, 2015
2 parents 3b27808 + b8bce16 commit 99e04fd
Show file tree
Hide file tree
Showing 18 changed files with 193 additions and 78 deletions.
43 changes: 43 additions & 0 deletions gcloud/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
This module is not part of the public API surface of `gcloud`.
"""

import datetime
import os
import socket

Expand Down Expand Up @@ -75,6 +77,41 @@ 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 @@ -168,3 +205,9 @@ def _determine_default_project(project=None):
project = _get_production_project()

return project


try:
from pytz import UTC # pylint: disable=unused-import
except ImportError:
UTC = _UTC() # Singleton instance to be used throughout.
7 changes: 4 additions & 3 deletions gcloud/bigquery/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
import datetime
import sys

import pytz
from gcloud._helpers import UTC

_EPOCH = datetime.datetime.utcfromtimestamp(0).replace(tzinfo=pytz.utc)

_EPOCH = datetime.datetime.utcfromtimestamp(0).replace(tzinfo=UTC)


def _millis(when):
Expand Down Expand Up @@ -62,7 +63,7 @@ def _prop_from_datetime(value):
if value is not None:
if value.tzinfo is None:
# Assume UTC
value = value.replace(tzinfo=pytz.utc)
value = value.replace(tzinfo=UTC)
# back-end wants timestamps as milliseconds since the epoch
return _millis(value)

Expand Down
40 changes: 25 additions & 15 deletions gcloud/bigquery/test__helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ def _callFUT(self, value):

def test_one_second_from_epoch(self):
import datetime
import pytz
WHEN = datetime.datetime(1970, 1, 1, 0, 0, 1, tzinfo=pytz.utc)
from gcloud._helpers import UTC

WHEN = datetime.datetime(1970, 1, 1, 0, 0, 1, tzinfo=UTC)
self.assertEqual(self._callFUT(WHEN), 1000)


Expand All @@ -39,11 +40,12 @@ def test_w_none(self):

def test_w_millis(self):
import datetime
import pytz
from gcloud._helpers import UTC
from gcloud.bigquery._helpers import _total_seconds

NOW = datetime.datetime(2015, 7, 29, 17, 45, 21, 123456,
tzinfo=pytz.utc)
EPOCH = datetime.datetime(1970, 1, 1, tzinfo=pytz.utc)
tzinfo=UTC)
EPOCH = datetime.datetime(1970, 1, 1, tzinfo=UTC)
MILLIS = _total_seconds(NOW - EPOCH) * 1000
self.assertEqual(self._callFUT(MILLIS), NOW)

Expand All @@ -59,34 +61,42 @@ def test_w_none(self):

def test_w_utc_datetime(self):
import datetime
import pytz
from gcloud._helpers import UTC
from gcloud.bigquery._helpers import _total_seconds
NOW = datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
EPOCH = datetime.datetime(1970, 1, 1, tzinfo=pytz.utc)

NOW = datetime.datetime.utcnow().replace(tzinfo=UTC)
EPOCH = datetime.datetime(1970, 1, 1, tzinfo=UTC)
MILLIS = int(_total_seconds(NOW - EPOCH) * 1000)
result = self._callFUT(NOW)
self.assertTrue(isinstance(result, int))
self.assertEqual(result, MILLIS)

def test_w_non_utc_datetime(self):
import datetime
import pytz
from gcloud._helpers import UTC
from gcloud._helpers import _UTC
from gcloud.bigquery._helpers import _total_seconds
eastern = pytz.timezone('US/Eastern')
NOW = datetime.datetime(2015, 7, 28, 16, 34, 47, tzinfo=eastern)
EPOCH = datetime.datetime(1970, 1, 1, tzinfo=pytz.utc)

class CET(_UTC):
_tzname = 'CET'
_utcoffset = datetime.timedelta(hours=-1)

zone = CET()
NOW = datetime.datetime(2015, 7, 28, 16, 34, 47, tzinfo=zone)
EPOCH = datetime.datetime(1970, 1, 1, tzinfo=UTC)
MILLIS = int(_total_seconds(NOW - EPOCH) * 1000)
result = self._callFUT(NOW)
self.assertTrue(isinstance(result, int))
self.assertEqual(result, MILLIS)

def test_w_naive_datetime(self):
import datetime
import pytz
from gcloud._helpers import UTC
from gcloud.bigquery._helpers import _total_seconds

NOW = datetime.datetime.utcnow()
UTC_NOW = NOW.replace(tzinfo=pytz.utc)
EPOCH = datetime.datetime(1970, 1, 1, tzinfo=pytz.utc)
UTC_NOW = NOW.replace(tzinfo=UTC)
EPOCH = datetime.datetime(1970, 1, 1, tzinfo=UTC)
MILLIS = int(_total_seconds(UTC_NOW - EPOCH) * 1000)
result = self._callFUT(NOW)
self.assertTrue(isinstance(result, int))
Expand Down
5 changes: 3 additions & 2 deletions gcloud/bigquery/test_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ def _makeOne(self, *args, **kw):

def _makeResource(self):
import datetime
import pytz
from gcloud._helpers import UTC

self.WHEN_TS = 1437767599.006
self.WHEN = datetime.datetime.utcfromtimestamp(self.WHEN_TS).replace(
tzinfo=pytz.UTC)
tzinfo=UTC)
self.ETAG = 'ETAG'
self.DS_ID = '%s:%s' % (self.PROJECT, self.DS_NAME)
self.RESOURCE_URL = 'http://example.com/path/to/resource'
Expand Down
42 changes: 25 additions & 17 deletions gcloud/bigquery/test_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,11 @@ def _makeOne(self, *args, **kw):

def _makeResource(self):
import datetime
import pytz
from gcloud._helpers import UTC

self.WHEN_TS = 1437767599.006
self.WHEN = datetime.datetime.utcfromtimestamp(self.WHEN_TS).replace(
tzinfo=pytz.UTC)
tzinfo=UTC)
self.ETAG = 'ETAG'
self.TABLE_ID = '%s:%s:%s' % (
self.PROJECT, self.DS_NAME, self.TABLE_NAME)
Expand Down Expand Up @@ -206,10 +207,11 @@ def test_schema_setter(self):

def test_props_set_by_server(self):
import datetime
import pytz
from gcloud._helpers import UTC
from gcloud.bigquery._helpers import _millis
CREATED = datetime.datetime(2015, 7, 29, 12, 13, 22, tzinfo=pytz.utc)
MODIFIED = datetime.datetime(2015, 7, 29, 14, 47, 15, tzinfo=pytz.utc)

CREATED = datetime.datetime(2015, 7, 29, 12, 13, 22, tzinfo=UTC)
MODIFIED = datetime.datetime(2015, 7, 29, 14, 47, 15, tzinfo=UTC)
TABLE_ID = '%s:%s:%s' % (
self.PROJECT, self.DS_NAME, self.TABLE_NAME)
URL = 'http://example.com/projects/%s/datasets/%s/tables/%s' % (
Expand Down Expand Up @@ -258,8 +260,9 @@ def test_expires_setter_bad_value(self):

def test_expires_setter(self):
import datetime
import pytz
WHEN = datetime.datetime(2015, 7, 28, 16, 39, tzinfo=pytz.utc)
from gcloud._helpers import UTC

WHEN = datetime.datetime(2015, 7, 28, 16, 39, tzinfo=UTC)
client = _Client(self.PROJECT)
dataset = _Dataset(client)
table = self._makeOne(self.TABLE_NAME, dataset)
Expand Down Expand Up @@ -443,9 +446,10 @@ def test_create_w_bound_client(self):

def test_create_w_alternate_client(self):
import datetime
import pytz
from gcloud._helpers import UTC
from gcloud.bigquery.table import SchemaField
from gcloud.bigquery._helpers import _millis

PATH = 'projects/%s/datasets/%s/tables' % (self.PROJECT, self.DS_NAME)
DESCRIPTION = 'DESCRIPTION'
TITLE = 'TITLE'
Expand All @@ -454,7 +458,7 @@ def test_create_w_alternate_client(self):
RESOURCE['description'] = DESCRIPTION
RESOURCE['friendlyName'] = TITLE
self.EXP_TIME = datetime.datetime(2015, 8, 1, 23, 59, 59,
tzinfo=pytz.utc)
tzinfo=UTC)
RESOURCE['expirationTime'] = _millis(self.EXP_TIME)
RESOURCE['view'] = {}
RESOURCE['view']['query'] = QUERY
Expand Down Expand Up @@ -642,9 +646,10 @@ def test_patch_w_bound_client(self):

def test_patch_w_alternate_client(self):
import datetime
import pytz
from gcloud._helpers import UTC
from gcloud.bigquery._helpers import _millis
from gcloud.bigquery.table import SchemaField

PATH = 'projects/%s/datasets/%s/tables/%s' % (
self.PROJECT, self.DS_NAME, self.TABLE_NAME)
QUERY = 'select fullname, age from person_ages'
Expand All @@ -654,7 +659,7 @@ def test_patch_w_alternate_client(self):
RESOURCE['type'] = 'VIEW'
RESOURCE['location'] = LOCATION
self.EXP_TIME = datetime.datetime(2015, 8, 1, 23, 59, 59,
tzinfo=pytz.utc)
tzinfo=UTC)
RESOURCE['expirationTime'] = _millis(self.EXP_TIME)
conn1 = _Connection()
client1 = _Client(project=self.PROJECT, connection=conn1)
Expand Down Expand Up @@ -750,9 +755,10 @@ def test_update_w_bound_client(self):

def test_update_w_alternate_client(self):
import datetime
import pytz
from gcloud._helpers import UTC
from gcloud.bigquery._helpers import _millis
from gcloud.bigquery.table import SchemaField

PATH = 'projects/%s/datasets/%s/tables/%s' % (
self.PROJECT, self.DS_NAME, self.TABLE_NAME)
DEF_TABLE_EXP = 12345
Expand All @@ -762,7 +768,7 @@ def test_update_w_alternate_client(self):
RESOURCE['defaultTableExpirationMs'] = 12345
RESOURCE['location'] = LOCATION
self.EXP_TIME = datetime.datetime(2015, 8, 1, 23, 59, 59,
tzinfo=pytz.utc)
tzinfo=UTC)
RESOURCE['expirationTime'] = _millis(self.EXP_TIME)
RESOURCE['view'] = {'query': QUERY}
RESOURCE['type'] = 'VIEW'
Expand Down Expand Up @@ -837,14 +843,15 @@ def test_delete_w_alternate_client(self):

def test_fetch_data_w_bound_client(self):
import datetime
import pytz
from gcloud._helpers import UTC
from gcloud.bigquery.table import SchemaField
from gcloud.bigquery._helpers import _prop_from_datetime

PATH = 'projects/%s/datasets/%s/tables/%s/data' % (
self.PROJECT, self.DS_NAME, self.TABLE_NAME)
WHEN_TS = 1437767599.006
WHEN = datetime.datetime.utcfromtimestamp(WHEN_TS).replace(
tzinfo=pytz.UTC)
tzinfo=UTC)
WHEN_1 = WHEN + datetime.timedelta(seconds=1)
WHEN_2 = WHEN + datetime.timedelta(seconds=2)
ROWS = 1234
Expand Down Expand Up @@ -1068,12 +1075,13 @@ def test_fetch_data_w_record_schema(self):

def test_insert_data_w_bound_client(self):
import datetime
import pytz
from gcloud._helpers import UTC
from gcloud.bigquery._helpers import _prop_from_datetime
from gcloud.bigquery.table import SchemaField

WHEN_TS = 1437767599.006
WHEN = datetime.datetime.utcfromtimestamp(WHEN_TS).replace(
tzinfo=pytz.UTC)
tzinfo=UTC)
PATH = 'projects/%s/datasets/%s/tables/%s/insertAll' % (
self.PROJECT, self.DS_NAME, self.TABLE_NAME)
conn = _Connection({})
Expand Down
9 changes: 5 additions & 4 deletions gcloud/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
from oauth2client.client import _get_application_default_credential_from_file
from oauth2client import crypt
from oauth2client import service_account
import pytz

try:
from google.appengine.api import app_identity
Expand All @@ -40,6 +39,8 @@
class _GAECreds(object):
"""Dummy class if not in App Engine environment."""

from gcloud._helpers import UTC


def get_credentials():
"""Gets credentials implicitly from the current environment.
Expand Down Expand Up @@ -274,17 +275,17 @@ def _get_expiration_seconds(expiration):
"""
# If it's a timedelta, add it to `now` in UTC.
if isinstance(expiration, datetime.timedelta):
now = _utcnow().replace(tzinfo=pytz.utc)
now = _utcnow().replace(tzinfo=UTC)
expiration = now + expiration

# If it's a datetime, convert to a timestamp.
if isinstance(expiration, datetime.datetime):
# Make sure the timezone on the value is UTC
# (either by converting or replacing the value).
if expiration.tzinfo:
expiration = expiration.astimezone(pytz.utc)
expiration = expiration.astimezone(UTC)
else:
expiration = expiration.replace(tzinfo=pytz.utc)
expiration = expiration.replace(tzinfo=UTC)

# Turn the datetime into a timestamp (seconds, not microseconds).
expiration = int(calendar.timegm(expiration.timetuple()))
Expand Down
8 changes: 4 additions & 4 deletions gcloud/datastore/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
import datetime

from google.protobuf.internal.type_checkers import Int64ValueChecker
import pytz
import six

from gcloud._helpers import UTC
from gcloud.datastore import _datastore_v1_pb2 as datastore_pb
from gcloud.datastore.entity import Entity
from gcloud.datastore.key import Key
Expand Down Expand Up @@ -185,9 +185,9 @@ def _pb_attr_value(val):
# If the datetime is naive (no timezone), consider that it was
# intended to be UTC and replace the tzinfo to that effect.
if not val.tzinfo:
val = val.replace(tzinfo=pytz.utc)
val = val.replace(tzinfo=UTC)
# Regardless of what timezone is on the value, convert it to UTC.
val = val.astimezone(pytz.utc)
val = val.astimezone(UTC)
# Convert the datetime to a microsecond timestamp.
value = int(calendar.timegm(val.timetuple()) * 1e6) + val.microsecond
elif isinstance(val, Key):
Expand Down Expand Up @@ -233,7 +233,7 @@ def _get_value_from_value_pb(value_pb):
microseconds = value_pb.timestamp_microseconds_value
naive = (datetime.datetime.utcfromtimestamp(0) +
datetime.timedelta(microseconds=microseconds))
result = naive.replace(tzinfo=pytz.utc)
result = naive.replace(tzinfo=UTC)

elif value_pb.HasField('key_value'):
result = key_from_protobuf(value_pb.key_value)
Expand Down
Loading

0 comments on commit 99e04fd

Please sign in to comment.