Skip to content

Commit

Permalink
Stop using 'metadata' to refer to all properties in bucket/key repres…
Browse files Browse the repository at this point in the history
…etnation.

Make helper methods which get / set those properties private.

Split some dual-mode methods.
  • Loading branch information
tseaver committed Nov 5, 2014
1 parent 2c3725d commit 36966e8
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 186 deletions.
154 changes: 82 additions & 72 deletions gcloud/storage/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,23 @@
"""


class _MetadataMixin(object):
"""Abstract mixin for cloud storage classes with associated metadata.
class _PropertyMixin(object):
"""Abstract mixin for cloud storage classes with associated propertties.
Non-abstract subclasses should implement:
- CUSTOM_METADATA_FIELDS
- CUSTOM_PROPERTY_ACCESSORS
- connection
- path
"""

CUSTOM_METADATA_FIELDS = None
CUSTOM_PROPERTY_ACCESSORS = None
"""Mapping of field name -> accessor for fields w/ custom accessors.
Expected to be set by subclasses. Fields in this mapping will cause
`get_metadata()` to raise a KeyError with a message to use the relevant
accessor methods.
:meth:`_get_property()` to raise a KeyError with a message to use the
relevant accessor methods.
"""

def __init__(self, name=None, metadata=None):
"""_MetadataMixin constructor.
:type name: string
:param name: The name of the object.
:type metadata: dict
:param metadata: All the other data provided by Cloud Storage.
"""
self.name = name
self.metadata = metadata

@property
def connection(self):
"""Abstract getter for the connection to use."""
Expand All @@ -43,90 +31,112 @@ def path(self):
"""Abstract getter for the object path."""
raise NotImplementedError

def has_metadata(self, field=None):
"""Check if metadata is available.
def __init__(self, name=None, properties=None):
"""_PropertyMixin constructor.
:type field: string
:param field: (optional) the particular field to check for.
:type name: string
:param name: The name of the object.
:type metadata: dict
:param metadata: All the other data provided by Cloud Storage.
"""
self.name = name
self._properties = {}
if properties is not None:
self._properties.update(properties)

:rtype: bool
:returns: Whether metadata is available locally.
@property
def properties(self):
"""Ensure properties are loaded, and return a copy.
"""
if not self.metadata:
return False
elif field and field not in self.metadata:
return False
else:
return True
if not self._properties:
self._reload_properties()
return self._properties.copy()

def reload_metadata(self):
"""Reload metadata from Cloud Storage.
metadata = properties # Backward-compatibiltiy alias

