From f1ae00e7fbea5dee35b71928e38e13c428014daf Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 28 Jul 2015 16:18:00 -0400 Subject: [PATCH 1/8] Tables: ctor, scalar property definitions. --- gcloud/bigquery/_helpers.py | 50 +++++++ gcloud/bigquery/dataset.py | 16 +-- gcloud/bigquery/table.py | 237 +++++++++++++++++++++++++++++++ gcloud/bigquery/test__helpers.py | 69 +++++++++ gcloud/bigquery/test_table.py | 148 +++++++++++++++++++ 5 files changed, 505 insertions(+), 15 deletions(-) create mode 100644 gcloud/bigquery/_helpers.py create mode 100644 gcloud/bigquery/table.py create mode 100644 gcloud/bigquery/test__helpers.py create mode 100644 gcloud/bigquery/test_table.py diff --git a/gcloud/bigquery/_helpers.py b/gcloud/bigquery/_helpers.py new file mode 100644 index 000000000000..5fb76553a5d6 --- /dev/null +++ b/gcloud/bigquery/_helpers.py @@ -0,0 +1,50 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""BigQuery utility functions.""" + + +import datetime + +import pytz + +_EPOCH = datetime.datetime(1970, 1, 1, tzinfo=pytz.utc) + + +def _datetime_from_prop(value): + """Convert non-none timestamp to datetime, assuming UTC. + + :rtype: ``datetime.datetime``, or ``NoneType`` + """ + if value is not None: + # back-end returns timestamps as milliseconds since the epoch + value = datetime.datetime.utcfromtimestamp(value / 1000.0) + return value.replace(tzinfo=pytz.utc) + + +def _prop_from_datetime(value): + """Convert non-none datetime to timestamp, assuming UTC. + + :type value: ``datetime.datetime``, or None + :param value: the timestamp + + :rtype: integer, or ``NoneType`` + :returns: the timestamp, in milliseconds, or None + """ + if value is not None: + if value.tzinfo is None: + # Assume UTC + value = value.replace(tzinfo=pytz.utc) + # back-end wants timestamps as milliseconds since the epoch + return (value - _EPOCH).total_seconds() * 1000.0 diff --git a/gcloud/bigquery/dataset.py b/gcloud/bigquery/dataset.py index e417cfcb398a..aaee1c7bd3a6 100644 --- a/gcloud/bigquery/dataset.py +++ b/gcloud/bigquery/dataset.py @@ -13,13 +13,10 @@ # limitations under the License. """Define API Datasets.""" - -import datetime - -import pytz import six from gcloud.exceptions import NotFound +from gcloud.bigquery._helpers import _datetime_from_prop class Dataset(object): @@ -356,14 +353,3 @@ def delete(self, client=None): """ client = self._require_client(client) client.connection.api_request(method='DELETE', path=self.path) - - -def _datetime_from_prop(value): - """Convert non-none timestamp to datetime, assuming UTC. - - :rtype: ``datetime.datetime``, or ``NoneType`` - """ - if value is not None: - # back-end returns timestamps as milliseconds since the epoch - value = datetime.datetime.utcfromtimestamp(value / 1000.0) - return value.replace(tzinfo=pytz.utc) diff --git a/gcloud/bigquery/table.py b/gcloud/bigquery/table.py new file mode 100644 index 000000000000..1c5b5746ae6c --- /dev/null +++ b/gcloud/bigquery/table.py @@ -0,0 +1,237 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Define API Datasets.""" + +import datetime + +import six + +from gcloud.bigquery._helpers import _datetime_from_prop +from gcloud.bigquery._helpers import _prop_from_datetime + + +class Table(object): + """Tables represent a set of rows whose values correspond to a schema. + + See: + https://cloud.google.com/bigquery/docs/reference/v2/tables + + :type name: string + :param name: the name of the table + + :type dataset: :class:`gcloud.bigquery.dataset.Dataset` + :param dataset: The dataset which contains the table. + """ + + def __init__(self, name, dataset): + self.name = name + self._dataset = dataset + self._properties = {} + + @property + def path(self): + """URL path for the table's APIs. + + :rtype: string + :returns: the path based on project and dataste name. + """ + return '%s/tables/%s' % (self._dataset.path, self.name) + + @property + def created(self): + """Datetime at which the table was created. + + :rtype: ``datetime.datetime``, or ``NoneType`` + :returns: the creation time (None until set from the server). + """ + return _datetime_from_prop(self._properties.get('creationTime')) + + @property + def dataset_id(self): + """ID for the table resource. + + :rtype: string, or ``NoneType`` + :returns: the ID (None until set from the server). + """ + return self._properties.get('id') + + @property + def etag(self): + """ETag for the table resource. + + :rtype: string, or ``NoneType`` + :returns: the ETag (None until set from the server). + """ + return self._properties.get('etag') + + @property + def modified(self): + """Datetime at which the table was last modified. + + :rtype: ``datetime.datetime``, or ``NoneType`` + :returns: the modification time (None until set from the server). + """ + return _datetime_from_prop(self._properties.get('lastModifiedTime')) + + @property + def num_bytes(self): + """The size of the table in bytes. + + :rtype: integer, or ``NoneType`` + :returns: the byte count (None until set from the server). + """ + return self._properties.get('numBytes') + + @property + def num_rows(self): + """The number of rows in the table. + + :rtype: integer, or ``NoneType`` + :returns: the row count (None until set from the server). + """ + return self._properties.get('numBytes') + + @property + def self_link(self): + """URL for the table resource. + + :rtype: string, or ``NoneType`` + :returns: the URL (None until set from the server). + """ + return self._properties.get('selfLink') + + @property + def table_type(self): + """The type of the table. + + Possible values are "TABLE" or "VIEW". + + :rtype: string, or ``NoneType`` + :returns: the URL (None until set from the server). + """ + return self._properties.get('type') + + @property + def description(self): + """Description of the table. + + :rtype: string, or ``NoneType`` + :returns: The description as set by the user, or None (the default). + """ + return self._properties.get('description') + + @description.setter + def description(self, value): + """Update description of the table. + + :type value: string, or ``NoneType`` + :param value: new description + + :raises: ValueError for invalid value types. + """ + if not isinstance(value, six.string_types) and value is not None: + raise ValueError("Pass a string, or None") + self._properties['description'] = value + + @property + def expires(self): + """Datetime at which the table will be removed. + + :rtype: ``datetime.datetime``, or ``NoneType`` + :returns: the expiration time, or None + """ + return _datetime_from_prop(self._properties.get('expirationTime')) + + @expires.setter + def expires(self, value): + """Update atetime at which the table will be removed. + + :type value: ``datetime.datetime``, or ``NoneType`` + :param value: the new expiration time, or None + """ + if not isinstance(value, datetime.datetime) and value is not None: + raise ValueError("Pass a datetime, or None") + self._properties['expirationTime'] = _prop_from_datetime(value) + + @property + def friendly_name(self): + """Title of the table. + + :rtype: string, or ``NoneType`` + :returns: The name as set by the user, or None (the default). + """ + return self._properties.get('friendlyName') + + @friendly_name.setter + def friendly_name(self, value): + """Update title of the table. + + :type value: string, or ``NoneType`` + :param value: new title + + :raises: ValueError for invalid value types. + """ + if not isinstance(value, six.string_types) and value is not None: + raise ValueError("Pass a string, or None") + self._properties['friendlyName'] = value + + @property + def location(self): + """Location in which the table is hosted. + + :rtype: string, or ``NoneType`` + :returns: The location as set by the user, or None (the default). + """ + return self._properties.get('location') + + @location.setter + def location(self, value): + """Update location in which the table is hosted. + + :type value: string, or ``NoneType`` + :param value: new location + + :raises: ValueError for invalid value types. + """ + if not isinstance(value, six.string_types) and value is not None: + raise ValueError("Pass a string, or None") + self._properties['location'] = value + + @property + def view_query(self): + """SQL query definiing the table as a view. + + :rtype: string, or ``NoneType`` + :returns: The query as set by the user, or None (the default). + """ + view = self._properties.get('view') + if view is not None: + return view.get('query') + + @view_query.setter + def view_query(self, value): + """Update SQL query definiing the table as a view. + + :type value: string, or ``NoneType`` + :param value: new location + + :raises: ValueError for invalid value types. + """ + if not isinstance(value, six.string_types) and value is not None: + raise ValueError("Pass a string, or None") + if value is None: + self._properties.pop('view', None) + else: + self._properties['view'] = {'query': value} diff --git a/gcloud/bigquery/test__helpers.py b/gcloud/bigquery/test__helpers.py new file mode 100644 index 000000000000..dfbeb65faf1f --- /dev/null +++ b/gcloud/bigquery/test__helpers.py @@ -0,0 +1,69 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest2 + + +class Test__datetime_from_prop(unittest2.TestCase): + + def _callFUT(self, value): + from gcloud.bigquery._helpers import _datetime_from_prop + return _datetime_from_prop(value) + + def test_w_none(self): + self.assertTrue(self._callFUT(None) is None) + + def test_w_millis(self): + import datetime + import pytz + NOW = datetime.datetime.utcnow().replace(tzinfo=pytz.utc) + EPOCH = datetime.datetime(1970, 1, 1, tzinfo=pytz.utc) + MILLIS = (NOW - EPOCH).total_seconds() * 1000 + self.assertEqual(self._callFUT(MILLIS), NOW) + + +class Test__prop_from_datetime(unittest2.TestCase): + + def _callFUT(self, value): + from gcloud.bigquery._helpers import _prop_from_datetime + return _prop_from_datetime(value) + + def test_w_none(self): + self.assertTrue(self._callFUT(None) is None) + + def test_w_utc_datetime(self): + import datetime + import pytz + NOW = datetime.datetime.utcnow().replace(tzinfo=pytz.utc) + EPOCH = datetime.datetime(1970, 1, 1, tzinfo=pytz.utc) + MILLIS = (NOW - EPOCH).total_seconds() * 1000 + self.assertEqual(self._callFUT(NOW), MILLIS) + + def test_w_non_utc_datetime(self): + import datetime + import pytz + 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) + MILLIS = (NOW - EPOCH).total_seconds() * 1000 + self.assertEqual(self._callFUT(NOW), MILLIS) + + def test_w_naive_datetime(self): + import datetime + import pytz + NOW = datetime.datetime.utcnow() + UTC_NOW = NOW.replace(tzinfo=pytz.utc) + EPOCH = datetime.datetime(1970, 1, 1, tzinfo=pytz.utc) + MILLIS = (UTC_NOW - EPOCH).total_seconds() * 1000 + self.assertEqual(self._callFUT(NOW), MILLIS) diff --git a/gcloud/bigquery/test_table.py b/gcloud/bigquery/test_table.py new file mode 100644 index 000000000000..7ffbcd0b06a6 --- /dev/null +++ b/gcloud/bigquery/test_table.py @@ -0,0 +1,148 @@ +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest2 + + +class TestTable(unittest2.TestCase): + PROJECT = 'project' + DS_NAME = 'dataset-name' + TABLE_NAME = 'table-name' + + def _getTargetClass(self): + from gcloud.bigquery.table import Table + return Table + + def _makeOne(self, *args, **kw): + return self._getTargetClass()(*args, **kw) + + def test_ctor(self): + client = _Client(self.PROJECT) + dataset = _Dataset(client) + table = self._makeOne(self.TABLE_NAME, dataset) + self.assertEqual(table.name, self.TABLE_NAME) + self.assertTrue(table._dataset is dataset) + self.assertEqual( + table.path, + '/projects/%s/datasets/%s/tables/%s' % ( + self.PROJECT, self.DS_NAME, self.TABLE_NAME)) + + self.assertEqual(table.created, None) + self.assertEqual(table.dataset_id, None) + self.assertEqual(table.etag, None) + self.assertEqual(table.modified, None) + self.assertEqual(table.num_bytes, None) + self.assertEqual(table.num_rows, None) + self.assertEqual(table.self_link, None) + self.assertEqual(table.table_type, None) + + self.assertEqual(table.description, None) + self.assertEqual(table.expires, None) + self.assertEqual(table.friendly_name, None) + self.assertEqual(table.location, None) + self.assertEqual(table.view_query, None) + + def test_description_setter_bad_value(self): + client = _Client(self.PROJECT) + dataset = _Dataset(client) + table = self._makeOne(self.TABLE_NAME, dataset) + with self.assertRaises(ValueError): + table.description = 12345 + + def test_description_setter(self): + client = _Client(self.PROJECT) + dataset = _Dataset(client) + table = self._makeOne(self.TABLE_NAME, dataset) + table.description = 'DESCRIPTION' + self.assertEqual(table.description, 'DESCRIPTION') + + def test_expires_setter_bad_value(self): + client = _Client(self.PROJECT) + dataset = _Dataset(client) + table = self._makeOne(self.TABLE_NAME, dataset) + with self.assertRaises(ValueError): + table.expires = object() + + def test_expires_setter(self): + import datetime + import pytz + WHEN = datetime.datetime(2015, 7, 28, 16, 39, tzinfo=pytz.utc) + client = _Client(self.PROJECT) + dataset = _Dataset(client) + table = self._makeOne(self.TABLE_NAME, dataset) + table.expires = WHEN + self.assertEqual(table.expires, WHEN) + + def test_friendly_name_setter_bad_value(self): + client = _Client(self.PROJECT) + dataset = _Dataset(client) + table = self._makeOne(self.TABLE_NAME, dataset) + with self.assertRaises(ValueError): + table.friendly_name = 12345 + + def test_friendly_name_setter(self): + client = _Client(self.PROJECT) + dataset = _Dataset(client) + table = self._makeOne(self.TABLE_NAME, dataset) + table.friendly_name = 'FRIENDLY' + self.assertEqual(table.friendly_name, 'FRIENDLY') + + def test_location_setter_bad_value(self): + client = _Client(self.PROJECT) + dataset = _Dataset(client) + table = self._makeOne(self.TABLE_NAME, dataset) + with self.assertRaises(ValueError): + table.location = 12345 + + def test_location_setter(self): + client = _Client(self.PROJECT) + dataset = _Dataset(client) + table = self._makeOne(self.TABLE_NAME, dataset) + table.location = 'LOCATION' + self.assertEqual(table.location, 'LOCATION') + + def test_view_query_setter_bad_value(self): + client = _Client(self.PROJECT) + dataset = _Dataset(client) + table = self._makeOne(self.TABLE_NAME, dataset) + with self.assertRaises(ValueError): + table.view_query = 12345 + + def test_view_query_setter(self): + client = _Client(self.PROJECT) + dataset = _Dataset(client) + table = self._makeOne(self.TABLE_NAME, dataset) + table.view_query = 'select * from foo' + self.assertEqual(table.view_query, 'select * from foo') + table.view_query = None + self.assertEqual(table.view_query, None) + + +class _Client(object): + + def __init__(self, project='project', connection=None): + self.project = project + self.connection = connection + + +class _Dataset(object): + + def __init__(self, client, name=TestTable.DS_NAME): + self._client = client + self._name = name + + @property + def path(self): + return '/projects/%s/datasets/%s' % ( + self._client.project, self._name) From a545ff0ccc3a92dfe553b1fd992f3a9aeadd8acb Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 28 Jul 2015 17:14:23 -0400 Subject: [PATCH 2/8] Restore Python 2.6 compatibility. --- gcloud/bigquery/_helpers.py | 14 +++++++++++++- gcloud/bigquery/test__helpers.py | 12 ++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/gcloud/bigquery/_helpers.py b/gcloud/bigquery/_helpers.py index 5fb76553a5d6..cb61ab46fbf3 100644 --- a/gcloud/bigquery/_helpers.py +++ b/gcloud/bigquery/_helpers.py @@ -16,6 +16,7 @@ import datetime +import sys import pytz @@ -47,4 +48,15 @@ def _prop_from_datetime(value): # Assume UTC value = value.replace(tzinfo=pytz.utc) # back-end wants timestamps as milliseconds since the epoch - return (value - _EPOCH).total_seconds() * 1000.0 + return _total_seconds(value - _EPOCH) * 1000.0 + + +if sys.version_info[:2] < (2, 7): + def _total_seconds(offset): # pragma: NO COVER + """Backport of timedelta.total_seconds() from python 2.7+.""" + seconds = offset.days * 24 * 60 * 60 + offset.seconds + microseconds = seconds * 10**6 + offset.microseconds + return microseconds / (10**6 * 1.0) +else: + def _total_seconds(offset): + return offset.total_seconds() diff --git a/gcloud/bigquery/test__helpers.py b/gcloud/bigquery/test__helpers.py index dfbeb65faf1f..3bc9ad148958 100644 --- a/gcloud/bigquery/test__helpers.py +++ b/gcloud/bigquery/test__helpers.py @@ -27,9 +27,10 @@ def test_w_none(self): def test_w_millis(self): import datetime import pytz + from gcloud.bigquery._helpers import _total_seconds NOW = datetime.datetime.utcnow().replace(tzinfo=pytz.utc) EPOCH = datetime.datetime(1970, 1, 1, tzinfo=pytz.utc) - MILLIS = (NOW - EPOCH).total_seconds() * 1000 + MILLIS = _total_seconds(NOW - EPOCH) * 1000 self.assertEqual(self._callFUT(MILLIS), NOW) @@ -45,25 +46,28 @@ def test_w_none(self): def test_w_utc_datetime(self): import datetime import pytz + from gcloud.bigquery._helpers import _total_seconds NOW = datetime.datetime.utcnow().replace(tzinfo=pytz.utc) EPOCH = datetime.datetime(1970, 1, 1, tzinfo=pytz.utc) - MILLIS = (NOW - EPOCH).total_seconds() * 1000 + MILLIS = _total_seconds(NOW - EPOCH) * 1000 self.assertEqual(self._callFUT(NOW), MILLIS) def test_w_non_utc_datetime(self): import datetime import pytz + 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) - MILLIS = (NOW - EPOCH).total_seconds() * 1000 + MILLIS = _total_seconds(NOW - EPOCH) * 1000 self.assertEqual(self._callFUT(NOW), MILLIS) def test_w_naive_datetime(self): import datetime import pytz + 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) - MILLIS = (UTC_NOW - EPOCH).total_seconds() * 1000 + MILLIS = _total_seconds(UTC_NOW - EPOCH) * 1000 self.assertEqual(self._callFUT(NOW), MILLIS) From 85e5ad785423b9fa8cb6de14fd2c4e86916e4dec Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Wed, 29 Jul 2015 11:27:37 -0400 Subject: [PATCH 3/8] Ensure that computed timestamp truncates to integer ms. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/1021#discussion_r35710591 --- gcloud/bigquery/_helpers.py | 14 +++++++++++++- gcloud/bigquery/test__helpers.py | 31 +++++++++++++++++++++++++------ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/gcloud/bigquery/_helpers.py b/gcloud/bigquery/_helpers.py index cb61ab46fbf3..36e2c3d1e384 100644 --- a/gcloud/bigquery/_helpers.py +++ b/gcloud/bigquery/_helpers.py @@ -23,6 +23,18 @@ _EPOCH = datetime.datetime(1970, 1, 1, tzinfo=pytz.utc) +def _millis(when): + """Convert a zone-aware datetime to integer milliseconds. + + :type when: ``datetime.datetime`` + :param when: the datetime to convert + + :rtype: integer + :returns: milliseconds since epoch for ``when`` + """ + return int(_total_seconds(when - _EPOCH) * 1000) + + def _datetime_from_prop(value): """Convert non-none timestamp to datetime, assuming UTC. @@ -48,7 +60,7 @@ def _prop_from_datetime(value): # Assume UTC value = value.replace(tzinfo=pytz.utc) # back-end wants timestamps as milliseconds since the epoch - return _total_seconds(value - _EPOCH) * 1000.0 + return _millis(value) if sys.version_info[:2] < (2, 7): diff --git a/gcloud/bigquery/test__helpers.py b/gcloud/bigquery/test__helpers.py index 3bc9ad148958..6dca59467c64 100644 --- a/gcloud/bigquery/test__helpers.py +++ b/gcloud/bigquery/test__helpers.py @@ -15,6 +15,19 @@ import unittest2 +class Test__millis(unittest2.TestCase): + + def _callFUT(self, value): + from gcloud.bigquery._helpers import _millis + return _millis(value) + + def test_one_second_from_epoch(self): + import datetime + import pytz + WHEN = datetime.datetime(1970, 1, 1, 0, 0, 1, tzinfo=pytz.utc) + self.assertEqual(self._callFUT(WHEN), 1000) + + class Test__datetime_from_prop(unittest2.TestCase): def _callFUT(self, value): @@ -49,8 +62,10 @@ def test_w_utc_datetime(self): from gcloud.bigquery._helpers import _total_seconds NOW = datetime.datetime.utcnow().replace(tzinfo=pytz.utc) EPOCH = datetime.datetime(1970, 1, 1, tzinfo=pytz.utc) - MILLIS = _total_seconds(NOW - EPOCH) * 1000 - self.assertEqual(self._callFUT(NOW), MILLIS) + 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 @@ -59,8 +74,10 @@ def test_w_non_utc_datetime(self): 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) - MILLIS = _total_seconds(NOW - EPOCH) * 1000 - self.assertEqual(self._callFUT(NOW), MILLIS) + 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 @@ -69,5 +86,7 @@ def test_w_naive_datetime(self): NOW = datetime.datetime.utcnow() UTC_NOW = NOW.replace(tzinfo=pytz.utc) EPOCH = datetime.datetime(1970, 1, 1, tzinfo=pytz.utc) - MILLIS = _total_seconds(UTC_NOW - EPOCH) * 1000 - self.assertEqual(self._callFUT(NOW), MILLIS) + MILLIS = int(_total_seconds(UTC_NOW - EPOCH) * 1000) + result = self._callFUT(NOW) + self.assertTrue(isinstance(result, int)) + self.assertEqual(result, MILLIS) From 019d3e49dab8a5279a654d81d422fc4d55a5ddd3 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Wed, 29 Jul 2015 11:29:42 -0400 Subject: [PATCH 4/8] Add tests for backend-set property values. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/1021#discussion_r35710947 --- gcloud/bigquery/table.py | 20 ++++++++++---------- gcloud/bigquery/test_table.py | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/gcloud/bigquery/table.py b/gcloud/bigquery/table.py index 1c5b5746ae6c..46ea8e9be540 100644 --- a/gcloud/bigquery/table.py +++ b/gcloud/bigquery/table.py @@ -58,15 +58,6 @@ def created(self): """ return _datetime_from_prop(self._properties.get('creationTime')) - @property - def dataset_id(self): - """ID for the table resource. - - :rtype: string, or ``NoneType`` - :returns: the ID (None until set from the server). - """ - return self._properties.get('id') - @property def etag(self): """ETag for the table resource. @@ -101,7 +92,7 @@ def num_rows(self): :rtype: integer, or ``NoneType`` :returns: the row count (None until set from the server). """ - return self._properties.get('numBytes') + return self._properties.get('numRows') @property def self_link(self): @@ -112,6 +103,15 @@ def self_link(self): """ return self._properties.get('selfLink') + @property + def table_id(self): + """ID for the table resource. + + :rtype: string, or ``NoneType`` + :returns: the ID (None until set from the server). + """ + return self._properties.get('id') + @property def table_type(self): """The type of the table. diff --git a/gcloud/bigquery/test_table.py b/gcloud/bigquery/test_table.py index 7ffbcd0b06a6..16f3ec39f149 100644 --- a/gcloud/bigquery/test_table.py +++ b/gcloud/bigquery/test_table.py @@ -39,12 +39,12 @@ def test_ctor(self): self.PROJECT, self.DS_NAME, self.TABLE_NAME)) self.assertEqual(table.created, None) - self.assertEqual(table.dataset_id, None) self.assertEqual(table.etag, None) self.assertEqual(table.modified, None) self.assertEqual(table.num_bytes, None) self.assertEqual(table.num_rows, None) self.assertEqual(table.self_link, None) + self.assertEqual(table.table_id, None) self.assertEqual(table.table_type, None) self.assertEqual(table.description, None) @@ -53,6 +53,37 @@ def test_ctor(self): self.assertEqual(table.location, None) self.assertEqual(table.view_query, None) + def test_props_set_by_server(self): + import datetime + import pytz + 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) + TABLE_ID = '%s:%s:%s' % ( + self.PROJECT, self.DS_NAME, self.TABLE_NAME) + URL = 'http://example.com/projects/%s/datasets/%s/tables/%s' % ( + self.PROJECT, self.DS_NAME, self.TABLE_NAME) + client = _Client(self.PROJECT) + dataset = _Dataset(client) + table = self._makeOne(self.TABLE_NAME, dataset) + table._properties['creationTime'] = _millis(CREATED) + table._properties['etag'] = 'ETAG' + table._properties['lastModifiedTime'] = _millis(MODIFIED) + table._properties['numBytes'] = 12345 + table._properties['numRows'] = 66 + table._properties['selfLink'] = URL + table._properties['id'] = TABLE_ID + table._properties['type'] = 'TABLE' + + self.assertEqual(table.created, CREATED) + self.assertEqual(table.etag, 'ETAG') + self.assertEqual(table.modified, MODIFIED) + self.assertEqual(table.num_bytes, 12345) + self.assertEqual(table.num_rows, 66) + self.assertEqual(table.self_link, URL) + self.assertEqual(table.table_id, TABLE_ID) + self.assertEqual(table.table_type, 'TABLE') + def test_description_setter_bad_value(self): client = _Client(self.PROJECT) dataset = _Dataset(client) From 0f939432add1ddc1dc0d605c9d00f90548ab018d Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Wed, 29 Jul 2015 11:30:25 -0400 Subject: [PATCH 5/8] Switch to deleter for 'view_query'. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/1021#discussion_r35711134 --- gcloud/bigquery/table.py | 14 ++++++++------ gcloud/bigquery/test_table.py | 8 +++++++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/gcloud/bigquery/table.py b/gcloud/bigquery/table.py index 46ea8e9be540..6fcdc52e9e37 100644 --- a/gcloud/bigquery/table.py +++ b/gcloud/bigquery/table.py @@ -224,14 +224,16 @@ def view_query(self): def view_query(self, value): """Update SQL query definiing the table as a view. - :type value: string, or ``NoneType`` + :type value: string :param value: new location :raises: ValueError for invalid value types. """ - if not isinstance(value, six.string_types) and value is not None: + if not isinstance(value, six.string_types): raise ValueError("Pass a string, or None") - if value is None: - self._properties.pop('view', None) - else: - self._properties['view'] = {'query': value} + self._properties['view'] = {'query': value} + + @view_query.deleter + def view_query(self): + """Delete SQL query definiing the table as a view.""" + self._properties.pop('view', None) diff --git a/gcloud/bigquery/test_table.py b/gcloud/bigquery/test_table.py index 16f3ec39f149..5a4bee8e9eb5 100644 --- a/gcloud/bigquery/test_table.py +++ b/gcloud/bigquery/test_table.py @@ -156,7 +156,13 @@ def test_view_query_setter(self): table = self._makeOne(self.TABLE_NAME, dataset) table.view_query = 'select * from foo' self.assertEqual(table.view_query, 'select * from foo') - table.view_query = None + + def test_view_query_deleter(self): + client = _Client(self.PROJECT) + dataset = _Dataset(client) + table = self._makeOne(self.TABLE_NAME, dataset) + table.view_query = 'select * from foo' + del table.view_query self.assertEqual(table.view_query, None) From a8a46d0730b32bf431ace171fe0ce794b69ab5fc Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Wed, 29 Jul 2015 17:09:25 -0400 Subject: [PATCH 6/8] Adjust ValueError message after dropping 'None' as possible value. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/1021#discussion_r35810304 --- gcloud/bigquery/table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcloud/bigquery/table.py b/gcloud/bigquery/table.py index 6fcdc52e9e37..53d8f216a71a 100644 --- a/gcloud/bigquery/table.py +++ b/gcloud/bigquery/table.py @@ -230,7 +230,7 @@ def view_query(self, value): :raises: ValueError for invalid value types. """ if not isinstance(value, six.string_types): - raise ValueError("Pass a string, or None") + raise ValueError("Pass a string") self._properties['view'] = {'query': value} @view_query.deleter From ca4a1be76edfc9bd87a3aecaab32ef1dd5a2aad2 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Wed, 29 Jul 2015 17:10:26 -0400 Subject: [PATCH 7/8] Typo: s/definiing/defining/g Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/1021#discussion-diff-35810402 --- gcloud/bigquery/table.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gcloud/bigquery/table.py b/gcloud/bigquery/table.py index 53d8f216a71a..82c5417d717d 100644 --- a/gcloud/bigquery/table.py +++ b/gcloud/bigquery/table.py @@ -211,7 +211,7 @@ def location(self, value): @property def view_query(self): - """SQL query definiing the table as a view. + """SQL query defining the table as a view. :rtype: string, or ``NoneType`` :returns: The query as set by the user, or None (the default). @@ -222,7 +222,7 @@ def view_query(self): @view_query.setter def view_query(self, value): - """Update SQL query definiing the table as a view. + """Update SQL query defining the table as a view. :type value: string :param value: new location @@ -235,5 +235,5 @@ def view_query(self, value): @view_query.deleter def view_query(self): - """Delete SQL query definiing the table as a view.""" + """Delete SQL query defining the table as a view.""" self._properties.pop('view', None) From 10eaddd0d4f27e01c26b6f336a6d19d4a602da14 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Wed, 29 Jul 2015 17:16:12 -0400 Subject: [PATCH 8/8] Compute epoch using same formula as in datastore. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/1021#discussion_r35710351 --- gcloud/bigquery/_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcloud/bigquery/_helpers.py b/gcloud/bigquery/_helpers.py index 36e2c3d1e384..b19ffd3a2742 100644 --- a/gcloud/bigquery/_helpers.py +++ b/gcloud/bigquery/_helpers.py @@ -20,7 +20,7 @@ import pytz -_EPOCH = datetime.datetime(1970, 1, 1, tzinfo=pytz.utc) +_EPOCH = datetime.datetime.utcfromtimestamp(0).replace(tzinfo=pytz.utc) def _millis(when):