summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkyleknap <kyleknap@amazon.com>2014-10-23 09:19:50 -0700
committerkyleknap <kyleknap@amazon.com>2014-10-23 09:19:50 -0700
commitd7be53ec6ffe400222cd002a4a0da417e81901eb (patch)
treee4cdc05b69408e5b01c91ac8a6eb06f422dbb8f4
parent30f9c068c92c7a91cc1c7d156ed28a10f2c69000 (diff)
parent3265cada6b98b8f83dfd4eb8916213b82a0e38dc (diff)
downloadboto-d7be53ec6ffe400222cd002a4a0da417e81901eb.tar.gz
Merge branch 'release-2.34.0'2.34.0
-rw-r--r--README.rst4
-rw-r--r--boto/__init__.py2
-rw-r--r--boto/auth.py51
-rw-r--r--boto/cloudtrail/exceptions.py4
-rw-r--r--boto/cloudtrail/layer1.py2
-rw-r--r--boto/datapipeline/layer1.py2
-rw-r--r--boto/directconnect/exceptions.py1
-rw-r--r--boto/directconnect/layer1.py2
-rw-r--r--boto/endpoints.json89
-rw-r--r--boto/iam/connection.py21
-rw-r--r--boto/logs/layer1.py1
-rw-r--r--boto/mws/connection.py2
-rw-r--r--boto/s3/key.py5
-rw-r--r--docs/source/index.rst5
-rw-r--r--docs/source/releasenotes/v2.34.0.rst21
-rw-r--r--requirements-docs.txt2
-rw-r--r--tests/integration/s3/test_multipart.py64
-rw-r--r--tests/unit/iam/test_connection.py33
-rw-r--r--tests/unit/mws/test_connection.py36
-rw-r--r--tests/unit/test_regioninfo.py4
20 files changed, 297 insertions, 54 deletions
diff --git a/README.rst b/README.rst
index 6ec7300c..44231ab3 100644
--- a/README.rst
+++ b/README.rst
@@ -1,9 +1,9 @@
####
boto
####
-boto 2.33.0
+boto 2.34.0
-Released: 08-Oct-2014
+Released: 23-Oct-2014
.. image:: https://travis-ci.org/boto/boto.svg?branch=develop
:target: https://travis-ci.org/boto/boto
diff --git a/boto/__init__.py b/boto/__init__.py
index cfcdcd67..fe5fd7d4 100644
--- a/boto/__init__.py
+++ b/boto/__init__.py
@@ -38,7 +38,7 @@ import logging.config
from boto.compat import urlparse
from boto.exception import InvalidUriError
-__version__ = '2.33.0'
+__version__ = '2.34.0'
Version = __version__ # for backware compatibility
# http://bugs.python.org/issue7980
diff --git a/boto/auth.py b/boto/auth.py
index f05bb275..3e9ded79 100644
--- a/boto/auth.py
+++ b/boto/auth.py
@@ -345,7 +345,7 @@ class HmacAuthV4Handler(AuthHandler, HmacKeys):
for param in sorted(http_request.params):
value = boto.utils.get_utf8_value(http_request.params[param])
l.append('%s=%s' % (urllib.parse.quote(param, safe='-_.~'),
- urllib.parse.quote(value.decode('utf-8'), safe='-_.~')))
+ urllib.parse.quote(value, safe='-_.~')))
return '&'.join(l)
def canonical_headers(self, headers_to_sign):
@@ -494,10 +494,25 @@ class HmacAuthV4Handler(AuthHandler, HmacKeys):
if self._provider.security_token:
req.headers['X-Amz-Security-Token'] = self._provider.security_token
qs = self.query_string(req)
- if qs and req.method == 'POST':
+
+ qs_to_post = qs
+
+ # We do not want to include any params that were mangled into
+ # the params if performing s3-sigv4 since it does not
+ # belong in the body of a post for some requests. Mangled
+ # refers to items in the query string URL being added to the
+ # http response params. However, these params get added to
+ # the body of the request, but the query string URL does not
+ # belong in the body of the request. ``unmangled_resp`` is the
+ # response that happened prior to the mangling. This ``unmangled_req``
+ # kwarg will only appear for s3-sigv4.
+ if 'unmangled_req' in kwargs:
+ qs_to_post = self.query_string(kwargs['unmangled_req'])
+
+ if qs_to_post and req.method == 'POST':
# Stash request parameters into post body
# before we generate the signature.
- req.body = qs
+ req.body = qs_to_post
req.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
req.headers['Content-Length'] = str(len(req.body))
else:
@@ -549,6 +564,17 @@ class S3HmacAuthV4Handler(HmacAuthV4Handler, AuthHandler):
encoded = urllib.parse.quote(unquoted)
return encoded
+ def canonical_query_string(self, http_request):
+ # Note that we just do not return an empty string for
+ # POST request. Query strings in url are included in canonical
+ # query string.
+ l = []
+ for param in sorted(http_request.params):
+ value = boto.utils.get_utf8_value(http_request.params[param])
+ l.append('%s=%s' % (urllib.parse.quote(param, safe='-_.~'),
+ urllib.parse.quote(value, safe='-_.~')))
+ return '&'.join(l)
+
def host_header(self, host, http_request):
port = http_request.port
secure = http_request.protocol == 'https'
@@ -641,6 +667,13 @@ class S3HmacAuthV4Handler(HmacAuthV4Handler, AuthHandler):
if modified_req.params is None:
modified_req.params = {}
+ else:
+ # To keep the original request object untouched. We must make
+ # a copy of the params dictionary. Because the copy of the
+ # original request directly refers to the params dictionary
+ # of the original request.
+ copy_params = req.params.copy()
+ modified_req.params = copy_params
raw_qs = parsed_path.query
existing_qs = urllib.parse.parse_qs(
@@ -670,9 +703,10 @@ class S3HmacAuthV4Handler(HmacAuthV4Handler, AuthHandler):
req.headers['x-amz-content-sha256'] = req.headers.pop('_sha256')
else:
req.headers['x-amz-content-sha256'] = self.payload(req)
-
- req = self.mangle_path_and_params(req)
- return super(S3HmacAuthV4Handler, self).add_auth(req, **kwargs)
+ updated_req = self.mangle_path_and_params(req)
+ return super(S3HmacAuthV4Handler, self).add_auth(updated_req,
+ unmangled_req=req,
+ **kwargs)
def presign(self, req, expires, iso_date=None):
"""
@@ -966,7 +1000,8 @@ def detect_potential_sigv4(func):
# ``boto/iam/connection.py``, as several things there are also
# endpoint-related.
if getattr(self.region, 'endpoint', ''):
- if '.cn-' in self.region.endpoint:
+ if '.cn-' in self.region.endpoint or \
+ '.eu-central' in self.region.endpoint:
return ['hmac-v4']
return func(self)
@@ -985,7 +1020,7 @@ def detect_potential_s3sigv4(func):
# If you're making changes here, you should also check
# ``boto/iam/connection.py``, as several things there are also
# endpoint-related.
- if '.cn-' in self.host:
+ if '.cn-' in self.host or '.eu-central' in self.host:
return ['hmac-v4-s3']
return func(self)
diff --git a/boto/cloudtrail/exceptions.py b/boto/cloudtrail/exceptions.py
index 35c5c3d3..ac4efbd2 100644
--- a/boto/cloudtrail/exceptions.py
+++ b/boto/cloudtrail/exceptions.py
@@ -24,6 +24,7 @@ class TrailAlreadyExistsException(BotoServerError):
"""
pass
+
class InsufficientSnsTopicPolicyException(BotoServerError):
"""
Raised when the SNS topic does not allow Cloudtrail to post
@@ -31,18 +32,21 @@ class InsufficientSnsTopicPolicyException(BotoServerError):
"""
pass
+
class InvalidTrailNameException(BotoServerError):
"""
Raised when the trail name is invalid.
"""
pass
+
class InternalErrorException(BotoServerError):
"""
Raised when there was an internal Cloudtrail error.
"""
pass
+
class TrailNotFoundException(BotoServerError):
"""
Raised when the given trail name is not found.
diff --git a/boto/cloudtrail/layer1.py b/boto/cloudtrail/layer1.py
index 0c18fa90..960b00da 100644
--- a/boto/cloudtrail/layer1.py
+++ b/boto/cloudtrail/layer1.py
@@ -75,7 +75,6 @@ class CloudTrailConnection(AWSQueryConnection):
"InsufficientS3BucketPolicyException": exceptions.InsufficientS3BucketPolicyException,
}
-
def __init__(self, **kwargs):
region = kwargs.pop('region', None)
if not region:
@@ -351,4 +350,3 @@ class CloudTrailConnection(AWSQueryConnection):
exception_class = self._faults.get(fault_name, self.ResponseError)
raise exception_class(response.status, response.reason,
body=json_body)
-
diff --git a/boto/datapipeline/layer1.py b/boto/datapipeline/layer1.py
index 6a3fb9b5..028fd9d2 100644
--- a/boto/datapipeline/layer1.py
+++ b/boto/datapipeline/layer1.py
@@ -83,7 +83,6 @@ class DataPipelineConnection(AWSQueryConnection):
"InternalServiceError": exceptions.InternalServiceError,
}
-
def __init__(self, **kwargs):
region = kwargs.pop('region', None)
if not region:
@@ -638,4 +637,3 @@ class DataPipelineConnection(AWSQueryConnection):
exception_class = self._faults.get(fault_name, self.ResponseError)
raise exception_class(response.status, response.reason,
body=json_body)
-
diff --git a/boto/directconnect/exceptions.py b/boto/directconnect/exceptions.py
index e3cac9ba..88168d30 100644
--- a/boto/directconnect/exceptions.py
+++ b/boto/directconnect/exceptions.py
@@ -20,6 +20,7 @@
# IN THE SOFTWARE.
#
+
class DirectConnectClientException(Exception):
pass
diff --git a/boto/directconnect/layer1.py b/boto/directconnect/layer1.py
index 08197ddf..a332b31b 100644
--- a/boto/directconnect/layer1.py
+++ b/boto/directconnect/layer1.py
@@ -65,7 +65,6 @@ class DirectConnectConnection(AWSQueryConnection):
"DirectConnectServerException": exceptions.DirectConnectServerException,
}
-
def __init__(self, **kwargs):
region = kwargs.pop('region', None)
if not region:
@@ -626,4 +625,3 @@ class DirectConnectConnection(AWSQueryConnection):
exception_class = self._faults.get(fault_name, self.ResponseError)
raise exception_class(response.status, response.reason,
body=json_body)
-
diff --git a/boto/endpoints.json b/boto/endpoints.json
index 46276438..14673142 100644
--- a/boto/endpoints.json
+++ b/boto/endpoints.json
@@ -9,7 +9,8 @@
"us-east-1": "autoscaling.us-east-1.amazonaws.com",
"us-gov-west-1": "autoscaling.us-gov-west-1.amazonaws.com",
"us-west-1": "autoscaling.us-west-1.amazonaws.com",
- "us-west-2": "autoscaling.us-west-2.amazonaws.com"
+ "us-west-2": "autoscaling.us-west-2.amazonaws.com",
+ "eu-central-1": "autoscaling.eu-central-1.amazonaws.com"
},
"cloudformation": {
"ap-northeast-1": "cloudformation.ap-northeast-1.amazonaws.com",
@@ -21,7 +22,8 @@
"us-east-1": "cloudformation.us-east-1.amazonaws.com",
"us-gov-west-1": "cloudformation.us-gov-west-1.amazonaws.com",
"us-west-1": "cloudformation.us-west-1.amazonaws.com",
- "us-west-2": "cloudformation.us-west-2.amazonaws.com"
+ "us-west-2": "cloudformation.us-west-2.amazonaws.com",
+ "eu-central-1": "cloudformation.eu-central-1.amazonaws.com"
},
"cloudfront": {
"ap-northeast-1": "cloudfront.amazonaws.com",
@@ -31,7 +33,8 @@
"sa-east-1": "cloudfront.amazonaws.com",
"us-east-1": "cloudfront.amazonaws.com",
"us-west-1": "cloudfront.amazonaws.com",
- "us-west-2": "cloudfront.amazonaws.com"
+ "us-west-2": "cloudfront.amazonaws.com",
+ "eu-central-1": "cloudfront.amazonaws.com"
},
"cloudsearch": {
"ap-southeast-1": "cloudsearch.ap-southeast-1.amazonaws.com",
@@ -41,7 +44,8 @@
"eu-west-1": "cloudsearch.eu-west-1.amazonaws.com",
"us-east-1": "cloudsearch.us-east-1.amazonaws.com",
"us-west-1": "cloudsearch.us-west-1.amazonaws.com",
- "us-west-2": "cloudsearch.us-west-2.amazonaws.com"
+ "us-west-2": "cloudsearch.us-west-2.amazonaws.com",
+ "eu-central-1": "cloudsearch.eu-central-1.amazonaws.com"
},
"cloudtrail": {
"ap-northeast-1": "cloudtrail.ap-northeast-1.amazonaws.com",
@@ -51,7 +55,8 @@
"sa-east-1": "cloudtrail.sa-east-1.amazonaws.com",
"us-east-1": "cloudtrail.us-east-1.amazonaws.com",
"us-west-1": "cloudtrail.us-west-1.amazonaws.com",
- "us-west-2": "cloudtrail.us-west-2.amazonaws.com"
+ "us-west-2": "cloudtrail.us-west-2.amazonaws.com",
+ "eu-central-1": "cloudtrail.eu-central-1.amazonaws.com"
},
"cloudwatch": {
"ap-northeast-1": "monitoring.ap-northeast-1.amazonaws.com",
@@ -63,7 +68,8 @@
"us-east-1": "monitoring.us-east-1.amazonaws.com",
"us-gov-west-1": "monitoring.us-gov-west-1.amazonaws.com",
"us-west-1": "monitoring.us-west-1.amazonaws.com",
- "us-west-2": "monitoring.us-west-2.amazonaws.com"
+ "us-west-2": "monitoring.us-west-2.amazonaws.com",
+ "eu-central-1": "monitoring.eu-central-1.amazonaws.com"
},
"cognito-identity": {
"us-east-1": "cognito-identity.us-east-1.amazonaws.com"
@@ -76,7 +82,8 @@
"us-west-2": "datapipeline.us-west-2.amazonaws.com",
"eu-west-1": "datapipeline.eu-west-1.amazonaws.com",
"ap-southeast-2": "datapipeline.ap-southeast-2.amazonaws.com",
- "ap-northeast-1": "datapipeline.ap-northeast-1.amazonaws.com"
+ "ap-northeast-1": "datapipeline.ap-northeast-1.amazonaws.com",
+ "eu-central-1": "datapipeline.eu-central-1.amazonaws.com"
},
"directconnect": {
"ap-northeast-1": "directconnect.ap-northeast-1.amazonaws.com",
@@ -86,7 +93,8 @@
"sa-east-1": "directconnect.sa-east-1.amazonaws.com",
"us-east-1": "directconnect.us-east-1.amazonaws.com",
"us-west-1": "directconnect.us-west-1.amazonaws.com",
- "us-west-2": "directconnect.us-west-2.amazonaws.com"
+ "us-west-2": "directconnect.us-west-2.amazonaws.com",
+ "eu-central-1": "directconnect.eu-central-1.amazonaws.com"
},
"dynamodb": {
"ap-northeast-1": "dynamodb.ap-northeast-1.amazonaws.com",
@@ -98,7 +106,8 @@
"us-east-1": "dynamodb.us-east-1.amazonaws.com",
"us-gov-west-1": "dynamodb.us-gov-west-1.amazonaws.com",
"us-west-1": "dynamodb.us-west-1.amazonaws.com",
- "us-west-2": "dynamodb.us-west-2.amazonaws.com"
+ "us-west-2": "dynamodb.us-west-2.amazonaws.com",
+ "eu-central-1": "dynamodb.eu-central-1.amazonaws.com"
},
"ec2": {
"ap-northeast-1": "ec2.ap-northeast-1.amazonaws.com",
@@ -110,7 +119,8 @@
"us-east-1": "ec2.us-east-1.amazonaws.com",
"us-gov-west-1": "ec2.us-gov-west-1.amazonaws.com",
"us-west-1": "ec2.us-west-1.amazonaws.com",
- "us-west-2": "ec2.us-west-2.amazonaws.com"
+ "us-west-2": "ec2.us-west-2.amazonaws.com",
+ "eu-central-1": "ec2.eu-central-1.amazonaws.com"
},
"elasticache": {
"ap-northeast-1": "elasticache.ap-northeast-1.amazonaws.com",
@@ -121,7 +131,8 @@
"sa-east-1": "elasticache.sa-east-1.amazonaws.com",
"us-east-1": "elasticache.us-east-1.amazonaws.com",
"us-west-1": "elasticache.us-west-1.amazonaws.com",
- "us-west-2": "elasticache.us-west-2.amazonaws.com"
+ "us-west-2": "elasticache.us-west-2.amazonaws.com",
+ "eu-central-1": "elasticache.eu-central-1.amazonaws.com"
},
"elasticbeanstalk": {
"ap-northeast-1": "elasticbeanstalk.ap-northeast-1.amazonaws.com",
@@ -131,7 +142,8 @@
"sa-east-1": "elasticbeanstalk.sa-east-1.amazonaws.com",
"us-east-1": "elasticbeanstalk.us-east-1.amazonaws.com",
"us-west-1": "elasticbeanstalk.us-west-1.amazonaws.com",
- "us-west-2": "elasticbeanstalk.us-west-2.amazonaws.com"
+ "us-west-2": "elasticbeanstalk.us-west-2.amazonaws.com",
+ "eu-central-1": "elasticbeanstalk.eu-central-1.amazonaws.com"
},
"elasticloadbalancing": {
"ap-northeast-1": "elasticloadbalancing.ap-northeast-1.amazonaws.com",
@@ -143,7 +155,8 @@
"us-east-1": "elasticloadbalancing.us-east-1.amazonaws.com",
"us-gov-west-1": "elasticloadbalancing.us-gov-west-1.amazonaws.com",
"us-west-1": "elasticloadbalancing.us-west-1.amazonaws.com",
- "us-west-2": "elasticloadbalancing.us-west-2.amazonaws.com"
+ "us-west-2": "elasticloadbalancing.us-west-2.amazonaws.com",
+ "eu-central-1": "elasticloadbalancing.eu-central-1.amazonaws.com"
},
"elasticmapreduce": {
"ap-northeast-1": "ap-northeast-1.elasticmapreduce.amazonaws.com",
@@ -155,7 +168,8 @@
"us-east-1": "elasticmapreduce.us-east-1.amazonaws.com",
"us-gov-west-1": "us-gov-west-1.elasticmapreduce.amazonaws.com",
"us-west-1": "us-west-1.elasticmapreduce.amazonaws.com",
- "us-west-2": "us-west-2.elasticmapreduce.amazonaws.com"
+ "us-west-2": "us-west-2.elasticmapreduce.amazonaws.com",
+ "eu-central-1": "eu-central-1.elasticmapreduce.amazonaws.com"
},
"elastictranscoder": {
"ap-northeast-1": "elastictranscoder.ap-northeast-1.amazonaws.com",
@@ -163,7 +177,8 @@
"eu-west-1": "elastictranscoder.eu-west-1.amazonaws.com",
"us-east-1": "elastictranscoder.us-east-1.amazonaws.com",
"us-west-1": "elastictranscoder.us-west-1.amazonaws.com",
- "us-west-2": "elastictranscoder.us-west-2.amazonaws.com"
+ "us-west-2": "elastictranscoder.us-west-2.amazonaws.com",
+ "eu-central-1": "elastictranscoder.eu-central-1.amazonaws.com"
},
"glacier": {
"ap-northeast-1": "glacier.ap-northeast-1.amazonaws.com",
@@ -172,7 +187,8 @@
"eu-west-1": "glacier.eu-west-1.amazonaws.com",
"us-east-1": "glacier.us-east-1.amazonaws.com",
"us-west-1": "glacier.us-west-1.amazonaws.com",
- "us-west-2": "glacier.us-west-2.amazonaws.com"
+ "us-west-2": "glacier.us-west-2.amazonaws.com",
+ "eu-central-1": "glacier.eu-central-1.amazonaws.com"
},
"iam": {
"ap-northeast-1": "iam.amazonaws.com",
@@ -202,13 +218,18 @@
"eu-west-1": "kinesis.eu-west-1.amazonaws.com",
"ap-southeast-1": "kinesis.ap-southeast-1.amazonaws.com",
"ap-southeast-2": "kinesis.ap-southeast-2.amazonaws.com",
- "ap-northeast-1": "kinesis.ap-northeast-1.amazonaws.com"
+ "ap-northeast-1": "kinesis.ap-northeast-1.amazonaws.com",
+ "eu-central-1": "kinesis.eu-central-1.amazonaws.com"
},
"logs": {
- "us-east-1": "logs.us-east-1.amazonaws.com"
+ "us-east-1": "logs.us-east-1.amazonaws.com",
+ "us-west-2": "logs.us-west-2.amazonaws.com",
+ "eu-west-1": "logs.eu-west-1.amazonaws.com",
+ "eu-central-1": "logs.eu-central-1.amazonaws.com"
},
"opsworks": {
- "us-east-1": "opsworks.us-east-1.amazonaws.com"
+ "us-east-1": "opsworks.us-east-1.amazonaws.com",
+ "eu-central-1": "opsworks.eu-central-1.amazonaws.com"
},
"rds": {
"ap-northeast-1": "rds.ap-northeast-1.amazonaws.com",
@@ -220,7 +241,8 @@
"us-east-1": "rds.amazonaws.com",
"us-gov-west-1": "rds.us-gov-west-1.amazonaws.com",
"us-west-1": "rds.us-west-1.amazonaws.com",
- "us-west-2": "rds.us-west-2.amazonaws.com"
+ "us-west-2": "rds.us-west-2.amazonaws.com",
+ "eu-central-1": "rds.eu-central-1.amazonaws.com"
},
"redshift": {
"ap-northeast-1": "redshift.ap-northeast-1.amazonaws.com",
@@ -228,7 +250,8 @@
"ap-southeast-2": "redshift.ap-southeast-2.amazonaws.com",
"eu-west-1": "redshift.eu-west-1.amazonaws.com",
"us-east-1": "redshift.us-east-1.amazonaws.com",
- "us-west-2": "redshift.us-west-2.amazonaws.com"
+ "us-west-2": "redshift.us-west-2.amazonaws.com",
+ "eu-central-1": "redshift.eu-central-1.amazonaws.com"
},
"route53": {
"ap-northeast-1": "route53.amazonaws.com",
@@ -253,7 +276,8 @@
"us-east-1": "s3.amazonaws.com",
"us-gov-west-1": "s3-us-gov-west-1.amazonaws.com",
"us-west-1": "s3-us-west-1.amazonaws.com",
- "us-west-2": "s3-us-west-2.amazonaws.com"
+ "us-west-2": "s3-us-west-2.amazonaws.com",
+ "eu-central-1": "s3.eu-central-1.amazonaws.com"
},
"sdb": {
"ap-northeast-1": "sdb.ap-northeast-1.amazonaws.com",
@@ -263,12 +287,14 @@
"sa-east-1": "sdb.sa-east-1.amazonaws.com",
"us-east-1": "sdb.amazonaws.com",
"us-west-1": "sdb.us-west-1.amazonaws.com",
- "us-west-2": "sdb.us-west-2.amazonaws.com"
+ "us-west-2": "sdb.us-west-2.amazonaws.com",
+ "eu-central-1": "sdb.eu-central-1.amazonaws.com"
},
"ses": {
"eu-west-1": "email.eu-west-1.amazonaws.com",
"us-east-1": "email.us-east-1.amazonaws.com",
- "us-west-2": "email.us-west-2.amazonaws.com"
+ "us-west-2": "email.us-west-2.amazonaws.com",
+ "eu-central-1": "email.eu-central-1.amazonaws.com"
},
"sns": {
"ap-northeast-1": "sns.ap-northeast-1.amazonaws.com",
@@ -280,7 +306,8 @@
"us-east-1": "sns.us-east-1.amazonaws.com",
"us-gov-west-1": "sns.us-gov-west-1.amazonaws.com",
"us-west-1": "sns.us-west-1.amazonaws.com",
- "us-west-2": "sns.us-west-2.amazonaws.com"
+ "us-west-2": "sns.us-west-2.amazonaws.com",
+ "eu-central-1": "sns.eu-central-1.amazonaws.com"
},
"sqs": {
"ap-northeast-1": "ap-northeast-1.queue.amazonaws.com",
@@ -292,7 +319,8 @@
"us-east-1": "queue.amazonaws.com",
"us-gov-west-1": "us-gov-west-1.queue.amazonaws.com",
"us-west-1": "us-west-1.queue.amazonaws.com",
- "us-west-2": "us-west-2.queue.amazonaws.com"
+ "us-west-2": "us-west-2.queue.amazonaws.com",
+ "eu-central-1": "eu-central-1.queue.amazonaws.com"
},
"storagegateway": {
"ap-northeast-1": "storagegateway.ap-northeast-1.amazonaws.com",
@@ -302,7 +330,8 @@
"sa-east-1": "storagegateway.sa-east-1.amazonaws.com",
"us-east-1": "storagegateway.us-east-1.amazonaws.com",
"us-west-1": "storagegateway.us-west-1.amazonaws.com",
- "us-west-2": "storagegateway.us-west-2.amazonaws.com"
+ "us-west-2": "storagegateway.us-west-2.amazonaws.com",
+ "eu-central-1": "storagegateway.eu-central-1.amazonaws.com"
},
"sts": {
"ap-northeast-1": "sts.amazonaws.com",
@@ -317,7 +346,8 @@
"us-west-2": "sts.amazonaws.com"
},
"support": {
- "us-east-1": "support.us-east-1.amazonaws.com"
+ "us-east-1": "support.us-east-1.amazonaws.com",
+ "eu-central-1": "support.eu-central-1.amazonaws.com"
},
"swf": {
"ap-northeast-1": "swf.ap-northeast-1.amazonaws.com",
@@ -329,6 +359,7 @@
"us-east-1": "swf.us-east-1.amazonaws.com",
"us-gov-west-1": "swf.us-gov-west-1.amazonaws.com",
"us-west-1": "swf.us-west-1.amazonaws.com",
- "us-west-2": "swf.us-west-2.amazonaws.com"
+ "us-west-2": "swf.us-west-2.amazonaws.com",
+ "eu-central-1": "swf.eu-central-1.amazonaws.com"
}
}
diff --git a/boto/iam/connection.py b/boto/iam/connection.py
index 3a115302..8590971d 100644
--- a/boto/iam/connection.py
+++ b/boto/iam/connection.py
@@ -1523,3 +1523,24 @@ class IAMConnection(AWSQueryConnection):
"""
params = {}
return self.get_response('GetCredentialReport', params)
+
+ def create_virtual_mfa_device(self, path, device_name):
+ """
+ Creates a new virtual MFA device for the AWS account.
+
+ After creating the virtual MFA, use enable-mfa-device to
+ attach the MFA device to an IAM user.
+
+ :type path: string
+ :param path: The path for the virtual MFA device.
+
+ :type device_name: string
+ :param device_name: The name of the virtual MFA device.
+ Used with path to uniquely identify a virtual MFA device.
+
+ """
+ params = {
+ 'Path': path,
+ 'VirtualMFADeviceName': device_name
+ }
+ return self.get_response('CreateVirtualMFADevice', params)
diff --git a/boto/logs/layer1.py b/boto/logs/layer1.py
index 254e2855..26f7aff7 100644
--- a/boto/logs/layer1.py
+++ b/boto/logs/layer1.py
@@ -95,7 +95,6 @@ class CloudWatchLogsConnection(AWSQueryConnection):
"InvalidSequenceTokenException": exceptions.InvalidSequenceTokenException,
}
-
def __init__(self, **kwargs):
region = kwargs.pop('region', None)
if not region:
diff --git a/boto/mws/connection.py b/boto/mws/connection.py
index 01b0b30b..b372ffb4 100644
--- a/boto/mws/connection.py
+++ b/boto/mws/connection.py
@@ -309,7 +309,7 @@ class MWSConnection(AWSQueryConnection):
try:
response = self._mexe(request, override_num_retries=None)
except BotoServerError as bs:
- raise self._response_error_factor(bs.status, bs.reason, bs.body)
+ raise self._response_error_factory(bs.status, bs.reason, bs.body)
body = response.read()
boto.log.debug(body)
if not body:
diff --git a/boto/s3/key.py b/boto/s3/key.py
index ffceeb9c..bc490e30 100644
--- a/boto/s3/key.py
+++ b/boto/s3/key.py
@@ -934,7 +934,10 @@ class Key(object):
# the auth mechanism (because closures). Detect if it's SigV4 & embelish
# while we can before the auth calculations occur.
if 'hmac-v4-s3' in self.bucket.connection._required_auth_capability():
- headers['_sha256'] = compute_hash(fp, hash_algorithm=hashlib.sha256)[0]
+ kwargs = {'fp': fp, 'hash_algorithm': hashlib.sha256}
+ if size is not None:
+ kwargs['size'] = size
+ headers['_sha256'] = compute_hash(**kwargs)[0]
headers['Expect'] = '100-Continue'
headers = boto.utils.merge_meta(headers, self.metadata, provider)
resp = self.bucket.connection.make_request(
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 77b19647..8e3daa1f 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -73,8 +73,8 @@ Currently Supported Services
* :doc:`Simple Queue Service (SQS) <sqs_tut>` -- (:doc:`API Reference <ref/sqs>`) (Python 3)
* Simple Notification Service (SNS) -- (:doc:`API Reference <ref/sns>`) (Python 3)
* :doc:`Simple Email Service (SES) <ses_tut>` -- (:doc:`API Reference <ref/ses>`) (Python 3)
- * Amazon Cognito Identity -- (:doc:`API Reference <ref/cognito-identity`) (Python 3)
- * Amazon Cognito Sync -- (:doc:`API Reference <ref/cognito-sync`) (Python 3)
+ * Amazon Cognito Identity -- (:doc:`API Reference <ref/cognito-identity>`) (Python 3)
+ * Amazon Cognito Sync -- (:doc:`API Reference <ref/cognito-sync>`) (Python 3)
* **Monitoring**
@@ -135,6 +135,7 @@ Release Notes
.. toctree::
:titlesonly:
+ releasenotes/v2.34.0
releasenotes/v2.33.0
releasenotes/v2.32.1
releasenotes/v2.32.0
diff --git a/docs/source/releasenotes/v2.34.0.rst b/docs/source/releasenotes/v2.34.0.rst
new file mode 100644
index 00000000..6dd28d7c
--- /dev/null
+++ b/docs/source/releasenotes/v2.34.0.rst
@@ -0,0 +1,21 @@
+boto v2.34.0
+============
+
+:date: 2014/10/23
+
+This release adds region support for ``eu-central-1`` , support to create
+virtual mfa devices for Identity and Access Management, and fixes several
+sigv4 issues.
+
+
+Changes
+-------
+* Calculate sha_256 correctly for s3 (:issue:`2691`, :sha:`c0a001f`)
+* Fix MTurk typo. (:issue:`2429`, :issue:`2428`, :sha:`9bfff19`)
+* Fix Amazon Cognito links in docs (:issue:`2674`, :sha:`7c28577`)
+* Add the ability to IAM to create a virtual mfa device. (:issue:`2675`, :sha:`075d402`)
+* PEP8 tidy up for several modules. (:issue:`2673`, :sha:`38abbd9`)
+* Fix s3 create multipart upload for sigv4 (:issue:`2684`, :sha:`fc73641`)
+* Updated endpoints.json for cloudwatch logs to support more regions. (:issue:`2685`, :sha:`5db2ea8`)
+
+
diff --git a/requirements-docs.txt b/requirements-docs.txt
index 4d53a7d7..110534da 100644
--- a/requirements-docs.txt
+++ b/requirements-docs.txt
@@ -1,6 +1,6 @@
requests>=1.2.3,<=2.0.1
rsa==3.1.4
-Sphinx==1.1.3
+Sphinx>=1.1.3,<1.3
simplejson==3.5.2
argparse==1.2.1
paramiko>=1.10.0
diff --git a/tests/integration/s3/test_multipart.py b/tests/integration/s3/test_multipart.py
index 46a88465..78647963 100644
--- a/tests/integration/s3/test_multipart.py
+++ b/tests/integration/s3/test_multipart.py
@@ -32,10 +32,14 @@ Some unit tests for the S3 MultiPartUpload
# bigger than 5M. Hence we just use 1 part so we can keep
# things small and still test logic.
+import os
import unittest
import time
from boto.compat import StringIO
+import mock
+
+import boto
from boto.s3.connection import S3Connection
@@ -163,3 +167,63 @@ class S3MultiPartUploadTest(unittest.TestCase):
pn += 1
# Can't complete 2 small parts so just clean up.
mpu.cancel_upload()
+
+
+class S3MultiPartUploadSigV4Test(unittest.TestCase):
+ s3 = True
+
+ def setUp(self):
+ self.env_patch = mock.patch('os.environ', {'S3_USE_SIGV4': True})
+ self.env_patch.start()
+ self.conn = boto.s3.connect_to_region('us-west-2')
+ self.bucket_name = 'multipart-%d' % int(time.time())
+ self.bucket = self.conn.create_bucket(self.bucket_name,
+ location='us-west-2')
+
+ def tearDown(self):
+ for key in self.bucket:
+ key.delete()
+ self.bucket.delete()
+ self.env_patch.stop()
+
+ def test_initiate_multipart(self):
+ key_name = "multipart"
+ multipart_upload = self.bucket.initiate_multipart_upload(key_name)
+ multipart_uploads = self.bucket.get_all_multipart_uploads()
+ for upload in multipart_uploads:
+ # Check that the multipart upload was created.
+ self.assertEqual(upload.key_name, multipart_upload.key_name)
+ self.assertEqual(upload.id, multipart_upload.id)
+ multipart_upload.cancel_upload()
+
+ def test_upload_part_by_size(self):
+ key_name = "k"
+ contents = "01234567890123456789"
+ sfp = StringIO(contents)
+
+ # upload 20 bytes in 4 parts of 5 bytes each
+ mpu = self.bucket.initiate_multipart_upload(key_name)
+ mpu.upload_part_from_file(sfp, part_num=1, size=5)
+ mpu.upload_part_from_file(sfp, part_num=2, size=5)
+ mpu.upload_part_from_file(sfp, part_num=3, size=5)
+ mpu.upload_part_from_file(sfp, part_num=4, size=5)
+ sfp.close()
+
+ etags = {}
+ pn = 0
+ for part in mpu:
+ pn += 1
+ self.assertEqual(5, part.size)
+ etags[pn] = part.etag
+ self.assertEqual(pn, 4)
+ # etags for 01234
+ self.assertEqual(etags[1], etags[3])
+ # etags for 56789
+ self.assertEqual(etags[2], etags[4])
+ # etag 01234 != etag 56789
+ self.assertNotEqual(etags[1], etags[2])
+
+ # parts are too small to complete as each part must
+ # be a min of 5MB so so we'll assume that is enough
+ # testing and abort the upload.
+ mpu.cancel_upload()
diff --git a/tests/unit/iam/test_connection.py b/tests/unit/iam/test_connection.py
index d94c20a8..e1a521ff 100644
--- a/tests/unit/iam/test_connection.py
+++ b/tests/unit/iam/test_connection.py
@@ -360,3 +360,36 @@ class TestGetCredentialReport(AWSMockServiceTestCase):
b64decode(response['get_credential_report_response']
['get_credential_report_result']
['content'])
+
+class TestCreateVirtualMFADevice(AWSMockServiceTestCase):
+ connection_class = IAMConnection
+
+ def default_body(self):
+ return b"""
+ <CreateVirtualMFADeviceResponse>
+ <CreateVirtualMFADeviceResult>
+ <VirtualMFADevice>
+ <SerialNumber>arn:aws:iam::123456789012:mfa/ExampleName</SerialNumber>
+ <Base32StringSeed>2K5K5XTLA7GGE75TQLYEXAMPLEEXAMPLEEXAMPLECHDFW4KJYZ6
+ UFQ75LL7COCYKM</Base32StringSeed>
+ <QRCodePNG>89504E470D0A1A0AASDFAHSDFKJKLJFKALSDFJASDF</QRCodePNG>
+ </VirtualMFADevice>
+ </CreateVirtualMFADeviceResult>
+ <ResponseMetadata>
+ <RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
+ </ResponseMetadata>
+ </CreateVirtualMFADeviceResponse>
+ """
+
+ def test_create_virtual_mfa_device(self):
+ self.set_http_response(status_code=200)
+ response = self.service_connection.create_virtual_mfa_device('/', 'ExampleName')
+ self.assert_request_parameters(
+ {'Path': '/',
+ 'VirtualMFADeviceName': 'ExampleName',
+ 'Action': 'CreateVirtualMFADevice'},
+ ignore_params_values=['Version'])
+ self.assertEquals(response['create_virtual_mfa_device_response']
+ ['create_virtual_mfa_device_result']
+ ['virtual_mfa_device']
+ ['serial_number'], 'arn:aws:iam::123456789012:mfa/ExampleName')
diff --git a/tests/unit/mws/test_connection.py b/tests/unit/mws/test_connection.py
index e5e15f74..e3830492 100644
--- a/tests/unit/mws/test_connection.py
+++ b/tests/unit/mws/test_connection.py
@@ -23,10 +23,14 @@
from boto.mws.connection import MWSConnection, api_call_map, destructure_object
from boto.mws.response import (ResponseElement, GetFeedSubmissionListResult,
ResponseFactory)
+from boto.exception import BotoServerError
+
from tests.compat import unittest
from tests.unit import AWSMockServiceTestCase
+from mock import MagicMock
+
class TestMWSConnection(AWSMockServiceTestCase):
connection_class = MWSConnection
@@ -51,6 +55,22 @@ doc/2009-01-01/">
</ResponseMetadata>
</GetFeedSubmissionListResponse>"""
+ def default_body_error(self):
+ return b"""<?xml version="1.0" encoding="UTF-8"?>
+<ErrorResponse xmlns="http://mws.amazonaws.com/doc/2009-01-01/">
+ <!--1 or more repetitions:-->
+ <Error>
+ <Type>Sender</Type>
+ <Code>string</Code>
+ <Message>string</Message>
+ <Detail>
+ <!--You may enter ANY elements at this point-->
+ <AnyElement xmlns=""/>
+ </Detail>
+ </Error>
+ <RequestId>string</RequestId>
+</ErrorResponse>"""
+
def test_destructure_object(self):
# Test that parsing of user input to Amazon input works.
response = ResponseElement()
@@ -156,6 +176,22 @@ doc/2009-01-01/">
self.assertTrue('inventory' in str(err.exception))
self.assertTrue('feeds' in str(err.exception))
+ def test_post_request(self):
+
+ self.service_connection._mexe = MagicMock(
+ side_effect=
+ BotoServerError(500, 'You request has bee throttled', body=self.default_body_error()))
+
+ with self.assertRaises(BotoServerError) as err:
+ self.service_connection.get_lowest_offer_listings_for_asin(
+ MarketplaceId='12345',
+ ASINList='ASIN12345',
+ condition='Any',
+ SellerId='1234',
+ excludeme='True')
+
+ self.assertTrue('throttled' in str(err.reason))
+ self.assertEqual(int(err.status), 200)
if __name__ == '__main__':
unittest.main()
diff --git a/tests/unit/test_regioninfo.py b/tests/unit/test_regioninfo.py
index c46614d4..d262562c 100644
--- a/tests/unit/test_regioninfo.py
+++ b/tests/unit/test_regioninfo.py
@@ -104,7 +104,7 @@ class TestEndpointLoading(unittest.TestCase):
def test_get_regions(self):
# With defaults.
ec2_regions = get_regions('ec2')
- self.assertEqual(len(ec2_regions), 10)
+ self.assertTrue(len(ec2_regions) >= 10)
west_2 = None
for region_info in ec2_regions:
@@ -124,7 +124,7 @@ class TestEndpointLoading(unittest.TestCase):
region_cls=TestRegionInfo,
connection_cls=FakeConn
)
- self.assertEqual(len(ec2_regions), 10)
+ self.assertTrue(len(ec2_regions) >= 10)
west_2 = None
for region_info in ec2_regions: