Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@
omit =
*/demo/*
*/demo.py
exclude_lines =
# Re-enable the standard pragma
pragma: NO COVER
# Ignore debug-only repr
def __repr__
2 changes: 1 addition & 1 deletion gcloud/datastore/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ def delete(self):
dataset_id=self.dataset().id(), key_pb=self.key().to_protobuf())
# pylint: enable=maybe-no-member

def __repr__(self): # pragma NO COVER
def __repr__(self):
# An entity should have a key all the time (even if it's partial).
if self.key():
# pylint: disable=maybe-no-member
Expand Down
4 changes: 2 additions & 2 deletions gcloud/datastore/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ def id_or_name(self):
"""
return self.id() or self.name()

def parent(self): # pragma NO COVER
def parent(self):
"""Getter: return a new key for the next highest element in path.

:rtype: :class:`gcloud.datastore.key.Key`
Expand All @@ -286,5 +286,5 @@ def parent(self): # pragma NO COVER
return None
return self.path(self.path()[:-1])

def __repr__(self): # pragma NO COVER
def __repr__(self):
return '<Key%s>' % self.path()
4 changes: 2 additions & 2 deletions gcloud/storage/acl.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def __str__(self):
else:
return '{self.type}-{self.identifier}'.format(self=self)

def __repr__(self): # pragma NO COVER
def __repr__(self):
return '<ACL Entity: {self} ({roles})>'.format(
self=self, roles=', '.join(self.roles))

Expand Down Expand Up @@ -353,7 +353,7 @@ def get_entities(self):

return self.entities.values()

def save(self): # pragma NO COVER
def save(self):
"""A method to be overridden by subclasses.

:raises: NotImplementedError
Expand Down
4 changes: 2 additions & 2 deletions gcloud/storage/bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def from_dict(cls, bucket_dict, connection=None):
return cls(connection=connection, name=bucket_dict['name'],
metadata=bucket_dict)

def __repr__(self): # pragma NO COVER
def __repr__(self):
return '<Bucket: %s>' % self.name

def __iter__(self):
Expand Down Expand Up @@ -126,7 +126,7 @@ def new_key(self, key):
# Support Python 2 and 3.
try:
string_type = basestring
except NameError: # pragma NO COVER PY3k
except NameError: # pragma: NO COVER PY3k
string_type = str

if isinstance(key, string_type):
Expand Down
73 changes: 45 additions & 28 deletions gcloud/storage/connection.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""Create / interact with gcloud storage connections."""

import base64
import calendar
import datetime
import json
import time
import urllib

from Crypto.Hash import SHA256
Expand All @@ -18,6 +18,14 @@
from gcloud.storage.iterator import BucketIterator


def _utcnow(): # pragma: NO COVER testing replaces
"""Returns current time as UTC datetime.

NOTE: on the module namespace so tests can replace it.
"""
return datetime.datetime.utcnow()


class Connection(connection.Connection):
"""A connection to Google Cloud Storage via the JSON REST API.

Expand Down Expand Up @@ -419,7 +427,7 @@ def new_bucket(self, bucket):
# Support Python 2 and 3.
try:
string_type = basestring
except NameError: # pragma NO COVER PY3k
except NameError: # pragma: NO COVER PY3k
string_type = str

if isinstance(bucket, string_type):
Expand All @@ -429,7 +437,7 @@ def new_bucket(self, bucket):

def generate_signed_url(self, resource, expiration,
method='GET', content_md5=None,
content_type=None): # pragma NO COVER
content_type=None):
"""Generate signed URL to provide query-string auth'n to a resource.

:type resource: string
Expand All @@ -455,31 +463,7 @@ def generate_signed_url(self, resource, expiration,
until expiration.
"""

# expiration can be an absolute timestamp (int, long),
# an absolute time (datetime.datetime),
# or a relative time (datetime.timedelta).
# We should convert all of these into an absolute timestamp.

# If it's a timedelta, add it to `now` in UTC.
if isinstance(expiration, datetime.timedelta):
now = datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
expiration = now + expiration

# If it's a datetime, convert to a timestamp.
if isinstance(expiration, datetime.datetime):
# Make sure the timezone on the value is UTC
# (either by converting or replacing the value).
if expiration.tzinfo:
expiration = expiration.astimezone(pytz.utc)
else:
expiration = expiration.replace(tzinfo=pytz.utc)

# Turn the datetime into a timestamp (seconds, not microseconds).
expiration = int(time.mktime(expiration.timetuple()))

if not isinstance(expiration, (int, long)):
raise ValueError('Expected an integer timestamp, datetime, or '
'timedelta. Got %s' % type(expiration))
expiration = _get_expiration_seconds(expiration)

# Generate the string to sign.
signature_string = '\n'.join([
Expand Down Expand Up @@ -514,3 +498,36 @@ def generate_signed_url(self, resource, expiration,
return '{endpoint}{resource}?{querystring}'.format(
endpoint=self.API_ACCESS_ENDPOINT, resource=resource,
querystring=urllib.urlencode(query_params))


def _get_expiration_seconds(expiration):
"""Convert 'expiration' to a number of seconds in the future.

:type expiration: int, long, datetime.datetime, datetime.timedelta
:param expiration: When the signed URL should expire.

:rtype: int
:returns: a timestamp as an absolute number of seconds.
"""

# If it's a timedelta, add it to `now` in UTC.
if isinstance(expiration, datetime.timedelta):
now = _utcnow().replace(tzinfo=pytz.utc)
expiration = now + expiration

# If it's a datetime, convert to a timestamp.
if isinstance(expiration, datetime.datetime):
# Make sure the timezone on the value is UTC
# (either by converting or replacing the value).
if expiration.tzinfo:
expiration = expiration.astimezone(pytz.utc)
else:
expiration = expiration.replace(tzinfo=pytz.utc)

# Turn the datetime into a timestamp (seconds, not microseconds).
expiration = int(calendar.timegm(expiration.timetuple()))

if not isinstance(expiration, (int, long)):
raise TypeError('Expected an integer timestamp, datetime, or '
'timedelta. Got %s' % type(expiration))
return expiration
2 changes: 1 addition & 1 deletion gcloud/storage/iterator.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def reset(self):
self.page_number = 0
self.next_page_token = None

def get_items_from_response(self, response): # pragma NO COVER
def get_items_from_response(self, response):
"""Factory method called while iterating. This should be overriden.

This method should be overridden by a subclass.
Expand Down
11 changes: 3 additions & 8 deletions gcloud/storage/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def from_dict(cls, key_dict, bucket=None):

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

def __repr__(self): # pragma NO COVER
def __repr__(self):
if self.bucket:
bucket_name = self.bucket.name
else:
Expand Down Expand Up @@ -102,8 +102,7 @@ def public_url(self):
storage_base_url='http://commondatastorage.googleapis.com',
self=self)

def generate_signed_url(self, expiration,
method='GET'): # pragma NO COVER
def generate_signed_url(self, expiration, method='GET'):
"""Generates a signed URL for this key.

If you have a key that you want to allow access to
Expand Down Expand Up @@ -181,11 +180,7 @@ def get_contents_to_file(self, fh):
"""

for chunk in KeyDataIterator(self):
try:
fh.write(chunk)
except IOError, e: # pragma NO COVER
if e.errno == errno.ENOSPC:
raise Exception('No space left on device.')
fh.write(chunk)

def get_contents_to_filename(self, filename):
"""Get the contents of this key to a file by name.
Expand Down
15 changes: 15 additions & 0 deletions gcloud/storage/test_acl.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,21 @@ def test_all_authenticated(self):
self.assertEqual(list(acl),
[{'entity': 'allAuthenticatedUsers', 'role': ROLE}])

def test_get_entities_empty(self):
acl = self._makeOne()
self.assertEqual(acl.get_entities(), [])

def test_get_entities_nonempty(self):
TYPE = 'type'
ID = 'id'
acl = self._makeOne()
entity = acl.entity(TYPE, ID)
self.assertEqual(acl.get_entities(), [entity])

def test_save_raises_NotImplementedError(self):
acl = self._makeOne()
self.assertRaises(NotImplementedError, acl.save)


class Test_BucketACL(unittest2.TestCase):

Expand Down
Loading