Skip to content

Commit a410c6a

Browse files
committed
Merge pull request #333 from dhermes/refactor-key-bucket-shared-code
Refactoring duplicate code between storage.key and storage.bucket.
2 parents df05a0e + 2a1ab17 commit a410c6a

File tree

5 files changed

+179
-178
lines changed

5 files changed

+179
-178
lines changed

gcloud/storage/_helpers.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
"""Helper functions for Cloud Storage utility classes.
2+
3+
These are *not* part of the API.
4+
"""
5+
6+
7+
class _MetadataMixin(object):
8+
"""Abstract mixin for cloud storage classes with associated metadata.
9+
10+
Non-abstract subclasses should implement:
11+
- METADATA_ACL_FIELDS
12+
- connection
13+
- path
14+
"""
15+
16+
METADATA_ACL_FIELDS = None
17+
"""Tuple of fields which pertain to metadata.
18+
19+
Expected to be set by subclasses. Fields in this tuple will cause
20+
`get_metadata()` to raise a KeyError with a message to use get_acl()
21+
methods.
22+
"""
23+
24+
def __init__(self, name=None, metadata=None):
25+
"""_MetadataMixin constructor.
26+
27+
:type name: string
28+
:param name: The name of the object.
29+
30+
:type metadata: dict
31+
:param metadata: All the other data provided by Cloud Storage.
32+
"""
33+
self.name = name
34+
self.metadata = metadata
35+
36+
@property
37+
def connection(self):
38+
"""Abstract getter for the connection to use."""
39+
raise NotImplementedError
40+
41+
@property
42+
def path(self):
43+
"""Abstract getter for the object path."""
44+
raise NotImplementedError
45+
46+
def has_metadata(self, field=None):
47+
"""Check if metadata is available.
48+
49+
:type field: string
50+
:param field: (optional) the particular field to check for.
51+
52+
:rtype: bool
53+
:returns: Whether metadata is available locally.
54+
"""
55+
if not self.metadata:
56+
return False
57+
elif field and field not in self.metadata:
58+
return False
59+
else:
60+
return True
61+
62+
def reload_metadata(self):
63+
"""Reload metadata from Cloud Storage.
64+
65+
:rtype: :class:`_MetadataMixin`
66+
:returns: The object you just reloaded data for.
67+
"""
68+
# Pass only '?projection=noAcl' here because 'acl' and related
69+
# are handled via 'get_acl()' etc.
70+
query_params = {'projection': 'noAcl'}
71+
self.metadata = self.connection.api_request(
72+
method='GET', path=self.path, query_params=query_params)
73+
return self
74+
75+
def get_metadata(self, field=None, default=None):
76+
"""Get all metadata or a specific field.
77+
78+
If you request a field that isn't available, and that field can
79+
be retrieved by refreshing data from Cloud Storage, this method
80+
will reload the data using :func:`_MetadataMixin.reload_metadata`.
81+
82+
:type field: string
83+
:param field: (optional) A particular field to retrieve from metadata.
84+
85+
:type default: anything
86+
:param default: The value to return if the field provided wasn't found.
87+
88+
:rtype: dict or anything
89+
:returns: All metadata or the value of the specific field.
90+
91+
:raises: :class:`KeyError` if the field is in METADATA_ACL_FIELDS.
92+
"""
93+
# We ignore 'acl' and related fields because they are meant to be
94+
# handled via 'get_acl()' and related methods.
95+
if field in self.METADATA_ACL_FIELDS:
96+
message = 'Use get_acl() or related methods instead.'
97+
raise KeyError((field, message))
98+
99+
if not self.has_metadata(field=field):
100+
self.reload_metadata()
101+
102+
if field:
103+
return self.metadata.get(field, default)
104+
else:
105+
return self.metadata
106+
107+
def patch_metadata(self, metadata):
108+
"""Update particular fields of this object's metadata.
109+
110+
This method will only update the fields provided and will not
111+
touch the other fields.
112+
113+
It will also reload the metadata locally based on the server's
114+
response.
115+
116+
:type metadata: dict
117+
:param metadata: The dictionary of values to update.
118+
119+
:rtype: :class:`_MetadataMixin`
120+
:returns: The current object.
121+
"""
122+
self.metadata = self.connection.api_request(
123+
method='PATCH', path=self.path, data=metadata,
124+
query_params={'projection': 'full'})
125+
return self
126+
127+
def get_acl(self):
128+
"""Get ACL metadata as an object.
129+
130+
:returns: An ACL object for the current object.
131+
"""
132+
if not self.acl.loaded:
133+
self.acl.reload()
134+
return self.acl

gcloud/storage/acl.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@
4949
>>> acl.save()
5050
5151
You can alternatively save any existing :class:`gcloud.storage.acl.ACL`
52-
object (whether it was created by a factory method or not) with the
53-
:func:`gcloud.storage.bucket.Bucket.save_acl` method::
52+
object (whether it was created by a factory method or not) from a
53+
:class:`gcloud.storage.bucket.Bucket`::
5454
55-
>>> bucket.save_acl(acl)
55+
>>> bucket.acl.save(acl=acl)
5656
5757
To get the list of ``entity`` and ``role`` for each unique pair, the
5858
:class:`ACL` class is iterable::