:rtype: :class:`_MetadataMixin`
def _reload_properties(self):
"""Reload properties from Cloud Storage.
:rtype: :class:`_PropertyMixin`
:returns: The object you just reloaded data for.
"""
# Pass only '?projection=noAcl' here because 'acl' and related
# are handled via 'get_acl()' etc.
query_params = {'projection': 'noAcl'}
self.metadata = self.connection.api_request(
self._properties = self.connection.api_request(
method='GET', path=self.path, query_params=query_params)
return self
reload_metadata = _reload_properties # backward-compat alias

def _patch_properties(self, properties):
"""Update particular fields of this object's properties.
This method will only update the fields provided and will not
touch the other fields.
It will also reload the properties locally based on the server's
response.
def get_metadata(self, field=None, default=None):
"""Get all metadata or a specific field.
:type properties: dict
:param properties: The dictionary of values to update.
:rtype: :class:`_PropertyMixin`
:returns: The current object.
"""
# Pass '?projection=full' here because 'PATCH' documented not
# to work properly w/ 'noAcl'.
self._properties = self.connection.api_request(
method='PATCH', path=self.path, data=properties,
query_params={'projection': 'full'})
return self
patch_metadata = _patch_properties # backward-compat alias

def _has_property(self, field=None):
"""Check if property is available.
:type field: string
:param field: (optional) the particular field to check for.
:rtype: boolean
:returns: Whether property is available locally. If no ``field``
passed, return whether *any* properties are available.
"""
if field and field not in self._properties:
return False
return len(self._properties) > 0
has_metadata = _has_property # backward-compat alias

def _get_property(self, field, default=None):
"""Return the value of a field from the server-side representation.
If you request a field that isn't available, and that field can
be retrieved by refreshing data from Cloud Storage, this method
will reload the data using :func:`_MetadataMixin.reload_metadata`.
will reload the data using :func:`_PropertyMixin._reload_properties`.
:type field: string
:param field: (optional) A particular field to retrieve from metadata.
:param field: A particular field to retrieve from properties.
:type default: anything
:param default: The value to return if the field provided wasn't found.
:rtype: dict or anything
:returns: All metadata or the value of the specific field.
:raises: :class:`KeyError` if the field is in CUSTOM_METADATA_FIELDS.
:rtype: anything
:returns: value of the specific field, or the default if not found.
"""
# We ignore 'acl' and related fields because they are meant to be
# handled via 'get_acl()' and related methods.
custom = self.CUSTOM_METADATA_FIELDS.get(field)
# Raise for fields which have custom accessors.
custom = self.CUSTOM_PROPERTY_ACCESSORS.get(field)
if custom is not None:
message = 'Use %s or related methods instead.' % custom
raise KeyError((field, message))

if not self.has_metadata(field=field):
self.reload_metadata()
if not self._properties or field not in self._properties:
self._reload_properties()

if field:
return self.metadata.get(field, default)
else:
return self.metadata

def patch_metadata(self, metadata):
"""Update particular fields of this object's metadata.
This method will only update the fields provided and will not
touch the other fields.
It will also reload the metadata locally based on the server's
response.
:type metadata: dict
:param metadata: The dictionary of values to update.
:rtype: :class:`_MetadataMixin`
:returns: The current object.
"""
self.metadata = self.connection.api_request(
method='PATCH', path=self.path, data=metadata,
query_params={'projection': 'full'})
return self
return self._properties.get(field, default)
get_metadata = _get_property # Backward-compat alias

def get_acl(self):
"""Get ACL metadata as an object.
"""Get ACL as an object.
:returns: An ACL object for the current object.
"""
Expand Down
42 changes: 16 additions & 26 deletions gcloud/storage/bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import os

from gcloud.storage._helpers import _MetadataMixin
from gcloud.storage._helpers import _PropertyMixin
from gcloud.storage import exceptions
from gcloud.storage.acl import BucketACL
from gcloud.storage.acl import DefaultObjectACL
Expand All @@ -11,7 +11,7 @@
from gcloud.storage.key import _KeyIterator


class Bucket(_MetadataMixin):
class Bucket(_PropertyMixin):
"""A class representing a Bucket on Cloud Storage.
:type connection: :class:`gcloud.storage.connection.Connection`
Expand All @@ -21,18 +21,18 @@ class Bucket(_MetadataMixin):
:param name: The name of the bucket.
"""

CUSTOM_METADATA_FIELDS = {
CUSTOM_PROPERTY_ACCESSORS = {
'acl': 'get_acl',
'defaultObjectAcl': 'get_default_object_acl',
'lifecycle': 'get_lifecycle',
}
"""Mapping of field name -> accessor for fields w/ custom accessors."""
"""Map field name -> accessor for fields w/ custom accessors."""

# ACL rules are lazily retrieved.
_acl = _default_object_acl = None

def __init__(self, connection=None, name=None, metadata=None):
super(Bucket, self).__init__(name=name, metadata=metadata)
def __init__(self, connection=None, name=None, properties=None):
super(Bucket, self).__init__(name=name, properties=properties)
self._connection = connection

@property
Expand Down Expand Up @@ -60,7 +60,7 @@ def from_dict(cls, bucket_dict, connection=None):
:returns: A bucket constructed from the data provided.
"""
return cls(connection=connection, name=bucket_dict['name'],
metadata=bucket_dict)
properties=bucket_dict)

def __repr__(self):
return '<Bucket: %s>' % self.name
Expand Down Expand Up @@ -120,7 +120,7 @@ def get_all_keys(self):
"""List all the keys in this bucket.
This will **not** retrieve all the data for all the keys, it
will only retrieve metadata about the keys.
will only retrieve the keys.
This is equivalent to::
Expand Down Expand Up @@ -344,7 +344,7 @@ def upload_file_object(self, file_obj, key=None):
return key.upload_from_file(file_obj)

def configure_website(self, main_page_suffix=None, not_found_page=None):
"""Configure website-related metadata.
"""Configure website-related properties.
.. note::
This (apparently) only works
Expand Down Expand Up @@ -385,7 +385,7 @@ def configure_website(self, main_page_suffix=None, not_found_page=None):
'notFoundPage': not_found_page,
},
}
return self.patch_metadata(data)
return self._patch_properties(data)

