From e8c4391e0cc3c8bf94054a50a74aaa6fa99e029f Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Thu, 6 Nov 2014 11:47:08 -0500 Subject: [PATCH 1/6] Avoid re-iterating summary line. --- gcloud/storage/bucket.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/gcloud/storage/bucket.py b/gcloud/storage/bucket.py index 4d553a45c6ca..7dac8d91cbeb 100644 --- a/gcloud/storage/bucket.py +++ b/gcloud/storage/bucket.py @@ -400,7 +400,6 @@ def etag(self): https://cloud.google.com/storage/docs/json_api/v1/buckets :rtype: string - :returns: a unique identifier for the bucket and current metadata. """ return self.properties['etag'] @@ -411,7 +410,6 @@ def id(self): See: https://cloud.google.com/storage/docs/json_api/v1/buckets :rtype: string - :returns: a unique identifier for the bucket. """ return self.properties['id'] @@ -501,8 +499,6 @@ def metageneration(self): See: https://cloud.google.com/storage/docs/json_api/v1/buckets :rtype: integer - :returns: count of times since creation the bucket's metadata has - been updated. """ return self.properties['metageneration'] @@ -524,7 +520,6 @@ def project_number(self): See: https://cloud.google.com/storage/docs/json_api/v1/buckets :rtype: integer - :returns: a unique identifier for the bucket. """ return self.properties['projectNumber'] @@ -535,7 +530,6 @@ def self_link(self): See: https://cloud.google.com/storage/docs/json_api/v1/buckets :rtype: string - :returns: URI of the bucket. """ return self.properties['selfLink'] @@ -547,8 +541,7 @@ def storage_class(self): https://cloud.google.com/storage/docs/durable-reduced-availability :rtype: string - :returns: the storage class for the bucket (currently one of - ``STANDARD``, ``DURABLE_REDUCED_AVAILABILITY``) + :returns: Currently one of "STANDARD", "DURABLE_REDUCED_AVAILABILITY" """ return self.properties['storageClass'] @@ -559,7 +552,7 @@ def time_created(self): See: https://cloud.google.com/storage/docs/json_api/v1/buckets :rtype: string - :returns: timestamp for the bucket's creation, in RFC 3339 format. + :returns: timestamp in RFC 3339 format. """ return self.properties['timeCreated'] From bafe213c7c741550bf738cc69b16cc5d729f83c6 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Thu, 6 Nov 2014 11:47:55 -0500 Subject: [PATCH 2/6] Add accessors for read-only properties of a server-side object. Addresses part of #313. --- gcloud/storage/key.py | 140 +++++++++++++++++++++++++++++++++++++ gcloud/storage/test_key.py | 110 +++++++++++++++++++++++++++++ 2 files changed, 250 insertions(+) diff --git a/gcloud/storage/key.py b/gcloud/storage/key.py index 1c09d5c7fef1..964dfcf9f826 100644 --- a/gcloud/storage/key.py +++ b/gcloud/storage/key.py @@ -15,6 +15,19 @@ class Key(_PropertyMixin): CUSTOM_PROPERTY_ACCESSORS = { 'acl': 'get_acl()', + 'componentCount': 'component_count', + 'etag': 'etag', + 'generation': 'generation', + 'id': 'id', + 'mediaLink': 'media_link', + 'metageneration': 'metageneration', + 'name': 'name', + 'owner': 'owner', + 'selfLink': 'self_link', + 'size': 'size', + 'storageClass': 'storage_class', + 'timeDeleted': 'time_deleted', + 'updated': 'updated', } """Map field name -> accessor for fields w/ custom accessors.""" @@ -359,6 +372,133 @@ def make_public(self): self.acl.save() return self + @property + def component_count(self): + """Number of underlying components that make up this object. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: integer + """ + return self.properties['componentCount'] + + @property + def etag(self): + """Retrieve the ETag for the object. + + See: http://tools.ietf.org/html/rfc2616#section-3.11 and + https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + """ + return self.properties['etag'] + + @property + def generation(self): + """Retrieve the generation for the object. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: integer + """ + return self.properties['generation'] + + @property + def id(self): + """Retrieve the ID for the object. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + """ + return self.properties['id'] + + @property + def media_link(self): + """Retrieve the media download URI for the object. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + """ + return self.properties['selfLink'] + + @property + def metageneration(self): + """Retrieve the metageneration for the object. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: integer + """ + return self.properties['metageneration'] + + @property + def owner(self): + """Retrieve info about the owner of the object. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: dict + :returns: mapping of owner's role/ID. + """ + return self.properties['owner'].copy() + + @property + def self_link(self): + """Retrieve the URI for the object. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + """ + return self.properties['selfLink'] + + @property + def size(self): + """Size of the object, in bytes. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: integer + """ + return self.properties['size'] + + @property + def storage_class(self): + """Retrieve the storage class for the object. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects and + https://cloud.google.com/storage/docs/durable-reduced-availability#_DRA_Bucket + + :rtype: string + :returns: Currently one of "STANDARD", "DURABLE_REDUCED_AVAILABILITY" + """ + return self.properties['storageClass'] + + @property + def time_deleted(self): + """Retrieve the timestamp at which the object was deleted. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string or None + :returns: timestamp in RFC 3339 format, or None if the object + has a "live" version. + """ + return self.properties.get('timeDeleted') + + @property + def updated(self): + """Retrieve the timestamp at which the object was updated. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + :returns: timestamp in RFC 3339 format. + """ + return self.properties['updated'] + class _KeyIterator(Iterator): """An iterator listing keys. diff --git a/gcloud/storage/test_key.py b/gcloud/storage/test_key.py index 7fdeb2733efe..ba1e6cea6f97 100644 --- a/gcloud/storage/test_key.py +++ b/gcloud/storage/test_key.py @@ -344,6 +344,116 @@ def test_make_public(self): self.assertEqual(kw[0]['data'], {'acl': permissive}) self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) + def test_component_count(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + COMPONENT_COUNT = 42 + properties = {'componentCount': COMPONENT_COUNT} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.component_count, COMPONENT_COUNT) + + def test_etag(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + ETAG = 'ETAG' + properties = {'etag': ETAG} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.etag, ETAG) + + def test_generation(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + GENERATION = 42 + properties = {'generation': GENERATION} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.generation, GENERATION) + + def test_id(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + ID = 'ID' + properties = {'id': ID} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.id, ID) + + def test_media_link(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + MEDIA_LINK = 'http://example.com/media/' + properties = {'selfLink': MEDIA_LINK} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.media_link, MEDIA_LINK) + + def test_metageneration(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + METAGENERATION = 42 + properties = {'metageneration': METAGENERATION} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.metageneration, METAGENERATION) + + def test_owner(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + OWNER = {'entity': 'project-owner-12345', 'entityId': '23456'} + properties = {'owner': OWNER} + key = self._makeOne(bucket, KEY, properties) + owner = key.owner + self.assertEqual(owner['entity'], 'project-owner-12345') + self.assertEqual(owner['entityId'], '23456') + + def test_self_link(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + SELF_LINK = 'http://example.com/self/' + properties = {'selfLink': SELF_LINK} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.self_link, SELF_LINK) + + def test_size(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + SIZE = 42 + properties = {'size': SIZE} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.size, SIZE) + + def test_storage_class(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + STORAGE_CLASS = 'http://example.com/self/' + properties = {'storageClass': STORAGE_CLASS} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.storage_class, STORAGE_CLASS) + + def test_time_deleted(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + TIME_DELETED = '2014-11-05T20:34:37Z' + properties = {'timeDeleted': TIME_DELETED} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.time_deleted, TIME_DELETED) + + def test_updated(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + UPDATED = '2014-11-05T20:34:37Z' + properties = {'updated': UPDATED} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.updated, UPDATED) + class Test__KeyIterator(unittest2.TestCase): From 2b26990887b15729f44f2e7fa870ef2e6d31c1bb Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Thu, 6 Nov 2014 12:35:19 -0500 Subject: [PATCH 3/6] Add writable 'Key' properties matching writable server-side properties. Addresses part of #314. --- gcloud/storage/key.py | 182 +++++++++++++++++++++++++++++++- gcloud/storage/test_key.py | 209 ++++++++++++++++++++++++++++++++++++- 2 files changed, 389 insertions(+), 2 deletions(-) diff --git a/gcloud/storage/key.py b/gcloud/storage/key.py index 964dfcf9f826..7fa8441f12bd 100644 --- a/gcloud/storage/key.py +++ b/gcloud/storage/key.py @@ -1,5 +1,6 @@ """Create / interact with gcloud storage keys.""" +import copy import mimetypes import os from StringIO import StringIO @@ -15,6 +16,11 @@ class Key(_PropertyMixin): CUSTOM_PROPERTY_ACCESSORS = { 'acl': 'get_acl()', + 'cacheControl': 'cache_control', + 'contentDisposition': 'content_disposition', + 'contentEncoding': 'content_encoding', + 'contentLanguage': 'content_language', + 'contentType': 'content_type', 'componentCount': 'component_count', 'etag': 'etag', 'generation': 'generation', @@ -372,6 +378,138 @@ def make_public(self): self.acl.save() return self + @property + def cache_control(self): + """Retrieve HTTP 'Cache-Control' header for this object. + + See: https://tools.ietf.org/html/rfc7234#section-5.2 and + https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + """ + return self.properties['cacheControl'] + + @cache_control.setter + def cache_control(self, value): + """Update HTTP 'Cache-Control' header for this object. + + See: https://tools.ietf.org/html/rfc7234#section-5.2 and + https://cloud.google.com/storage/docs/json_api/v1/objects + + :type value: string + """ + self._patch_properties({'cacheControl': value}) + + @property + def content_disposition(self): + """Retrieve HTTP 'Content-Disposition' header for this object. + + See: https://tools.ietf.org/html/rfc6266 and + https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + """ + return self.properties['contentDisposition'] + + @content_disposition.setter + def content_disposition(self, value): + """Update HTTP 'Content-Disposition' header for this object. + + See: https://tools.ietf.org/html/rfc6266 and + https://cloud.google.com/storage/docs/json_api/v1/objects + + :type value: string + """ + self._patch_properties({'contentDisposition': value}) + + @property + def content_encoding(self): + """Retrieve HTTP 'Content-Encoding' header for this object. + + See: https://tools.ietf.org/html/rfc7231#section-3.1.2.2 and + https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + """ + return self.properties['contentEncoding'] + + @content_encoding.setter + def content_encoding(self, value): + """Update HTTP 'Content-Encoding' header for this object. + + See: https://tools.ietf.org/html/rfc7231#section-3.1.2.2 and + https://cloud.google.com/storage/docs/json_api/v1/objects + + :type value: string + """ + self._patch_properties({'contentEncoding': value}) + + @property + def content_language(self): + """Retrieve HTTP 'Content-Language' header for this object. + + See: http://tools.ietf.org/html/bcp47 and + https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + """ + return self.properties['contentLanguage'] + + @content_language.setter + def content_language(self, value): + """Update HTTP 'Content-Language' header for this object. + + See: http://tools.ietf.org/html/bcp47 and + https://cloud.google.com/storage/docs/json_api/v1/objects + + :type value: string + """ + self._patch_properties({'contentLanguage': value}) + + @property + def content_type(self): + """Retrieve HTTP 'Content-Type' header for this object. + + See: https://tools.ietf.org/html/rfc2616#section-14.17 and + https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + """ + return self.properties['contentType'] + + @content_type.setter + def content_type(self, value): + """Update HTTP 'Content-Type' header for this object. + + See: https://tools.ietf.org/html/rfc2616#section-14.17 and + https://cloud.google.com/storage/docs/json_api/v1/objects + + :type value: string + """ + self._patch_properties({'contentType': value}) + + @property + def crc32c(self): + """Retrieve CRC32C checksum for this object. + + See: http://tools.ietf.org/html/rfc4960#appendix-B and + https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + """ + return self.properties['crc32c'] + + @crc32c.setter + def crc32c(self, value): + """Update CRC32C checksum for this object. + + See: http://tools.ietf.org/html/rfc4960#appendix-B and + https://cloud.google.com/storage/docs/json_api/v1/objects + + :type value: string + """ + self._patch_properties({'crc32c': value}) + @property def component_count(self): """Number of underlying components that make up this object. @@ -413,6 +551,28 @@ def id(self): """ return self.properties['id'] + @property + def md5_hash(self): + """Retrieve MD5 hash for this object. + + See: http://tools.ietf.org/html/rfc4960#appendix-B and + https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: string + """ + return self.properties['md5Hash'] + + @md5_hash.setter + def md5_hash(self, value): + """Update MD5 hash for this object. + + See: http://tools.ietf.org/html/rfc4960#appendix-B and + https://cloud.google.com/storage/docs/json_api/v1/objects + + :type value: string + """ + self._patch_properties({'md5Hash': value}) + @property def media_link(self): """Retrieve the media download URI for the object. @@ -421,7 +581,27 @@ def media_link(self): :rtype: string """ - return self.properties['selfLink'] + return self.properties['mediaLink'] + + @property + def metadata(self): + """Retrieve arbitrary/application specific metadata for the object. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :rtype: dict + """ + return copy.deepcopy(self.properties['metadata']) + + @metadata.setter + def metadata(self, value): + """Update arbitrary/application specific metadata for the object. + + See: https://cloud.google.com/storage/docs/json_api/v1/objects + + :type value: dict + """ + self._patch_properties({'metadata': value}) @property def metageneration(self): diff --git a/gcloud/storage/test_key.py b/gcloud/storage/test_key.py index ba1e6cea6f97..be6e4adbabc7 100644 --- a/gcloud/storage/test_key.py +++ b/gcloud/storage/test_key.py @@ -344,6 +344,31 @@ def test_make_public(self): self.assertEqual(kw[0]['data'], {'acl': permissive}) self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) + def test_cache_control_getter(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + CACHE_CONTROL = 'no-cache' + properties = {'cacheControl': CACHE_CONTROL} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.cache_control, CACHE_CONTROL) + + def test_cache_control_setter(self): + KEY = 'key' + CACHE_CONTROL = 'no-cache' + after = {'cacheControl': CACHE_CONTROL} + connection = _Connection(after) + bucket = _Bucket(connection) + key = self._makeOne(bucket, KEY) + key.cache_control = CACHE_CONTROL + self.assertEqual(key.cache_control, CACHE_CONTROL) + kw = connection._requested + self.assertEqual(len(kw), 1) + self.assertEqual(kw[0]['method'], 'PATCH') + self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY) + self.assertEqual(kw[0]['data'], {'cacheControl': CACHE_CONTROL}) + self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) + def test_component_count(self): KEY = 'key' connection = _Connection() @@ -353,6 +378,136 @@ def test_component_count(self): key = self._makeOne(bucket, KEY, properties) self.assertEqual(key.component_count, COMPONENT_COUNT) + def test_content_disposition_getter(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + CONTENT_DISPOSITION = 'Attachment; filename=example.jpg' + properties = {'contentDisposition': CONTENT_DISPOSITION} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.content_disposition, CONTENT_DISPOSITION) + + def test_content_disposition_setter(self): + KEY = 'key' + CONTENT_DISPOSITION = 'Attachment; filename=example.jpg' + after = {'contentDisposition': CONTENT_DISPOSITION} + connection = _Connection(after) + bucket = _Bucket(connection) + key = self._makeOne(bucket, KEY) + key.content_disposition = CONTENT_DISPOSITION + self.assertEqual(key.content_disposition, CONTENT_DISPOSITION) + kw = connection._requested + self.assertEqual(len(kw), 1) + self.assertEqual(kw[0]['method'], 'PATCH') + self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY) + self.assertEqual(kw[0]['data'], + {'contentDisposition': CONTENT_DISPOSITION}) + self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) + + def test_content_encoding_getter(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + CONTENT_ENCODING = 'gzip' + properties = {'contentEncoding': CONTENT_ENCODING} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.content_encoding, CONTENT_ENCODING) + + def test_content_encoding_setter(self): + KEY = 'key' + CONTENT_ENCODING = 'gzip' + after = {'contentEncoding': CONTENT_ENCODING} + connection = _Connection(after) + bucket = _Bucket(connection) + key = self._makeOne(bucket, KEY) + key.content_encoding = CONTENT_ENCODING + self.assertEqual(key.content_encoding, CONTENT_ENCODING) + kw = connection._requested + self.assertEqual(len(kw), 1) + self.assertEqual(kw[0]['method'], 'PATCH') + self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY) + self.assertEqual(kw[0]['data'], + {'contentEncoding': CONTENT_ENCODING}) + self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) + + def test_content_language_getter(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + CONTENT_LANGUAGE = 'pt-BR' + properties = {'contentLanguage': CONTENT_LANGUAGE} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.content_language, CONTENT_LANGUAGE) + + def test_content_language_setter(self): + KEY = 'key' + CONTENT_LANGUAGE = 'pt-BR' + after = {'contentLanguage': CONTENT_LANGUAGE} + connection = _Connection(after) + bucket = _Bucket(connection) + key = self._makeOne(bucket, KEY) + key.content_language = CONTENT_LANGUAGE + self.assertEqual(key.content_language, CONTENT_LANGUAGE) + kw = connection._requested + self.assertEqual(len(kw), 1) + self.assertEqual(kw[0]['method'], 'PATCH') + self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY) + self.assertEqual(kw[0]['data'], + {'contentLanguage': CONTENT_LANGUAGE}) + self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) + + def test_content_type_getter(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + CONTENT_TYPE = 'image/jpeg' + properties = {'contentType': CONTENT_TYPE} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.content_type, CONTENT_TYPE) + + def test_content_type_setter(self): + KEY = 'key' + CONTENT_TYPE = 'image/jpeg' + after = {'contentType': CONTENT_TYPE} + connection = _Connection(after) + bucket = _Bucket(connection) + key = self._makeOne(bucket, KEY) + key.content_type = CONTENT_TYPE + self.assertEqual(key.content_type, CONTENT_TYPE) + kw = connection._requested + self.assertEqual(len(kw), 1) + self.assertEqual(kw[0]['method'], 'PATCH') + self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY) + self.assertEqual(kw[0]['data'], + {'contentType': CONTENT_TYPE}) + self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) + + def test_crc32c_getter(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + CRC32C = 'DEADBEEF' + properties = {'crc32c': CRC32C} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.crc32c, CRC32C) + + def test_crc32c_setter(self): + KEY = 'key' + CRC32C = 'DEADBEEF' + after = {'crc32c': CRC32C} + connection = _Connection(after) + bucket = _Bucket(connection) + key = self._makeOne(bucket, KEY) + key.crc32c = CRC32C + self.assertEqual(key.crc32c, CRC32C) + kw = connection._requested + self.assertEqual(len(kw), 1) + self.assertEqual(kw[0]['method'], 'PATCH') + self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY) + self.assertEqual(kw[0]['data'], + {'crc32c': CRC32C}) + self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) + def test_etag(self): KEY = 'key' connection = _Connection() @@ -380,15 +535,67 @@ def test_id(self): key = self._makeOne(bucket, KEY, properties) self.assertEqual(key.id, ID) + def test_md5_hash_getter(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + MD5_HASH = 'DEADBEEF' + properties = {'md5Hash': MD5_HASH} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.md5_hash, MD5_HASH) + + def test_md5_hash_setter(self): + KEY = 'key' + MD5_HASH = 'DEADBEEF' + after = {'md5Hash': MD5_HASH} + connection = _Connection(after) + bucket = _Bucket(connection) + key = self._makeOne(bucket, KEY) + key.md5_hash = MD5_HASH + self.assertEqual(key.md5_hash, MD5_HASH) + kw = connection._requested + self.assertEqual(len(kw), 1) + self.assertEqual(kw[0]['method'], 'PATCH') + self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY) + self.assertEqual(kw[0]['data'], + {'md5Hash': MD5_HASH}) + self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) + def test_media_link(self): KEY = 'key' connection = _Connection() bucket = _Bucket(connection) MEDIA_LINK = 'http://example.com/media/' - properties = {'selfLink': MEDIA_LINK} + properties = {'mediaLink': MEDIA_LINK} key = self._makeOne(bucket, KEY, properties) self.assertEqual(key.media_link, MEDIA_LINK) + def test_metadata_getter(self): + KEY = 'key' + connection = _Connection() + bucket = _Bucket(connection) + METADATA = {'foo': 'Foo'} + properties = {'metadata': METADATA} + key = self._makeOne(bucket, KEY, properties) + self.assertEqual(key.metadata, METADATA) + + def test_metadata_setter(self): + KEY = 'key' + METADATA = {'foo': 'Foo'} + after = {'metadata': METADATA} + connection = _Connection(after) + bucket = _Bucket(connection) + key = self._makeOne(bucket, KEY) + key.metadata = METADATA + self.assertEqual(key.metadata, METADATA) + kw = connection._requested + self.assertEqual(len(kw), 1) + self.assertEqual(kw[0]['method'], 'PATCH') + self.assertEqual(kw[0]['path'], '/b/name/o/%s' % KEY) + self.assertEqual(kw[0]['data'], + {'metadata': METADATA}) + self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) + def test_metageneration(self): KEY = 'key' connection = _Connection() From 520de84dd650d8e4d1a27b3cc4ed1df136488d53 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Thu, 6 Nov 2014 18:24:52 -0500 Subject: [PATCH 4/6] Factor out simple scalar property creation using helper methods. --- gcloud/storage/key.py | 187 ++++++++++++------------------------------ 1 file changed, 54 insertions(+), 133 deletions(-) diff --git a/gcloud/storage/key.py b/gcloud/storage/key.py index 7fa8441f12bd..ee523479fcf1 100644 --- a/gcloud/storage/key.py +++ b/gcloud/storage/key.py @@ -11,6 +11,18 @@ from gcloud.storage.iterator import Iterator +def _scalar_property(fieldname): + """Create a property descriptor around the :class:`_PropertyMixin` helpers. + """ + def _getter(self): + return self.properties[fieldname] + + def _setter(self, value): + self._patch_properties({fieldname: value}) + + return property(_getter, _setter) + + class Key(_PropertyMixin): """A wrapper around Cloud Storage's concept of an ``Object``.""" @@ -378,137 +390,59 @@ def make_public(self): self.acl.save() return self - @property - def cache_control(self): - """Retrieve HTTP 'Cache-Control' header for this object. - - See: https://tools.ietf.org/html/rfc7234#section-5.2 and - https://cloud.google.com/storage/docs/json_api/v1/objects - - :rtype: string - """ - return self.properties['cacheControl'] - - @cache_control.setter - def cache_control(self, value): - """Update HTTP 'Cache-Control' header for this object. - - See: https://tools.ietf.org/html/rfc7234#section-5.2 and - https://cloud.google.com/storage/docs/json_api/v1/objects - - :type value: string - """ - self._patch_properties({'cacheControl': value}) - - @property - def content_disposition(self): - """Retrieve HTTP 'Content-Disposition' header for this object. - - See: https://tools.ietf.org/html/rfc6266 and - https://cloud.google.com/storage/docs/json_api/v1/objects - - :rtype: string - """ - return self.properties['contentDisposition'] - - @content_disposition.setter - def content_disposition(self, value): - """Update HTTP 'Content-Disposition' header for this object. + cache_control = _scalar_property('cacheControl') + """HTTP 'Cache-Control' header for this object. - See: https://tools.ietf.org/html/rfc6266 and - https://cloud.google.com/storage/docs/json_api/v1/objects - - :type value: string - """ - self._patch_properties({'contentDisposition': value}) - - @property - def content_encoding(self): - """Retrieve HTTP 'Content-Encoding' header for this object. + See: https://tools.ietf.org/html/rfc7234#section-5.2 and + https://cloud.google.com/storage/docs/json_api/v1/objects - See: https://tools.ietf.org/html/rfc7231#section-3.1.2.2 and - https://cloud.google.com/storage/docs/json_api/v1/objects - - :rtype: string - """ - return self.properties['contentEncoding'] - - @content_encoding.setter - def content_encoding(self, value): - """Update HTTP 'Content-Encoding' header for this object. - - See: https://tools.ietf.org/html/rfc7231#section-3.1.2.2 and - https://cloud.google.com/storage/docs/json_api/v1/objects - - :type value: string - """ - self._patch_properties({'contentEncoding': value}) + :rtype: string + """ - @property - def content_language(self): - """Retrieve HTTP 'Content-Language' header for this object. + content_disposition = _scalar_property('contentDisposition') + """HTTP 'Content-Disposition' header for this object. - See: http://tools.ietf.org/html/bcp47 and - https://cloud.google.com/storage/docs/json_api/v1/objects - - :rtype: string - """ - return self.properties['contentLanguage'] + See: https://tools.ietf.org/html/rfc6266 and + https://cloud.google.com/storage/docs/json_api/v1/objects - @content_language.setter - def content_language(self, value): - """Update HTTP 'Content-Language' header for this object. + :rtype: string + """ - See: http://tools.ietf.org/html/bcp47 and - https://cloud.google.com/storage/docs/json_api/v1/objects + content_encoding = _scalar_property('contentEncoding') + """HTTP 'Content-Encoding' header for this object. - :type value: string - """ - self._patch_properties({'contentLanguage': value}) + See: https://tools.ietf.org/html/rfc7231#section-3.1.2.2 and + https://cloud.google.com/storage/docs/json_api/v1/objects - @property - def content_type(self): - """Retrieve HTTP 'Content-Type' header for this object. - - See: https://tools.ietf.org/html/rfc2616#section-14.17 and - https://cloud.google.com/storage/docs/json_api/v1/objects + :rtype: string + """ - :rtype: string - """ - return self.properties['contentType'] + content_language = _scalar_property('contentLanguage') + """HTTP 'Content-Language' header for this object. - @content_type.setter - def content_type(self, value): - """Update HTTP 'Content-Type' header for this object. + See: http://tools.ietf.org/html/bcp47 and + https://cloud.google.com/storage/docs/json_api/v1/objects - See: https://tools.ietf.org/html/rfc2616#section-14.17 and - https://cloud.google.com/storage/docs/json_api/v1/objects + :rtype: string + """ - :type value: string - """ - self._patch_properties({'contentType': value}) + content_type = _scalar_property('contentType') + """HTTP 'Content-Type' header for this object. - @property - def crc32c(self): - """Retrieve CRC32C checksum for this object. + See: https://tools.ietf.org/html/rfc2616#section-14.17 and + https://cloud.google.com/storage/docs/json_api/v1/objects - See: http://tools.ietf.org/html/rfc4960#appendix-B and - https://cloud.google.com/storage/docs/json_api/v1/objects + :rtype: string + """ - :rtype: string - """ - return self.properties['crc32c'] + crc32c = _scalar_property('crc32c') + """CRC32C checksum for this object. - @crc32c.setter - def crc32c(self, value): - """Update CRC32C checksum for this object. + See: http://tools.ietf.org/html/rfc4960#appendix-B and + https://cloud.google.com/storage/docs/json_api/v1/objects - See: http://tools.ietf.org/html/rfc4960#appendix-B and - https://cloud.google.com/storage/docs/json_api/v1/objects - - :type value: string - """ - self._patch_properties({'crc32c': value}) + :rtype: string + """ @property def component_count(self): @@ -551,27 +485,14 @@ def id(self): """ return self.properties['id'] - @property - def md5_hash(self): - """Retrieve MD5 hash for this object. - - See: http://tools.ietf.org/html/rfc4960#appendix-B and - https://cloud.google.com/storage/docs/json_api/v1/objects - - :rtype: string - """ - return self.properties['md5Hash'] + md5_hash = _scalar_property('md5Hash') + """MD5 hash for this object. - @md5_hash.setter - def md5_hash(self, value): - """Update MD5 hash for this object. + See: http://tools.ietf.org/html/rfc4960#appendix-B and + https://cloud.google.com/storage/docs/json_api/v1/objects - See: http://tools.ietf.org/html/rfc4960#appendix-B and - https://cloud.google.com/storage/docs/json_api/v1/objects - - :type value: string - """ - self._patch_properties({'md5Hash': value}) + :rtype: string + """ @property def media_link(self): From f8d8844e3e02de16e1a2042d56bfb8c3ab1dafc6 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Thu, 6 Nov 2014 20:29:42 -0500 Subject: [PATCH 5/6] Docstrings. --- gcloud/storage/key.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gcloud/storage/key.py b/gcloud/storage/key.py index ee523479fcf1..dadd0ae8ecc1 100644 --- a/gcloud/storage/key.py +++ b/gcloud/storage/key.py @@ -15,9 +15,13 @@ def _scalar_property(fieldname): """Create a property descriptor around the :class:`_PropertyMixin` helpers. """ def _getter(self): + """Scalar property getter. + """ return self.properties[fieldname] def _setter(self, value): + """Scalar property setter. + """ self._patch_properties({fieldname: value}) return property(_getter, _setter) From a9154476be93649e06743400584681b8b28bf3f4 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Thu, 6 Nov 2014 21:25:28 -0500 Subject: [PATCH 6/6] One-line docstrings. Sigh. That is *never* gonna look right to me. --- gcloud/storage/key.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/gcloud/storage/key.py b/gcloud/storage/key.py index dadd0ae8ecc1..86d018e66b11 100644 --- a/gcloud/storage/key.py +++ b/gcloud/storage/key.py @@ -15,13 +15,11 @@ def _scalar_property(fieldname): """Create a property descriptor around the :class:`_PropertyMixin` helpers. """ def _getter(self): - """Scalar property getter. - """ + """Scalar property getter.""" return self.properties[fieldname] def _setter(self, value): - """Scalar property setter. - """ + """Scalar property setter.""" self._patch_properties({fieldname: value}) return property(_getter, _setter)