diff --git a/gcloud/exceptions.py b/gcloud/exceptions.py index dc5458eedccb..0c04f80a12a0 100644 --- a/gcloud/exceptions.py +++ b/gcloud/exceptions.py @@ -17,6 +17,7 @@ See: https://cloud.google.com/storage/docs/json_api/v1/status-codes """ +import copy import json import six @@ -38,7 +39,7 @@ def __init__(self, message, errors=()): super(GCloudError, self).__init__() # suppress deprecation warning under 2.6.x self.message = message - self._errors = [error.copy() for error in errors] + self._errors = errors def __str__(self): return '%d %s' % (self.code, self.message) @@ -50,7 +51,7 @@ def errors(self): :rtype: list(dict) :returns: a list of mappings describing each error. """ - return [error.copy() for error in self._errors] + return [copy.deepcopy(error) for error in self._errors] class Redirection(GCloudError): diff --git a/gcloud/storage/_helpers.py b/gcloud/storage/_helpers.py index 54f15146b2cf..0f36e7f04214 100644 --- a/gcloud/storage/_helpers.py +++ b/gcloud/storage/_helpers.py @@ -98,7 +98,7 @@ def _scalar_property(fieldname): """ def _getter(self): """Scalar property getter.""" - return self._properties[fieldname] + return self._properties.get(fieldname) def _setter(self, value): """Scalar property setter.""" diff --git a/gcloud/storage/blob.py b/gcloud/storage/blob.py index 1bf134b4a152..ff5aef4f9f4b 100644 --- a/gcloud/storage/blob.py +++ b/gcloud/storage/blob.py @@ -429,52 +429,64 @@ def make_public(self): See: https://tools.ietf.org/html/rfc7234#section-5.2 and https://cloud.google.com/storage/docs/json_api/v1/objects - :rtype: string + If the property is not set locally, returns ``None``. + + :rtype: string or ``NoneType`` """ content_disposition = _scalar_property('contentDisposition') """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 + https://cloud.google.com/storage/docs/json_api/v1/objects + + If the property is not set locally, returns ``None``. - :rtype: string + :rtype: string or ``NoneType`` """ content_encoding = _scalar_property('contentEncoding') """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 + https://cloud.google.com/storage/docs/json_api/v1/objects + + If the property is not set locally, returns ``None``. - :rtype: string + :rtype: string or ``NoneType`` """ content_language = _scalar_property('contentLanguage') """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 + https://cloud.google.com/storage/docs/json_api/v1/objects + + If the property is not set locally, returns ``None``. - :rtype: string + :rtype: string or ``NoneType`` """ content_type = _scalar_property('contentType') """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 + https://cloud.google.com/storage/docs/json_api/v1/objects - :rtype: string + If the property is not set locally, returns ``None``. + + :rtype: string or ``NoneType`` """ crc32c = _scalar_property('crc32c') """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 + https://cloud.google.com/storage/docs/json_api/v1/objects - :rtype: string + If the property is not set locally, returns ``None``. + + :rtype: string or ``NoneType`` """ @property @@ -483,9 +495,14 @@ def component_count(self): See: https://cloud.google.com/storage/docs/json_api/v1/objects - :rtype: integer + :rtype: integer or ``NoneType`` + :returns: The component count (in case of a composed object) or + ``None`` if the property is not set locally. This property + will not be set on objects not created via ``compose``. """ - return self._properties['componentCount'] + component_count = self._properties.get('componentCount') + if component_count is not None: + return int(component_count) @property def etag(self): @@ -494,9 +511,10 @@ def etag(self): See: http://tools.ietf.org/html/rfc2616#section-3.11 and https://cloud.google.com/storage/docs/json_api/v1/objects - :rtype: string + :rtype: string or ``NoneType`` + :returns: The blob etag or ``None`` if the property is not set locally. """ - return self._properties['etag'] + return self._properties.get('etag') @property def generation(self): @@ -504,9 +522,13 @@ def generation(self): See: https://cloud.google.com/storage/docs/json_api/v1/objects - :rtype: integer + :rtype: integer or ``NoneType`` + :returns: The generation of the blob or ``None`` if the property + is not set locally. """ - return self._properties['generation'] + generation = self._properties.get('generation') + if generation is not None: + return int(generation) @property def id(self): @@ -514,17 +536,21 @@ def id(self): See: https://cloud.google.com/storage/docs/json_api/v1/objects - :rtype: string + :rtype: string or ``NoneType`` + :returns: The ID of the blob or ``None`` if the property is not + set locally. """ - return self._properties['id'] + return self._properties.get('id') md5_hash = _scalar_property('md5Hash') """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 + https://cloud.google.com/storage/docs/json_api/v1/objects + + If the property is not set locally, returns ``None``. - :rtype: string + :rtype: string or ``NoneType`` """ @property @@ -533,9 +559,11 @@ def media_link(self): See: https://cloud.google.com/storage/docs/json_api/v1/objects - :rtype: string + :rtype: string or ``NoneType`` + :returns: The media link for the blob or ``None`` if the property is + not set locally. """ - return self._properties['mediaLink'] + return self._properties.get('mediaLink') @property def metadata(self): @@ -543,9 +571,11 @@ def metadata(self): See: https://cloud.google.com/storage/docs/json_api/v1/objects - :rtype: dict + :rtype: dict or ``NoneType`` + :returns: The metadata associated with the blob or ``None`` if the + property is not set locally. """ - return copy.deepcopy(self._properties['metadata']) + return copy.deepcopy(self._properties.get('metadata')) @metadata.setter def metadata(self, value): @@ -553,7 +583,8 @@ def metadata(self, value): See: https://cloud.google.com/storage/docs/json_api/v1/objects - :type value: dict + :type value: dict or ``NoneType`` + :param value: The blob metadata to set. """ self._patch_property('metadata', value) @@ -563,9 +594,13 @@ def metageneration(self): See: https://cloud.google.com/storage/docs/json_api/v1/objects - :rtype: integer + :rtype: integer or ``NoneType`` + :returns: The metageneration of the blob or ``None`` if the property + is not set locally. """ - return self._properties['metageneration'] + metageneration = self._properties.get('metageneration') + if metageneration is not None: + return int(metageneration) @property def owner(self): @@ -573,10 +608,11 @@ def owner(self): See: https://cloud.google.com/storage/docs/json_api/v1/objects - :rtype: dict - :returns: mapping of owner's role/ID. + :rtype: dict or ``NoneType`` + :returns: Mapping of owner's role/ID. If the property is not set + locally, returns ``None``. """ - return self._properties['owner'].copy() + return copy.deepcopy(self._properties.get('owner')) @property def self_link(self): @@ -584,31 +620,39 @@ def self_link(self): See: https://cloud.google.com/storage/docs/json_api/v1/objects - :rtype: string + :rtype: string or ``NoneType`` + :returns: The self link for the blob or ``None`` if the property is + not set locally. """ - return self._properties['selfLink'] + return self._properties.get('selfLink') @property def size(self): - """Size of the object, in bytes. + """Size of the object, in bytes. See: https://cloud.google.com/storage/docs/json_api/v1/objects - :rtype: integer + :rtype: integer or ``NoneType`` + :returns: The size of the blob or ``None`` if the property + is not set locally. """ - return self._properties['size'] + size = self._properties.get('size') + if size is not None: + return int(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 + See: https://cloud.google.com/storage/docs/storage-classes + https://cloud.google.com/storage/docs/nearline-storage + https://cloud.google.com/storage/docs/durable-reduced-availability - :rtype: string - :returns: Currently one of "STANDARD", "DURABLE_REDUCED_AVAILABILITY" + :rtype: string or ``NoneType`` + :returns: If set, one of "STANDARD", "NEARLINE", or + "DURABLE_REDUCED_AVAILABILITY", else ``None``. """ - return self._properties['storageClass'] + return self._properties.get('storageClass') @property def time_deleted(self): @@ -616,9 +660,10 @@ def time_deleted(self): 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. + :rtype: string or ``NoneType`` + :returns: RFC3339 valid timestamp, or ``None`` if the property is not + set locally. If the blob has not been deleted, this will + never be set. """ return self._properties.get('timeDeleted') @@ -628,10 +673,11 @@ def updated(self): See: https://cloud.google.com/storage/docs/json_api/v1/objects - :rtype: string - :returns: timestamp in RFC 3339 format. + :rtype: string or ``NoneType`` + :returns: RFC3339 valid timestamp, or ``None`` if the property is not + set locally. """ - return self._properties['updated'] + return self._properties.get('updated') class _UploadConfig(object): diff --git a/gcloud/storage/bucket.py b/gcloud/storage/bucket.py index 817e65fc0ed0..49e0dcae5459 100644 --- a/gcloud/storage/bucket.py +++ b/gcloud/storage/bucket.py @@ -513,9 +513,11 @@ def etag(self): See: http://tools.ietf.org/html/rfc2616#section-3.11 and https://cloud.google.com/storage/docs/json_api/v1/buckets - :rtype: string + :rtype: string or ``NoneType`` + :returns: The bucket etag or ``None`` if the property is not + set locally. """ - return self._properties['etag'] + return self._properties.get('etag') @property def id(self): @@ -523,9 +525,11 @@ def id(self): See: https://cloud.google.com/storage/docs/json_api/v1/buckets - :rtype: string + :rtype: string or ``NoneType`` + :returns: The ID of the bucket or ``None`` if the property is not + set locally. """ - return self._properties['id'] + return self._properties.get('id') @property def lifecycle_rules(self): @@ -558,7 +562,9 @@ def lifecycle_rules(self, rules): See: https://cloud.google.com/storage/docs/json_api/v1/buckets and https://cloud.google.com/storage/docs/concepts-techniques#specifyinglocations - :rtype: string + If the property is not set locally, returns ``None``. + + :rtype: string or ``NoneType`` """ def get_logging(self): @@ -571,8 +577,7 @@ def get_logging(self): (if logging is enabled), or None (if not). """ info = self._properties.get('logging') - if info is not None: - return info.copy() + return copy.deepcopy(info) def enable_logging(self, bucket_name, object_prefix=''): """Enable access logging for this bucket. @@ -601,9 +606,13 @@ def metageneration(self): See: https://cloud.google.com/storage/docs/json_api/v1/buckets - :rtype: integer + :rtype: integer or ``NoneType`` + :returns: The metageneration of the bucket or ``None`` if the property + is not set locally. """ - return self._properties['metageneration'] + metageneration = self._properties.get('metageneration') + if metageneration is not None: + return int(metageneration) @property def owner(self): @@ -611,10 +620,11 @@ def owner(self): See: https://cloud.google.com/storage/docs/json_api/v1/buckets - :rtype: dict - :returns: mapping of owner's role/ID. + :rtype: dict or ``NoneType`` + :returns: Mapping of owner's role/ID. If the property is not set + locally, returns ``None``. """ - return self._properties['owner'].copy() + return copy.deepcopy(self._properties.get('owner')) @property def project_number(self): @@ -622,9 +632,13 @@ def project_number(self): See: https://cloud.google.com/storage/docs/json_api/v1/buckets - :rtype: integer + :rtype: integer or ``NoneType`` + :returns: The project number that owns the bucket or ``None`` if the + property is not set locally. """ - return self._properties['projectNumber'] + project_number = self._properties.get('projectNumber') + if project_number is not None: + return int(project_number) @property def self_link(self): @@ -632,21 +646,25 @@ def self_link(self): See: https://cloud.google.com/storage/docs/json_api/v1/buckets - :rtype: string + :rtype: string or ``NoneType`` + :returns: The self link for the bucket or ``None`` if the property is + not set locally. """ - return self._properties['selfLink'] + return self._properties.get('selfLink') @property def storage_class(self): """Retrieve the storage class for the bucket. - See: https://cloud.google.com/storage/docs/json_api/v1/buckets and + See: https://cloud.google.com/storage/docs/storage-classes + https://cloud.google.com/storage/docs/nearline-storage https://cloud.google.com/storage/docs/durable-reduced-availability - :rtype: string - :returns: Currently one of "STANDARD", "DURABLE_REDUCED_AVAILABILITY" + :rtype: string or ``NoneType`` + :returns: If set, one of "STANDARD", "NEARLINE", or + "DURABLE_REDUCED_AVAILABILITY", else ``None``. """ - return self._properties['storageClass'] + return self._properties.get('storageClass') @property def time_created(self): @@ -654,10 +672,11 @@ def time_created(self): See: https://cloud.google.com/storage/docs/json_api/v1/buckets - :rtype: string - :returns: timestamp in RFC 3339 format. + :rtype: string or ``NoneType`` + :returns: RFC3339 valid timestamp, or ``None`` if the property is not + set locally. """ - return self._properties['timeCreated'] + return self._properties.get('timeCreated') @property def versioning_enabled(self): diff --git a/gcloud/storage/test_blob.py b/gcloud/storage/test_blob.py index f28bdfafab75..242477630780 100644 --- a/gcloud/storage/test_blob.py +++ b/gcloud/storage/test_blob.py @@ -667,12 +667,23 @@ def test_cache_control_setter(self): self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) def test_component_count(self): - BLOB_NAME = 'blob-name' - connection = _Connection() - bucket = _Bucket(connection) + BUCKET = object() COMPONENT_COUNT = 42 - properties = {'componentCount': COMPONENT_COUNT} - blob = self._makeOne(BLOB_NAME, bucket=bucket, properties=properties) + blob = self._makeOne('blob-name', bucket=BUCKET, + properties={'componentCount': COMPONENT_COUNT}) + self.assertEqual(blob.component_count, COMPONENT_COUNT) + + def test_component_count_unset(self): + BUCKET = object() + blob = self._makeOne('blob-name', bucket=BUCKET) + self.assertEqual(blob.component_count, None) + + def test_component_count_string_val(self): + BUCKET = object() + COMPONENT_COUNT = 42 + blob = self._makeOne( + 'blob-name', bucket=BUCKET, + properties={'componentCount': str(COMPONENT_COUNT)}) self.assertEqual(blob.component_count, COMPONENT_COUNT) def test_content_disposition_getter(self): @@ -820,12 +831,22 @@ def test_etag(self): self.assertEqual(blob.etag, ETAG) def test_generation(self): - BLOB_NAME = 'blob-name' - connection = _Connection() - bucket = _Bucket(connection) + BUCKET = object() GENERATION = 42 - properties = {'generation': GENERATION} - blob = self._makeOne(BLOB_NAME, bucket=bucket, properties=properties) + blob = self._makeOne('blob-name', bucket=BUCKET, + properties={'generation': GENERATION}) + self.assertEqual(blob.generation, GENERATION) + + def test_generation_unset(self): + BUCKET = object() + blob = self._makeOne('blob-name', bucket=BUCKET) + self.assertEqual(blob.generation, None) + + def test_generation_string_val(self): + BUCKET = object() + GENERATION = 42 + blob = self._makeOne('blob-name', bucket=BUCKET, + properties={'generation': str(GENERATION)}) self.assertEqual(blob.generation, GENERATION) def test_id(self): @@ -901,12 +922,23 @@ def test_metadata_setter(self): self.assertEqual(kw[0]['query_params'], {'projection': 'full'}) def test_metageneration(self): - BLOB_NAME = 'blob-name' - connection = _Connection() - bucket = _Bucket(connection) + BUCKET = object() METAGENERATION = 42 - properties = {'metageneration': METAGENERATION} - blob = self._makeOne(BLOB_NAME, bucket=bucket, properties=properties) + blob = self._makeOne('blob-name', bucket=BUCKET, + properties={'metageneration': METAGENERATION}) + self.assertEqual(blob.metageneration, METAGENERATION) + + def test_metageneration_unset(self): + BUCKET = object() + blob = self._makeOne('blob-name', bucket=BUCKET) + self.assertEqual(blob.metageneration, None) + + def test_metageneration_string_val(self): + BUCKET = object() + METAGENERATION = 42 + blob = self._makeOne( + 'blob-name', bucket=BUCKET, + properties={'metageneration': str(METAGENERATION)}) self.assertEqual(blob.metageneration, METAGENERATION) def test_owner(self): @@ -930,12 +962,22 @@ def test_self_link(self): self.assertEqual(blob.self_link, SELF_LINK) def test_size(self): - BLOB_NAME = 'blob-name' - connection = _Connection() - bucket = _Bucket(connection) + BUCKET = object() SIZE = 42 - properties = {'size': SIZE} - blob = self._makeOne(BLOB_NAME, bucket=bucket, properties=properties) + blob = self._makeOne('blob-name', bucket=BUCKET, + properties={'size': SIZE}) + self.assertEqual(blob.size, SIZE) + + def test_size_unset(self): + BUCKET = object() + blob = self._makeOne('blob-name', bucket=BUCKET) + self.assertEqual(blob.size, None) + + def test_size_string_val(self): + BUCKET = object() + SIZE = 42 + blob = self._makeOne('blob-name', bucket=BUCKET, + properties={'size': str(SIZE)}) self.assertEqual(blob.size, SIZE) def test_storage_class(self): diff --git a/gcloud/storage/test_bucket.py b/gcloud/storage/test_bucket.py index 10ca83b590c1..ddd9dc245225 100644 --- a/gcloud/storage/test_bucket.py +++ b/gcloud/storage/test_bucket.py @@ -821,6 +821,16 @@ def test_metageneration(self): bucket = self._makeOne(properties=properties) self.assertEqual(bucket.metageneration, METAGENERATION) + def test_metageneration_unset(self): + bucket = self._makeOne() + self.assertEqual(bucket.metageneration, None) + + def test_metageneration_string_val(self): + METAGENERATION = 42 + properties = {'metageneration': str(METAGENERATION)} + bucket = self._makeOne(properties=properties) + self.assertEqual(bucket.metageneration, METAGENERATION) + def test_owner(self): OWNER = {'entity': 'project-owner-12345', 'entityId': '23456'} properties = {'owner': OWNER} @@ -835,6 +845,16 @@ def test_project_number(self): bucket = self._makeOne(properties=properties) self.assertEqual(bucket.project_number, PROJECT_NUMBER) + def test_project_number_unset(self): + bucket = self._makeOne() + self.assertEqual(bucket.project_number, None) + + def test_project_number_string_val(self): + PROJECT_NUMBER = 12345 + properties = {'projectNumber': str(PROJECT_NUMBER)} + bucket = self._makeOne(properties=properties) + self.assertEqual(bucket.project_number, PROJECT_NUMBER) + def test_self_link(self): SELF_LINK = 'http://example.com/self/' properties = {'selfLink': SELF_LINK}