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..0079e8c5142b 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: string + :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..e417cfcb398a --- /dev/null +++ b/gcloud/bigquery/dataset.py @@ -0,0 +1,369 @@ +# 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 +import six + +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. + + :rtype: string + :returns: the project (derived from the client). + """ + return self._client.project + + @property + def path(self): + """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 + 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')) + + @property + 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') + + @property + 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') + + @property + 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')) + + @property + 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') + + @property + 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') + + @default_table_expiration_ms.setter + def default_table_expiration_ms(self, value): + """Update default expiration time for tables in the dataset. + + :type value: integer, or ``NoneType`` + :param value: new default time, in milliseconds + + :raises: ValueError for invalid value types. + """ + if not isinstance(value, six.integer_types) and value is not None: + raise ValueError("Pass an integer, or None") + self._properties['defaultTableExpirationMs'] = value + + @property + 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') + + @description.setter + def description(self, value): + """Update description of the dataset. + + :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 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') + + @friendly_name.setter + def friendly_name(self, value): + """Update title of the dataset. + + :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 dataset 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 dataset 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 + + 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``.""" + resource = { + 'datasetReference': { + 'projectId': self.project, 'datasetId': self.name}, + } + 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 + + 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, + query_params={'fields': 'id'}) + 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. + + :raises: ValueError for invalid value types. + """ + client = self._require_client(client) + + partial = {} + + if 'default_table_expiration_ms' in kw: + value = kw['default_table_expiration_ms'] + if not isinstance(value, six.integer_types) and value is not None: + raise ValueError("Pass an integer, or None") + 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: + # 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/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..d15e9d751712 --- /dev/null +++ b/gcloud/bigquery/test_dataset.py @@ -0,0 +1,419 @@ +# 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}, + } + 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) + + 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}, + 'description': DESCRIPTION, + 'friendlyName': TITLE, + } + 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) + 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) + 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) + self.assertEqual(req['query_params'], {'fields': 'id'}) + + 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}, + 'description': DESCRIPTION, + 'friendlyName': TITLE, + } + 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, + '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