gcloud/storage/bucket.py

Lines changed: 17 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import os
44

5+
from gcloud.storage._helpers import _MetadataMixin
56
from gcloud.storage import exceptions
67
from gcloud.storage.acl import BucketACL
78
from gcloud.storage.acl import DefaultObjectACL
@@ -10,7 +11,7 @@
1011
from gcloud.storage.key import _KeyIterator
1112

1213

13-
class Bucket(object):
14+
class Bucket(_MetadataMixin):
1415
"""A class representing a Bucket on Cloud Storage.
1516
1617
:type connection: :class:`gcloud.storage.connection.Connection`
@@ -19,13 +20,16 @@ class Bucket(object):
1920
:type name: string
2021
:param name: The name of the bucket.
2122
"""
23+
24+
METADATA_ACL_FIELDS = ('acl', 'defaultObjectAcl')
25+
"""Tuple of metadata fields pertaining to bucket ACLs."""
26+
2227
# ACL rules are lazily retrieved.
2328
_acl = _default_object_acl = None
2429

2530
def __init__(self, connection=None, name=None, metadata=None):
26-
self.connection = connection
27-
self.name = name
28-
self.metadata = metadata
31+
super(Bucket, self).__init__(name=name, metadata=metadata)
32+
self._connection = connection
2933

3034
@property
3135
def acl(self):
@@ -63,6 +67,15 @@ def __iter__(self):
6367
def __contains__(self, key):
6468
return self.get_key(key) is not None
6569

70+
@property
71+
def connection(self):
72+
"""Getter property for the connection to use with this Bucket.
73+
74+
:rtype: :class:`gcloud.storage.connection.Connection`
75+
:returns: The connection to use.
76+
"""
77+
return self._connection
78+
6679
@property
6780
def path(self):
6881
"""The URL path to this bucket."""
@@ -326,85 +339,6 @@ def upload_file_object(self, file_obj, key=None):
326339
key = self.new_key(os.path.basename(file_obj.name))
327340
return key.set_contents_from_file(file_obj)
328341

329-
def has_metadata(self, field=None):
330-
"""Check if metadata is available locally.
331-
332-
:type field: string
333-
:param field: (optional) the particular field to check for.
334-
335-
:rtype: bool
336-
:returns: Whether metadata is available locally.
337-
"""
338-
if not self.metadata:
339-
return False
340-
elif field and field not in self.metadata:
341-
return False
342-
else:
343-
return True
344-
345-
def reload_metadata(self):
346-
"""Reload metadata from Cloud Storage.
347-
348-
:rtype: :class:`Bucket`
349-
:returns: The bucket you just reloaded data for.
350-
"""
351-
# Pass only '?projection=noAcl' here because 'acl'/'defaultObjectAcl'
352-
# are handled via 'get_acl()'/'get_default_object_acl()'
353-
query_params = {'projection': 'noAcl'}
354-
self.metadata = self.connection.api_request(
355-
method='GET', path=self.path, query_params=query_params)
356-
return self
357-
358-
def get_metadata(self, field=None, default=None):
359-
"""Get all metadata or a specific field.
360-
361-
If you request a field that isn't available, and that field can
362-
be retrieved by refreshing data from Cloud Storage, this method
363-
will reload the data using :func:`Bucket.reload_metadata`.
364-
365-
:type field: string
366-
:param field: (optional) A particular field to retrieve from metadata.
367-
368-
:type default: anything
369-
:param default: The value to return if the field provided wasn't found.
370-
371-
:rtype: dict or anything
372-
:returns: All metadata or the value of the specific field.
373-
"""
374-
if field == 'acl':
375-
raise KeyError("Use 'get_acl()'")
376-
377-
if field == 'defaultObjectAcl':
378-
raise KeyError("Use 'get_default_object_acl()'")
379-
380-
if not self.has_metadata(field=field):
381-
self.reload_metadata()
382-
383-
if field:
384-
return self.metadata.get(field, default)
385-
else:
386-
return self.metadata
387-
388-
def patch_metadata(self, metadata):
389-
"""Update particular fields of this bucket's metadata.
390-
391-
This method will only update the fields provided and will not
392-
touch the other fields.
393-
394-
It will also reload the metadata locally based on the servers
395-
response.
396-
397-
:type metadata: dict
398-
:param metadata: The dictionary of values to update.
399-
400-
:rtype: :class:`Bucket`
401-
:returns: The current bucket.
402-
"""
403-
self.metadata = self.connection.api_request(
404-
method='PATCH', path=self.path, data=metadata,
405-
query_params={'projection': 'full'})
406-
return self
407-
408342
def configure_website(self, main_page_suffix=None, not_found_page=None):
409343
"""Configure website-related metadata.
410344

0 commit comments

Comments
 (0)