def disable_website(self):
"""Disable the website configuration for this bucket.
Expand All @@ -395,21 +395,11 @@ def disable_website(self):
"""
return self.configure_website(None, None)

def get_acl(self):
"""Get ACL metadata as a :class:`gcloud.storage.acl.BucketACL` object.
:rtype: :class:`gcloud.storage.acl.BucketACL`
:returns: An ACL object for the current bucket.
"""
if not self.acl.loaded:
self.acl.reload()
return self.acl

def get_default_object_acl(self):
"""Get the current Default Object ACL rules.
If the appropriate metadata isn't available locally, this method
will reload it from Cloud Storage.
If the acl isn't available locally, this method will reload it from
Cloud Storage.
:rtype: :class:`gcloud.storage.acl.DefaultObjectACL`
:returns: A DefaultObjectACL object for this bucket.
Expand Down Expand Up @@ -451,10 +441,10 @@ def get_lifecycle(self):
:rtype: list(dict)
:returns: A sequence of mappings describing each CORS policy.
"""
if not self.has_metadata('lifecycle'):
self.reload_metadata()
if not self._has_property('lifecycle'):
self._reload_properties()
result = []
info = self.metadata.get('lifecycle', {})
info = self._properties.get('lifecycle', {})
for rule in info.get('rule', ()):
rule = rule.copy()
result.append(rule)
Expand All @@ -469,7 +459,7 @@ def update_lifecycle(self, rules):
:type rules: list(dict)
:param rules: A sequence of mappings describing each lifecycle policy.
"""
self.patch_metadata({'lifecycle': {'rule': rules}})
self._patch_properties({'lifecycle': {'rule': rules}})


class BucketIterator(Iterator):
Expand Down
18 changes: 9 additions & 9 deletions gcloud/storage/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@
import os
from StringIO import StringIO

from gcloud.storage._helpers import _MetadataMixin
from gcloud.storage._helpers import _PropertyMixin
from gcloud.storage.acl import ObjectACL
from gcloud.storage.exceptions import StorageError
from gcloud.storage.iterator import Iterator


class Key(_MetadataMixin):
class Key(_PropertyMixin):
"""A wrapper around Cloud Storage's concept of an ``Object``."""

CUSTOM_METADATA_FIELDS = {
CUSTOM_PROPERTY_ACCESSORS = {
'acl': 'get_acl',
}
"""Mapping of field name -> accessor for fields w/ custom accessors."""
"""Map field name -> accessor for fields w/ custom accessors."""

CHUNK_SIZE = 1024 * 1024 # 1 MB.
"""The size of a chunk of data whenever iterating (1 MB).
Expand All @@ -26,7 +26,7 @@ class Key(_MetadataMixin):
# ACL rules are lazily retrieved.
_acl = None

def __init__(self, bucket=None, name=None, metadata=None):
def __init__(self, bucket=None, name=None, properties=None):
"""Key constructor.
:type bucket: :class:`gcloud.storage.bucket.Bucket`
Expand All @@ -36,10 +36,10 @@ def __init__(self, bucket=None, name=None, metadata=None):
:param name: The name of the key. This corresponds to the
unique path of the object in the bucket.
:type metadata: dict
:param metadata: All the other data provided by Cloud Storage.
:type properties: dict
:param properties: All the other data provided by Cloud Storage.
"""
super(Key, self).__init__(name=name, metadata=metadata or {})
super(Key, self).__init__(name=name, properties=properties)
self.bucket = bucket

@property
Expand All @@ -65,7 +65,7 @@ def from_dict(cls, key_dict, bucket=None):
:returns: A key based on the data provided.
"""

return cls(bucket=bucket, name=key_dict['name'], metadata=key_dict)
return cls(bucket=bucket, name=key_dict['name'], properties=key_dict)

def __repr__(self):
if self.bucket:
Expand Down
Loading

0 comments on commit 36966e8

Please sign in to comment.