Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 83 additions & 73 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
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
Loading