From a33aad772606adf9f9e18e4b3a15966bba087626 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 1 Aug 2017 16:41:39 -0400 Subject: [PATCH 1/3] Add 'update' API wrapper for buckets/blobs. Turns out some properties (i.e., 'labels', see #3711) behave differently under 'patch semantics'[1], which makes 'update' useful. [1] https://cloud.google.com/storage/docs/json_api/v1/how-tos/performance#patch --- storage/google/cloud/storage/_helpers.py | 16 ++++++++++++++++ storage/tests/unit/test__helpers.py | 20 ++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/storage/google/cloud/storage/_helpers.py b/storage/google/cloud/storage/_helpers.py index 88f9b8dc0ca7..56a75c684f4c 100644 --- a/storage/google/cloud/storage/_helpers.py +++ b/storage/google/cloud/storage/_helpers.py @@ -147,6 +147,22 @@ def patch(self, client=None): query_params={'projection': 'full'}, _target_object=self) self._set_properties(api_response) + def update(self, client=None): + """Sends all properties in a PUT request. + + Updates the ``_properties`` with the response from the backend. + + :type client: :class:`~google.cloud.storage.client.Client` or + ``NoneType`` + :param client: the client to use. If not passed, falls back to the + ``client`` stored on the current object. + """ + client = self._require_client(client) + api_response = client._connection.api_request( + method='PUT', path=self.path, data=self._properties, + query_params={'projection': 'full'}, _target_object=self) + self._set_properties(api_response) + def _scalar_property(fieldname): """Create a property descriptor around the :class:`_PropertyMixin` helpers. diff --git a/storage/tests/unit/test__helpers.py b/storage/tests/unit/test__helpers.py index 89967f3a0db0..90def4867268 100644 --- a/storage/tests/unit/test__helpers.py +++ b/storage/tests/unit/test__helpers.py @@ -95,6 +95,26 @@ def test_patch(self): # Make sure changes get reset by patch(). self.assertEqual(derived._changes, set()) + def test_update(self): + connection = _Connection({'foo': 'Foo'}) + client = _Client(connection) + derived = self._derivedClass('/path')() + # Make sure changes is non-empty, so we can observe a change. + BAR = object() + BAZ = object() + derived._properties = {'bar': BAR, 'baz': BAZ} + derived._changes = set(['bar']) # Update sends 'baz' anyway. + derived.update(client=client) + self.assertEqual(derived._properties, {'foo': 'Foo'}) + kw = connection._requested + self.assertEqual(len(kw), 1) + self.assertEqual(kw[0]['method'], 'PUT') + self.assertEqual(kw[0]['path'], '/path') + self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) + self.assertEqual(kw[0]['data'], {'bar': BAR, 'baz': BAZ}) + # Make sure changes get reset by patch(). + self.assertEqual(derived._changes, set()) + class Test__scalar_property(unittest.TestCase): From 0dfe7270976830e7c2b3680361fd6c40720b9a2c Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 1 Aug 2017 15:56:27 -0400 Subject: [PATCH 2/3] Add system test of updating/deleting bucket labels. Note that the test uses the new 'Bucket.update' method, rather than 'Bucket.patch', because patch semantics would require that we remember deleted labels (in order to pass 'null' as their value). Closes #7311 --- storage/tests/system.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/storage/tests/system.py b/storage/tests/system.py index a89c45edbf25..bc8169c356b3 100644 --- a/storage/tests/system.py +++ b/storage/tests/system.py @@ -114,6 +114,26 @@ def test_list_buckets(self): if bucket.name in buckets_to_create] self.assertEqual(len(created_buckets), len(buckets_to_create)) + def test_bucket_update_labels(self): + bucket_name = 'update-labels' + unique_resource_id('-') + bucket = retry_429(Config.CLIENT.create_bucket)(bucket_name) + self.case_buckets_to_delete.append(bucket_name) + self.assertTrue(bucket.exists()) + + updated_labels = {'test-label': 'label-value'} + bucket.labels = updated_labels + bucket.update() + self.assertEqual(bucket.labels, updated_labels) + + new_labels = {'another-label': 'another-value'} + bucket.labels = new_labels + bucket.update() + self.assertEqual(bucket.labels, new_labels) + + bucket.labels = {} + bucket.update() + self.assertEqual(bucket.labels, {}) + class TestStorageFiles(unittest.TestCase): From 30a04b655bdcdd880f59b7125afd18fe99095dee Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 1 Aug 2017 17:27:51 -0400 Subject: [PATCH 3/3] Extend snippets for updating 'cors'/'labels'/'lifecycle_rules'. Show using 'bucket.update()' to avoid running into patch semantics. --- storage/google/cloud/storage/bucket.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/storage/google/cloud/storage/bucket.py b/storage/google/cloud/storage/bucket.py index f9ff7219f4b8..06550b09ffcb 100644 --- a/storage/google/cloud/storage/bucket.py +++ b/storage/google/cloud/storage/bucket.py @@ -556,6 +556,7 @@ def cors(self): >>> policies[1]['maxAgeSeconds'] = 3600 >>> del policies[0] >>> bucket.cors = policies + >>> bucket.update() :setter: Set CORS policies for this bucket. :getter: Gets the CORS policies for this bucket. @@ -595,6 +596,7 @@ def labels(self): >>> labels['new_key'] = 'some-label' >>> del labels['old_key'] >>> bucket.labels = labels + >>> bucket.update() :setter: Set labels for this bucket. :getter: Gets the labels for this bucket. @@ -663,6 +665,7 @@ def lifecycle_rules(self): >>> rules[1]['rule']['action']['type'] = 'Delete' >>> del rules[0] >>> bucket.lifecycle_rules = rules + >>> bucket.update() :setter: Set lifestyle rules for this bucket. :getter: Gets the lifestyle rules for this bucket.