Repository: googleapis/google-cloud-python

Issue:
`storage.key.Key`:  add direct support for additional resource fields 
The [Object API resource representation](https://cloud.google.com/storage/docs/json_api/v1/objects) defines a number of fields as "writable":
- `acl[]`
- `cacheControl`
- `contentDisposition`
- `contentEncoding`
- `contentLanguage`
- `contentType`
- `crc32c`
- `md5Hash`
- `metadata{}`
- `name`

`Key.from_dict` currently captures them _all_ in the key's `metadata` attribute, but that name creates confusion with its `metadata` key (which stores application-defined key-value pairs).

I think it would be cleaner to map each the non-user-defined fields onto separate attributes, and propagate them to the server via the [`insert`](https://cloud.google.com/storage/docs/json_api/v1/objects/insert), and [`patch`](https://cloud.google.com/storage/docs/json_api/v1/objects/patch) or [`update`](https://cloud.google.com/storage/docs/json_api/v1/objects/update) endpoints.

`Key` should also expose read-only properties for the non-writable resource fields not already mapped, and which aren't constants:
- `componentCount`
- `etag`
- `generation`
- `id`
- `mediaLink`
- `metageneration`
- `owner{}`
- `selfLink`
- `size`
- `storageClass`
- `timeDeleted`
- `updated`



Discussion:
Is there some sort of existing implementation to compare against? I'm all for renaming the `metadata` dictionary as something else to clean up the namespace, but implementing all of these seems like it should be done carefully (hence learning from existing would be useful).

I don't know about other implementations. This issue (and the parallel one for Bucket) are pointing out gaps where we don't fully expose the API.  It would be fine for @silvolu to say, "don't worry about those gaps for M1".

I think we should take action on both, but let's switch to `apitools` first, and then we'll get back to this issue.

It's fine to do this now, see https://github.com/GoogleCloudPlatform/gcloud-python/pull/343#issuecomment-61719326


Resolving patch:
diff --git a/gcloud/storage/bucket.py b/gcloud/storage/bucket.py
--- 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']
 
diff --git a/gcloud/storage/key.py b/gcloud/storage/key.py
--- 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
@@ -10,11 +11,43 @@
 from gcloud.storage.iterator import Iterator
 
 
+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)
+
+
 class Key(_PropertyMixin):
     """A wrapper around Cloud Storage's concept of an ``Object``."""
 
     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',
+        '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 +392,216 @@ def make_public(self):
         self.acl.save()
         return self
 
+    cache_control = _scalar_property('cacheControl')
+    """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
+    """
+
+    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
+
+    :rtype: string
+    """
+
+    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
+
+    :rtype: string
+    """
+
+    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
+
+    :rtype: string
+    """
+
+    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
+
+    :rtype: string
+    """
+
+    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
+
+    :rtype: string
+    """
+
+    @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']
+
+    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
+
+    :rtype: string
+    """
+
+    @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['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):
+        """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.


Test patch:
diff --git a/gcloud/storage/test_key.py b/gcloud/storage/test_key.py
--- a/gcloud/storage/test_key.py
+++ b/gcloud/storage/test_key.py
@@ -344,6 +344,323 @@ 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()
+        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_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()
+        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_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 = {'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()
+        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):