From 57c354ebc2b20f2c8a87ff0b31e142091a1f5e45 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Fri, 24 Jul 2015 14:41:01 -0400 Subject: [PATCH 01/12] Implement 'bigquery.dataset.Dataset'. 'table' factory still TBD. --- gcloud/bigquery/__init__.py | 1 + gcloud/bigquery/client.py | 12 + gcloud/bigquery/dataset.py | 329 +++++++++++++++++++++++++ gcloud/bigquery/test_client.py | 12 + gcloud/bigquery/test_dataset.py | 421 ++++++++++++++++++++++++++++++++ 5 files changed, 775 insertions(+) create mode 100644 gcloud/bigquery/dataset.py create mode 100644 gcloud/bigquery/test_dataset.py diff --git a/gcloud/bigquery/__init__.py b/gcloud/bigquery/__init__.py index 330233bbc8a8..3a0ad33aaafa 100644 --- a/gcloud/bigquery/__init__.py +++ b/gcloud/bigquery/__init__.py @@ -23,3 +23,4 @@ from gcloud.bigquery.client import Client from gcloud.bigquery.connection import SCOPE +from gcloud.bigquery.dataset import Dataset diff --git a/gcloud/bigquery/client.py b/gcloud/bigquery/client.py index 86ab16d5a7b9..6f0235ba2a17 100644 --- a/gcloud/bigquery/client.py +++ b/gcloud/bigquery/client.py @@ -17,6 +17,7 @@ from gcloud.client import JSONClient from gcloud.bigquery.connection import Connection +from gcloud.bigquery.dataset import Dataset class Client(JSONClient): @@ -41,3 +42,14 @@ class Client(JSONClient): """ _connection_class = Connection + + def dataset(self, name): + """Construct a dataset bound to this client. + + :type name: ``str`` + :param name: Name of the dataset. + + :rtype: :class:`gcloud.bigquery.dataset.Dataset` + :returns: a new ``Dataset`` instance + """ + return Dataset(name, client=self) diff --git a/gcloud/bigquery/dataset.py b/gcloud/bigquery/dataset.py new file mode 100644 index 000000000000..d2570ab54a80 --- /dev/null +++ b/gcloud/bigquery/dataset.py @@ -0,0 +1,329 @@ +# 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 pytz + +from gcloud.exceptions import NotFound + + +class Dataset(object): + """Datasets are containers for tables. + + See: + https://cloud.google.com/bigquery/docs/reference/v2/datasets + + :type name: string + :param name: the name of the dataset + + :type client: :class:`gcloud.bigquery.client.Client` + :param client: A client which holds credentials and project configuration + for the dataset (which requires a project). + """ + + def __init__(self, name, client): + self.name = name + self._client = client + self._properties = {} + + @property + def project(self): + """Project bound to the dataset.""" + return self._client.project + + @property + def path(self): + """URL path for the dataset's APIs""" + return '/projects/%s/datasets/%s' % (self.project, self.name) + + @property + def created(self): + """Datetime at which the dataset was created. + + :rtype: ``datetime.datetime``, or ``NoneType`` + """ + return _datetime_from_prop(self._properties.get('creationTime')) + + @property + def dataset_id(self): + """ID for the dataset resource. + + :rtype: ``str``, or ``NoneType`` + """ + return self._properties.get('id') + + @property + def etag(self): + """ETag for the dataset resource. + + :rtype: ``str``, or ``NoneType`` + """ + return self._properties.get('etag') + + @property + def modified(self): + """Datetime at which the dataset was last modified. + + :rtype: ``datetime.datetime``, or ``NoneType`` + """ + return _datetime_from_prop(self._properties.get('lastModifiedTime')) + + @property + def self_link(self): + """URL for the dataset resource. + + :rtype: ``str``, or ``NoneType`` + """ + return self._properties.get('selfLink') + + @property + def default_table_expiration_ms(self): + """Default expiration time for tables in the dataset. + + :rtype: ``int``, or ``NoneType`` + """ + return self._properties.get('defaultTableExpirationMs') + + @default_table_expiration_ms.setter + def default_table_expiration_ms(self, value): + """Update default expiration time for tables in the dataset. + + :type value: ``int``, or ``NoneType`` + :param value: new default time, in milliseconds + """ + if not isinstance(value, int) and value is not None: + raise ValueError() + self._properties['defaultTableExpirationMs'] = value + + @property + def description(self): + """Description of the dataset. + + :rtype: ``str``, or ``NoneType`` + """ + return self._properties.get('description') + + @description.setter + def description(self, value): + """Update description of the dataset. + + :type value: ``str``, or ``NoneType`` + :param value: new description + """ + if not isinstance(value, str) and value is not None: + raise ValueError() + self._properties['description'] = value + + @property + def friendly_name(self): + """Title of the dataset. + + :rtype: ``str``, or ``NoneType`` + """ + return self._properties.get('friendlyName') + + @friendly_name.setter + def friendly_name(self, value): + """Update title of the dataset. + + :type value: ``str``, or ``NoneType`` + :param value: new title + """ + if not isinstance(value, str) and value is not None: + raise ValueError() + self._properties['friendlyName'] = value + + @property + def location(self): + """Location in which the dataset is hosted. + + :rtype: ``str``, or ``NoneType`` + """ + return self._properties.get('location') + + @location.setter + def location(self, value): + """Update location in which the dataset is hosted. + + :type value: ``str``, or ``NoneType`` + :param value: new location + """ + if not isinstance(value, str) and value is not None: + raise ValueError() + self._properties['location'] = value + + def _require_client(self, client): + """Check client or verify over-ride. + + :type client: :class:`gcloud.bigquery.client.Client` or ``NoneType`` + :param client: the client to use. If not passed, falls back to the + ``client`` stored on the current dataset. + + :rtype: :class:`gcloud.bigquery.client.Client` + :returns: The client passed in or the currently bound client. + """ + if client is None: + client = self._client + return client + + def _set_properties(self, api_response): + """Update properties from resource in body of ``api_response`` + + :type api_response: httplib2.Response + :param api_response: response returned from an API call + """ + self._properties.clear() + cleaned = api_response.copy() + cleaned['creationTime'] = float(cleaned['creationTime']) + cleaned['lastModifiedTime'] = float(cleaned['lastModifiedTime']) + self._properties.update(cleaned) + + def _build_resource(self): + """Generate a resource for ``create`` or ``update``.""" + return { + 'datasetReference': { + 'projectId': self.project, 'datasetId': self.name}, + 'defaultTableExpirationMs': self.default_table_expiration_ms, + 'description': self.description, + 'friendlyName': self.friendly_name, + 'location': self.location, + } + + def create(self, client=None): + """API call: create the dataset via a PUT request + + See: + https://cloud.google.com/bigquery/reference/rest/v2/tables/insert + + :type client: :class:`gcloud.bigquery.client.Client` or ``NoneType`` + :param client: the client to use. If not passed, falls back to the + ``client`` stored on the current dataset. + """ + client = self._require_client(client) + path = '/projects/%s/datasets' % self.project + api_response = client.connection.api_request( + method='POST', path=path, data=self._build_resource()) + self._set_properties(api_response) + + def exists(self, client=None): + """API call: test for the existence of the dataset via a GET request + + See + https://cloud.google.com/bigquery/docs/reference/v2/datasets/get + + :type client: :class:`gcloud.bigquery.client.Client` or ``NoneType`` + :param client: the client to use. If not passed, falls back to the + ``client`` stored on the current dataset. + """ + client = self._require_client(client) + + try: + client.connection.api_request(method='GET', path=self.path) + except NotFound: + return False + else: + return True + + def reload(self, client=None): + """API call: refresh dataset properties via a GET request + + See + https://cloud.google.com/bigquery/docs/reference/v2/datasets/get + + :type client: :class:`gcloud.bigquery.client.Client` or ``NoneType`` + :param client: the client to use. If not passed, falls back to the + ``client`` stored on the current dataset. + """ + client = self._require_client(client) + + api_response = client.connection.api_request( + method='GET', path=self.path) + self._set_properties(api_response) + + def patch(self, client=None, **kw): + """API call: update individual dataset properties via a PATCH request + + See + https://cloud.google.com/bigquery/docs/reference/v2/datasets/patch + + :type client: :class:`gcloud.bigquery.client.Client` or ``NoneType`` + :param client: the client to use. If not passed, falls back to the + ``client`` stored on the current dataset. + + :type kw: ``dict`` + :param kw: properties to be patched. + """ + client = self._require_client(client) + + partial = {} + + if 'default_table_expiration_ms' in kw: + value = kw['default_table_expiration_ms'] + if not isinstance(value, int) and value is not None: + raise ValueError() + partial['defaultTableExpirationMs'] = value + + if 'description' in kw: + partial['description'] = kw['description'] + + if 'friendly_name' in kw: + partial['friendlyName'] = kw['friendly_name'] + + if 'location' in kw: + partial['location'] = kw['location'] + + api_response = client.connection.api_request( + method='PATCH', path=self.path, data=partial) + self._set_properties(api_response) + + def update(self, client=None): + """API call: update dataset properties via a PUT request + + See + https://cloud.google.com/bigquery/docs/reference/v2/datasets/update + + :type client: :class:`gcloud.bigquery.client.Client` or ``NoneType`` + :param client: the client to use. If not passed, falls back to the + ``client`` stored on the current dataset. + """ + client = self._require_client(client) + api_response = client.connection.api_request( + method='PUT', path=self.path, data=self._build_resource()) + self._set_properties(api_response) + + def delete(self, client=None): + """API call: delete the dataset via a DELETE request + + See: + https://cloud.google.com/bigquery/reference/rest/v2/datasets/delete + + :type client: :class:`gcloud.bigquery.client.Client` or ``NoneType`` + :param client: the client to use. If not passed, falls back to the + ``client`` stored on the current dataset. + """ + 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: + value = datetime.datetime.utcfromtimestamp(value / 1000.0) + return value.replace(tzinfo=pytz.utc) diff --git a/gcloud/bigquery/test_client.py b/gcloud/bigquery/test_client.py index a8461c8719be..b9d547fd359a 100644 --- a/gcloud/bigquery/test_client.py +++ b/gcloud/bigquery/test_client.py @@ -34,6 +34,18 @@ def test_ctor(self): self.assertTrue(client.connection.credentials is creds) self.assertTrue(client.connection.http is http) + def test_dataset(self): + from gcloud.bigquery.dataset import Dataset + PROJECT = 'PROJECT' + DATASET = 'dataset_name' + creds = _Credentials() + http = object() + client = self._makeOne(project=PROJECT, credentials=creds, http=http) + dataset = client.dataset(DATASET) + self.assertTrue(isinstance(dataset, Dataset)) + self.assertEqual(dataset.name, DATASET) + self.assertTrue(dataset._client is client) + class _Credentials(object): diff --git a/gcloud/bigquery/test_dataset.py b/gcloud/bigquery/test_dataset.py new file mode 100644 index 000000000000..94bdf6338ea3 --- /dev/null +++ b/gcloud/bigquery/test_dataset.py @@ -0,0 +1,421 @@ +# 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 TestDataset(unittest2.TestCase): + PROJECT = 'project' + DS_NAME = 'dataset-name' + + def _getTargetClass(self): + from gcloud.bigquery.dataset import Dataset + return Dataset + + def _makeOne(self, *args, **kw): + return self._getTargetClass()(*args, **kw) + + def _makeResource(self): + import datetime + import pytz + self.WHEN_TS = 1437767599.006 + self.WHEN = datetime.datetime.utcfromtimestamp(self.WHEN_TS).replace( + tzinfo=pytz.UTC) + self.ETAG = 'ETAG' + self.DS_ID = '%s:%s' % (self.PROJECT, self.DS_NAME) + self.RESOURCE_URL = 'http://example.com/path/to/resource' + return { + 'creationTime': self.WHEN_TS * 1000, + 'datasetReference': + {'projectId': self.PROJECT, 'datasetId': self.DS_NAME}, + 'etag': 'ETAG', + 'id': self.DS_ID, + 'lastModifiedTime': self.WHEN_TS * 1000, + 'location': 'US', + 'selfLink': self.RESOURCE_URL, + } + + def _verifyResourceProperties(self, dataset, resource): + self.assertEqual(dataset.created, self.WHEN) + self.assertEqual(dataset.dataset_id, self.DS_ID) + self.assertEqual(dataset.etag, self.ETAG) + self.assertEqual(dataset.modified, self.WHEN) + self.assertEqual(dataset.self_link, self.RESOURCE_URL) + + self.assertEqual(dataset.default_table_expiration_ms, + resource.get('defaultTableExpirationMs')) + self.assertEqual(dataset.description, resource.get('description')) + self.assertEqual(dataset.friendly_name, resource.get('friendlyName')) + self.assertEqual(dataset.location, resource.get('location')) + + def test_ctor(self): + client = _Client(self.PROJECT) + dataset = self._makeOne(self.DS_NAME, client) + self.assertEqual(dataset.name, self.DS_NAME) + self.assertTrue(dataset._client is client) + self.assertEqual(dataset.project, client.project) + self.assertEqual( + dataset.path, + '/projects/%s/datasets/%s' % (self.PROJECT, self.DS_NAME)) + + self.assertEqual(dataset.created, None) + self.assertEqual(dataset.dataset_id, None) + self.assertEqual(dataset.etag, None) + self.assertEqual(dataset.modified, None) + self.assertEqual(dataset.self_link, None) + + self.assertEqual(dataset.default_table_expiration_ms, None) + self.assertEqual(dataset.description, None) + self.assertEqual(dataset.friendly_name, None) + self.assertEqual(dataset.location, None) + + def test_default_table_expiration_ms_setter_bad_value(self): + client = _Client(self.PROJECT) + dataset = self._makeOne(self.DS_NAME, client) + with self.assertRaises(ValueError): + dataset.default_table_expiration_ms = 'bogus' + + def test_default_table_expiration_ms_setter(self): + client = _Client(self.PROJECT) + dataset = self._makeOne(self.DS_NAME, client) + dataset.default_table_expiration_ms = 12345 + self.assertEqual(dataset.default_table_expiration_ms, 12345) + + def test_description_setter_bad_value(self): + client = _Client(self.PROJECT) + dataset = self._makeOne(self.DS_NAME, client) + with self.assertRaises(ValueError): + dataset.description = 12345 + + def test_description_setter(self): + client = _Client(self.PROJECT) + dataset = self._makeOne(self.DS_NAME, client) + dataset.description = 'DESCRIPTION' + self.assertEqual(dataset.description, 'DESCRIPTION') + + def test_friendly_name_setter_bad_value(self): + client = _Client(self.PROJECT) + dataset = self._makeOne(self.DS_NAME, client) + with self.assertRaises(ValueError): + dataset.friendly_name = 12345 + + def test_friendly_name_setter(self): + client = _Client(self.PROJECT) + dataset = self._makeOne(self.DS_NAME, client) + dataset.friendly_name = 'FRIENDLY' + self.assertEqual(dataset.friendly_name, 'FRIENDLY') + + def test_location_setter_bad_value(self): + client = _Client(self.PROJECT) + dataset = self._makeOne(self.DS_NAME, client) + with self.assertRaises(ValueError): + dataset.location = 12345 + + def test_location_setter(self): + client = _Client(self.PROJECT) + dataset = self._makeOne(self.DS_NAME, client) + dataset.location = 'LOCATION' + self.assertEqual(dataset.location, 'LOCATION') + + def test_create_w_bound_client(self): + PATH = 'projects/%s/datasets' % self.PROJECT + RESOURCE = self._makeResource() + conn = _Connection(RESOURCE) + CLIENT = _Client(project=self.PROJECT, connection=conn) + dataset = self._makeOne(self.DS_NAME, client=CLIENT) + + dataset.create() + + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'POST') + self.assertEqual(req['path'], '/%s' % PATH) + SENT = { + 'datasetReference': + {'projectId': self.PROJECT, 'datasetId': self.DS_NAME}, + 'defaultTableExpirationMs': None, + 'description': None, + 'friendlyName': None, + 'location': None, + } + self.assertEqual(req['data'], SENT) + self._verifyResourceProperties(dataset, RESOURCE) + + def test_create_w_alternate_client(self): + PATH = 'projects/%s/datasets' % self.PROJECT + RESOURCE = self._makeResource() + conn1 = _Connection() + CLIENT1 = _Client(project=self.PROJECT, connection=conn1) + conn2 = _Connection(RESOURCE) + CLIENT2 = _Client(project=self.PROJECT, connection=conn2) + dataset = self._makeOne(self.DS_NAME, client=CLIENT1) + + dataset.create(client=CLIENT2) + + self.assertEqual(len(conn1._requested), 0) + self.assertEqual(len(conn2._requested), 1) + req = conn2._requested[0] + self.assertEqual(req['method'], 'POST') + self.assertEqual(req['path'], '/%s' % PATH) + SENT = { + 'datasetReference': + {'projectId': self.PROJECT, 'datasetId': self.DS_NAME}, + 'defaultTableExpirationMs': None, + 'description': None, + 'friendlyName': None, + 'location': None, + } + self.assertEqual(req['data'], SENT) + self._verifyResourceProperties(dataset, RESOURCE) + + def test_exists_miss_w_bound_client(self): + PATH = 'projects/%s/datasets/%s' % (self.PROJECT, self.DS_NAME) + conn = _Connection() + CLIENT = _Client(project=self.PROJECT, connection=conn) + dataset = self._makeOne(self.DS_NAME, client=CLIENT) + + self.assertFalse(dataset.exists()) + + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % PATH) + + def test_exists_hit_w_alternate_client(self): + PATH = 'projects/%s/datasets/%s' % (self.PROJECT, self.DS_NAME) + conn1 = _Connection() + CLIENT1 = _Client(project=self.PROJECT, connection=conn1) + conn2 = _Connection({}) + CLIENT2 = _Client(project=self.PROJECT, connection=conn2) + dataset = self._makeOne(self.DS_NAME, client=CLIENT1) + + self.assertTrue(dataset.exists(client=CLIENT2)) + + self.assertEqual(len(conn1._requested), 0) + self.assertEqual(len(conn2._requested), 1) + req = conn2._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % PATH) + + def test_reload_w_bound_client(self): + PATH = 'projects/%s/datasets/%s' % (self.PROJECT, self.DS_NAME) + RESOURCE = self._makeResource() + conn = _Connection(RESOURCE) + CLIENT = _Client(project=self.PROJECT, connection=conn) + dataset = self._makeOne(self.DS_NAME, client=CLIENT) + + dataset.reload() + + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % PATH) + self._verifyResourceProperties(dataset, RESOURCE) + + def test_reload_w_alternate_client(self): + PATH = 'projects/%s/datasets/%s' % (self.PROJECT, self.DS_NAME) + RESOURCE = self._makeResource() + conn1 = _Connection() + CLIENT1 = _Client(project=self.PROJECT, connection=conn1) + conn2 = _Connection(RESOURCE) + CLIENT2 = _Client(project=self.PROJECT, connection=conn2) + dataset = self._makeOne(self.DS_NAME, client=CLIENT1) + + dataset.reload(client=CLIENT2) + + self.assertEqual(len(conn1._requested), 0) + self.assertEqual(len(conn2._requested), 1) + req = conn2._requested[0] + self.assertEqual(req['method'], 'GET') + self.assertEqual(req['path'], '/%s' % PATH) + self._verifyResourceProperties(dataset, RESOURCE) + + def test_patch_w_invalid_expiration(self): + RESOURCE = self._makeResource() + conn = _Connection(RESOURCE) + CLIENT = _Client(project=self.PROJECT, connection=conn) + dataset = self._makeOne(self.DS_NAME, client=CLIENT) + + with self.assertRaises(ValueError): + dataset.patch(default_table_expiration_ms='BOGUS') + + def test_patch_w_bound_client(self): + PATH = 'projects/%s/datasets/%s' % (self.PROJECT, self.DS_NAME) + DESCRIPTION = 'DESCRIPTION' + TITLE = 'TITLE' + RESOURCE = self._makeResource() + RESOURCE['description'] = DESCRIPTION + RESOURCE['friendlyName'] = TITLE + conn = _Connection(RESOURCE) + CLIENT = _Client(project=self.PROJECT, connection=conn) + dataset = self._makeOne(self.DS_NAME, client=CLIENT) + + dataset.patch(description=DESCRIPTION, friendly_name=TITLE) + + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'PATCH') + SENT = { + 'description': DESCRIPTION, + 'friendlyName': TITLE, + } + self.assertEqual(req['data'], SENT) + self.assertEqual(req['path'], '/%s' % PATH) + self._verifyResourceProperties(dataset, RESOURCE) + + def test_patch_w_alternate_client(self): + PATH = 'projects/%s/datasets/%s' % (self.PROJECT, self.DS_NAME) + DEF_TABLE_EXP = 12345 + LOCATION = 'EU' + RESOURCE = self._makeResource() + RESOURCE['defaultTableExpirationMs'] = DEF_TABLE_EXP + RESOURCE['location'] = LOCATION + conn1 = _Connection() + CLIENT1 = _Client(project=self.PROJECT, connection=conn1) + conn2 = _Connection(RESOURCE) + CLIENT2 = _Client(project=self.PROJECT, connection=conn2) + dataset = self._makeOne(self.DS_NAME, client=CLIENT1) + + dataset.patch(client=CLIENT2, + default_table_expiration_ms=DEF_TABLE_EXP, + location=LOCATION) + + self.assertEqual(len(conn1._requested), 0) + self.assertEqual(len(conn2._requested), 1) + req = conn2._requested[0] + self.assertEqual(req['method'], 'PATCH') + self.assertEqual(req['path'], '/%s' % PATH) + SENT = { + 'defaultTableExpirationMs': DEF_TABLE_EXP, + 'location': LOCATION, + } + self.assertEqual(req['data'], SENT) + self._verifyResourceProperties(dataset, RESOURCE) + + def test_update_w_bound_client(self): + PATH = 'projects/%s/datasets/%s' % (self.PROJECT, self.DS_NAME) + DESCRIPTION = 'DESCRIPTION' + TITLE = 'TITLE' + RESOURCE = self._makeResource() + RESOURCE['description'] = DESCRIPTION + RESOURCE['friendlyName'] = TITLE + conn = _Connection(RESOURCE) + CLIENT = _Client(project=self.PROJECT, connection=conn) + dataset = self._makeOne(self.DS_NAME, client=CLIENT) + dataset.description = DESCRIPTION + dataset.friendly_name = TITLE + + dataset.update() + + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'PUT') + SENT = { + 'datasetReference': + {'projectId': self.PROJECT, 'datasetId': self.DS_NAME}, + 'defaultTableExpirationMs': None, + 'description': DESCRIPTION, + 'friendlyName': TITLE, + 'location': None, + } + self.assertEqual(req['data'], SENT) + self.assertEqual(req['path'], '/%s' % PATH) + self._verifyResourceProperties(dataset, RESOURCE) + + def test_update_w_alternate_client(self): + PATH = 'projects/%s/datasets/%s' % (self.PROJECT, self.DS_NAME) + DEF_TABLE_EXP = 12345 + LOCATION = 'EU' + RESOURCE = self._makeResource() + RESOURCE['defaultTableExpirationMs'] = 12345 + RESOURCE['location'] = LOCATION + conn1 = _Connection() + CLIENT1 = _Client(project=self.PROJECT, connection=conn1) + conn2 = _Connection(RESOURCE) + CLIENT2 = _Client(project=self.PROJECT, connection=conn2) + dataset = self._makeOne(self.DS_NAME, client=CLIENT1) + dataset.default_table_expiration_ms = DEF_TABLE_EXP + dataset.location = LOCATION + + dataset.update(client=CLIENT2) + + self.assertEqual(len(conn1._requested), 0) + self.assertEqual(len(conn2._requested), 1) + req = conn2._requested[0] + self.assertEqual(req['method'], 'PUT') + self.assertEqual(req['path'], '/%s' % PATH) + SENT = { + 'datasetReference': + {'projectId': self.PROJECT, 'datasetId': self.DS_NAME}, + 'defaultTableExpirationMs': 12345, + 'description': None, + 'friendlyName': None, + 'location': 'EU', + } + self.assertEqual(req['data'], SENT) + self._verifyResourceProperties(dataset, RESOURCE) + + def test_delete_w_bound_client(self): + PATH = 'projects/%s/datasets/%s' % (self.PROJECT, self.DS_NAME) + conn = _Connection({}) + CLIENT = _Client(project=self.PROJECT, connection=conn) + dataset = self._makeOne(self.DS_NAME, client=CLIENT) + + dataset.delete() + + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'DELETE') + self.assertEqual(req['path'], '/%s' % PATH) + + def test_delete_w_alternate_client(self): + PATH = 'projects/%s/datasets/%s' % (self.PROJECT, self.DS_NAME) + conn1 = _Connection() + CLIENT1 = _Client(project=self.PROJECT, connection=conn1) + conn2 = _Connection({}) + CLIENT2 = _Client(project=self.PROJECT, connection=conn2) + dataset = self._makeOne(self.DS_NAME, client=CLIENT1) + + dataset.delete(client=CLIENT2) + + self.assertEqual(len(conn1._requested), 0) + self.assertEqual(len(conn2._requested), 1) + req = conn2._requested[0] + self.assertEqual(req['method'], 'DELETE') + self.assertEqual(req['path'], '/%s' % PATH) + + +class _Client(object): + + def __init__(self, project='project', connection=None): + self.project = project + self.connection = connection + + +class _Connection(object): + + def __init__(self, *responses): + self._responses = responses + self._requested = [] + + def api_request(self, **kw): + from gcloud.exceptions import NotFound + self._requested.append(kw) + + try: + response, self._responses = self._responses[0], self._responses[1:] + except: + raise NotFound('miss') + else: + return response From 3d3b9e37a13e52cd4554e7946d47fac164ef51e0 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Fri, 24 Jul 2015 17:34:13 -0400 Subject: [PATCH 02/12] Don't send null values w/ resource body in 'create'/'update'. Found during live testing. --- gcloud/bigquery/dataset.py | 20 +++++++++++++++----- gcloud/bigquery/test_dataset.py | 20 ++++++++------------ 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/gcloud/bigquery/dataset.py b/gcloud/bigquery/dataset.py index d2570ab54a80..ba23406a4031 100644 --- a/gcloud/bigquery/dataset.py +++ b/gcloud/bigquery/dataset.py @@ -194,14 +194,24 @@ def _set_properties(self, api_response): def _build_resource(self): """Generate a resource for ``create`` or ``update``.""" - return { + resource = { 'datasetReference': { 'projectId': self.project, 'datasetId': self.name}, - 'defaultTableExpirationMs': self.default_table_expiration_ms, - 'description': self.description, - 'friendlyName': self.friendly_name, - 'location': self.location, } + if self.default_table_expiration_ms is not None: + value = self.default_table_expiration_ms + resource['defaultTableExpirationMs'] = value + + if self.description is not None: + resource['description'] = self.description + + if self.friendly_name is not None: + resource['friendlyName'] = self.friendly_name + + if self.location is not None: + resource['location'] = self.location + + return resource def create(self, client=None): """API call: create the dataset via a PUT request diff --git a/gcloud/bigquery/test_dataset.py b/gcloud/bigquery/test_dataset.py index 94bdf6338ea3..19dccf3f2292 100644 --- a/gcloud/bigquery/test_dataset.py +++ b/gcloud/bigquery/test_dataset.py @@ -144,22 +144,24 @@ def test_create_w_bound_client(self): SENT = { 'datasetReference': {'projectId': self.PROJECT, 'datasetId': self.DS_NAME}, - 'defaultTableExpirationMs': None, - 'description': None, - 'friendlyName': None, - 'location': None, } self.assertEqual(req['data'], SENT) self._verifyResourceProperties(dataset, RESOURCE) def test_create_w_alternate_client(self): PATH = 'projects/%s/datasets' % self.PROJECT + DESCRIPTION = 'DESCRIPTION' + TITLE = 'TITLE' RESOURCE = self._makeResource() + RESOURCE['description'] = DESCRIPTION + RESOURCE['friendlyName'] = TITLE conn1 = _Connection() CLIENT1 = _Client(project=self.PROJECT, connection=conn1) conn2 = _Connection(RESOURCE) CLIENT2 = _Client(project=self.PROJECT, connection=conn2) dataset = self._makeOne(self.DS_NAME, client=CLIENT1) + dataset.friendly_name = TITLE + dataset.description = DESCRIPTION dataset.create(client=CLIENT2) @@ -171,10 +173,8 @@ def test_create_w_alternate_client(self): SENT = { 'datasetReference': {'projectId': self.PROJECT, 'datasetId': self.DS_NAME}, - 'defaultTableExpirationMs': None, - 'description': None, - 'friendlyName': None, - 'location': None, + 'description': DESCRIPTION, + 'friendlyName': TITLE, } self.assertEqual(req['data'], SENT) self._verifyResourceProperties(dataset, RESOURCE) @@ -324,10 +324,8 @@ def test_update_w_bound_client(self): SENT = { 'datasetReference': {'projectId': self.PROJECT, 'datasetId': self.DS_NAME}, - 'defaultTableExpirationMs': None, 'description': DESCRIPTION, 'friendlyName': TITLE, - 'location': None, } self.assertEqual(req['data'], SENT) self.assertEqual(req['path'], '/%s' % PATH) @@ -359,8 +357,6 @@ def test_update_w_alternate_client(self): 'datasetReference': {'projectId': self.PROJECT, 'datasetId': self.DS_NAME}, 'defaultTableExpirationMs': 12345, - 'description': None, - 'friendlyName': None, 'location': 'EU', } self.assertEqual(req['data'], SENT) From 5e3fd0c46223c27cc18004a76461c29714647c93 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 27 Jul 2015 13:18:22 -0400 Subject: [PATCH 03/12] Use words for native types. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/1018#discussion-diff-35472493 --- gcloud/bigquery/dataset.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/gcloud/bigquery/dataset.py b/gcloud/bigquery/dataset.py index ba23406a4031..b1d7cc2663d6 100644 --- a/gcloud/bigquery/dataset.py +++ b/gcloud/bigquery/dataset.py @@ -62,7 +62,7 @@ def created(self): def dataset_id(self): """ID for the dataset resource. - :rtype: ``str``, or ``NoneType`` + :rtype: string, or ``NoneType`` """ return self._properties.get('id') @@ -70,7 +70,7 @@ def dataset_id(self): def etag(self): """ETag for the dataset resource. - :rtype: ``str``, or ``NoneType`` + :rtype: string, or ``NoneType`` """ return self._properties.get('etag') @@ -86,7 +86,7 @@ def modified(self): def self_link(self): """URL for the dataset resource. - :rtype: ``str``, or ``NoneType`` + :rtype: string, or ``NoneType`` """ return self._properties.get('selfLink') @@ -94,7 +94,7 @@ def self_link(self): def default_table_expiration_ms(self): """Default expiration time for tables in the dataset. - :rtype: ``int``, or ``NoneType`` + :rtype: integer, or ``NoneType`` """ return self._properties.get('defaultTableExpirationMs') @@ -102,7 +102,7 @@ def default_table_expiration_ms(self): def default_table_expiration_ms(self, value): """Update default expiration time for tables in the dataset. - :type value: ``int``, or ``NoneType`` + :type value: integer, or ``NoneType`` :param value: new default time, in milliseconds """ if not isinstance(value, int) and value is not None: @@ -113,7 +113,7 @@ def default_table_expiration_ms(self, value): def description(self): """Description of the dataset. - :rtype: ``str``, or ``NoneType`` + :rtype: string, or ``NoneType`` """ return self._properties.get('description') @@ -121,7 +121,7 @@ def description(self): def description(self, value): """Update description of the dataset. - :type value: ``str``, or ``NoneType`` + :type value: string, or ``NoneType`` :param value: new description """ if not isinstance(value, str) and value is not None: @@ -132,7 +132,7 @@ def description(self, value): def friendly_name(self): """Title of the dataset. - :rtype: ``str``, or ``NoneType`` + :rtype: string, or ``NoneType`` """ return self._properties.get('friendlyName') @@ -140,7 +140,7 @@ def friendly_name(self): def friendly_name(self, value): """Update title of the dataset. - :type value: ``str``, or ``NoneType`` + :type value: string, or ``NoneType`` :param value: new title """ if not isinstance(value, str) and value is not None: @@ -151,7 +151,7 @@ def friendly_name(self, value): def location(self): """Location in which the dataset is hosted. - :rtype: ``str``, or ``NoneType`` + :rtype: string, or ``NoneType`` """ return self._properties.get('location') @@ -159,7 +159,7 @@ def location(self): def location(self, value): """Update location in which the dataset is hosted. - :type value: ``str``, or ``NoneType`` + :type value: string, or ``NoneType`` :param value: new location """ if not isinstance(value, str) and value is not None: From 6fdf1efe430d541c5c140c279449f89efbea2a30 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 27 Jul 2015 13:20:52 -0400 Subject: [PATCH 04/12] Add an error message for ValueErrors. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/1018#discussion_r35472574 https://github.com/GoogleCloudPlatform/gcloud-python/pull/1018#discussion_r35472627 --- gcloud/bigquery/dataset.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gcloud/bigquery/dataset.py b/gcloud/bigquery/dataset.py index b1d7cc2663d6..a8b905f680ca 100644 --- a/gcloud/bigquery/dataset.py +++ b/gcloud/bigquery/dataset.py @@ -106,7 +106,7 @@ def default_table_expiration_ms(self, value): :param value: new default time, in milliseconds """ if not isinstance(value, int) and value is not None: - raise ValueError() + raise ValueError("Pass an integer, or None") self._properties['defaultTableExpirationMs'] = value @property @@ -125,7 +125,7 @@ def description(self, value): :param value: new description """ if not isinstance(value, str) and value is not None: - raise ValueError() + raise ValueError("Pass a string, or None") self._properties['description'] = value @property @@ -144,7 +144,7 @@ def friendly_name(self, value): :param value: new title """ if not isinstance(value, str) and value is not None: - raise ValueError() + raise ValueError("Pass a string, or None") self._properties['friendlyName'] = value @property @@ -163,7 +163,7 @@ def location(self, value): :param value: new location """ if not isinstance(value, str) and value is not None: - raise ValueError() + raise ValueError("Pass a string, or None") self._properties['location'] = value def _require_client(self, client): @@ -284,7 +284,7 @@ def patch(self, client=None, **kw): if 'default_table_expiration_ms' in kw: value = kw['default_table_expiration_ms'] if not isinstance(value, int) and value is not None: - raise ValueError() + raise ValueError("Pass an integer, or None") partial['defaultTableExpirationMs'] = value if 'description' in kw: From f5015c47a7e74cd321bb6de3c39059eb3b98131e Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 27 Jul 2015 13:22:44 -0400 Subject: [PATCH 05/12] Add ':raises' docs. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/1018#discussion_r35472582. --- gcloud/bigquery/dataset.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/gcloud/bigquery/dataset.py b/gcloud/bigquery/dataset.py index a8b905f680ca..c4d3ef99d2e0 100644 --- a/gcloud/bigquery/dataset.py +++ b/gcloud/bigquery/dataset.py @@ -104,6 +104,8 @@ def default_table_expiration_ms(self, value): :type value: integer, or ``NoneType`` :param value: new default time, in milliseconds + + :raises: ValueError for invalid value types. """ if not isinstance(value, int) and value is not None: raise ValueError("Pass an integer, or None") @@ -123,6 +125,8 @@ def description(self, value): :type value: string, or ``NoneType`` :param value: new description + + :raises: ValueError for invalid value types. """ if not isinstance(value, str) and value is not None: raise ValueError("Pass a string, or None") @@ -142,6 +146,8 @@ def friendly_name(self, value): :type value: string, or ``NoneType`` :param value: new title + + :raises: ValueError for invalid value types. """ if not isinstance(value, str) and value is not None: raise ValueError("Pass a string, or None") @@ -161,6 +167,8 @@ def location(self, value): :type value: string, or ``NoneType`` :param value: new location + + :raises: ValueError for invalid value types. """ if not isinstance(value, str) and value is not None: raise ValueError("Pass a string, or None") @@ -276,6 +284,8 @@ def patch(self, client=None, **kw): :type kw: ``dict`` :param kw: properties to be patched. + + :raises: ValueError for invalid value types. """ client = self._require_client(client) From 9860e3b57b2cc9b484da781887a78ba8f8da2dc1 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 27 Jul 2015 13:26:12 -0400 Subject: [PATCH 06/12] Use 'six.string_types' for a type check. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/1018#discussion_r35472695 --- gcloud/bigquery/dataset.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gcloud/bigquery/dataset.py b/gcloud/bigquery/dataset.py index c4d3ef99d2e0..5c7122eb647c 100644 --- a/gcloud/bigquery/dataset.py +++ b/gcloud/bigquery/dataset.py @@ -17,6 +17,7 @@ import datetime import pytz +import six from gcloud.exceptions import NotFound @@ -128,7 +129,7 @@ def description(self, value): :raises: ValueError for invalid value types. """ - if not isinstance(value, str) and value is not None: + if not isinstance(value, six.string_types) and value is not None: raise ValueError("Pass a string, or None") self._properties['description'] = value @@ -149,7 +150,7 @@ def friendly_name(self, value): :raises: ValueError for invalid value types. """ - if not isinstance(value, str) and value is not None: + if not isinstance(value, six.string_types) and value is not None: raise ValueError("Pass a string, or None") self._properties['friendlyName'] = value @@ -170,7 +171,7 @@ def location(self, value): :raises: ValueError for invalid value types. """ - if not isinstance(value, str) and value is not None: + if not isinstance(value, six.string_types) and value is not None: raise ValueError("Pass a string, or None") self._properties['location'] = value From ff626b790743e30de15bbaf3bb32ab8b5a33e1cd Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 28 Jul 2015 01:57:33 -0400 Subject: [PATCH 07/12] Replace overlooked str w/ 'string'. --- gcloud/bigquery/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcloud/bigquery/client.py b/gcloud/bigquery/client.py index 6f0235ba2a17..0079e8c5142b 100644 --- a/gcloud/bigquery/client.py +++ b/gcloud/bigquery/client.py @@ -46,7 +46,7 @@ class Client(JSONClient): def dataset(self, name): """Construct a dataset bound to this client. - :type name: ``str`` + :type name: string :param name: Name of the dataset. :rtype: :class:`gcloud.bigquery.dataset.Dataset` From 43bafba08a3c14c803718ac3c3808fdced8d28f1 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 28 Jul 2015 12:12:37 -0400 Subject: [PATCH 08/12] Restrict requested fields in 'Dataset.exists' API request. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/1018#discussion_r35473028 --- gcloud/bigquery/dataset.py | 3 ++- gcloud/bigquery/test_dataset.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/gcloud/bigquery/dataset.py b/gcloud/bigquery/dataset.py index 5c7122eb647c..9a40eb882646 100644 --- a/gcloud/bigquery/dataset.py +++ b/gcloud/bigquery/dataset.py @@ -251,7 +251,8 @@ def exists(self, client=None): client = self._require_client(client) try: - client.connection.api_request(method='GET', path=self.path) + client.connection.api_request(method='GET', path=self.path, + query_params={'fields': 'id'}) except NotFound: return False else: diff --git a/gcloud/bigquery/test_dataset.py b/gcloud/bigquery/test_dataset.py index 19dccf3f2292..d15e9d751712 100644 --- a/gcloud/bigquery/test_dataset.py +++ b/gcloud/bigquery/test_dataset.py @@ -191,6 +191,7 @@ def test_exists_miss_w_bound_client(self): req = conn._requested[0] self.assertEqual(req['method'], 'GET') self.assertEqual(req['path'], '/%s' % PATH) + self.assertEqual(req['query_params'], {'fields': 'id'}) def test_exists_hit_w_alternate_client(self): PATH = 'projects/%s/datasets/%s' % (self.PROJECT, self.DS_NAME) @@ -207,6 +208,7 @@ def test_exists_hit_w_alternate_client(self): req = conn2._requested[0] self.assertEqual(req['method'], 'GET') self.assertEqual(req['path'], '/%s' % PATH) + self.assertEqual(req['query_params'], {'fields': 'id'}) def test_reload_w_bound_client(self): PATH = 'projects/%s/datasets/%s' % (self.PROJECT, self.DS_NAME) From 9b2734b17faa4f23aec0f1ca4a96d5ed90ab65c1 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 28 Jul 2015 12:15:41 -0400 Subject: [PATCH 09/12] Use 'six.integer_types' vs. 'int' for type check. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/1018#discussion_r35473159 --- gcloud/bigquery/dataset.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gcloud/bigquery/dataset.py b/gcloud/bigquery/dataset.py index 9a40eb882646..fb1f92aee701 100644 --- a/gcloud/bigquery/dataset.py +++ b/gcloud/bigquery/dataset.py @@ -108,7 +108,7 @@ def default_table_expiration_ms(self, value): :raises: ValueError for invalid value types. """ - if not isinstance(value, int) and value is not None: + if not isinstance(value, six.integer_types) and value is not None: raise ValueError("Pass an integer, or None") self._properties['defaultTableExpirationMs'] = value @@ -295,7 +295,7 @@ def patch(self, client=None, **kw): if 'default_table_expiration_ms' in kw: value = kw['default_table_expiration_ms'] - if not isinstance(value, int) and value is not None: + if not isinstance(value, six.integer_types) and value is not None: raise ValueError("Pass an integer, or None") partial['defaultTableExpirationMs'] = value From b84696851b497553b7f836bc8243ef1dc1fd9b0d Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 28 Jul 2015 12:17:56 -0400 Subject: [PATCH 10/12] Note rationale for division of back-end's timestamp by 1000.0. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/1018#discussion_r35473332. --- gcloud/bigquery/dataset.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gcloud/bigquery/dataset.py b/gcloud/bigquery/dataset.py index fb1f92aee701..567f6f46fdc3 100644 --- a/gcloud/bigquery/dataset.py +++ b/gcloud/bigquery/dataset.py @@ -347,5 +347,6 @@ def _datetime_from_prop(value): :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) From 538aa92bb6ebc167780849b8aedebd4629852f77 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 28 Jul 2015 12:23:05 -0400 Subject: [PATCH 11/12] Ensure property getters have ':rtype:'/':returns:' in docstrings. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/1018#discussion_r35611802. --- gcloud/bigquery/dataset.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/gcloud/bigquery/dataset.py b/gcloud/bigquery/dataset.py index 567f6f46fdc3..681763676195 100644 --- a/gcloud/bigquery/dataset.py +++ b/gcloud/bigquery/dataset.py @@ -43,12 +43,20 @@ def __init__(self, name, client): @property def project(self): - """Project bound to the dataset.""" + """Project bound to the dataset. + + :rtype: string + :returns: the project (derived from the client). + """ return self._client.project @property def path(self): - """URL path for the dataset's APIs""" + """URL path for the dataset's APIs. + + :rtype: string + :returns: the path based on project and dataste name. + """ return '/projects/%s/datasets/%s' % (self.project, self.name) @property @@ -56,6 +64,7 @@ def created(self): """Datetime at which the dataset 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')) @@ -64,6 +73,7 @@ def dataset_id(self): """ID for the dataset resource. :rtype: string, or ``NoneType`` + :returns: the ID (None until set from the server). """ return self._properties.get('id') @@ -72,6 +82,7 @@ def etag(self): """ETag for the dataset resource. :rtype: string, or ``NoneType`` + :returns: the ETag (None until set from the server). """ return self._properties.get('etag') @@ -80,6 +91,7 @@ def modified(self): """Datetime at which the dataset 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')) @@ -88,6 +100,7 @@ def self_link(self): """URL for the dataset resource. :rtype: string, or ``NoneType`` + :returns: the URL (None until set from the server). """ return self._properties.get('selfLink') @@ -96,6 +109,7 @@ def default_table_expiration_ms(self): """Default expiration time for tables in the dataset. :rtype: integer, or ``NoneType`` + :returns: The time in milliseconds, or None (the default). """ return self._properties.get('defaultTableExpirationMs') @@ -117,6 +131,7 @@ def description(self): """Description of the dataset. :rtype: string, or ``NoneType`` + :returns: The description as set by the user, or None (the default). """ return self._properties.get('description') @@ -138,6 +153,7 @@ def friendly_name(self): """Title of the dataset. :rtype: string, or ``NoneType`` + :returns: The name as set by the user, or None (the default). """ return self._properties.get('friendlyName') @@ -159,6 +175,7 @@ def location(self): """Location in which the dataset is hosted. :rtype: string, or ``NoneType`` + :returns: The location as set by the user, or None (the default). """ return self._properties.get('location') From 81c05b7c1890e807fdc34eb180bb194325126d61 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 28 Jul 2015 12:56:31 -0400 Subject: [PATCH 12/12] Adhere to @dhermes' sensibility about formatting string usage. --- gcloud/bigquery/dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcloud/bigquery/dataset.py b/gcloud/bigquery/dataset.py index 681763676195..e417cfcb398a 100644 --- a/gcloud/bigquery/dataset.py +++ b/gcloud/bigquery/dataset.py @@ -250,7 +250,7 @@ def create(self, client=None): ``client`` stored on the current dataset. """ client = self._require_client(client) - path = '/projects/%s/datasets' % self.project + path = '/projects/%s/datasets' % (self.project,) api_response = client.connection.api_request( method='POST', path=path, data=self._build_resource()) self._set_properties(api_response)